着色器预处理器

为什么要使用着色器预处理器?

In programming languages, a preprocessor allows changing the code before the compiler reads it. Unlike the compiler, the preprocessor does not care about whether the syntax of the preprocessed code is valid. The preprocessor always performs what the directives tell it to do. A directive is a statement starting with a hash symbol (#). It is not a keyword of the shader language (such as if or for), but a special kind of token within the language.

From Godot 4.0 onwards, you can use a shader preprocessor within text-based shaders. The syntax is similar to what most GLSL shader compilers support (which in turn is similar to the C/C++ preprocessor).

备注

The shader preprocessor is not available in visual shaders. If you need to introduce preprocessor statements to a visual shader, you can convert it to a text-based shader using the Convert to Shader option in the VisualShader inspector resource dropdown. This conversion is a one-way operation; text shaders cannot be converted back to visual shaders.

指令

常规语法

  • 预处理器指令不使用大括号({}),但会用到括号。

  • 预处理器指令从不以分号结尾(除非是 #define,允许这么做,但是可能比较危险)。

  • Preprocessor directives can span several lines by ending each line with a backslash (\). The first line break not featuring a backslash will end the preprocessor statement.

#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).

Defines with replacements may also have one or more arguments, which can then be passed when referencing the define (similar to a function call).

If the replacement code is not defined, the identifier may only be used with #ifdef or #ifndef directives.

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.

  1. uniform sampler2D material0;
  2. #define SAMPLE(N) vec4 tex##N = texture(material##N, UV)
  3. void fragment() {
  4. SAMPLE(0);
  5. ALBEDO = tex0.rgb;
  6. }

Compared to constants (const CONSTANT = value;), #define can be used anywhere within the shader (including in uniform hints). #define can also be used to insert arbitrary shader code at any location, while constants can’t do that.

  1. shader_type spatial;
  2. // Notice the lack of semicolon at the end of the line, as the replacement text
  3. // shouldn't insert a semicolon on its own.
  4. // If the directive ends with a semicolon, the semicolon is inserted in every usage
  5. // of the directive, even when this causes a syntax error.
  6. #define USE_MY_COLOR
  7. #define MY_COLOR vec3(1, 0, 0)
  8. // Replacement with arguments.
  9. // All arguments are required (no default values can be provided).
  10. #define BRIGHTEN_COLOR(r, g, b) vec3(r + 0.5, g + 0.5, b + 0.5)
  11. // Multiline replacement using backslashes for continuation:
  12. #define SAMPLE(param1, param2, param3, param4) long_function_call( \
  13. param1, \
  14. param2, \
  15. param3, \
  16. param4 \
  17. )
  18. void fragment() {
  19. #ifdef USE_MY_COLOR
  20. ALBEDO = MY_COLOR;
  21. #endif
  22. }

Defining a #define for an identifier that is already defined results in an error. To prevent this, use #undef <identifier>.

#undef

语法:#undef 标识符

The #undef directive may be used to cancel a previously defined #define directive:

  1. #define MY_COLOR vec3(1, 0, 0)
  2. vec3 get_red_color() {
  3. return MY_COLOR;
  4. }
  5. #undef MY_COLOR
  6. #define MY_COLOR vec3(0, 1, 0)
  7. vec3 get_green_color() {
  8. return MY_COLOR;
  9. }
  10. // Like in most preprocessors, undefining a define that was not previously defined is allowed
  11. // (and won't print any warning or error).
  12. #undef THIS_DOES_NOT_EXIST

Without #undef in the above example, there would be a macro redefinition error.

#if

语法:#if <条件>

The #if directive checks whether the condition passed. If it evaluates to a non-zero value, the code block is included, otherwise it is skipped.

To evaluate correctly, the condition must be an expression giving a simple floating-point, integer or boolean result. There may be multiple condition blocks connected by && (AND) or || (OR) operators. It may be continued by a #else block, but must be ended with the #endif directive.

  1. #define VAR 3
  2. #define USE_LIGHT 0 // Evaluates to `false`.
  3. #define USE_COLOR 1 // Evaluates to `true`.
  4. #if VAR == 3 && (USE_LIGHT || USE_COLOR)
  5. // Condition is `true`. Include this portion in the final shader.
  6. #endif

Using the defined() preprocessor function, you can check whether the passed identifier is defined a by #define placed above that directive. This is useful for creating multiple shader versions in the same file. It may be continued by a #else block, but must be ended with the #endif directive.

The defined() function’s result can be negated by using the ! (boolean NOT) symbol in front of it. This can be used to check whether a define is not set.

  1. #define USE_LIGHT
  2. #define USE_COLOR
  3. // Correct syntax:
  4. #if defined(USE_LIGHT) || defined(USE_COLOR) || !defined(USE_REFRACTION)
  5. // Condition is `true`. Include this portion in the final shader.
  6. #endif

Be careful, as defined() must only wrap a single identifier within parentheses, never more:

  1. // Incorrect syntax (parentheses are not placed where they should be):
  2. #if defined(USE_LIGHT || USE_COLOR || !USE_REFRACTION)
  3. // This will cause an error or not behave as expected.
  4. #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 run-time if statements.

#if 预处理器与 if 语句:性能注意事项

着色语言支持运行时 if 语句:

  1. uniform bool USE_LIGHT = true;
  2. if (USE_LIGHT) {
  3. // This part is included in the compiled shader, and always run.
  4. } else {
  5. // This part is included in the compiled shader, but never run.
  6. }

如果 uniform 从未改变,那么行为和下面的 #if 预处理语句用法是等价的:

  1. #define USE_LIGHT
  2. #if defined(USE_LIGHT)
  3. // This part is included in the compiled shader, and always run.
  4. #else
  5. // This part is *not* included in the compiled shader (and therefore never run).
  6. #endif

不过部分场合 #if 的版本会更快一些。这是因为着色器中的运行时分支仍然是会参与编译的,即便运行时不会用到,这些分支中的变量仍然可能会占据寄存器空间。

现代 GPU 在执行“静态”分支时相当高效。这里的“静态”分支指的是在一次给定的着色器调用中,对所有像素/顶点都求得相同结果的 if 语句。不过大量的 VGPR(分支过多就可能造成这种情况)仍然会显著拖慢着色器的运行。

#elif

#elif 指令就是“else if”的意思,会在之前的 #if 求得 false 时检查条件是否成立。#elif 只能在 #if 块中使用。一个 #if 语句后面可以使用多个 #elif

  1. #define VAR 2
  2. #if VAR == 0
  3. // Not included.
  4. #elif VAR == 1
  5. // Not included.
  6. #elif VAR == 2
  7. // Condition is `true`. Include this portion in the final shader.
  8. #else
  9. // Not included.
  10. #endif

可以和 #if 一样使用预处理器函数 defined()

  1. #define SHADOW_QUALITY_MEDIUM
  2. #if defined(SHADOW_QUALITY_HIGH)
  3. // High shadow quality.
  4. #elif defined(SHADOW_QUALITY_MEDIUM)
  5. // Medium shadow quality.
  6. #else
  7. // Low shadow quality.
  8. #endif

#ifdef

语法:#ifdef <标识符>

This is a shorthand for #if defined(...). Checks whether the passed identifier is defined by #define placed above that directive. This is useful for creating multiple shader versions in the same file. It may be continued by a #else block, but must be ended with the #endif directive.

  1. #define USE_LIGHT
  2. #ifdef USE_LIGHT
  3. // USE_LIGHT is defined. Include this portion in the final shader.
  4. #endif

The processor does not support #elifdef as a shortcut for #elif defined(...). Instead, use the following series of #ifdef and #else when you need more than two branches:

  1. #define SHADOW_QUALITY_MEDIUM
  2. #ifdef SHADOW_QUALITY_HIGH
  3. // High shadow quality.
  4. #else
  5. #ifdef SHADOW_QUALITY_MEDIUM
  6. // Medium shadow quality.
  7. #else
  8. // Low shadow quality.
  9. #endif // This ends `SHADOW_QUALITY_MEDIUM`'s branch.
  10. #endif // This ends `SHADOW_QUALITY_HIGH`'s branch.

#ifndef

语法:#ifndef <标识符>

This is a shorthand for #if !defined(...). Similar to #ifdef, but checks whether the passed identifier is not defined by #define before that directive.

This is the exact opposite of #ifdef; it will always match in situations where #ifdef would never match, and vice versa.

  1. #define USE_LIGHT
  2. #ifndef USE_LIGHT
  3. // Evaluates to `false`. This portion won't be included in the final shader.
  4. #endif
  5. #ifndef USE_COLOR
  6. // Evaluates to `true`. This portion will be included in the final shader.
  7. #endif

#else

语法:#else

Defines the optional block which is included when the previously defined #if, #elif, #ifdef or #ifndef directive evaluates to false.

  1. shader_type spatial;
  2. #define MY_COLOR vec3(1.0, 0, 0)
  3. void fragment() {
  4. #ifdef MY_COLOR
  5. ALBEDO = MY_COLOR;
  6. #else
  7. ALBEDO = vec3(0, 0, 1.0);
  8. #endif
  9. }

#endif

语法:#endif

Used as terminator for the #if, #ifdef, #ifndef or subsequent #else directives.

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

示例着色器头文件:

  1. // fancy_color.gdshaderinc
  2. // While technically allowed, there is usually no `shader_type` declaration in include files.
  3. vec3 get_fancy_color() {
  4. return vec3(0.3, 0.6, 0.9);
  5. }

Example base shader (using the include file we created above):

  1. // material.gdshader
  2. shader_type spatial;
  3. #include "res://fancy_color.gdshaderinc"
  4. void fragment() {
  5. // No error, as we've included a definition for `get_fancy_color()` via the shader include.
  6. COLOR = get_fancy_color();
  7. }

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

  1. #pragma disable_preprocessor
  2. #if USE_LIGHT
  3. // This causes a shader compilation error, as the `#if USE_LIGHT` and `#endif`
  4. // are included as-is in the final shader code.
  5. #endif