Quando comecei a construir meu RPG tático no Godot 4, um mundo 3D com sprites de personagens 2D, eu tinha uma lista mental clara de coisas que não sabia fazer. Shaders estavam no topo dessa lista. Não perto do topo. No topo mesmo, sublinhado duas vezes. Eu sabia que shaders eram programas que rodavam na GPU. Sabia que controlavam a aparência dos pixels. Além disso, tinha um medo vago de transformações de vértice e uma suposição de que shaders eram para especialistas com background em programação gráfica, não para desenvolvedores de aplicações que construíam UIs Angular para viver.
Em fevereiro de 2026, cerca de um ano depois do início, o projeto tinha 28 shaders customizados. Chuva com refração e profundidade. Acúmulo de neve em superfícies. Terreno destrutível que revela uma subsuperfície carbonizada. Um ciclo dinâmico de dia/noite que muda a temperatura de cor em toda a cena. Shaders de destaque de unidade que pulsam e indicam alcance de movimento. Não contratei um programador gráfico. Aprendi sozinho, principalmente por tentativa e erro, documentação do Godot e muita navegação no ShaderToy.
Este post é o relato honesto dessa jornada. O que foi mais difícil do que o esperado, o que foi surpreendentemente acessível, e como o ambiente de shaders do Godot se compara ao GLSL puro.
A Linguagem de Shader do Godot vs GLSL Puro
O Godot usa sua própria linguagem de shader, que é baseada em GLSL mas não é GLSL. Ela tem os mesmos tipos fundamentais. Vec2, vec3, vec4, sampler2D, mat4. E as mesmas funções matemáticas. Mas adiciona built-ins específicos do Godot, esconde boilerplate atrás de uma estrutura mais limpa, e integra diretamente com o pipeline de material e renderização.
Para um iniciante, essa é uma vantagem genuína. Escrever direto contra a API gráfica, OpenGL ou Vulkan, significa gerenciar você mesmo os vertex buffers, os uniform buffers e os framebuffers. O sistema de shaders do Godot lida com tudo isso. Você declara um shader_type, implementa as funções fragment() ou vertex(), e o Godot te passa tudo que você precisa. Você pode focar no efeito visual ao invés da encanação da API gráfica.
O porém é que a documentação do Godot sobre shaders é completa mas nem sempre amigável para iniciantes. A referência te diz o que cada built-in faz. Nem sempre te diz por que você usaria ou como as coisas interagem. Passei muito tempo lendo a documentação e depois lendo o código fonte dos próprios shaders do Godot para entender como materiais built-in eram implementados antes de conseguir estendê-los.
O Primeiro Shader: Destaque de Unidade
Meu primeiro shader customizado real foi um destaque de unidade. O brilho pulsante que indica que uma unidade está selecionada no meu jogo tático. Premissa simples: pegar o sprite da unidade, fazer brilhar com um contorno pulsante, e mudar a cor do brilho dependendo se a unidade é do jogador, inimiga ou aliada.
Esperava que levasse um dia. Levou quatro. Não porque o código do shader era complexo. O shader final tem cerca de 30 linhas. Mas porque eu não entendia os sistemas de coordenadas do Godot, não entendia como amostragem UV funcionava para contornos, e ficava confundindo espaço de tela com espaço de textura. O breakthrough veio quando entendi que um efeito de contorno funciona amostrando pixels vizinhos e verificando se algum deles é transparente onde o pixel atual é opaco. É isso. Uma vez que entendi isso, a implementação foi direta.
// Godot Shader Language, unit highlight with pulse
shader_type canvas_item;
uniform vec4 outline_color : source_color = vec4(1.0, 1.0, 0.0, 1.0);
uniform float outline_width : hint_range(0.0, 10.0) = 2.0;
uniform float pulse_speed : hint_range(0.0, 5.0) = 2.0;
uniform float pulse_min : hint_range(0.0, 1.0) = 0.4;
void fragment() {
vec2 size = outline_width * TEXTURE_PIXEL_SIZE;
vec4 color = texture(TEXTURE, UV);
float alpha = 0.0;
alpha = max(alpha, texture(TEXTURE, UV + vec2(size.x, 0.0)).a);
alpha = max(alpha, texture(TEXTURE, UV - vec2(size.x, 0.0)).a);
alpha = max(alpha, texture(TEXTURE, UV + vec2(0.0, size.y)).a);
alpha = max(alpha, texture(TEXTURE, UV - vec2(0.0, size.y)).a);
float pulse = pulse_min + (1.0 - pulse_min) *
(0.5 + 0.5 * sin(TIME * pulse_speed));
vec4 outline = outline_color * vec4(1.0, 1.0, 1.0, pulse);
COLOR = mix(outline * (1.0 - color.a), color, color.a);
COLOR.a = max(color.a, alpha * outline_color.a * pulse);
}Shaders Climáticos: Chuva e Neve
Efeitos climáticos foram os primeiros shaders em que me senti genuinamente orgulhoso do resultado. Meu shader de chuva roda como efeito de CanvasLayer em tela cheia e cria a ilusão de chuva caminhando múltiplas texturas de ruído em velocidades e opacidades diferentes, depois aplicando um passo de refração que distorce levemente a cena por baixo.
A chuva em si não foi a parte difícil. Rolar uma textura é uma das primeiras coisas que qualquer tutorial de shader ensina. A parte difícil foi a refração, fazer as listras de chuva distorcerem visivelmente o que está atrás delas, o que requer ler da textura de tela em uma posição UV deslocada. O Godot 4 expõe isso por um uniform sampler2D com hint_screen_texture (o antigo built-in SCREEN_TEXTURE não existe mais), mas a interação entre a ordenação de z do CanvasLayer e quando a textura de tela é capturada é sutil e me fez passar uma noite inteira com renderização incorreta antes de entender a ordem de captura.
Neve na verdade foi mais fácil que chuva porque não precisei de refração. O shader de neve usa uma abordagem simples de partícula-no-shader. Gero posições de flocos de neve proceduralmente usando ondas seno e ruído fracional, o que dá uma distribuição natural sem precisar de um sistema de partículas real. O shader expõe intensidade e direção do vento como uniforms, então posso animar o vento via GDScript durante eventos climáticos.
Acúmulo de neve, o segundo shader de neve, foi mais interessante. Ele lê uma máscara de textura customizada que define onde a neve pode acumular (superfícies planas, topos de muros) e progressivamente faz blend entre a textura base e uma versão coberta de neve usando um uniform dirigido por tempo. Combinado com o shader de neve caindo, a cena transiciona de limpa para nevando de uma forma que afeta tanto a atmosfera quanto o chão.
O Ciclo Dia/Noite: 6 Shaders Trabalhando Juntos
Meu ciclo dia/noite não é um único shader. São seis shaders que se coordenam entre si através de uniforms compartilhados dirigidos por um nó central DayCycleManager. Cada shader lida com um aspecto da mudança visual: temperatura de cor geral, intensidade de sombra, cor da luz ambiente, gradiente do céu, visibilidade de estrelas e posição da lua/sol.
Projetar esse sistema me ensinou a lição mais importante de arquitetura de shaders que conheço: shaders devem ser dirigidos por dados, não por tempo diretamente. Minha tentativa inicial tinha cada shader lendo TIME independentemente e computando a fase atual do dia por conta própria. O resultado era inconsistência sutil. Cada shader carregava sua própria cópia da conta de fase, então pequenas diferenças iam afastando uns dos outros, e nenhum deles seguia o relógio do jogo: o TIME do Godot continua correndo enquanto o jogo está pausado e dá a volta a cada 3.600 segundos por padrão.
A abordagem correta: o DayCycleManager computa um único valor normalizado de day_progress de 0.0 a 1.0 a cada tick de _process() e passa como uniform para todos os seis shaders. Todo shader usa o mesmo valor no mesmo frame. A saída visual é perfeitamente sincronizada.
Shaders de Ambiente Destrutível
Os shaders tecnicamente mais complexos do projeto são os efeitos de ambiente destrutível. Quando um muro recebe dano, quero que a superfície visível rache, carbonize e eventualmente desmorone. O shader para isso lê um valor de dano como uniform e o usa para fazer blend entre três camadas de textura: superfície intacta, superfície rachada e subsuperfície carbonizada.
O efeito de rachadura usa um padrão de ruído Voronoi para gerar a geometria das rachaduras proceduralmente. Conforme o dano aumenta, o threshold das rachaduras diminui e mais bordas das células Voronoi ficam visíveis, dando uma aparência natural de fratura. As áreas carbonizadas usam um passo separado de detecção de borda que destaca as bordas das rachaduras em laranja-vermelho antes de exporem totalmente a textura de subsuperfície.
Acertar isso exigiu entender como fazer blend entre camadas de textura usando uma máscara, como gerar ruído Voronoi em um shader (que é mais envolvido que ruído Perlin), e como mapear corretamente o valor de dano da lógica do jogo para os thresholds visuais de blend. O shader final tem cerca de 80 linhas e levou quase duas semanas de iteração no tempo livre para chegar a um resultado que me satisfez.
O Que Foi Surpreendentemente Fácil
Troca de paleta. Esperava que fosse complicado, substituir um conjunto de cores por outro para variações de cor de unidades. Não é. Você cria uma textura de paleta pequena, amostra a cor do sprite, procura a posição dessa cor na paleta de origem, e retorna a cor correspondente da paleta alvo. Se seu sprite é desenhado com paleta limitada (o que o meu é, por design), a troca inteira acontece em cerca de 15 linhas de código de shader.
Efeitos de dissolução também foram mais fáceis que o esperado. Amostrar uma textura de ruído, comparar o valor do ruído com um threshold de dissolução como uniform, e descartar o pixel se estiver abaixo do threshold. A dissolução come o sprite a partir de um padrão de ruído ao invés de um wipe simples. Adicione uma cor de borda no boundary do threshold e você ganha um efeito de borda queimando de graça. Uso isso para animações de morte de unidade.
- Color grading e aplicação de LUT. Lookup de textura 3D direto
- Efeitos simples de contorno. Amostragem de pixels vizinhos como descrito acima
- Scanlines em screen-space. Padrão repetitivo no UV.y, nada mais
- Flash de sprite (para feedback de dano). Mix entre cor do sprite e branco usando um uniform
- Revelação de fog of war. SDF circular no overlay de fog, falloff suave na borda
O Que Foi Genuinamente Difícil
Normal mapping para sprites de personagens. O Godot suporta normal maps em sprites, mas fazê-los parecer corretos para personagens 2D dentro de um mundo 3D, sob uma única direção de luz consistente, levou muito tempo para ajustar. O problema é que normal maps assumem um modelo de iluminação específico, e o visual artístico que eu queria não era esse modelo. Acabei escrevendo um shader de iluminação de sprites customizado que interpreta o normal map de forma diferente para produzir um resultado mais estilizado.
Ordenação de transparência. Quando múltiplos shaders transparentes se empilham uns sobre os outros, chuva sobre um destaque de unidade transparente sobre uma névoa semitransparente, o renderizador pode produzir blending incorreto dependendo da ordem de desenho. Entender a ordem de desenho, o z-index do CanvasItem para a camada de sprites, e quando a composição de transparência acontece corretamente levou tempo significativo de debugging.
Otimização de performance. Shaders rodam na GPU, então shaders individuais são rápidos. Mas 28 shaders rodando simultaneamente em uma cena com muitos objetos visíveis começou a mostrar impacto de performance em hardware de menor potência. Tive que adicionar tiers de qualidade. Versões simplificadas de shaders climáticos que desabilitam operações custosas como refração em configurações de baixa qualidade. Profiling de uso de GPU no Godot é menos maduro que profiling de CPU, o que tornou isso mais difícil do que deveria.
A Transferência de Habilidade para Trabalho de UI Web
Escrever shaders mudou como eu penso sobre efeitos visuais em trabalho de UI web. Animações CSS, filtros SVG, efeitos canvas. Todos operam nas mesmas primitivas matemáticas que código de shader. Lerp, smoothstep, funções de ruído, coordenadas UV. Quando vejo um efeito de filtro CSS agora, automaticamente penso em como seria o código de shader equivalente. Quando implemento uma animação de transição em um componente Angular, penso em qual curva de interpolação daria a sensação certa.
Também me deu uma apreciação muito mais profunda do que o hardware de GPU realmente faz. Desenvolvedores Angular tendem a pensar em transforms CSS e animações como mágica. Saber que elas compilam para operações de GPU com o mesmo tipo de multiplicações de matriz e amostragem de textura que eu faço em shaders me torna um desenvolvedor mais informado ao tomar decisões de performance.
Shaders parecem intimidadores de fora porque o vocabulário é desconhecido. Quando você percebe que são apenas funções que transformam inputs em cores de pixel, e que esses inputs são apenas vetores e texturas, a intimidação some e a criatividade começa.
Referências
- Godot shading language reference. https://docs.godotengine.org/en/stable/tutorials/shaders/shader_reference/shading_language.html
- Your first 2D shader. https://docs.godotengine.org/en/stable/tutorials/shaders/your_first_shader/your_first_2d_shader.html
- Shadertoy. https://www.shadertoy.com