Voltar para o Blog
Quest Log

Shader 3D na Godot 4: Primeiros Passos com shader_type spatial

Esfera 3D com brilho de emission e rim light azul dentro do editor da Godot 4, com o painel do shader aberto ao lado

Shader 3D na Godot 4: aprenda shader_type spatial do zero, com ALBEDO, ROUGHNESS, emission pulsante, vertex displacement e fresnel em exemplos completos.

Se você já montou uma cena 3D na Godot, colocou um MeshInstance3D, uma luz e uma câmera, o próximo salto de qualidade visual quase sempre passa por materiais. E cedo ou tarde o StandardMaterial3D deixa de ser suficiente. Este guia de shader 3D na Godot 4 apresenta o shader_type spatial do zero: o que muda em relação ao shader 2D, quando vale escrever um shader próprio, a anatomia das funções vertex(), fragment() e light(), e quatro exemplos completos que você pode colar num material e ver funcionando agora.

O pré-requisito é pouco: saber criar uma cena 3D básica e aplicar um material numa malha. Se você ainda está nos primeiros experimentos, o guia de primeiro jogo 3D na Godot cobre essa base antes de você voltar aqui.

canvas_item vs spatial: o que muda no 3D

Todo shader da Godot começa declarando um shader_type, e essa linha define o mundo em que você está trabalhando.

No 2D, shader_type canvas_item opera sobre texturas planas. A função fragment() recebe a cor da textura, você mexe nela e escreve o resultado em COLOR. Simples e direto: entra pixel, sai pixel. Se você já brincou com isso, os fundamentos estão no post de shaders na Godot para iniciantes.

No 3D, shader_type spatial funciona diferente porque o material 3D da Godot é PBR (physically based rendering). Você não escreve a cor final do pixel. Você descreve as propriedades da superfície: qual a cor base (ALBEDO), quão metálica ela é (METALLIC), quão áspera (ROUGHNESS), quanta luz própria emite (EMISSION). A engine pega essas propriedades, cruza com todas as luzes da cena, com o ambiente e com as sombras, e calcula a cor final sozinha.

Essa é a mudança mental mais importante do shader 3D na Godot 4: no spatial você descreve o material, não pinta o pixel. É por isso que o mesmo shader fica diferente de manhã e de noite no seu jogo, sem você mudar uma linha.

StandardMaterial3D ou ShaderMaterial?

Antes de escrever qualquer shader, uma regra prática que economiza horas: comece no StandardMaterial3D e só escreva shader quando precisar de um efeito que ele não faz.

O StandardMaterial3D já expõe, em forma de sliders e checkboxes, quase tudo que um spatial shader controla: albedo com textura, metallic, roughness, normal map, emission, transparência, rim, até UV scrolling básico. Por baixo dos panos ele é um spatial shader gigante escrito pela equipe da engine, testado e otimizado. Para 80 por cento dos materiais de um jogo (chão, parede, props, personagem com textura), ele resolve.

O ShaderMaterial entra quando você quer comportamento que não existe em checkbox:

  • Animar vértices da malha (bandeira ao vento, água, grama balançando).
  • Efeitos que dependem de TIME (pulsação, scanline, dissolve).
  • Lógica própria por pixel (fresnel customizado, misturar texturas por altura, highlight de seleção).
  • Substituir o modelo de iluminação, como no cel shading toon.

Dica útil: clique com o botão direito num StandardMaterial3D no Inspector e escolha "Convert to ShaderMaterial". A Godot gera o código do shader equivalente, e você pode ler e editar a partir dele em vez de começar do zero.

Anatomia de um spatial shader

Um spatial shader tem até três funções, todas opcionais:

shader_type spatial;

void vertex() {
    // Roda uma vez por VERTICE da malha.
    // Aqui voce pode mover VERTEX, mexer em NORMAL, UV, etc.
}

