Up to date

This page is up to date for Godot 4.2. If you still find outdated information, please open an issue.

Шейдерный препроцессор

Зачем нужен шейдерный препроцессор?

В языках программирования препроцессор позволяет изменить код до того, как его прочитает компилятор. В отличие от компилятора, препроцессор не заботится о том, является ли синтаксис препроцессированного кода правильным. Препроцессор всегда выполняет то, что ему предписывают директивы. Директива - это утверждение, начинающееся с хэш-символа (#). Это не ключевое слово языка шейдеров (такое как 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

Совет

В редакторе шейдеров ветви препроцессора, которые оцениваются как false (и поэтому исключаются из окончательно скомпилированного шейдера), будут отображаться серым цветом. Это не относится к операторам ``if'' во время выполнения.

Препроцессор #if в сравнении с оператором if: Предостережения по производительности

Язык шейдеров поддерживает операторы 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.

#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