Como montar um sistema de placas oceânicas para jogos
Muita gente perde tempo tentando renderizar oceanos inteiros como uma malha única. Não funciona bem na prática. O que eu faço há anos é dividir o mar em placas oceânicas — tiles de geometria que carregam de acordo com a posição do jogador e se reciclam conforme ele se move. O resultado é muito mais leve e você ainda consegue controlar detalhes localizados.
O que são placas oceanicas e por que elas existem
Placas oceanicas são segmentos discretos de terreno aquático, cada um com sua própria geometria, textura e parâmetros de onda. A ideia básica é simples: você não renderiza tudo de uma vez. Carrega apenas o necessário ao redor do jogador e descarta o resto. O que a maioria dos tutoriais não explica é que o truque não está em criar as placas, mas em como gerenciá-las sem causar pop-in visível ou perda de performance. Eu já vi projetos inteiros travarem porque alguém tentou usar um plano único de 5km por 5km com displacement map em alta resolução. O GPU entra em colapso, o carregamento de texturas gasta toda a memória disponível e você ainda tem que lidar com LOD em tempo real em algo que basicamente nunca muda. Placas resolvem isso ao permitir que cada segmento tenha seu próprio nível de detalhe e carga sob demanda.
Estrutura básica do sistema
Você começa definindo o tamanho de cada placa. O tamanho padrão que eu uso é 200x200 unidades no Unity ou 100x100 no Unreal, dependendo da escala do seu mundo. Isso dá uma boa relação entre detalhe e performance. Placas menores causam muitos limites visíveis entre os tiles. Placas maiores esquecem o benefício do streaming. Cada placa precisa de:
- Uma malha base com segmentos suficientes para displacement. Recomendo pelo menos 64x64 vértices por placa. Menos que isso e as ondas ficam com aspecto pixelado quando você aproxima a câmera. - Um shader de água que considere profundidade, normal map e fresnel. Shaders genéricos de "água azul" funcionam para protótipos mas rapidamente mostram suas limitações em produção.
- Um sistema de coordenadas de tile que mapeie a posição no mundo para índices de placa. Isso é simplesmente uma divisão do espaço por GridX = floor(posX / tileSize) e GridZ = floor(posZ / tileSize). - Um pool de placas recicláveis. Quando uma placa sai do raio de visibilidade, ela não é destruída. É reposicionada na borda oposta do cluster ativo e recarregada com os dados do novo tile.
Implementação prática
A parte mais crítica é o manager que gerencia o ciclo de vida das placas. Eu uso uma abordagem com dicionário onde a chave é a cordenada do tile (GridX, GridZ) e o valor é a referência à placa ativa. Quando o jogador se move, calculo quais tiles estão dentro do raio de visão, digamos 8 tiles de raio, e carrego apenas esses. O processo de carregamento envolve três etapas: Instanciar ou reciclar a placa, aplicar o material correto com os parâmetros de onda específicos daquele tile, e configurar a altura dos vértices baseada em simplex noise ou um heightmap pré-computado. Isso leva em média 3 a 5 milissegundos por placa no meu setup, então consigo atualizar 64 placas novas em cerca de 200ms, o que é aceitável para um frame de 16ms se você distribuir ao longo de vários frames.
👉 Clique no botão abaixo para saber mais sobre o assunto!
Para as ondas, eu uso uma combinação de várias ondas senoidesais com frequência e amplitude diferentes. O segredo é adicionar uma componente de direção variável ao longo do tempo. Sem isso, a água parece estática ou repetitiva demais. Eu aplico cerca de 4 a 6 camadas de onda no shader, cada uma com período e amplitude únicos.
Problema real que encontrei e a solução
Num projeto específico, tive um problema persistente de seams visíveis entre placas adjacentes. As bordas das placas não se conectavam perfeitamente porque o cálculo de heightmap era feito de forma independente para cada tile, gerando descontinuidades nos limites. O jogador notava imediatamente, especialmente em planos rasos onde a água é quase transparente. A solução foi simples mas demorei para pensar nela: calcular o heightmap usando coordenadas mundiais contínuas em vez de coordenadas locais por tile. Cada placa ainda armazena seus vértices localmente, mas o valor de altura de cada vértice é determinado por uma função de noise que recebe a posição absoluta no mundo. Como a função de noise é contínua, as bordas das placas adjacentes produzem alturas idênticas automaticamente. Non precisa de blending complicado ou sobreposição de geometria. Basta garantir que o seed do noise seja consistente entre chamadas.
Pegadinhas que ninguém conta
Uma coisa que causa problemas constante é o gerenciamento de memória com texturas. Cada placa pode ter seu próprio conjunto de texturas, mas se você não usar atlasing ou compartilhamento de assets, a memória explode rápido. Em um mapa com 200 placas ativas, cada uma com diffuse, normal map e heightmap em 1024x1024, você está falando de dezenas de megabytes apenas em texturas de água. A solução é usar atlas de texturas e materials instanciados. O Unity faz isso bem com MaterialPropertyBlock, por exemplo. Outro ponto importante: o sistema de LOD para placas. Placas mais distantes do jogador precisam de menos detalhes geométricos. Eu reduzo a resolução da malha progressivamente, indo de 64x64 para 32x32, depois 16x16 e finalmente 8x8 para os tiles mais afastados. A transição entre LODs deve ser suave para evitar pop-in. Uma técnica que funciona é fazer o cross-fade geométrico gradual entre as versões de alta e baixa resolução ao longo de alguns frames.
Quando o sistema não funciona
Placas oceânicas têm limitações claras. Se o seu jogo requer interação profunda com o fundo do mar — como navios afundidos, vida marinha distribuída em alta densidade, ou mudanças dinâmicas no relevo submarino — o sistema de placas simples não vai cortar. Você vai precisar de uma camada adicional de detalhe, possivelmente usando um heightmap global de alta resolução aplicado seletivamente nos tiles relevantes. Isso aumenta a complexidade significativamente. Também não recomendo esse approccio para Oceanos infinitos realmente infinitos sem boundaries definidas. O sistema de recycling funciona bem quando você tem um raio de atuação previsível. Se o jogador pode viajar milhares de quilômetros em qualquer direção sem pontos de referência, o gerenciamento de estado das placas fica exponencialmente mais complicado. Nesses casos, uma abordagem baseada em chunk loading tradicional, similar ao que o Minecraft faz com o terreno, pode ser mais adequada.
Onde conseguir os recursos
Para quem quer começar, o Asset Store do Unity tem vários pacotes de placas oceanicas que cobrem desde shaders básicos até sistemas completos de streaming. Eu recomendo começar com algo simples e construir por cima, em vez de tentar adaptar um pacote complexo que tem cinquenta opções que você não vai usar. Isso apenas adiciona overhead e confusão. Se estiver usando Unreal, o sistema Lumen combinado com material functions de água já oferece uma base sólida. O trabalho principal será na parte de streaming de placas, que não vem pronta e precisa ser implementada via World Partition ou manualmente com streaming volumes.
A parte de código do gerenciador de placas, a lógica de recycling e cálculo de LOD, é algo que você vai acabar escrevendo do zero de qualquer forma, porque cada projeto tem requisitos diferentes de escala, performance e estilo visual. Não existe solução universal que se encaixe perfeitamente sem adaptação.
Resumo técnico
O pipeline completo, do carregamento ao renderização de uma placa, leva aproximadamente 5 a 10ms no total incluindo setup de geometria, aplicação de material e upload de dados para a GPU. Um cluster de 64 placas em torno do jogador ocupa cerca de 2 a 4GB de VRAM dependendo da resolução das texturas. A memória RAM para os dados de geometria e heightmap é significativamente menor, na casa dos 100 a 200MB para um mapa razoavelmente grande. Se você planeja suporte a múltiplas plataformas, testem em hardware de baixo custo desde o início. Sistemas que funcionam bem em uma RTX 4090 podem ter problemas sérios em um iPad ou console de gerações anteriores, especialmente na parte de shader complexity e número de draw calls. Instancing de meshes é praticamente obrigatório para manter o frame rate estável em plataformas restritas.