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),而是语言中的一种特殊标记。

为了避免重复编写代码并提高代码的复用率,你可以在基于文本的着色器中使用着色器预处理器。它的语法与大多数 GLSL 着色器编译器所支持的语法非常相似(而 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 指令检查 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

通过使用 defined() 这个 预处理器函数 ,你可以检查传入的标识符是否已经被该指令上方的 #define 定义过。这个功能在同一个文件中创建多个着色器(shader)版本时非常有用。它的后面可以接一个 #else 代码块,但必须用 #endif 指令来结束。

可以在 defined() 函数的结果前面加 !(逻辑非)来取反。可借此检查是否未设置某个定义。

#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() 的括号内只能包含 1 个标识符,不能有多个:

// 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 语句:性能注意事项

shading language 支持运行时的 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 语句。不过大量的 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 一样使用预处理器函数 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 定义过。这个功能在同一个文件中创建多个着色器(shader)版本时非常有用。它后面可以接一个 #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 不匹配的情况下匹配,反之亦然。

#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 <message>

#error 指令会强制预处理器抛出一个错误,并且可以附带一段可选的错误信息。比如,当它用在 #if 代码块里时,就能对定义的数值进行严格的限制,这在很多场景下都非常有用。

#define MAX_LOD 3
#define LOD 4

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

#include

语法:#include "路径"

#include 指令将着色器包含文件的 全部 内容包含到着色器中。 "path" 可以是绝对路径 res:// 或相对于当前着色器文件的路径。相对路径仅允许在保存为 .gdshader.gdshaderinc 文件的着色器中使用,而绝对路径可以用于嵌入场景/资源文件中的着色器。

你可以通过着色器编辑器的 文件 > 创建着色器包含 菜单选项来新建着色器包含文件,或者在文件系统面板中直接创建一个新的 ShaderInclude (着色器包含)资源。

着色器包含文件可以在文件内的任意位置,被任何着色器或其他着色器包含文件所引入。

当在着色器的全局作用域中引入着色器包含文件时,建议将其放在初始的 shader_type 语句之后。

你甚至可以在函数体内部使用 #include 来引入代码。不过请注意,着色器编辑器很可能会对你的包含文件报错,因为这些代码一旦脱离了原本设定的上下文环境,可能就不符合语法规范了。你可以选择直接忽略这些报错(因为着色器最终依然能正常编译通过),或者你也可以用一个 #ifdef 代码块把 #include 包裹起来,让它只在检测到特定的宏定义时才生效。

#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

内置定义

当前渲染器

从 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
}