Skip to content

Construindo um mundo aberto no navegador, parte 26: Três maneiras de criar água

Por Oleg Sidorkin, CTO e cofundador da Cinevva

Chegou agora? Consulte o guia da série. Ele explica o que é um spike e traz links para todas as partes.

A Parte 25 vestiu os avatares. Esta parte trata de água e contém três spikes, porque a água é a superfície em que um atalho barato e a solução correta parecem idênticos em uma captura de tela, mas completamente diferentes em movimento. O Spike 51 cria reflexos em espaço de tela, a opção tentadora, e esbarra diretamente em sua limitação inerente. O Spike 52 muda para o método que todos os jogos lançados realmente usam. O Spike 53 integra uma biblioteca de água pronta para vermos a distância entre o resultado “finalizado” e o ponto em que estamos. Os três compartilham a mesma camada de refração, portanto a única variável que muda entre os dois primeiros é a forma como o reflexo é calculado.

Reflexo a partir da tela que você já tem

Abrir o Spike 51 em uma nova aba ↗ · Ver código-fonte

O reflexo em espaço de tela reutiliza o quadro que já foi renderizado. Para cada pixel da água, você reflete o raio de visão na superfície, percorre o buffer de profundidade com esse raio refletido e, quando ele passa por trás de uma superfície registrada, encontra aquilo que a água reflete, amostrado diretamente do buffer de cores. A adaptação segue o SSRNode do three.js linha por linha, que, por sua vez, segue a introdução a SSR de lettier. O percurso é feito por DDA no espaço de tela: projeta-se o início e o fim do raio em coordenadas de pixels e avança-se pelo eixo mais longo, com uma amostra por pixel. A profundidade do raio refletido em cada passo exige interpolação com correção de perspectiva, 11/z0+s(1/z11/z0), porque interpolar linearmente o Z no espaço de visão é simplesmente incorreto e produz interseções no lugar errado.

Duas coisas tornam isso utilizável em vez de uma apresentação de slides. O percurso aproximado é limitado a 64 passos, pois um raio longo projetado sobre mil pixels poderia executar centenas de iterações por fragmento; um plano de água com um milhão de fragmentos, multiplicado por centenas de iterações e por algumas amostras de textura, resulta em uma cena a 30 fps. A qualidade controla o espaçamento efetivo dentro desse limite, em vez do número de iterações. Além disso, como um percurso aproximado limitado deixa faixas visíveis em degraus, um refinamento binário de seis iterações divide ao meio o intervalo entre o último ponto sem interseção e a interseção encontrada. Isso proporciona uma precisão de subpasso 64 vezes maior, suficiente para impedir que fragmentos vizinhos da água se fixem na mesma posição aproximada de interseção. Uma verificação final da distância entre ponto e linha confirma que o candidato realmente está sobre o raio de reflexão, em vez de apenas ter a mesma profundidade, usando uma tolerância de espessura ajustada automaticamente à largura, no espaço de visão, de um pixel naquela profundidade: menor de perto e maior à distância.

A parte honesta deste spike está escrita em seus próprios comentários: SSR não consegue refletir aquilo que a câmera principal nunca amostrou. A parte inferior de uma árvore, qualquer coisa fora da tela ou qualquer elemento oculto simplesmente não existe nos buffers e, portanto, não pode aparecer no reflexo. Essa é a “perda de informação do lado errado” que nenhuma melhoria na qualidade do percurso consegue corrigir — e é exatamente por isso que existe o próximo spike.

O espelho que não pode mentir

Abrir o Spike 52 em uma nova aba ↗ · Ver código-fonte

Um reflexo planar renderiza a cena uma segunda vez, a partir de uma câmera espelhada em relação ao plano da água, em um alvo fora da tela, que então é amostrado pelo shader da água. Esse é o padrão canônico, usado pelo Water do UE5, pelo próprio WaterMesh do three.js, por ABZÛ e por Sea of Thieves, porque ele tem todos os pixels da cena à disposição, incluindo a geometria que o SSR jamais consegue ver. Em TSL, chega a ser anticlimático: reflector() aloca a câmera auxiliar espelhada e seu alvo de renderização; você adiciona esse alvo à malha para que ele seja atualizado a cada quadro e amostra sua cor. Quando as ondas forem adicionadas mais tarde, o reflexo irá oscilar com a adição de um deslocamento de distorção ao nó UV do refletor, exatamente como faz a linha correspondente do WaterMesh.

O bug que vale registrar estava na rede de segurança, não no espelho. Uma versão anterior misturava a saída do refletor com um céu procedural como fallback, ponderada pela magnitude da cor refletida, partindo da teoria de que um reflexo próximo de zero significava que o alvo não continha nada naquele ponto. Porém, a sombra escura da copa de uma árvore também tem baixa magnitude, então o valor limitado nunca atingia a intensidade máxima e esses pixels genuinamente escuros acabavam misturados ao céu claro. O sintoma detectado pelo usuário foi preciso: o alvo bruto do espelho, no modo de depuração, mostrava árvores escuras perfeitas, enquanto a renderização composta apresentava reflexos desbotados. O diagnóstico foi que o shader “faz as cores escuras desaparecerem”. A correção foi apagar código. O alvo do refletor é confiável depois do primeiro quadro, portanto não há necessidade alguma de fallback. Os dois spikes compartilham a mesma refração por baixo: amostrar a cena atrás da superfície, reconstruir a distância de cada pixel abaixo da linha-d’água e aplicar extinção de Beer-Lambert por canal, para que o vermelho desapareça em poucos metros enquanto o azul persiste, com uma máscara de céu para impedir que o plano de fundo distante receba neblina. O efeito Fresnel de Schlick mistura refração quando se olha diretamente para baixo, dentro da água, e reflexo quando se olha através de sua superfície.

