著色器預處理器

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

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

自 Godot 4.0 起,你可以在文字式著色器內使用著色器預處理器。其語法與大多數 GLSL 著色器編譯器所支援的語法類似(又與 C/C++ 的預處理器相近)。

備註

著色器預處理器無法在 視覺化著色器 中使用。如果你需要在視覺化著色器中加入預處理器指令,可以在 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
}