void fragment() {
    // Roda uma vez por PIXEL desenhado.
    // Aqui voce define as propriedades do material.
}

void light() {
    // Roda uma vez por LUZ que atinge o objeto.
    // Substitui o modelo de iluminacao padrao. Avancado, raramente necessario.
}

Dentro de fragment(), as saídas que você vai usar com mais frequência:

  • ALBEDO (vec3): a cor base da superfície, antes da luz.
  • METALLIC (float, 0 a 1): 0 é plástico ou pedra, 1 é metal. Na prática, use 0 ou 1, valores intermediários raramente parecem certos.
  • ROUGHNESS (float, 0 a 1): 0 é espelhado, 1 é fosco.
  • EMISSION (vec3): luz que o próprio material emite. Combina com o glow do WorldEnvironment.
  • NORMAL_MAP (vec3): recebe uma textura de normal para simular relevo sem geometria extra.
  • ALPHA (float): transparência. Só escreva nele se realmente precisar, pelo motivo que veremos na parte de performance.

Além do shader_type, existe a linha opcional render_mode, que ajusta o comportamento geral: render_mode cull_disabled; desenha os dois lados dos polígonos (essencial para uma bandeira), unshaded ignora as luzes da cena, blend_add faz mistura aditiva. Você combina vários separando por vírgula.

Com a teoria no lugar, vamos aos exemplos. Todos são código real de Godot 4: crie um ShaderMaterial no seu MeshInstance3D, crie um Shader novo dentro dele e cole.

Exemplo 1: albedo e roughness com uniforms

O "olá mundo" do spatial. Ele replica o básico do StandardMaterial3D, mas serve para você ver os uniforms aparecendo no Inspector.

shader_type spatial;

uniform vec3 base_color : source_color = vec3(0.2, 0.5, 0.8);
uniform float roughness_value : hint_range(0.0, 1.0) = 0.6;
uniform float metallic_value : hint_range(0.0, 1.0) = 0.0;

void fragment() {
    ALBEDO = base_color;
    ROUGHNESS = roughness_value;
    METALLIC = metallic_value;
}

Repare nos hints depois dos dois-pontos. source_color faz o uniform aparecer como um seletor de cor no Inspector, já no espaço de cor correto para o pipeline 3D (sem ele, cores escolhidas no seletor ficam mais claras ou mais escuras do que deveriam). hint_range(0.0, 1.0) vira um slider com limites. Selecione o material, abra "Shader Parameters" no Inspector e arraste os sliders com a cena rodando: a resposta é imediata.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

Exemplo 2: emission pulsante com TIME

TIME é um float global que cresce continuamente enquanto o jogo roda. Com sin() você transforma esse crescimento em oscilação, e com EMISSION a oscilação vira um brilho que pulsa. Ótimo para cristais, painéis de nave, itens colecionáveis.

shader_type spatial;

uniform vec3 glow_color : source_color = vec3(0.1, 0.9, 0.4);
uniform float pulse_speed : hint_range(0.0, 10.0) = 2.0;
uniform float glow_strength : hint_range(0.0, 8.0) = 2.5;

void fragment() {
    // sin() devolve -1 a 1; remapeamos para 0 a 1
    float pulse = (sin(TIME * pulse_speed) + 1.0) * 0.5;
    ALBEDO = vec3(0.05, 0.05, 0.05);
    EMISSION = glow_color * pulse * glow_strength;
}

Duas observações. Primeiro, o remapeamento (sin(x) + 1.0) * 0.5 é um padrão que você vai reutilizar sempre: converte a onda de -1..1 para 0..1, evitando emission negativa. Segundo, para o brilho "vazar" ao redor do objeto (bloom), ative Glow no seu WorldEnvironment; a emission sozinha só deixa a superfície brilhante.

Exemplo 3: vertex displacement (bandeira ao vento)

