“Estamos começando a desenvolver um novo jogo e precisamos de água fria. Você pode fazer isso? ”
- me perguntou. “Sim, sem dúvida! Claro que posso - respondi, mas minha voz tremia traiçoeira. “E também na Unity?” - e ficou claro para mim que havia muito trabalho pela frente.
Então, um pouco de água. Até aquele momento, eu não tinha visto o Unity, exatamente como C #, então decidi fazer um protótipo nas ferramentas que conhecia: C ++ e DX9. O que eu sabia e era capaz de praticar na época era as texturas de rolagem dos normais para formar a superfície e o mapeamento de deslocamento primitivo baseado nelas. Imediatamente foi necessário mudar absolutamente tudo. Forma animada realista da superfície da água. Sombra complicada (fortemente). Geração de espuma. Sistema LOD conectado à câmera. Comecei a procurar informações na Internet sobre como fazer tudo isso.
O primeiro ponto, é claro, foi o entendimento da
Simulating Ocean Water de
Jerry Tessendorf .
Pagers acadêmicos com um monte de fórmulas abstrusas nunca me foram muito dados; portanto, depois de algumas leituras, eu entendi pouco. Os princípios gerais eram claros: cada quadro é gerado por um mapa de altura usando a transformação rápida de Fourier, que, em função do tempo, muda suavemente sua forma para formar uma superfície de água realista. Mas como e o que contar, eu não sabia. Eu lentamente mergulhei na sabedoria de calcular a FFT em shaders no D3D9, e o código-fonte com um artigo em algum lugar selvagem da Internet, que tentei encontrar por uma hora, mas sem sucesso (infelizmente), realmente me ajudou. O primeiro resultado foi obtido (assustador como uma guerra nuclear):
Os sucessos iniciais agradaram e a transferência de água para a Unity começou com sua conclusão.
Vários requisitos foram apresentados para a água no jogo sobre batalhas navais:
- Olhar realista. Belas como escorços próximos e distantes, espuma dinâmica, dispersão, etc.
- Suporte para várias condições climáticas: calma, tempestade e condições intermediárias. Mudança de hora do dia.
- Física da flutuabilidade dos navios em uma superfície simulada, objetos flutuantes.
- Como o jogo é multiplayer, a água deve ser a mesma para todos os participantes da batalha.
- Desenho de superfície: áreas desenhadas do vôo dos núcleos de vôlei, espuma da entrada de núcleos na água.
Geometria
Decidiu-se construir uma estrutura semelhante a um quadtree, com um centro ao redor da câmera, que é discretamente reconstruído quando o observador se move. Por que discreto? Se você mover a malha suavemente com a câmera ou usar a reprojeção do espaço na tela, como no artigo
Renderização em água em tempo real - introduzindo o conceito de grade projetada , nos planos de longo prazo, devido à resolução insuficiente da malha geométrica, os polígonos “pularão” para cima e para baixo. Isso é muito impressionante. A imagem está ondulando. Para superar isso, é preciso aumentar bastante a resolução do polígono da malha de água ou "achatar" a geometria por longas distâncias, ou construir e mover os polígonos para que essas mudanças não sejam visíveis. Nossa água é progressiva (hehe) e eu escolhi o terceiro caminho. Como em qualquer técnica semelhante (especialmente familiar para todos que criaram terreno nos jogos), você precisa se livrar das junções em T nas fronteiras das transições dos níveis de detalhe. Para resolver esse problema, no início são calculados 3 tipos de quadriláteros com determinados parâmetros de mosaico:

O primeiro tipo é para aqueles quads que não são transições para detalhes mais baixos. Nenhum dos lados possui um número reduzido de duas vezes de vértices. O segundo tipo é para quadríceps de limite, mas não angulares. O terceiro tipo são quadriláteros de limite angular. A malha final da água é construída girando e escalando esses três tipos de malhas.
É assim que uma renderização com uma cor diferente dos níveis de água LOD se parece.
Os primeiros quadros mostram a conexão de dois níveis diferentes de detalhes.
O vídeo como um quadro é preenchido com quads de água:
Deixe-me lembrá-lo de que tudo isso foi há muito tempo (e não é verdade). Agora, de maneira mais otimizada e flexível, isso pode ser feito diretamente na GPU (GPU Pro 5. Quadtrees na GPU). E ele fará uma chamada de empate e o mosaico pode aumentar os detalhes.
Mais tarde, o projeto mudou para o D3D11, mas as mãos não alcançaram a atualização dessa parte da renderização oceânica.
Geração de forma de onda
Para isso, precisamos da Transformada rápida de Fourier. Para a resolução selecionada (necessária) da textura da onda (vamos chamá-lo assim, por enquanto, explicarei quais dados são armazenados lá), preparamos os dados iniciais usando os parâmetros definidos pelos artistas (força, direção do vento, dependência das ondas na direção do vento e outros). Tudo isso deve ser alimentado nas chamadas fórmulas. Espectro Phillips Modificamos os dados iniciais obtidos para cada quadro, levando em consideração o tempo e executamos a FFT neles. Na saída, obtemos um mosaico de textura em todas as direções que contém o deslocamento dos vértices da malha plana. Por que não apenas um mapa de altura? Se você armazenar apenas o deslocamento em altura, o resultado será uma massa "borbulhante" irrealista, que remotamente se assemelha ao mar:
Se considerarmos os deslocamentos para as três coordenadas, serão geradas ondas realistas "nítidas":
Uma textura animada não é suficiente. A telha é visível, não há detalhes suficientes no futuro próximo. Nós pegamos o algoritmo descrito e criamos não uma, mas três texturas geradas em fft. O primeiro são ondas grandes. Ele define a forma de onda básica e é usado para a física. O segundo são ondas médias. E, finalmente, o menor. 3 geradores FFT (a 4ª opção é o mix final):
Os parâmetros das camadas são definidos independentemente um do outro e as texturas resultantes são misturadas no sombreador de água na forma de onda final. Paralelamente às compensações, também são gerados mapas normais de cada camada.
A "uniformidade" da água para todos os participantes da batalha é garantida pela sincronização dos parâmetros oceânicos no início da batalha. Esta informação é transmitida pelo servidor para cada cliente.
Modelo de flutuação física
Uma vez que era necessário fazer não apenas uma imagem bonita, mas também o comportamento realista dos navios. E também levando em conta o fato de que um mar tempestuoso (ondas grandes) deveria estar presente no jogo, outra tarefa que precisava ser resolvida era garantir a flutuabilidade dos objetos na superfície do mar gerado. Primeiro, tentei fazer o GPU readback a textura da onda. Mas, como ficou rapidamente claro que toda a física do combate naval deve ser feita no servidor, no mar, ou melhor, na sua primeira camada, que define a forma de onda, também deve ser lida no servidor (e, muito provavelmente, não existe um método rápido e / ou rápido). GPU compatível), foi decidido escrever uma cópia funcional completa do gerador GPU FFT na CPU na forma de um plug-in C ++ nativo para o Unity. Eu não implementei o algoritmo FFT e o usei na biblioteca Intel Performance Primitives (IPP). Mas toda a ligação e pós-processamento dos resultados foi feita por mim, seguida pela otimização no SSE e pela paralelização por threads. Isso incluiu a preparação da matriz de dados para a FFT em cada quadro e a conversão final dos valores calculados em um mapa de deslocamento de onda.
Havia outra característica interessante do algoritmo, que era baseada nos requisitos para a física da água. O que era necessário era a função de obter rapidamente a altura das ondas em um determinado ponto do mundo. É lógico, porque esta é a base para construir a flutuabilidade de qualquer objeto. Porém, como na saída do processador FFT obtemos o mapa de deslocamento, não o mapa de altura, a seleção usual da textura não nos deu a altura da onda, quando necessário. Para simplificar, considere a opção 2D:

