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

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


