Up to date

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

Shader-Präprozessor

Warum einen Shader-Präprozessor verwenden?

In Programmiersprachen ermöglicht ein Präprozessor die Änderung des Codes, bevor der Compiler ihn liest. Anders als der Compiler kümmert sich der Präprozessor nicht darum, ob die Syntax des vorverarbeiteten Codes gültig ist. Der Präprozessor führt immer das aus, was die Direktiven ihm vorgeben. Eine Direktive ist eine Anweisung, die mit einem Rautezeichen (#) beginnt. Sie ist kein Schlüsselwort der Shader-Sprache (wie if oder for), sondern eine spezielle Art von Token innerhalb der Sprache.

Ab Godot 4.0 können Sie einen Shader-Präprozessor in textbasierten Shadern verwenden. Die Syntax ähnelt dem, was die meisten GLSL-Shader-Compiler unterstützen (was wiederum dem C/C++-Präprozessor ähnelt).

Bemerkung

Der Shader-Präprozessor ist in visuellen Shadern nicht verfügbar. Wenn Sie Präprozessoranweisungen in einen visuellen Shader einfügen müssen, können Sie ihn in einen textbasierten Shader konvertieren, indem Sie die Option In Shader konvertieren im Ressourcen-Dropdown des VisualShader-Inspektors verwenden. Diese Konvertierung ist eine Einweg-Operation; Text-Shader können nicht zurück in visuelle Shader konvertiert werden.

Direktiven

Allgemeine Syntax

  • Präprozessor-Direktiven verwenden keine geschweiften Klammern({}), können aber runde Klammern verwenden.

  • Präprozessor-Direktiven enden nie mit Semikolons (mit Ausnahme von #define, wo dies erlaubt, aber potentiell gefährlich ist).

  • Präprozessor-Direktiven können sich über mehrere Zeilen erstrecken, indem jede Zeile mit einem Backslash (\) abgeschlossen wird. Der erste Zeilenumbruch, der keinen Backslash enthält, beendet die Präprozessor-Direktive.

#define

Syntax: #define <bezeichner> [ersetzungscode].

Definiert den Bezeichner nach dieser Direktive als Makro und ersetzt alle aufeinanderfolgenden Vorkommen durch den im Shader angegebenen Ersetzungscode. Die Ersetzung erfolgt auf der Basis "ganzer Wörter", d.h. es wird keine Ersetzung vorgenommen, wenn der String Teil eines anderen Strings ist (ohne Leerzeichen oder Operatoren als Trennzeichen).

Defines mit Ersetzungen können auch ein oder mehrere Argumente haben, die dann beim Verweis auf das Define übergeben werden können (ähnlich wie bei einem Funktionsaufruf).

Wenn der Ersetzungscode nicht definiert ist, darf der Bezeichner nur mit #ifdef oder #ifndef-Direktiven verwendet werden.

Wenn das Verkettungs-Symbol (##) im Ersetzungscode vorhanden ist, wird es beim Einfügen des Makros zusammen mit einem etwaigen umgebenden Leerzeichen entfernt und die umgebenden Wörter und Argumente zu einem neuen Token zusammengefügt.

uniform sampler2D material0;

#define SAMPLE(N) vec4 tex##N = texture(material##N, UV)

void fragment() {
    SAMPLE(0);
    ALBEDO = tex0.rgb;
}

Im Vergleich zu Konstanten (const CONSTANT = value;) kann #define an jeder beliebigen Stelle innerhalb des Shaders verwendet werden (auch in Uniform-Hints). #define kann auch verwendet werden, um beliebigen Shadercode an jeder beliebigen Stelle einzufügen, während dies bei Konstanten nicht möglich ist.

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
}

Die Definition eines #define für einen Bezeichner, der bereits definiert ist, führt zu einem Fehler. Um dies zu verhindern, verwenden Sie #undef <bezeichner>.

#undef

Syntax: #undef bezeichner

Die #undef-Direktive kann verwendet werden, um eine zuvor definierte #define-Direktive zu beenden:

#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

Ohne #undef im obigen Beispiel würde es einen Fehler bei der Neudefinition des Makros geben.

#if

Syntax: #if <bedingung>

Die #if-Direktive prüft, ob die bedingung erfüllt ist. Wenn sie einen Wert ungleich Null ergibt, wird der Codeblock verwendet, andernfalls wird er übersprungen.

Um korrekt ausgewertet zu werden, muss die Bedingung ein Ausdruck sein, der ein einfaches Float-, Integer- oder boolesches Ergebnis liefert. Es kann mehrere Bedingungsblöcke geben, die mit den Operatoren && (AND) oder || (OR) verbunden sind. Er kann durch einen #else-Block fortgesetzt werden, muss aber mit der Anweisung #endif abgeschlossen werden.

#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

Mit Hilfe der defined() Präprozessorfunktion können Sie überprüfen, ob der übergebene Bezeichner durch das #define definiert ist, das über dieser Direktive steht. Dies ist nützlich, um mehrere Shaderversionen in der gleichen Datei zu erstellen. Sie kann durch einen #else Block fortgesetzt werden, muss aber mit der #endif-Direktive beendet werden.

Das Ergebnis der Funktion defined() kann mit dem vorangestellten Symbol ! (boolesches NOT) negiert werden. Dies kann verwendet werden, um zu prüfen, ob ein Define nicht gesetzt ist.

#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

Seien Sie vorsichtig, da defined() nur einen einzigen Bezeichner in Klammern einschließen darf, niemals mehr:

// 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

Tipp

Im Shader-Editor werden Präprozessor-Zweige, die als falsch ausgewertet werden (und daher vom endgültigen kompilierten Shader ausgeschlossen sind), ausgegraut dargestellt. Dies gilt nicht für if-Anweisungen zur Laufzeit.

#if-Präprozessor vs. if-Anweisung: Performance-Hinweise

Die Shading-Sprache unterstützt if-Anweisungen zur Laufzeit:

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

Wenn das Uniform nie geändert wird, verhält sich dies genauso wie die folgende Verwendung der #if-Präprozessoranweisung:

#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

Die #if-Variante kann jedoch in bestimmten Szenarien schneller sein. Das liegt daran, dass alle Laufzeitverzweigungen in einem Shader immer noch kompiliert werden und die Variablen innerhalb dieser Verzweigungen immer noch Registerplatz belegen können, auch wenn sie in der Praxis nie ausgeführt werden.

Moderne GPUs sind recht effektiv bei der Durchführung von "statischen" Verzweigungen. "Statische" Verzweigungen beziehen sich auf if-Anweisungen, bei denen alle Pixel/Vertices bei einem bestimmten Shader-Aufruf das gleiche Ergebnis liefern. Allerdings können große Mengen von VGPRs (die durch zu viele Verzweigungen verursacht werden können) die Shader-Ausführung immer noch erheblich verlangsamen.

#elif

Die Anweisung #elif steht für "else if" und prüft die Bedingung, die übergeben wird, wenn die obige #if als false ausgewertet wird. #elif kann nur innerhalb eines #if-Blocks verwendet werden. Es ist möglich, mehrere #elif-Anweisungen nach einer #if-Anweisung zu verwenden.

#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

Wie bei #if kann die Präprozessorfunktion defined() verwendet werden:

#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

Syntax: #ifdef <bezeichner>

Dies ist eine Kurzform für #if defined(...). Prüft, ob der übergebene Bezeichner durch ein #define definiert ist, das über dieser Direktive steht. Dies ist nützlich, um mehrere Shaderversionen in der gleichen Datei zu erzeugen. Sie kann durch einen #else Block fortgesetzt werden, muss aber mit der #endif Direktive beendet werden.

#define USE_LIGHT

#ifdef USE_LIGHT
// USE_LIGHT is defined. Include this portion in the final shader.
#endif

Der Prozessor unterstützt nicht #elifdef als Abkürzung für #elif defined(...). Verwenden Sie stattdessen die folgende Reihe von #ifdef und #else, wenn Sie mehr als zwei Verzweigungen benötigen:

#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

Syntax: #ifndef <bezeichner>

Dies ist eine Kurzform für #if !defined(...). Ähnlich wie #ifdef, prüft aber, ob der übergebene Bezeichner nicht durch #define vor dieser Direktive definiert ist.

Dies ist das genaue Gegenteil von #ifdef; es wird immer in Situationen passen, in denen #ifdef niemals passen würde, und andersherum.

#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

Syntax: #else

Definiert den optionalen Block, der eingeschlossen wird, wenn die zuvor definierte #if-, #elif-, #ifdef- oder #ifndef-Direktive als false ausgewertet wird.

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

Syntax: #endif

Wird als Endelement für die #if, #ifdef, #ifndef oder nachfolgende #else-Direktiven verwendet.

#include

Syntax: #include "pfad"

Die #include-Direktive schließt den gesamten Inhalt einer Shader-Include-Datei in einen Shader ein. Der pfad kann ein absoluter res:// Pfad oder relativ zur aktuellen Shaderdatei sein. Relative Pfade sind nur in Shadern erlaubt, die in .gdshader oder .gdshaderinc Dateien gespeichert sind, während absolute Pfade in Shadern verwendet werden können, die in eine Szene/Ressourcendatei eingebaut sind.

Sie können neue Shader-Includes erstellen, indem Sie die Menüoption Datei > Shader-Include erstellen des Shader-Editors verwenden oder indem Sie eine neue ShaderInclude-Ressource im FileSystem-Dock erstellen.

Shader-Includes können von jedem Shader oder einem anderen Shader-Include aus an jeder beliebigen Stelle der Datei eingefügt werden.

Wenn Sie Shader-Includes in den globalen Bereich eines Shaders aufnehmen, wird empfohlen, dies nach der ersten shader_type-Anweisung zu tun.

Sie können Shader-Includes auch innerhalb eines Funktionsrumpfs einbinden. Bitte beachten Sie, dass der Shader-Editor wahrscheinlich Fehler für den Code Ihres Shader-Includes melden wird, da er außerhalb des Kontextes, für den er geschrieben wurde, nicht gültig sein könnte. Sie können diese Fehler entweder ignorieren (der Shader wird trotzdem kompiliert), oder Sie können das Include in einen #ifdef-Block einbinden, der auf eine Definition des Shaders prüft.

#include ist nützlich, um Bibliotheken von Hilfsfunktionen (oder Makros) zu erstellen und Code-Duplikation zu reduzieren. Bei der Verwendung von #include ist auf Namenskollisionen zu achten, da das Umdefinieren von Funktionen oder Makros nicht erlaubt ist.

#include unterliegt mehreren Einschränkungen:

  • Nur Shader-Include-Ressourcen (mit der Endung .gdshaderinc) können eingebunden werden. .gdshader-Dateien können nicht von einem anderen Shader eingebunden werden, aber eine .gdshaderinc-Datei kann andere .gdshaderinc-Dateien einbinden.

  • Zyklische Abhängigkeiten sind nicht erlaubt und führen zu einem Fehler.

  • Um eine unendliche Rekursion zu vermeiden, ist die Inklusionstiefe auf 25 Schritte begrenzt.

Beispiel für eine Shader-Include-Datei:

// 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);
}

Beispiel für einen Basisshader (unter Verwendung der oben erstellten Include-Datei):

// 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

Syntax: #pragma wert

Die #pragma-Direktive liefert zusätzliche Informationen an den Präprozessor oder Compiler.

Derzeit kann sie nur einen Wert haben: disable_preprocessor. Wenn Sie den Präprozessor nicht benötigen, verwenden Sie diese Direktive, um die Shader-Kompilierung zu beschleunigen, indem Sie den Präprozessorschritt weglassen.

#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