Para formar uma onda, os texels (elementos de textura mostrados por linhas verticais) contêm um vetor (setas) que define o deslocamento do vértice da malha plana (pontos azuis) na direção de sua posição final (a ponta da seta). Suponha que tomemos esses dados e tentemos extrair dele a altura da água no ponto de seu interesse. Por exemplo, precisamos saber a altura em hB. Se pegarmos o vetor texel tB, obtemos um deslocamento para um ponto próximo a hC, que pode ser muito diferente do que precisamos. Existem duas opções para resolver esse problema: a cada solicitação de altura, verifique o conjunto de texels adjacentes até encontrar um que tenha um deslocamento para a posição de interesse para nós. No nosso exemplo, encontramos texel tA como contendo o deslocamento mais próximo. Mas essa abordagem não pode ser chamada rapidamente. A varredura do raio texel não está clara de que tamanho (e se o mar tempestuoso ou calmo, os deslocamentos podem variar bastante) podem demorar muito tempo.
A segunda opção - depois de calcular o mapa de deslocamento, converta-o em um mapa de altura usando a abordagem de dispersão. Isso significa que, para cada vetor de deslocamento, escrevemos a altura da onda que ela define no ponto em que é deslocada. Essa será uma matriz de dados separada, que será usada para obter a altura no ponto de interesse. Usando nossa ilustração, a célula tB conterá a altura hB obtida no vetor tA → hB. Há mais um recurso. A célula tA não conterá um valor válido, pois não há nenhum vetor movendo-se para ele. Para preencher esses "buracos", é feita uma passagem para preenchê-los com valores vizinhos.
É assim que parece se você fizer a visualização dos deslocamentos usando vetores (vermelho - deslocamento grande, verde - pequeno):
O resto é simples. O plano da linha de flutuação condicional é definido para o navio. Nela, é determinada uma grade retangular de pontos de amostra, que define os locais de aplicação das forças que empurram a água para o navio. Então, para cada ponto, verificamos se está embaixo da água ou se não está usando o mapa de altura da água descrito acima. Se o ponto estiver embaixo da água, aplique a força vertical até o casco físico do corpo nesse ponto, escalado pela distância do ponto à superfície da água. Se estiver acima da água, não faremos nada, a gravidade fará tudo por nós. De fato, as fórmulas são um pouco mais complicadas (todas para o ajuste fino do comportamento do navio), mas o princípio básico é esse. No vídeo de visualização de flutuabilidade abaixo, os cubos azuis são os locais das amostras e as linhas a partir delas são a magnitude da força que empurra para fora da água.
Na implementação do servidor, há outro ponto de otimização interessante. Não é necessário simular água diferente para diferentes instâncias de combate se elas passarem nas mesmas condições climáticas (os mesmos parâmetros do simulador de FFT). Portanto, a decisão lógica foi criar um conjunto de simuladores, para os quais as unidades de combate atendam aos pedidos de obtenção de água simulada com os parâmetros fornecidos. Se os parâmetros forem os mesmos em várias instâncias, a mesma água retornará a eles. Isso é implementado usando a API Memor Mapped File. Quando o simulador de FFT é criado, ele fornece acesso aos seus dados exportando descritores dos blocos necessários. A instância do servidor, em vez de iniciar um simulador real, lança um "manequim" que simplesmente fornece dados abertos por esses descritores. Houve alguns bugs engraçados relacionados a essa funcionalidade. Devido a erros de contagem de referência, o simulador foi destruído, mas o arquivo mapeado na memória está ativo enquanto pelo menos um identificador está aberto. Os dados pararam de atualizar (não há simulador) e a água "parou".
No lado do cliente, precisamos de informações sobre a forma de onda para calcular a penetração dos núcleos na onda e reproduzir os sistemas de partículas e espuma. O dano é calculado no servidor e também é necessário determinar corretamente se o núcleo entrou na água (a onda pode fechar o navio, especialmente em tempestades). Aqui já é necessário fazer o rastreamento do mapa de altura por analogia, como é feito no mapeamento de paralaxe ou nos efeitos SSAO.
Sombreamento
Em princípio, como em outros lugares. Reflexões, refrações, espalhamento de subsuperfície são habilmente amassadas, levando em conta a profundidade do fundo, levamos em conta o efeito fresnel, consideramos o especular. Consideramos a dispersão de cumes, dependendo da posição do sol. A espuma é gerada da seguinte forma: crie um "ponto de espuma" nas cristas das ondas (use a altura como uma métrica) e aplique pontos recém-criados aos pontos dos quadros anteriores, reduzindo sua intensidade. Assim, obtemos uma mancha de manchas de espuma na forma de uma cauda de uma crista de onda corrente.
Usamos a textura de “manchas” obtida como uma máscara na qual misturamos as texturas de bolhas, manchas etc. Temos um padrão dinâmico de espuma bastante realista na superfície das ondas. Essa máscara é criada para cada camada de FFT (lembre-se, temos três) e, na mistura final, todas elas se misturam.
O vídeo acima visualiza uma máscara de espuma. A primeira e a segunda camadas. Modifico os parâmetros do gerador e o resultado é visível na textura.
E um vídeo de um mar tempestuoso um pouco desajeitado. Aqui você pode ver claramente a forma de onda, os recursos do gerador e a espuma:
Desenho de água
Imagem de uso:

Usado para:
- Marcadores, visualização da zona de expansão dos núcleos.
- Desenhando espuma no ponto em que os núcleos atingem a água.
- Espumoso velório do navio
- Espremer a água sob o navio para remover o efeito das ondas que inundam o convés e o porão inundado.
O caso básico óbvio é a texturização projetiva. Foi implementado. Mas existem requisitos adicionais. Espécies próximas - sabão devido à resolução insuficiente (você pode aumentar, mas não infinitamente), e eu quero que esses desenhos projetivos na água sejam muito visíveis. Onde o mesmo problema foi resolvido? É isso mesmo, nas sombras (mapa das sombras). Como ela é resolvida lá? À direita, mapas de sombra em cascata (divisão paralela). Também colocaremos essa tecnologia em serviço e aplicá-la à nossa tarefa. Dividimos o frustum da câmera em N (3-4 geralmente) subfrustes. Para cada um, construímos um retângulo descritivo no plano horizontal. Para cada retângulo, construímos uma matriz de projeção ortográfica e desenhamos todos os objetos de interesse para cada uma dessas N câmeras orto. Cada uma dessas câmeras atrai uma textura separada e, no sombreador do oceano, as combinamos em uma imagem projetiva sólida.
Então eu coloquei um avião enorme com uma textura de bandeira no mar:

Aqui está o que as divisões contêm:

Além das figuras usuais, é necessário desenhar uma máscara adicional de espuma (para vestígios de navios e locais onde os núcleos atingem) exatamente da mesma maneira, bem como uma máscara para espremer a água sob os navios. São muitas câmeras e muitos corredores. No início, funcionou de maneira tão freada, mas, depois de mudar para o D3D11, usando a “propagação” da geometria no sombreador geométrico e desenhando cada cópia em um destino de renderização separado via SV_RenderTergetArrayIndex, foi possível acelerar bastante esse efeito.
Melhorias e upgrades
D3D11 é mãos muito livres em muitos momentos. Depois de mudar para ele e para o Unity 5, criei um gerador de FFT nos shaders de computação. Visualmente, nada mudou, mas tornou-se um pouco mais rápido. A conversão do erro de cálculo da textura dos reflexos de uma renderização completa da câmera para a tecnologia
Screen Space Planar Reflections deu um bom impulso no desempenho. Escrevi sobre a otimização dos objetos da superfície da água acima, mas minhas mãos não atingiram a transferência da malha para a GPU Quadtree.
Muito poderia ser feito de maneira mais otimizada e simples. Por exemplo, não rodeie jardins com um simulador de CPU, mas simplesmente execute a opção GPU em um servidor com um dispositivo d3d WARP (software). As matrizes de dados não são muito grandes.
Bem, em geral, de alguma forma. No momento em que o desenvolvimento começou, tudo era moderno e legal. Agora já está fora do lugar em alguns lugares. Existem mais materiais disponíveis, mesmo que haja um análogo semelhante ao github:
Crest . A maioria dos jogos que têm mar utiliza uma abordagem semelhante.