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.
Checking the stable version of the documentation...
着色器预处理器
为什么要使用着色器预处理器?
编程语言中,预处理器的作用是在编译器读取代码前对代码进行修改。与编译器不同,预处理器并不关心被处理代码的语法是否有效。预处理器会一丝不苟地执行每一条指令。指令是以井号(#)开头的语句。这并不是着色器语言中的关键字(例如 if 和 for),而是语言中的一种特殊标记。
为了避免重复编写代码并提高代码的复用率,你可以在基于文本的着色器中使用着色器预处理器。它的语法与大多数 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_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
}