Voltar para o Blog
Quest Log

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

Cena de jogo 2D com um lago estilizado refletindo e distorcendo o cenário

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 direito
  • TEXTURE: a textura do próprio node (o sprite, por exemplo)
  • COLOR: a cor final que você escreve, é o output do shader
  • TIME: 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 água
  • frequencia é 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.

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

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.