Шейдерный препроцессор
Зачем нужен шейдерный препроцессор?
В языках программирования препроцессор позволяет изменить код до того, как его прочитает компилятор. В отличие от компилятора, препроцессор не заботится о том, является ли синтаксис препроцессированного кода правильным. Препроцессор всегда выполняет то, что ему предписывают директивы. Директива - это утверждение, начинающееся с хэш-символа (#). Это не ключевое слово языка шейдеров (такое как if или for), а особый вид лексем внутри языка.
To avoid repetition and improve code reuse, 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).
Примечание
Препроцессор шейдеров недоступен в визуальных шейдерах. Если вам нужно добавить операторы препроцессора в визуальный шейдер, вы можете преобразовать его в текстовый шейдер с помощью опции 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 проверяет, прошло ли условие. Если оно оценивается в ненулевое значение, блок кода включается, в противном случае он пропускается.
Для корректного вычисления условие должно быть выражением, возвращающим простой результат с плавающей точкой, целочисленный или логический. Условие может содержать несколько блоков, соединённых операторами && (И) или || (ИЛИ). Блок может быть продолжен блоком #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
Совет
В редакторе шейдеров ветви препроцессора, дающие результат 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 может быть быстрее в некоторых сценариях. Это связано с тем, что все ветви выполнения шейдера по-прежнему компилируются, и переменные в этих ветвях могут занимать место в регистре, даже если они никогда не выполняются на практике.
Современные графические процессоры достаточно эффективно выполняют "статическое" ветвление. Под "статическим" ветвлением подразумеваются операторы 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>
Директива #error заставляет препроцессор выдавать ошибку с необязательным сообщением. Например, она полезна при использовании в блоке #if для строгого ограничения определяемого значения.
#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
Встроенные определения
Текущий рендерер
Начиная с 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
}