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.

Preprocessore di shader

Perché utilizzare un preprocessore di shader?

Nei linguaggi di programmazione, un preprocessore consente di modificare il codice prima che il compilatore lo legga. A differenza del compilatore, al preprocessore non importa se la sintassi del codice pre-elaborato sia valida. Il preprocessore esegue sempre ciò che le direttive gli dicono di fare. Una direttiva è un'istruzione che inizia con un simbolo cancelletto (#). Non è una parola chiave del linguaggio shader (come if o for), ma un tipo speciale di token all'interno del linguaggio.

Per evitare ripetizioni e riciclare più codice, è possibile utilizzare un preprocessore di shader all'interno di shader testuali. La sintassi è simile a quella supportata dalla maggior parte dei compilatori di shader GLSL (che a sua volta è simile al preprocessore C/C++).

Nota

Il preprocessore di shader non è disponibile nei visual shader. Se è necessario introdurre istruzioni di preprocessore in un shader visuale, è possibile convertirlo in uno shader testuale tramite l'opzione Converti in shader nel menu a discesa delle risorse dell'ispettore in VisualShader. Questa conversione è un'operazione unidirezionale; non è possibile riconvertire gli shader testuali in shader visuali.

Direttive

Sintassi generale

  • Le direttive di preprocessore non usano parentesi quadre ({}), ma possono usare le parentesi tonde.

  • Le direttive di preprocessore non terminano mai con un punto e virgola (ad eccezione di #define, dove questo è consentito ma potenzialmente pericoloso).

  • Le direttive di preprocessore possono estendersi su più righe, terminando ciascuna riga con una barra rovesciata (\). La prima interruzione di riga senza barra rovesciata terminerà l'istruzione di preprocessore.

#define

Sintassi: #define <identificatore> [codice_sostitutivo].

Definisce l'identificatore dopo tale direttiva come macro e sostituisce tutte le sue occorrenze successive con il codice di sostituzione specificato nello shader. La sostituzione è eseguita per "parole intere", il che significa che nessuna sostituzione è eseguita se la stringa fa parte di un'altra stringa (senza spazi o operatori che la separano).

Le definizioni con sostituzioni possono anche avere uno o più argomenti, che si possono passare quando si referenzia la definizione (simile a una chiamata di una funzione).

Se il codice sostitutivo non è definito, l'identificatore può essere utilizzato solo con le direttive #ifdef o #ifndef.

Se nel codice sostitutivo è presente il simbolo di concatenazione (##), verrà rimosso al momento dell'inserimento della macro, insieme a qualsiasi spazio circostante, e le parole e gli argomenti circostanti verranno uniti in un nuovo token.

uniform sampler2D material0;

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

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

Rispetto alle costanti (const CONSTANT = value;), #define si può utilizzare ovunque all'interno dello shader (anche nei suggerimenti di uniformi). È possibile utilizzare #define anche per inserire codice arbitrario in qualsiasi posizione, mentre le costanti non possono farlo.

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
}

Definire #define per un identificatore già definito genera un errore. Per evitarlo, usare #undef <identificatore>.

#undef

Sintassi: #undef identifier

La direttiva #undef può servire ad annullare una direttiva #define definita in precedenza:

#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

Senza #undef nell'esempio precedente, si verificherebbe un errore di ridefinizione della macro.

#if

Sintassi: #if <conditione>

La direttiva #if verifica se la condizione è stata soddisfatta. Se restituisce un valore diverso da zero, il blocco di codice viene incluso, altrimenti viene saltato.

Per valutare corretta, la condizione deve essere un'espressione che restituisce un semplice risultato in virgola mobile, intero o booleano. Ci possono essere più blocchi di condizioni collegati dagli operatori && (AND) o || (OR). Può essere continuata da un blocco #else, ma deve essere terminata con la direttiva #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

Attraverso la funzione di preprocessore defined(), è possibile verificare se l'identificatore passato è definito da #define posto sopra la direttiva. Questo è utile per creare più versioni dello shader nello stesso file. Può essere continuato da un blocco #else, ma deve essere terminato con la direttiva #endif.

Il risultato della funzione defined() può essere negato prefissandolo con il simbolo ! (NOT booleano). Questo può servire per verificare se una definizione non è impostata.

#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

Fare attenzione, perché defined() deve racchiudere solo un singolo identificatore tra parentesi, mai di più:

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

Suggerimento

Nell'editor di shader, i rami del preprocessore che restituiscono false (e sono quindi esclusi dallo shader compilato finale) appariranno in grigio. Questo non si applica alle istruzioni if in fase di esecuzione.

Preprocessore #if contro istruzione if: avvertenze sulle prestazioni

Il linguaggio di shading supporta le istruzioni if in fase di esecuzione:

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

Se l'uniforme non viene mai modificata, questo si comporta in modo identico al seguente utilizzo dell'istruzione di preprocessore #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

Tuttavia, la versione #if può essere più veloce in certi scenari. Questo perché tutti i rami in fase di esecuzione in uno shader sono comunque compilati e le variabili al loro interno potrebbero comunque occupare spazio nei registri, anche se non vengono mai eseguite nella pratica.

Le GPU moderne sono piuttosto efficaci nell'eseguire "static branching". Lo "static branching" si riferisce a istruzioni if in cui tutti i pixel/vertici restituiscono lo stesso risultato in una determinata invocazione dello shader. Tuttavia, un numero elevato di VGPR (possibilmente causato da un numero eccessivo di rami) può comunque rallentare significativamente l'esecuzione dello shader.

#elif

La direttiva #elif sta per "else if" e controlla la condizione passata se l'istruzione #if sopra riportata restituisce false. #elif si può utilizzare solo all'interno di un blocco #if. È possibile utilizzare più istruzioni #elif dopo un'istruzione #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

Come con #if, è possibile utilizzare la funzione di preprocessore 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

Sintassi: #ifdef <identifier>

Questa è una forma abbreviata di #if defined(...). Verifica se l'identificatore passato è definito da #define posto prima di tale direttiva. Questo è utile per creare più versioni dello shader nello stesso file. Può essere continuato da un blocco #else, ma deve essere terminato con la direttiva #endif.

#define USE_LIGHT

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

Il processore non supporta #elifdef come scorciatoia per #elif defined(...). Invece, usare la seguente serie di #ifdef e #else quando c'è bisogno di più di due rami:

#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

Sintassi: #ifndef <identifier>

Questa è una forma abbreviata di #if !defined(...). Simile a #ifdef, ma verifica se l'identificatore passato non è definito da #define prima di tale direttiva.

Questo è l'esatto opposto di #ifdef; corrisponderà sempre in situazioni in cui #ifdef non corrisponderebbe mai, e viceversa.

#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

Sintassi: #else

Definisce il blocco facoltativo che viene incluso quando la direttiva #if, #elif, #ifdef o #ifndef definita in precedenza viene valutata come falsa.

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

Sintassi: #endif

Utilizzato come terminatore per le direttive #if, #ifdef, #ifndef o successive #else.

#error

Sintassi: #error <message>

La direttiva #error forza il preprocessore a generare un errore con un messaggio opzionale. Ad esempio, è utile se utilizzata all'interno di un block #if per fornire una limitazione rigorosa del valore definito.

#define MAX_LOD 3
#define LOD 4

#if LOD > MAX_LOD
#error LOD exceeds MAX_LOD
#endif

#include

Sintassi: #include "path"

La direttiva #include include l'intero contenuto di un file di inclusione shader in uno shader. "path" può essere un percorso assoluto res:// o relativo al file shader attuale. I percorsi relativi sono consentiti solo negli shader salvati nei file .gdshader o .gdshaderinc, mentre i percorsi assoluti si possono utilizzare negli shader integrati in un file di scena/risorsa.

È possibile creare nuove inclusioni di shader tramite l'opzione di menu File > Crea inclusione shader nell'editor di shader oppure creando una nuova risorsa ShaderInclude nel pannello FileSystem.

Le inclusioni di shader possono essere incluse dall'interno di qualsiasi shader o da altre inclusioni di shader, in qualsiasi punto nel file.

Quando si includono le inclusioni di shader nell'ambito globale di uno shader, si consiglia di farlo dopo l'istruzione shader_type iniziale.

È anche possibile includere le inclusioni di shader dentro il corpo di una funzione. Si noti che l'editor di shader probabilmente segnalerà errori per il codice dell'inclusione, poiché potrebbe non essere valido fuori dal contesto per cui è stato scritto. È possibile scegliere di ignorare questi errori (lo shader verrà comunque compilato correttamente) oppure racchiudere l'inclusione in un blocco #ifdef che verifica la presenza di una definizione dallo shader.

#include è utile per creare librerie di funzioni ausiliari (o macro) e ridurre la duplicazione del codice. Quando si utilizza #include, fare attenzione alle collisioni tra i nomi, poiché non è consentito ridefinire le funzioni o le macro.

#include è soggetto a diverse restrizioni:

  • È possibile includere solo le risorse di tipo inclusione di shader (che terminano con .gdshaderinc). I file .gdshader non possono essere inclusi da un altro shader, ma un file .gdshaderinc può includere altri file .gdshaderinc.

  • Le dipendenze cicliche non sono consentite e genereranno un errore.

  • Per evitare una ricorsione infinita, la profondità di inclusione è limitata a 25 passaggi.

Esempio di file di inclusione di shader:

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

Esempio di shader di base (utilizzando il file di inclusione creato in precedenza):

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

Sintassi: #pragma value

La direttiva #pragma fornisce ulteriori informazioni al preprocessore o al compilatore.

Attualmente, può avere un solo valore: disable_preprocessor. Se non c'è bisogno del preprocessore, utilizzare questa direttiva accelera la compilazione dello shader, escludendo il passaggio del preprocessore.

#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

Definizioni integrate

Renderer attuale

A partire da Godot 4.4, è possibile verificare quale renderer è attualmente utilizzato con le definizioni integrate CURRENT_RENDERER, RENDERER_COMPATIBILITY, RENDERER_MOBILE e RENDERER_FORWARD_PLUS:

  • CURRENT_RENDERER è impostato su 0, 1 o 2 a seconda del renderer attuale.

  • RENDERER_COMPATIBILITY è sempre 0.

  • RENDERER_MOBILE è sempre 1.

  • RENDERER_FORWARD_PLUS è sempre 2.

Ad esempio, questo shader imposta ALBEDO su un colore diverso in ogni 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
}