著色器預處理器
為什麼要使用著色器預處理器?
在程式語言中, 預處理器 允許在編譯器讀取程式碼之前對其進行更改。與編譯器不同,預處理器不會檢查預處理後程式碼的語法是否正確。預處理器只會執行 指令 所指定的動作。指令是以井字號( # )開頭的敘述,它不是著色器語言的 關鍵字 (如 if 或 for ),而是一種類型特殊的語言標記。
自 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_RENDERER、RENDERER_COMPATIBILITY、RENDERER_MOBILE 與 RENDERER_FORWARD_PLUS 定義判斷目前使用的繪圖引擎:
CURRENT_RENDERER會根據目前的渲染器設為0、1或2。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
}