Como é um resultado finalizado

Abrir o Spike 53 em uma nova aba ↗ · Ver código-fonte

O Spike 53 é a avaliação entre desenvolver ou comprar. Ele integra o threejs-water-pro como é distribuído, com seu preset tropical, clipmap oceânico padrão, acompanhamento de câmera e céu de Rayleigh, além de carregar a mesma ilha em glTF usada pela demonstração da própria biblioteca. O objetivo é enxergar a diferença entre um shader de água plana feito à mão e um sistema oceânico completo — e essa diferença é grande. Este sistema tem flutuabilidade que amostra a altura das ondas em vários pontos sob o casco de um navio, fazendo-o inclinar e balançar em vez de apenas subir e descer; um gerador de esteira; espuma na costa e na superfície; e uma passagem de máscara que impede a renderização da água dentro do casco, para que as ondulações não atravessem o convés.

A integração revelou o tipo de detalhe que só se aprende usando uma biblioteca, em vez de apenas ler seu README. As texturas de espuma são referenciadas pelo nome do arquivo no preset, mas o carregamento é responsabilidade do consumidor; sem elas, a costa parece uma borda rígida na linha-d’água, em vez de uma zona de arrebentação. A ilha é posicionada de modo que sua geometria subaquática desça além do fundo do oceano, permitindo que a malha do fundo da biblioteca oculte o anel externo do modelo sem deixar visível a borda do plano. Esse é o padrão pretendido pela biblioteca: trazer um modelo 3D, não sintetizar o terreno. E o antialiasing é deliberadamente desativado no renderizador em favor de uma passagem FXAA de pós-processamento, porque o MSAA mistura fragmentos das bordas com o plano de fundo antes da execução da neblina atmosférica baseada em profundidade, deixando uma fina franja escura em todos os pontos onde a geometria encontra a neblina. Resolver o serrilhado depois da neblina, em vez de antes, elimina essa franja. O que isso reduz é o risco da própria decisão: um oceano pronto para produção é um sistema grande e especializado e, nos casos em que precisamos de um, adotar uma biblioteca com manutenção ativa é melhor do que reconstruir do zero esteiras, flutuabilidade e espuma. Enquanto isso, o shader de espelho planar do Spike 52 continua sendo a solução certa para as águas interiores menores que um criador coloca em seu próprio mundo.

Tecnologias mencionadas neste capítulo

Água com reflexos em espaço de tela. Um raio de visão refletido percorre o buffer de profundidade em espaço de tela por meio de DDA, usando interpolação 1/z com correção de perspectiva, um limite rígido de passos para restringir o custo por fragmento e uma etapa de refinamento binário para remover as faixas deixadas pelo percurso limitado. Uma confirmação da distância entre ponto e linha, com espessura escalada pela profundidade, rejeita interseções falsas. O limite absoluto do método é que ele só consegue refletir a geometria já amostrada pela câmera principal, portanto superfícies fora da tela ou voltadas para o lado errado nunca aparecem. Consulte materiais de terreno.

Reflexo com espelho planar. Uma câmera auxiliar espelhada em relação ao plano da água renderiza a cena em um alvo fora da tela, amostrado pelo shader da água, produzindo reflexos perfeitos em nível de pixel, inclusive da geometria que o SSR não consegue ver. Esse é o padrão usado pelo Water do UE5 e pelo WaterMesh do three.js, com a distorção das ondas aplicada como um deslocamento no nó UV do refletor. Um fallback de céu ponderado pela magnitude apagava incorretamente os pixels escuros dos reflexos; o alvo do refletor é confiável depois do primeiro quadro, portanto remover o fallback resolveu o problema.

Refração com coloração de profundidade de Beer-Lambert. Os dois shaders amostram a cena atrás da superfície, reconstroem a profundidade de cada pixel do plano de fundo abaixo da linha-d’água e aplicam extinção exponencial por canal — o vermelho desaparece em poucos metros, enquanto o azul persiste —, compondo-a em direção a uma cor de neblina aquática, com uma máscara de céu para que o plano distante não receba neblina. O efeito Fresnel de Schlick combina refração em incidência normal com reflexo em ângulos rasantes.

Adoção de uma biblioteca de água pronta para produção. O threejs-water-pro inclui um clipmap oceânico, céu de Rayleigh, flutuabilidade multiponto para a inclinação e o balanço de navios, esteiras, espuma e uma passagem de máscara do casco. Os detalhes do lado do consumidor importam: as texturas de espuma precisam ser carregadas explicitamente, a geometria subaquática da ilha deve descer além do fundo do oceano para que ele oculte suas bordas e o antialiasing deve ser executado como uma passagem FXAA de pós-processamento, em vez de MSAA, para evitar uma franja escura de neblina nas bordas da geometria.


Parte 26 de 29. Anterior: Parte 25 — Um esqueleto para todos os trajes Próxima: Parte 27 — Uma ilha criada com ruído e um solo que parece solo Guia da série: /blog/2026-02-25-open-world-browser-series-guide