Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

著色器前置處理器

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

在程式語言中,“預處理器”允許在編譯器讀取程式碼之前更改程式碼。與編譯器不同,預處理器不關心預處理程式碼的語法是否有效。預處理器總是執行*指令*告訴它要做的事情。指令是以哈希符號 (#) 開頭的敘述。它不是著色器語言的“關鍵字”(例如“if”或“for”),而是語言中的一種特殊型別的標記。

從 Godot 4.0 開始,您可以在基於文字的著色器中使用著色器預處理器。語法類似於大多數 GLSL 著色器編譯器支援的語法(又類似於 C/C++ 預處理器)。

備註

著色器預處理器在視覺著色器 <doc_visual_shaders> 中不可用。如果需要將預處理器敘述引入視覺著色器,可以使用 VisualShader 屬性面板資源下拉列表中的 轉換為著色器 選項將其轉換為基於文字的著色器。這種轉換是一種單向操作;文字著色器無法轉換回視覺著色器。

字典

一般性定義

  • 前置處理器指令不使用大括弧({}),但會用到括弧。

  • 前置處理器指令**從不**以分號結尾(除非是 #define,允許這麼做,但是可能比較危險)。

  • 預處理器指令可以通過以反斜線 (''') 結尾每行來跨越多行。第一個帶有反斜杠的換行符將 not 結束預處理器語句。

#define

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

Defines the identifier after that directive as a macro, and replaces all successive occurrences of it with the replacement code given in the shader. Replacement is performed on a "whole words" basis, which means no replacement is performed if the string is part of another string (without any spaces or operators separating it).

帶有替換的定義也可能有一個或多個*參數*,然後可以在引用定義時傳遞這些參數(類似於函式呼叫)。

如果未定義替換程式碼,則識別碼只能與「#ifdef」或「#ifndef」指令一起使用。

If the concatenation symbol (##) is present in the replacement code then it will be removed upon macro insertion, together with any space surrounding it, and join the surrounding words and arguments into a new token.

uniform sampler2D material0;

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

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

與常數(const CONSTANT = value;)相比,#define 可以在著色器中的任何位置使用(包括在均勻提示中)。 #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 <identifier>``。

#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 指令檢查``condition`` 是否通過。如果其計算結果為非零值,則包含該程式碼區塊,否則將跳過該程式碼區塊。

為了正確計算,條件必須是給出簡單浮點、整數或布林結果的表達式。可能有多個條件區塊透過「&&」(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

使用“define()”預處理器函式,您可以檢查傳遞的識別碼是否由放置在該指令上方的“#define”定義。這對於在同一檔案中建立多個著色器版本非常有用。它可以由“#else”區塊繼續,但必須以“#endif”指令結束。

define() 函式的結果可以透過在其前面使用 ! (布爾 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

請小心,因為「define()」只能將單一標識符括在括號內,不能再多了:

// 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.
}

如果制服從未改變,則其行為與「#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 在執行「靜態」分支方面「非常有效」<https://medium.com/@jasonbooth_86226/branching-on-a-gpu-18bfc83694f2>」。 「靜態」分支是指「if」敘述,其中*所有*像素/頂點在給定著色器呼叫中計算結果相同。然而,大量的 VGPR(向量通用暫存器)(這可能是由於分支過多引起的)仍然會顯著減慢著色器的執行速度。

elif

“#elif” 指令代表“else if”,並檢查如果上述“#if” 計算結果為“false” 則傳遞的條件。 #elif 只能在``#if`` 區塊中使用。在「#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

與“#if”一樣,可以使用“define()”預處理器函式:

#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 !define(...)」的簡寫。與“#ifdef”類似,但檢查傳遞的標識符是否不是由該指令之前的“#define”定義的。

這與“#ifdef”完全相反;在“#ifdef”永遠不會配對的情況下它總是配對,反之亦然。

#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」指令的終止符。

標頭引用

**語法:**#include "路徑"

#include 指令將著色器包含檔案的*整個*內容包含在著色器中。 "path" 可以是絕對的``res://`` 路徑或相對於目前著色器檔案的路徑。僅允許在儲存至「.gdshader」或「.gdshaderinc」檔案的著色器中使用相對路徑,而絕對路徑可在內建於場景/資源檔案中的著色器中使用。

您可以使用著色器編輯器的 檔案 > 建立著色器包含 選單選項,或透過在檔案系統停靠區中建立新的 ShaderInclude<class_ShaderInclude> 資源來建立新的著色器包含。

著色器包含可以包含在檔案中任何點的任何著色器或其他著色器包含中。

當著色器包含在著色器的全域範圍中時,建議在初始「shader_type」敘述之後執行此操作。

您也可以將著色器包含在主體內的函式中。請注意,著色器編輯器可能會報告著色器包含程式碼的錯誤,因為它在其編寫的本文之外可能無效。您可以選擇忽略這些錯誤(著色器仍然可以正常編譯),也可以將包含內容包裝在「#ifdef」區塊中,該區塊檢查著色器中的定義。

#include 對於建立輔助函式(或巨集)函式庫和減少程式碼重複很有用。使用“#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