Até agora só mexemos em pixels. A função vertex() mexe na geometria. Este shader desloca cada vértice no eixo Y usando uma senóide baseada na posição X, criando a onda clássica de bandeira. Use num PlaneMesh com subdivisões (uns 32x32 em Subdivide Width e Depth), porque o efeito move vértices: sem vértices suficientes, não há o que ondular.

shader_type spatial;
render_mode cull_disabled;

uniform vec3 flag_color : source_color = vec3(0.9, 0.2, 0.2);
uniform float wave_speed : hint_range(0.0, 10.0) = 3.0;
uniform float wave_height : hint_range(0.0, 1.0) = 0.15;
uniform float wave_frequency : hint_range(0.0, 10.0) = 4.0;

void vertex() {
    VERTEX.y += sin(TIME * wave_speed + VERTEX.x * wave_frequency) * wave_height;
}

void fragment() {
    ALBEDO = flag_color;
    ROUGHNESS = 0.9;
}

O detalhe que faz a onda "andar" é somar VERTEX.x dentro do sin(): cada vértice fica numa fase diferente da onda, e o TIME desloca todas as fases juntas. Sem o VERTEX.x, a malha inteira subiria e desceria em bloco. O render_mode cull_disabled desenha os dois lados do plano, senão a bandeira some quando vista por trás.

Um ponto importante: o deslocamento acontece só na GPU. A física e as colisões continuam vendo a malha original parada. Para efeito visual isso é perfeito e muito barato; para gameplay que depende da posição real, não serve.

Exemplo 4: fresnel (rim light) no fragment

O fresnel ilumina as bordas do objeto em relação à câmera, aquele contorno brilhante de personagem selecionado, escudo de energia ou silhueta destacada. A base é o produto escalar entre a normal da superfície e a direção da câmera: dot(NORMAL, VIEW) fica perto de 1 quando a superfície aponta para você e perto de 0 na borda. Invertendo, temos exatamente a máscara da borda.

shader_type spatial;

uniform vec3 base_color : source_color = vec3(0.1, 0.1, 0.15);
uniform vec3 rim_color : source_color = vec3(0.3, 0.7, 1.0);
uniform float rim_power : hint_range(0.5, 8.0) = 3.0;
uniform float rim_strength : hint_range(0.0, 4.0) = 1.5;

void fragment() {
    float fresnel = pow(1.0 - clamp(dot(NORMAL, VIEW), 0.0, 1.0), rim_power);
    ALBEDO = base_color;
    ROUGHNESS = 0.4;
    EMISSION = rim_color * fresnel * rim_strength;
}

O rim_power controla a espessura da borda: valores altos concentram o brilho numa linha fina, valores baixos espalham. Jogar o resultado em EMISSION (em vez de somar no ALBEDO) faz o rim funcionar mesmo em áreas na sombra, que costuma ser o que se espera do efeito. Teste num modelo orgânico, tipo uma esfera ou um personagem: em cubos de faces chapadas o fresnel quase não aparece, porque as normais não fazem a transição gradual até a borda.

Controlando uniforms por GDScript

Uniform exposto no Inspector também pode ser alterado em tempo de execução, o que conecta o shader ao gameplay: acelerar a pulsação quando o item está perto de expirar, ligar o rim light quando o inimigo é selecionado.

@onready var mesh: MeshInstance3D = $MeshInstance3D

func _ready() -> void:
    var mat: ShaderMaterial = mesh.get_surface_override_material(0)
    mat.set_shader_parameter("pulse_speed", 6.0)
    mat.set_shader_parameter("glow_color", Color(1.0, 0.3, 0.2))

O nome passado para set_shader_parameter é exatamente o nome do uniform no shader. Se o material foi atribuído direto no recurso da malha (e não como override), pegue com mesh.mesh.surface_get_material(0).

Performance: o que olhar desde o início

Shader 3D não precisa ser assustador em performance, mas três hábitos evitam dor de cabeça:

Um shader é compartilhado por material, não por objeto. Cem objetos usando o mesmo ShaderMaterial custam bem menos que cem materiais diferentes, porque a GPU agrupa o desenho por material. Se vários objetos precisam do mesmo efeito com pequenas variações, prefira uniforms por instância (uniform float meu_valor : instance_uniform na Godot 4.3+) ou MultiMeshInstance3D para instancing de verdade, em vez de duplicar o material.

Cuidado com ALPHA. No momento em que o fragment() escreve em ALPHA, o material sai do pipeline opaco e entra no de transparência. Objetos transparentes não escrevem no depth buffer do mesmo jeito, precisam ser ordenados por distância da câmera e podem aparecer na ordem errada quando se sobrepõem (o famoso depth sorting quebrado, com o objeto de trás desenhado na frente). Além disso, cada camada transparente empilhada custa preenchimento extra. Se o efeito funciona opaco, deixe opaco.

vertex() é mais barato que fragment(). A função de vértice roda milhares de vezes por malha; a de fragmento pode rodar milhões de vezes por frame. Se um cálculo pode ser feito por vértice e interpolado, mova para o vertex(). E lembre que TIME anima de graça: nada de recompilar ou tocar o shader via script a cada frame para animar algo que uma senóide resolve.

Por onde continuar

Você agora tem o mapa do território: spatial descreve materiais PBR, StandardMaterial3D primeiro e shader quando precisar, vertex() move geometria, fragment() define superfície, uniforms com hint_range e source_color deixam tudo ajustável no Inspector. Os quatro exemplos deste post são pontos de partida honestos: combine o fresnel com a emission pulsante, aplique a onda de vértice numa malha de água, troque as cores chapadas por texturas.

O próximo degrau natural é a função light(), onde você substitui a iluminação padrão e cria estilos visuais próprios. Quando chegar lá, o post de cel shading toon citado acima é a continuação direta deste. Abra a Godot, crie uma esfera e comece a arrastar sliders: shader se aprende com o material reagindo na sua frente.

Perguntas frequentes

Qual a diferença entre canvas_item e spatial na Godot 4?

canvas_item é o shader_type para tudo que é 2D (Sprite2D, TextureRect, interface), trabalha em pixels na tela e a saída principal é COLOR. spatial é o shader_type para materiais 3D, roda dentro do pipeline PBR da engine e a saída se divide em propriedades como ALBEDO, ROUGHNESS, METALLIC e EMISSION, que a Godot combina com as luzes da cena.

Quando devo usar ShaderMaterial em vez de StandardMaterial3D?

Comece sempre no StandardMaterial3D. Ele cobre cor, textura, metallic, roughness, normal map, emission e transparência só com checkboxes e sliders. Escreva um ShaderMaterial quando precisar de algo que ele não faz: animar vértices, efeitos com TIME, fresnel customizado, ou qualquer lógica própria por pixel.

Para que servem as funções vertex, fragment e light num spatial shader?

vertex() roda uma vez por vértice da malha e serve para mover geometria (ondas, bandeiras, grama). fragment() roda por pixel e define as propriedades do material, como ALBEDO e ROUGHNESS. light() roda uma vez por luz que atinge o objeto e substitui o modelo de iluminação padrão, útil para efeitos como cel shading.

Como exponho um uniform do shader no Inspector da Godot?

Basta declarar o uniform no topo do shader que ele aparece no Inspector, na seção Shader Parameters do material. Use hints para melhorar a edição: hint_range(0.0, 10.0) vira um slider com limites, e source_color mostra um seletor de cor com o espaço de cor correto.

Usar ALPHA no spatial shader afeta a performance?

Afeta. Escrever em ALPHA move o material para o pipeline de transparência, que desliga parte da otimização de profundidade e obriga a engine a ordenar objetos por distância. Isso pode causar artefatos de sorting e custo extra de preenchimento. Só use ALPHA quando o efeito realmente precisa de transparência.