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
), а особый вид лексем внутри языка.
Начиная с Godot 4.0, вы можете использовать шейдерный препроцессор в текстовых шейдерах. Синтаксис похож на тот, который поддерживают большинство компиляторов шейдеров GLSL (которые, в свою очередь, похожи на препроцессор C/C++).
Примечание
Препроцессор шейдеров недоступен в визуальных шейдерах. Если вам нужно добавить операторы препроцессора в визуальный шейдер, вы можете преобразовать его в текстовый шейдер с помощью опции Convert to Shader в выпадающем списке ресурсов инспектора VisualShader. Это преобразование является односторонней операцией; текстовые шейдеры не могут быть преобразованы обратно в визуальные шейдеры.
Директивы
Общий синтаксис
Директивы препроцессора не используют фигурные скобки (
{}
), но могут использовать круглые скобки.Директивы препроцессора никогда не заканчиваются точкой с запятой (за исключением
#define
, где это разрешено, но потенциально опасно).Директивы препроцессора могут занимать несколько строк, завершая каждую строку обратной косой чертой (
\
). Первый разрыв строки, не содержащий обратную косую черту, завершает операцию препроцессора.
#define
Синтаксис: #define <identifier> [заменяемый_код]
.
Определяет идентификатор после этой директивы как макрос и заменяет все последующие его вхождения на код замены, указанный в шейдере. Замена выполняется по принципу "целых слов", то есть замена не выполняется, если строка является частью другой строки (без пробелов и операторов, разделяющих её).
Определения с заменой могут также иметь один или несколько аргументов, которые могут передаваться при обращении к определению (подобно вызову функции).
Если код замены не определён, идентификатор можно использовать только с директивами #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
можно использовать в любом месте шейдера (в том числе в унифицированных подсказках). #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 <identifier>
.
#undef
Синтаксис: #undef identifier
Директива #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
проверяет, прошло ли условие
. Если оно оценивается в ненулевое значение, блок кода включается, в противном случае он пропускается.
Для правильной оценки условие должно быть выражением, дающим простой результат с плавающей точкой, целым числом или логическим значением. Может быть несколько блоков условия, связанных операторами &&
(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
, расположенной над этой директивой. Это полезно для создания нескольких версий шейдеров в одном файле. Она может быть продолжена блоком #else
, но должна быть завершена директивой #endif
.
Результат функции defined()
можно отрицать, используя перед ним символ !
(логическое НЕТ). Это можно использовать для проверки того, что define не задан.
#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()
должен заключать в круглые скобки только один идентификатор, и никогда больше:
// 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.
Современные графические процессоры достаточно эффективно выполняют "статическое" ветвление. Под "статическим" ветвлением подразумеваются операторы if
, в которых все пиксели/вершины вычисляются одинаково при данном вызове шейдера. Однако большое количество VGPRs (которое может быть вызвано слишком большим количеством ветвлений) всё ещё может значительно замедлить выполнение шейдера.
#elif
Директива #elif
расшифровывается как "else if" и проверяет условие, переданное, если вышеприведенное #if
равняется false
. Директива #elif
может использоваться только внутри блока #if
. Можно использовать несколько операторов #elif
после оператора #if
.
#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 <identifier>
Это сокращение для #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
Определяет необязательный блок, который включается, когда ранее определённая директива #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
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 "путь"
Директива #include
включает полное содержимое включаемого файла шейдера в шейдер. путь
может быть абсолютным res://
или относительным к текущему файлу шейдера. Относительные пути допустимы только в шейдерах, которые сохраняются в файлах .gdshader
или .gdshaderinc
, в то время как абсолютные пути могут использоваться в шейдерах, встроенных в файл сцены/ресурса.
Вы можете создавать новые шейдерные включения, используя опцию меню File > Create Shader Include редактора шейдеров или создав новый ресурс ShaderInclude в панели FileSystem.
Шейдерные включения могут быть включены в любой шейдер или другой шейдерный элемент в любой точке файла.
При включении шейдерных инклудов в глобальную область видимости шейдера рекомендуется делать это после начального оператора shader_type
.
Вы также можете включать шейдерные включения в тело функции. Обратите внимание, что редактор шейдеров, скорее всего, сообщит об ошибках для кода вашего шейдерного включения, поскольку он может быть недействительным вне контекста, для которого он был написан. Вы можете либо проигнорировать эти ошибки (шейдер всё равно скомпилируется), либо обернуть include в блок #ifdef
, который будет проверять наличие define в шейдере.
#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);
}
Пример базового шейдера (с использованием файла include, который мы создали выше):
// 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
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
}