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.

Pré-processador de shader

Por que usar um pré-processador de shader?

Em linguagens de programação, um pré-processador permite alterar o código antes que o compilador o leia. Diferente do compilador, o pré-processador não se importa se a sintaxe do código pré-processado é válida. O pré-processador sempre executa o que as diretivas mandam fazer. Uma diretiva é uma instrução que começa com o símbolo de cerquilha (#). Ela não é uma palavra-chave da linguagem de shader (como if ou for), mas sim um tipo especial de token dentro da linguagem.

Para evitar repetição e melhorar o reaproveitamento de código, você pode usar um pré-processador de shader dentro de shaders baseados em texto. A sintaxe é semelhante à que a maioria dos compiladores de shader GLSL suporta (que por sua vez é semelhante ao pré-processador C/C++).

Nota

O pré-processador de shader não está disponível em visual shaders. Se precisar introduzir instruções de pré-processador em um visual shader, você pode convertê-lo em um shader baseado em texto usando a opção Convert to Shader no menu suspenso do recurso VisualShader no inspetor. Essa conversão é uma operação de via única; shaders de texto não podem ser convertidos de volta para visual shaders.

Diretivas

Sintaxe geral

  • Diretivas de pré-processador não usam chaves ({}), mas podem usar parênteses.

  • Diretivas de pré-processador nunca terminam com ponto e vírgula (com exceção do #define, onde isso é permitido mas potencialmente perigoso).

  • Diretivas de pré-processador podem se estender por várias linhas terminando cada linha com uma barra invertida (\). A primeira quebra de linha que não apresentar uma barra invertida encerrará a instrução do pré-processador.

#define

Sintaxe: #define <identificador> [codigo_de_substituicao].

Define o identificador após essa diretiva como uma macro e substitui todas as ocorrências sucessivas dele pelo código de substituição fornecido no shader. A substituição é realizada com base em "palavras inteiras", o que significa que nenhuma substituição é feita se a string for parte de outra string (sem espaços ou operadores a separando).

Definições com substituições também podem ter um ou mais argumentos, que podem ser passados ao fazer referência ao define (semelhante a uma chamada de função).

Se o código de substituição não for definido, o identificador só poderá ser usado com as diretivas #ifdef ou #ifndef.

Se o símbolo de concatenação (##) estiver presente no código de substituição, ele será removido no momento da inserção da macro, junto com qualquer espaço ao seu redor, e unirá as palavras e argumentos adjacentes em um novo token.

uniform sampler2D material0;

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

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

Em comparação com as constantes (const CONSTANTE = valor;), o #define pode ser usado em qualquer lugar dentro do shader (inclusive em dicas de uniform). O #define também pode ser usado para inserir códigos arbitrários de shader em qualquer local, enquanto as constantes não podem fazer isso.

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
}

Definir um #define para um identificador que já está definido resulta em um erro. Para evitar isso, use #undef <identificador>.

#undef

Sintaxe: #undef identificador

A diretiva #undef pode ser usada para cancelar uma diretiva #define definida anteriormente:

#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

Sem o #undef no exemplo acima, haveria um erro de redefinição de macro.

#if

Sintaxe: #if <condicao>

A diretiva #if verifica se a condicao passada foi atendida. Se ela for avaliada como um valor diferente de zero, o bloco de código é incluído; caso contrário, é pulado.

Para ser avaliada corretamente, a condição deve ser uma expressão que resulte em um valor simples de ponto flutuante, inteiro ou booleano. Pode haver múltiplos blocos de condição conectados pelos operadores && (E) ou || (OU). Pode ser continuado por um bloco #else, mas deve ser encerrado com a diretiva #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

Usando a função de pré-processador defined(), você pode verificar se o identificador passado está definido por um #define colocado acima dessa diretiva. Isso é útil para criar várias versões de shader no mesmo arquivo. Ela pode ser continuada por um bloco #else, mas deve ser finalizada com a diretiva #endif.

O resultado da função defined() pode ser negado usando o símbolo ! (NÃO booleano) na frente dela. Isso pode ser usado para verificar se um define não está configurado.

#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

Tenha cuidado, pois o defined() deve envolver apenas um único identificador entre parênteses, nunca mais do que isso:

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

Dica

No editor de shader, as ramificações do pré-processador que forem avaliadas como false (e, portanto, excluídas do shader final compilado) aparecerão acinzentadas. Isso não se aplica às instruções if de tempo de execução.

Pré-processador #if versus instrução if: Restrições de desempenho

A linguagem de sombreamento suporta instruções if em tempo de execução:

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 o uniform nunca for alterado, isso se comporta de forma idêntica ao seguinte uso da instrução de pré-processador #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

No entanto, a variante com #if pode ser mais rápida em certos cenários. Isso ocorre porque todas as ramificações de tempo de execução em um shader ainda são compiladas e as variáveis dentro dessas ramificações ainda podem ocupar espaço de registradores, mesmo que nunca sejam executadas na prática.

As GPUs modernas são bastante eficazes ao realizar ramificações "estáticas". Ramificações "estáticas" referem-se a instruções if onde todos os pixels/vértices são avaliados com o mesmo resultado em uma determinada invocação do shader. No entanto, grandes quantidades de VGPRs (que podem ser causadas por excesso de ramificações) ainda podem desacelerar a execução do shader significativamente.

#elif

A diretiva #elif significa "else if" (senão se) e verifica a condição passada se o #if acima tiver sido avaliado como false. O #elif só pode ser usado dentro de um bloco #if. É possível usar várias instruções #elif após uma instrução #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

Assim como no #if, a função de pré-processador defined() pode ser usada:

#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

Sintaxe: #ifdef <identificador>

Este é um atalho para #if defined(...). Verifica se o identificador passado está definido por um #define colocado acima dessa diretiva. Isso é útil para criar múltiplas versões de shader no mesmo arquivo. Pode ser continuado por um bloco #else, mas deve ser encerrado com a diretiva #endif.

#define USE_LIGHT

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

O processador não suporta #elifdef como um atalho para #elif defined(...). Em vez disso, use a seguinte série de #ifdef e #else quando precisar de mais de duas ramificações:

#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

Sintaxe: #ifndef <identificador>

Este é um atalho para #if !defined(...). Semelhante ao #ifdef, mas verifica se o identificador passado não está definido por um #define antes dessa diretiva.

Este é o oposto exato de #ifdef; ele sempre corresponderá em situações onde o #ifdef nunca corresponderia, e vice-versa.

#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

Sintaxe: #else

Define o bloco opcional que é incluído quando a diretiva #if, #elif, #ifdef ou #ifndef definida anteriormente é avaliada como 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

Sintaxe: #endif

Usado como terminador para as diretivas #if, #ifdef, #ifndef ou diretivas subsequentes #else.

#error

Sintaxe: #error <mensagem>

A diretiva #error força o pré-processador a emitir um erro com uma mensagem opcional. Por exemplo, é útil quando usada dentro de um bloco #if para fornecer uma limitação estrita do valor definido.

#define MAX_LOD 3
#define LOD 4

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

#include

Sintaxe: #include "caminho"

A diretiva #include inclui o conteúdo completo de um arquivo de inclusão de shader em um shader. "caminho" pode ser um caminho absoluto res:// ou relativo ao arquivo de shader atual. Caminhos relativos só são permitidos em shaders que são salvos em arquivos .gdshader ou .gdshaderinc, enquanto caminhos absolutos podem ser usados em shaders integrados a um arquivo de cena/recurso.

Você pode criar novas inclusões de shader usando a opção de menu Arquivo > Criar Inclusão de Shader do editor de shader, ou criando um novo recurso ShaderInclude na aba Sistema de Arquivos.

As inclusões de shader podem ser incluídas de dentro de qualquer shader, ou de outra inclusão de shader, em qualquer ponto do arquivo.

Ao incluir arquivos de inclusão de shader no escopo global de um shader, é recomendado fazer isso após a instrução inicial shader_type.

Você também pode incluir arquivos de inclusão de shader de dentro do corpo de uma função. Observe que o editor de shader provavelmente reportará erros para o código da sua inclusão de shader, pois ele pode não ser válido fora do contexto para o qual foi escrito. Você pode optar por ignorar esses erros (o shader ainda será compilado normalmente) ou pode envolver a inclusão em um bloco #ifdef que verifica um define do seu shader.

O #include é útil para criar bibliotecas de funções auxiliares (ou macros) e reduzir a duplicação de código. Ao usar #include, tenha cuidado com colisões de nomes, pois a redefinição de funções ou macros não é permitida.

O #include está sujeito a várias restrições:

  • Apenas recursos de inclusão de shader (terminando com .gdshaderinc) podem ser incluídos. Arquivos .gdshader não podem ser incluídos por outro shader, mas um arquivo .gdshaderinc pode incluir outros arquivos .gdshaderinc.

  • Dependências cíclicas não são permitidas e resultarão em um erro.

  • Para evitar recursão infinita, a profundidade de inclusão é limitada a 25 etapas.

Exemplo de arquivo include de 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);
}

Exemplo de shader base (usando o arquivo de inclusão que criamos acima):

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

Sintaxe: #pragma valor

A diretiva #pragma fornece informações adicionais ao pré-processador ou compilador.

Atualmente, ela pode ter apenas um valor: disable_preprocessor. Se você não precisa do pré-processador, use essa diretiva para acelerar a compilação do shader excluindo a etapa do pré-processador.

#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

Definições nativas

Renderizador atual

Desde o Godot 4.4, você pode verificar qual renderizador está sendo usado atualmente com os defines integrados CURRENT_RENDERER, RENDERER_COMPATIBILITY, RENDERER_MOBILE e RENDERER_FORWARD_PLUS:

  • CURRENT_RENDERER é definido como 0, 1 ou 2 dependendo do renderizador atual.

  • RENDERER_COMPATIBILITY é sempre 0.

  • RENDERER_MOBILE é sempre 1.

  • RENDERER_FORWARD_PLUS é always 2.

Como exemplo, este shader define o ALBEDO para uma cor diferente em cada renderizador:

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
}