Shader preprocessor¶
Why use a shader preprocessor?¶
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).
Note
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.
Directives¶
General syntax¶
Preprocessor directives do not use brackets (
{}
), but can use parentheses.Preprocessor directives never end with semicolons (with the exception of
#define
, where this is allowed but potentially dangerous).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¶
Syntax: #define <identifier> [replacement_code]
.
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 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.
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.
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
}
Defining a #define
for an identifier that is already defined results in an
error. To prevent this, use #undef <identifier>
.
#undef¶
Syntax: #undef identifier
The #undef
directive may be used to cancel a previously defined #define
directive:
#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
Without #undef
in the above example, there would be a macro redefinition error.
#if¶
Syntax: #if <condition>
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.
#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
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.
#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
Be careful, as defined()
must only wrap a single identifier within parentheses, never more:
// 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
Tip
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 preprocessor versus if statement: Performance caveats
The shading language supports run-time 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.
}
If the uniform is never changed, this behaves identical to the following usage
of the #if
preprocessor statement:
#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 run-time 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.
Modern GPUs are quite effective
at performing "static" branching. "Static" branching refers to if
statements where
all pixels/vertices evaluate to the same result in a given shader invocation. However,
high amounts of VGPRs (which can be caused by
having too many branches) can still slow down shader execution significantly.
#elif¶
The #elif
directive stands for "else if" and checks the condition passed if
the above #if
evaluated to fals