Attention: Here be dragons

This is the latest (unstable) version of this documentation, which may document features not available in or compatible with released stable versions of Godot.

著色器預處理器

為什麼要使用著色器預處理器?

在程式語言中, 預處理器 允許在編譯器讀取程式碼之前對其進行更改。與編譯器不同,預處理器不會檢查預處理後程式碼的語法是否正確。預處理器只會執行 指令 所指定的動作。指令是以井字號( # )開頭的敘述,它不是著色器語言的 關鍵字 (如 iffor ),而是一種類型特殊的語言標記。

To avoid repetition and improve code reuse, you can use a shader preprocessor within text-based shaders. The syntax is similar to what most GLSL shader compilers support (which in turn is similar to the C/C++ preprocessor).

備註

著色器預處理器無法在 視覺化著色器 中使用。如果你需要在視覺化著色器中加入預處理器指令,可以在 VisualShader 屬性檢視器的資源下拉選單中使用 轉換為著色器 選項,將其轉換為文字式著色器。此轉換僅為單向操作,無法將文字式著色器再轉回視覺化著色器。

指令

一般語法

  • 預處理器指令不使用大括號({}),但可以使用小括號。

  • 預處理器指令**絕不**以分號結尾(除了 #define,雖然允許但有潛在風險)。

  • 預處理器指令可以透過在每行末尾加上反斜線(\)來換行,直到遇到沒有反斜線的換行符才會結束該指令。

#define

語法: #define <識別字> [替換程式碼]

這個指令會將後面的識別字定義為巨集,並用指定的替換程式碼取代後續出現的所有同名識別字。替換只針對「完整單字」進行,也就是說,如果該字串是其他字串的一部分(未被空白或運算子分隔),就不會取代。

帶有替換程式碼的巨集定義,也可以有一個或多個*參數*,在使用時像函式一樣傳入參數。

若未指定替換程式碼,該識別字只能搭配 #ifdef#ifndef 指令使用。

如果替換程式碼中出現*串接*符號(##),在巨集展開時會一併移除該符號及其兩側的空白,並將相鄰的字或參數合併成新的標記。

uniform sampler2D material0;

#define SAMPLE(N) vec4 tex##N = texture(material##N, UV)

void fragment() {
    SAMPLE(0);
    ALBEDO = tex0.rgb;
}

與常數(const CONSTANT = value;)相比,#define 可以在著色器中的任何位置使用(包括 uniform 提示中)。#define 也能在任意位置插入自訂程式碼,而常數無法做到這一點。

shader_type spatial;

// Notice the lack of semicolon at the end of the line, as the replacement text
// shouldn't insert a semicolon on its own.
// If the directive ends with a semicolon, the semicolon is inserted in every usage
// of the directive, even when this causes a syntax error.
#define USE_MY_COLOR
#define MY_COLOR vec3(1, 0, 0)

// Replacement with arguments.
// All arguments are required (no default values can be provided).
#define BRIGHTEN_COLOR(r, g, b) vec3(r + 0.5, g + 0.5, b + 0.5)

// Multiline replacement using backslashes for continuation:
#define SAMPLE(param1, param2, param3, param4) long_function_call( \
        param1, \
        param2, \
        param3, \
        param4 \
)

void fragment() {
#ifdef USE_MY_COLOR
    ALBEDO = MY_COLOR;
#endif
}

若對已定義過的識別字再次 #define 會導致錯誤。要避免這種情形,請先使用 #undef <識別字>

#undef

語法: #undef 識別字

#undef 指令可用來取消先前定義的 #define 指令:

#define MY_COLOR vec3(1, 0, 0)

vec3 get_red_color() {
    return MY_COLOR;
}

#undef MY_COLOR
#define MY_COLOR vec3(0, 1, 0)

vec3 get_green_color() {
    return MY_COLOR;
}

// Like in most preprocessors, undefining a define that was not previously defined is allowed
// (and won't print any warning or error).
#undef THIS_DOES_NOT_EXIST

上例若沒有 #undef,會出現巨集重複定義錯誤。

#if

語法: #if <條件>

#if 指令會檢查指定的 條件,若結果為非零,則包含該程式碼區塊,否則跳過。

條件必須是能得到浮點數、整數或布林值結果的簡單運算式, 才能正確判斷。可以用 && (AND) 或 || (OR) 運算子串聯多個條件區塊。可以接續 #else 區塊, 但 必須#endif 指令結束。

#define VAR 3
#define USE_LIGHT 0 // Evaluates to `false`.
#define USE_COLOR 1 // Evaluates to `true`.

#if VAR == 3 && (USE_LIGHT || USE_COLOR)
// Condition is `true`. Include this portion in the final shader.
#endif

利用 defined() 預處理器函式,你可以檢查指定的識別字是否已由上方的 #define 定義。這對在同一檔案內建立多個著色器版本很實用。可以接續 #else 區塊,但必須以 #endif 指令結束。

你可在 defined() 前加上 ! (布林 NOT)符號,反向判斷是否 設定該定義。

#define USE_LIGHT
#define USE_COLOR

// Correct syntax:
#if defined(USE_LIGHT) || defined(USE_COLOR) || !defined(USE_REFRACTION)
// Condition is `true`. Include this portion in the final shader.
#endif

請注意,defined() 小括號內只能包住一個識別字,不能有多個:

// Incorrect syntax (parentheses are not placed where they should be):
#if defined(USE_LIGHT || USE_COLOR || !USE_REFRACTION)
// This will cause an error or not behave as expected.
#endif

小訣竅

在著色器編輯器中,經預處理判斷為 false``(因此未編譯進最終著色器)的分支,會顯示為灰色。這不適用於執行階段的 ``if 敘述。

#if 預處理器與 if 敘述:效能注意事項

著色語言 支援執行時期的 if 敘述:

uniform bool USE_LIGHT = true;

if (USE_LIGHT) {
    // This part is included in the compiled shader, and always run.
} else {
    // This part is included in the compiled shader, but never run.
}

如果 uniform 變數不會被更動,其效果就等同於下方這種 #if 預處理器語法:

#define USE_LIGHT

#if defined(USE_LIGHT)
// This part is included in the compiled shader, and always run.
#else
// This part is *not* included in the compiled shader (and therefore never run).
#endif

不過,在某些情況下,使用 #if 預處理器版本會更快。因為著色器的所有執行時分支都會被編譯,即使從未實際執行過,其中的變數仍可能佔用暫存器空間。

現代 GPU 在處理「靜態」分支時 效率相當高。「靜態」分支指的是同一次著色器執行時,所有 像素或頂點都會得到相同結果的 if 敘述。不過,如果分支太多,會導致 VGPRs 占用過多,還是會大幅影響著色器效能。

#elif

#elif 指令代表「else if」,僅當前一個 #if 條件為 false 時才檢查其條件。#elif 只能用在 #if 區塊內,並且可以連續使用多個 #elif

#define VAR 2

#if VAR == 0
// Not included.
#elif VAR == 1
// Not included.
#elif VAR == 2
// Condition is `true`. Include this portion in the final shader.
#else
// Not included.
#endif

同樣地,也可在 #elif 中使用 defined() 預處理器函式:

#define SHADOW_QUALITY_MEDIUM

#if defined(SHADOW_QUALITY_HIGH)
// High shadow quality.
#elif defined(SHADOW_QUALITY_MEDIUM)
// Medium shadow quality.
#else
// Low shadow quality.
#endif

#ifdef

語法: #ifdef <識別字>

這是 #if defined(...) 的簡寫。會檢查指定的識別字是否已由前面 #define 定義。這在同一檔案內設計多種著色器版本時非常實用。可接續 #else 區塊,但必須以 #endif 指令結束。

#define USE_LIGHT

#ifdef USE_LIGHT
// USE_LIGHT is defined. Include this portion in the final shader.
#endif

處理器*不*支援 #elifdef 作為 #elif defined(...) 的簡寫。如果需要超過兩個分支,請依下方範例使用多個 #ifdef#else

#define SHADOW_QUALITY_MEDIUM

#ifdef SHADOW_QUALITY_HIGH
// High shadow quality.
#else
#ifdef SHADOW_QUALITY_MEDIUM
// Medium shadow quality.
#else
// Low shadow quality.
#endif // This ends `SHADOW_QUALITY_MEDIUM`'s branch.
#endif // This ends `SHADOW_QUALITY_HIGH`'s branch.

#ifndef

語法: #ifndef <識別字>

這是 #if !defined(...) 的簡寫。和 #ifdef 類似,但會檢查指定的識別字在該指令前**沒有**被 #define 定義。

這與 #ifdef 完全相反;凡是 #ifdef 不成立的情況,#ifndef 就會成立,反之亦然。

#define USE_LIGHT

#ifndef USE_LIGHT
// Evaluates to `false`. This portion won't be included in the final shader.
#endif

#ifndef USE_COLOR
// Evaluates to `true`. This portion will be included in the final shader.
#endif

#else

語法: #else

定義可選區塊,僅當上一個 #if#elif#ifdef#ifndef 條件為 false 時才包含之。

shader_type spatial;

#define MY_COLOR vec3(1.0, 0, 0)

void fragment() {
#ifdef MY_COLOR
    ALBEDO = MY_COLOR;
#else
    ALBEDO = vec3(0, 0, 1.0);
#endif
}

#endif

語法: #endif

用於結束 #if#ifdef#ifndef 或後續 #else 指令。

#error

語法: #error <訊息>

#error 指令會強制預處理器產生錯誤,可選擇輸出訊息。例如,可以在 #if 區塊中使用,對定義值設下嚴格限制時給予提示。

#define MAX_LOD 3
#define LOD 4

#if LOD > MAX_LOD
#error LOD exceeds MAX_LOD
#endif

#include

語法: #include "路徑"

#include 指令會把指定著色器包含檔案的*全部內容*插入著色器中。"路徑" 可以是絕對的 res:// 路徑,或相對於目前著色器檔的路徑。相對路徑僅能用在儲存為 .gdshader.gdshaderinc 檔案的著色器中;內嵌於場景/資源檔案的著色器則必須使用絕對路徑。

你可以透過著色器編輯器的 檔案 > 建立著色器包含 選單,或在檔案系統面板中建立新的 ShaderInclude 資源,來新增著色器包含檔案。

著色器包含檔可以在任何著色器或其他包含檔的任何位置被引用。

當在著色器全域範圍內引用包含檔時,建議放在 shader_type 敘述之後。

你也可以在函式內部引用著色器包含檔。不過請注意,著色器編輯器可能會對包含檔中的程式碼顯示錯誤,因為該程式碼在原本的上下文以外可能不合法。你可以選擇忽略這些錯誤(著色器仍然能正常編譯),或是將引用包在 #ifdef 區塊內,只在需要時才導入。

#include 適合用來建立輔助函式(或巨集)庫並減少重複程式碼。使用時要小心命名衝突,因為不允許重複定義函式或巨集。

#include 有幾項限制:

  • 只能引用以 .gdshaderinc 結尾的著色器包含資源。.gdshader 檔案不能被其他著色器引用,但 .gdshaderinc 可以包含其他 .gdshaderinc 檔案。

  • 不允許循環依賴,否則會導致錯誤。

  • 為避免無限遞迴,包含層數上限為 25。

範例著色器包含檔:

// fancy_color.gdshaderinc

// While technically allowed, there is usually no `shader_type` declaration in include files.

vec3 get_fancy_color() {
    return vec3(0.3, 0.6, 0.9);
}

範例基礎著色器(引用了上方建立的包含檔):

// material.gdshader

shader_type spatial;

#include "res://fancy_color.gdshaderinc"

void fragment() {
    // No error, as we've included a definition for `get_fancy_color()` via the shader include.
    COLOR = get_fancy_color();
}

#pragma

語法: #pragma

#pragma 指令可提供額外資訊給預處理器或編譯器。

目前僅支援一個值:disable_preprocessor。如果你不需要預處理器,可加上該指令來略過預處理步驟,加快著色器編譯。

#pragma disable_preprocessor

#if USE_LIGHT
// This causes a shader compilation error, as the `#if USE_LIGHT` and `#endif`
// are included as-is in the final shader code.
#endif

內建定義

目前使用的繪圖引擎

自 Godot 4.4 起,你可以透過內建的 CURRENT_RENDERERRENDERER_COMPATIBILITYRENDERER_MOBILERENDERER_FORWARD_PLUS 定義判斷目前使用的繪圖引擎:

  • CURRENT_RENDERER 會根據目前的渲染器設為 012

  • RENDERER_COMPATIBILITY 永遠是 0

  • RENDERER_MOBILE 永遠是 1

  • RENDERER_FORWARD_PLUS 永遠是 2

舉例來說,這個著色器會根據不同繪圖引擎設定不同的 ALBEDO 顏色:

shader_type spatial;

void fragment() {
#if CURRENT_RENDERER == RENDERER_COMPATIBILITY
    ALBEDO = vec3(0.0, 0.0, 1.0);
#elif CURRENT_RENDERER == RENDERER_MOBILE
    ALBEDO = vec3(1.0, 0.0, 0.0);
#else // CURRENT_RENDERER == RENDERER_FORWARD_PLUS
    ALBEDO = vec3(0.0, 1.0, 0.0);
#endif
}