Shader de Água 2D no Godot: Onda, Distorção e um Exemplo Aplicado

Aprenda a criar um shader de água 2D no Godot 4: efeito de onda, distorção com noise, screen texture e um exemplo completo de lago pronto pra usar.
Shader de Água 2D no Godot: Onda, Distorção e um Exemplo Aplicado
Água é um daqueles efeitos que separam um jogo 2D amador de um que parece profissional. E a boa notícia: um shader de água 2D no Godot convincente cabe em umas 30 linhas de código. Não precisa de física de fluido, não precisa de sprite animado frame a frame, não precisa de plugin. Precisa de um shader canvas_item, uma textura de noise e entender dois truques: deslocar UV com seno e distorcer o que está atrás usando a screen texture.
Esse tutorial monta o efeito em camadas: primeiro a onda básica, depois a distorção orgânica com noise, depois a versão que distorce a cena inteira atrás da água. No final tem um shader completo de lago que você cola num jogo de plataforma e funciona. Todo código é da linguagem de shader do Godot 4.x.
Como um shader 2D funciona no Godot
Antes do primeiro efeito, o mínimo que você precisa saber pra não copiar código no escuro.
Um shader 2D no Godot começa com shader_type canvas_item; e roda na GPU pra cada pixel do node que ele está aplicado. A função fragment() é chamada uma vez por pixel, e dentro dela você tem acesso a algumas variáveis built-in que vamos usar o tempo todo:
UV: a coordenada do pixel dentro da textura, de(0, 0)no canto superior esquerdo até(1, 1)no inferior direitoTEXTURE: a textura do próprio node (o sprite, por exemplo)COLOR: a cor final que você escreve, é o output do shaderTIME: segundos desde que o jogo começou, sempre crescendo. É o que anima tudo
Pra aplicar um shader num node, selecione o node (um Sprite2D ou ColorRect), vá em Material no Inspector, crie um ShaderMaterial e dentro dele um Shader novo. O editor abre o painel de código e você vê o resultado ao vivo enquanto digita. Esse feedback imediato é o que torna shader divertido de aprender: você muda um número e a tela responde na hora.
Uma pegadinha de quem está começando: ShaderMaterial é um Resource, e Resources são compartilhados por padrão. Se você duplicar um node, os dois apontam pro mesmo material. Pra cada cópia ter valores próprios de uniform, marque Resource > Local to Scene no material ou use material.set_shader_parameter() em runtime numa cópia única.
Efeito de onda: o primeiro shader de água
A base de quase todo shader de água 2D é a mesma ideia: em vez de ler a textura na coordenada certa, leia numa coordenada levemente deslocada por uma função de seno animada pelo tempo. O olho interpreta esse balanço como ondulação.
shader_type canvas_item;
uniform float amplitude : hint_range(0.0, 0.1) = 0.02;
uniform float frequencia = 12.0;
uniform float velocidade = 2.0;
void fragment() {
vec2 uv = UV;
// Desloca cada linha horizontal por um seno que varia com a altura e o tempo.
uv.x += sin(uv.y * frequencia + TIME * velocidade) * amplitude;
COLOR = texture(TEXTURE, uv);
}
Aplique isso num Sprite2D com qualquer textura e ela passa a ondular. Os três uniforms controlam o caráter da água:
amplitudeé o quanto a imagem entorta. Valores acima de 0.05 já parecem gelatina, não águafrequenciaé quantas ondas cabem na textura. Mais ondas, água mais "nervosa"velocidadeé o ritmo da animação
Os hint_range e os uniforms aparecem no Inspector como sliders, então dá pra ajustar tudo sem tocar no código. Faça isso. Tuning de shader no slider é dez vezes mais rápido que recompilar valor chumbado.
Esse shader sozinho já serve pra um caso real: o reflexo de cenário na água. Duplique o sprite do cenário, espelhe verticalmente (scale.y = -1 ou Flip V), posicione abaixo da linha d'água, escureça com modulate e aplique o shader de onda. É o truque clássico de reflexo 2D, usado em jogo de plataforma desde sempre, e custa quase nada de GPU.
Distorção com noise: tirando a cara de "função de seno"
O problema do seno puro é que ele é regular demais. Água de verdade não ondula em padrão perfeito. A solução é trocar (ou somar) o seno por uma textura de noise que rola com o tempo, e usar o valor do noise como deslocamento.
O Godot gera o noise pra você, sem precisar de imagem externa. No painel de Shader Parameters do material, crie a textura assim: no uniform de noise, escolha New NoiseTexture2D, dentro dela crie um FastNoiseLite no campo Noise, e marque Seamless como true. O seamless importa: sem ele, a textura mostra uma emenda visível quando o UV passa da borda e repete.
shader_type canvas_item;
uniform sampler2D noise_tex : repeat_enable;
uniform float forca : hint_range(0.0, 0.1) = 0.03;
uniform vec2 direcao = vec2(0.05, 0.02);
void fragment() {
// O noise "escorre" na direção configurada, simulando correnteza.
float n = texture(noise_tex, UV + TIME * direcao).r;
// O noise vai de 0 a 1; centraliza em 0 pra distorcer pros dois lados.
vec2 deslocamento = vec2(n - 0.5) * forca;
COLOR = texture(TEXTURE, UV + deslocamento);
}
O repeat_enable no sampler é obrigatório aqui, porque UV + TIME * direcao cresce pra sempre e precisa dar a volta na textura. E repare no n - 0.5: sem isso o deslocamento só empurra pra um lado e a imagem inteira deriva, em vez de balançar no lugar.
A diferença visual entre essa versão e a do seno é grande. O seno parece efeito de TV antiga; o noise parece superfície líquida. Na prática eu uso os dois juntos: noise pra distorção do corpo da água, seno pra superfície, como você vai ver no exemplo final.
Distorcendo a cena atrás da água com screen texture
Até agora o shader distorce a textura do próprio node. Mas água num jogo fica por cima de outras coisas: o fundo do lago, peixes, o player mergulhando. O que você quer distorcer é tudo que já foi desenhado atrás da água. Pra isso existe a screen texture.
No Godot 4 ela é declarada como um uniform com hint próprio (no Godot 3 era a variável SCREEN_TEXTURE direto, então cuidado com tutorial antigo):
shader_type canvas_item;
uniform sampler2D screen_tex : hint_screen_texture, repeat_disable, filter_linear;
uniform sampler2D noise_tex : repeat_enable;
uniform float forca : hint_range(0.0, 0.1) = 0.02;
uniform vec4 cor_agua : source_color = vec4(0.1, 0.4, 0.6, 0.35);
void fragment() {
float n = texture(noise_tex, UV + TIME * 0.05).r;
vec2 deslocamento = vec2(n - 0.5) * forca;
// SCREEN_UV é a posição deste pixel na tela, não na textura do node.
vec4 fundo = texture(screen_tex, SCREEN_UV + deslocamento);
// Mistura o fundo distorcido com a cor da água.
COLOR = mix(fundo, cor_agua, cor_agua.a);
COLOR.a = 1.0;
}
Aplique isso num ColorRect cobrindo a área da água. Tudo que estiver desenhado atrás dele aparece distorcido e tingido de azul. O source_color no uniform faz o Inspector mostrar um color picker de verdade, com alpha, e aqui o alpha da cor controla o quão opaca a água é.
Um detalhe de arquitetura: o pixel só distorce o que foi desenhado antes dele. Ordem de desenho importa. O node da água precisa estar abaixo na árvore (ou com Z Index maior) do que o cenário e os objetos que devem aparecer dentro dela.
Exemplo aplicado: um lago de plataforma completo
Agora o shader que junta tudo: distorção do fundo com noise, gradiente de profundidade, linha de superfície ondulando com seno e uma faixa de espuma clara no topo. É o que eu uso como ponto de partida pra qualquer água de plataforma 2D.
Setup: um ColorRect do tamanho do lago, com este shader no material e a NoiseTexture2D seamless configurada como na seção anterior.
shader_type canvas_item;
uniform sampler2D screen_tex : hint_screen_texture, repeat_disable, filter_linear;
uniform sampler2D noise_tex : repeat_enable;
uniform vec4 cor_rasa : source_color = vec4(0.2, 0.6, 0.7, 0.3);
uniform vec4 cor_funda : source_color = vec4(0.05, 0.15, 0.35, 0.75);
uniform float forca_distorcao : hint_range(0.0, 0.1) = 0.02;
uniform float altura_onda : hint_range(0.0, 0.05) = 0.012;
uniform float freq_onda = 18.0;
uniform float vel_onda = 1.5;
uniform float espuma : hint_range(0.0, 0.1) = 0.025;
void fragment() {
// Linha da superfície: um seno que sobe e desce ao longo do X.
float superficie = altura_onda + sin(UV.x * freq_onda + TIME * vel_onda) * altura_onda;
// Acima da linha da superfície não tem água: pixel transparente.
if (UV.y < superficie) {
COLOR = vec4(0.0);
} else {
// Distorção do que está atrás, mais forte conforme afunda.
float n = texture(noise_tex, UV + TIME * vec2(0.04, 0.02)).r;
vec2 deslocamento = vec2(n - 0.5) * forca_distorcao * UV.y;
vec4 fundo = texture(screen_tex, SCREEN_UV + deslocamento);
// Gradiente: raso perto da superfície, fundo e escuro embaixo.
vec4 cor = mix(cor_rasa, cor_funda, UV.y);
COLOR = mix(fundo, cor, cor.a);
// Faixa de espuma logo abaixo da superfície.
if (UV.y < superficie + espuma) {
COLOR = mix(COLOR, vec4(1.0), 0.6);
}
COLOR.a = 1.0;
}
}
O que cada pedaço faz, na ordem em que aparece:
- Superfície com seno. Em vez de mover vértices, o shader decide pixel a pixel se está acima ou abaixo da linha d'água. Isso funciona em qualquer quad, sem precisar subdividir mesh.
- Distorção escalada por
UV.y. Multiplicar a força pela profundidade faz a água rasa distorcer pouco e a funda distorcer muito, que é como água se comporta de verdade. - Gradiente de cor.
mix(cor_rasa, cor_funda, UV.y)interpola as duas cores ao longo da altura. O alpha de cada cor controla a opacidade naquela profundidade. - Espuma. Uma faixa fina clareada logo abaixo da superfície. É um detalhe barato que vende o efeito inteiro.
Pra fechar o gameplay, a água visual não detecta nada: coloque uma Area2D com a mesma área do ColorRect e use os sinais body_entered e body_exited pra ativar física de nado, som de splash ou partículas. Shader é só apresentação; a lógica continua sendo trabalho dos nodes.
Sobre performance, dá pra ficar tranquilo. Esse fragment shader é leve, com duas leituras de textura por pixel, e GPU come isso de café da manhã mesmo em mobile. O único cuidado real: cada material com hint_screen_texture força a engine a copiar o que foi desenhado até ali, então evite espalhar dezenas de materiais de água diferentes pela mesma cena. Pra vários lagos, reutilize o mesmo material.
Conclusão
Recapitulando o caminho: seno desloca UV e cria onda, noise quebra a regularidade do seno, screen texture deixa a distorção agir sobre a cena em vez de uma textura isolada, e o exemplo do lago empilha tudo com superfície, gradiente e espuma. Cada camada é simples; o efeito bom vem da soma.
O melhor jeito de fixar é abrir um projeto, colar o primeiro shader e ir evoluindo até o último, mexendo nos sliders a cada etapa. Shader se aprende com a tela respondendo na frente de você, e água é a porta de entrada perfeita: o mesmo padrão de UV deslocado por noise depois vira calor tremulando, bandeira ao vento, portal mágico. Aprendeu um, destravou todos.

