着色器预处理器
为什么要使用着色器预处理器?
编程语言中,预处理器的作用是在编译器读取代码前对代码进行修改。与编译器不同,预处理器并不关心被处理代码的语法是否有效。预处理器会一丝不苟地执行每一条指令。指令是以井号(#
)开头的语句。这并不是着色器语言中的关键字(例如 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
指令检查 condition
是否成立。若结果为非零值,则代码块被包含;否则,代码块被跳过。
为保证计算正确,条件必须是能够得出简单浮点数、整数或布尔值的表达式。允许用 &&
(逻辑与)及 ||
(逻辑或)两种运算符连接多个条件块。后续可接一个 #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()
函数的结果前面加 !
(逻辑非)来取反。可借此检查是否未设置某个定义。
#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
小技巧
In the shader editor, preprocessor branches that evaluate to false
(and
are therefore excluded from the final compiled shader) will appear grayed
out. This does not apply to runtime if
statements.
#if 预处理器与 if 语句:性能注意事项
The shading language supports runtime if
statements:
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
However, the #if
variant can be faster in certain scenarios. This is because
all runtime branches in a shader are still compiled and variables within
those branches may still take up register space, even if they are never run in
practice.
现代 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
定义。可用于在同一文件中创建多个不同版本的着色器。可以由 #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
Defines the optional block which is included when the previously defined #if
,
#elif
, #ifdef
or #ifndef
directive evaluates to 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
Syntax: #error <message>
The #error
directive forces the preprocessor to emit an error with optional message.
For example, it's useful when used within #if
block to provide a strict limitation of the
defined value.
#define MAX_LOD 3
#define LOD 4
#if LOD > MAX_LOD
#error LOD exceeds MAX_LOD
#endif
#include
语法:#include "路径"
The #include
directive includes the entire content of a shader include
file in a shader. "path"
can be an absolute res://
path or relative to
the current shader file. Relative paths are only allowed in shaders that are
saved to .gdshader
or .gdshaderinc
files, while absolute paths can be
used in shaders that are built into a scene/resource file.
You can create new shader includes by using the File > Create Shader Include menu option of the shader editor, or by creating a new ShaderInclude resource in the FileSystem dock.
Shader includes can be included from within any shader, or other shader include, at any point in the file.
When including shader includes in the global scope of a shader, it is recommended
to do this after the initial shader_type
statement.
You can also include shader includes from within the body a function. Please note that
the shader editor is likely going to report errors for your shader include's code, as it
may not be valid outside of the context that it was written for. You can either choose
to ignore these errors (the shader will still compile fine), or you can wrap the include
in an #ifdef
block that checks for a define from your shader.
#include
is useful for creating libraries of helper functions (or macros)
and reducing code duplication. When using #include
, be careful about naming
collisions, as redefining functions or macros is not allowed.
#include
is subject to several restrictions:
Only shader include resources (ending with
.gdshaderinc
) can be included..gdshader
files cannot be included by another shader, but a.gdshaderinc
file can include other.gdshaderinc
files.Cyclic dependencies are not allowed and will result in an error.
To avoid infinite recursion, include depth is limited to 25 steps.
示例着色器头文件:
// 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);
}
Example base shader (using the include file we created above):
// 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 值
The #pragma
directive provides additional information to the preprocessor or compiler.
Currently, it may have only one value: disable_preprocessor
. If you don't need
the preprocessor, use that directive to speed up shader compilation by excluding
the preprocessor step.
#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
Built-in defines
Current renderer
Since Godot 4.4, you can check which renderer is currently used with the built-in
defines CURRENT_RENDERER
, RENDERER_COMPATIBILITY
, RENDERER_MOBILE
,
and RENDERER_FORWARD_PLUS
:
CURRENT_RENDERER
is set to either0
,1
, or2
depending on the current renderer.RENDERER_COMPATIBILITY
is always0
.RENDERER_MOBILE
is always1
.RENDERER_FORWARD_PLUS
is always2
.
As an example, this shader sets ALBEDO
to a different color in each renderer:
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
}