Limite de GPU. Parte Dois Floresta sem fim



Em quase todos os jogos, é necessário preencher os níveis do jogo com objetos que criam riqueza visual, beleza e variabilidade do mundo virtual. Faça qualquer jogo de mundo aberto. Lá árvores, grama, terra e água são os principais "espaços reservados" da imagem. Hoje haverá muito poucas GPGPUs, mas tentarei dizer como desenhar muitas árvores e pedras na moldura quando você não puder, mas realmente quiser.

Deve-se notar imediatamente que temos um pequeno estúdio independente, e geralmente não temos recursos para desenhar e modelar todas as pequenas coisas. Portanto, o requisito de vários subsistemas serem uma "superestrutura" sobre a funcionalidade pronta do mecanismo. Foi o que aconteceu no primeiro artigo do ciclo sobre animações (lá usamos e aceleramos o sistema de animação concluído do Unity), por isso estará aqui. Isso simplifica bastante a introdução de novas funcionalidades no jogo (menos para aprender, menos bugs, etc.).

Então, a tarefa: você precisa desenhar muita floresta. O jogo possui uma estratégia em tempo real (RTS) com grandes níveis (30x30 km), e isso define os requisitos básicos para o sistema de renderização:

  • Com a ajuda do minimapa, podemos transferir instantaneamente para qualquer ponto do nível. E os dados sobre os objetos para a nova posição devem estar prontos. Não podemos confiar no carregamento de recursos depois de algum tempo nos jogos FPS ou TPS.
  • Objetos em níveis tão grandes precisam de um número realmente grande. Centenas de milhares, se não milhões.
  • Novamente, grandes níveis tornam muito longo e difícil configurar manualmente "florestas". A geração processual de florestas, pedras e arbustos é necessária, mas com a possibilidade de ajuste e organização manual em locais importantes do nível do jogo.

Como esse problema pode ser resolvido? Esse número de objetos comuns da unidade ainda não será puxado. Nós morreremos na seleção e no lote. A renderização é possível usando instanciamento. É necessário escrever um sistema de controle. As árvores precisam ser modeladas. Um sistema de animação em árvore precisa ser feito. Ooh Quero lindamente e imediatamente. Existe o SpeedTree, mas não há APIs para animações, a vista superior dos outdoors é terrível, pois não há "outdoor horizontal" e a documentação é ruim. Mas quando isso nos impediu? Vamos otimizar a renderização do SpeedTree.

Renderização


Vamos ver se tudo está tão ruim com os objetos comuns do speedtree:



Aqui estão cerca de 2.000 árvores no palco. Tudo está em ordem com a renderização, instanciando as árvores em lotes, mas com a CPU tudo está ruim. Metade do tempo que a câmera está renderizando está esfriando. E precisamos de centenas de milhares. Definitivamente recusamos GameObjects, mas agora precisamos descobrir a estrutura do modelo SpeedTree, o mecanismo para alternar LODs e fazer tudo com alças.

A árvore do SpeedTree consiste em vários LODs (geralmente 4), o último dos quais é um outdoor, e todo o resto é geometria de vários graus de detalhes. Cada um deles consiste em vários sabmesh, com seu próprio material:


Essa não é a especificidade do SpeedTree. Qualquer estrutura pode ter essa estrutura. A comutação LOD é implementada em dois modos disponíveis:

  1. Cross Fade:

  2. Árvore de velocidade:


O CrossFade (em termos de shaders do Unity, é definido pelo pré-processador LOD_FADE_CROSSFADE define) é o principal método para alternar LODs para qualquer objeto de cena com vários níveis de detalhe. Consiste no fato de que, quando ocorre uma alteração no LOD, a malha que deve desaparecer não desaparece (isso mostra claramente o salto na qualidade do modelo), mas “se dissolve” na tela usando o pontilhamento . Um efeito simples e evita o uso de transparência verdadeira (mistura alfa). O modelo que deve aparecer exatamente da mesma maneira "aparece" na tela.

O SpeedTree (LOD_FADE_PERCENTAGE) é feito especificamente para árvores. Além das coordenadas principais, as coordenadas adicionais da posição dos vértices juniores em relação ao nível atual de LOD são registradas na geometria das folhas, galhos e tronco. O grau de transição de um nível para outro é o valor do peso para a interpolação linear dessas duas posições. A mudança de / para o outdoor é feita usando o método CrossFade.

Em princípio, é tudo o que você precisa saber para implementar seu próprio sistema de comutação LOD. A renderização em si é simples. Percorremos todos os tipos de árvores, todos os LODs e todos os sabmesh de cada LOD. Instalamos o material apropriado e desenhamos todas as instâncias desse objeto de uma só vez usando instanciamento. Assim, o número de DrawCalls é igual ao número de objetos exclusivos na cena. Como sabemos o que desenhar? Isso nos ajudará

Gerador Florestal


O desembarque em si é simples e despretensioso. Para cada tipo de árvore, dividimos o mundo em quads para que cada árvore caiba em uma árvore. Examinamos todos os quadríceps e verificamos a máscara do formulário:



em um determinado ponto do nível, é possível plantar uma árvore aqui? A máscara, com locais "arborizados", é desenhada pelo designer de níveis. No começo, tudo estava na CPU e no C #. O gerador funcionou devagar e o tamanho dos níveis aumentou, de modo que a espera pela regeneração por várias dezenas de minutos se tornou estressante. Foi decidido transferir o gerador para o GPU e o sombreamento de computação. Aqui também tudo é simples. Precisamos do mapa de altura da terra, da máscara de plantio de árvores e do AppendStructuredBuffer, onde adicionamos as árvores geradas (posição e ID, são todos os dados).

Organizado por árvores manuais em pontos-chave, um script especial entra em matrizes comuns e remove o objeto original da cena.

Seleção e seleção de LOD


Conhecer a posição e o tipo de árvore não é suficiente para gerar uma renderização eficaz. É necessário determinar cada quadro quais objetos são visíveis e qual LOD (considerando a lógica de transição) a ser enviada para a renderização.

Um shader de computação especial também fará isso. Para cada objeto, o Frustum Culling é executado pela primeira vez:


Se o objeto estiver visível, a lógica de comutação LOD será executada. De acordo com o tamanho na tela, determinamos o nível de LOD desejado. Se o modo CrossFade estiver definido para o LOD do grupo, aumentamos o tempo de transição para o pontilhamento. Se o percentual do SpeedTree, consideraremos o valor de transição normalizado entre os LODs.

As APIs gráficas modernas têm funções maravilhosas que permitem que as informações de envio de desenhos sejam passadas para a chamada de desenho no buffer de computação (por exemplo, ID3D11DeviceContext :: DrawIndexedInstancedIndirect para D3D11). Isso significa que você também pode preencher esse buffer de computação na GPU. Assim, acaba criando um sistema totalmente independente da CPU (bem, basta chamar Graphics.DrawMeshInstancedIndirect). No nosso caso, é necessário apenas registrar o número de instâncias de cada sabmesh. O restante das informações (o número de índices na malha e compensações) é estático.

O buffer de computação, com argumentos para chamada de empate, é dividido em seções, cada uma das quais é responsável por chamar a renderização de sua submesh. No sombreador de computação da malha a ser desenhada no quadro atual, aumente o valor InstanceCount correspondente.

Veja como fica na renderização:


A seleção de oclusão de GPU é um próximo passo óbvio, mas para o RTS com uma câmera assim, e não montes muito grandes, os ganhos não são tão óbvios (e aqui é para os interessados). Ainda não o fiz.

Para que tudo seja desenhado corretamente, você precisa ajustar um pouco os shaders do SpeedTree para assumir a posição e os valores das transições entre os LODs dos buffers de computação correspondentes.

Agora desenhamos árvores bonitas, mas estáticas. E as árvores do SpeedTree são realisticamente influenciadas pelo vento, animando-as. Toda a lógica dessas animações está no arquivo SpeedTreeWind.cginc, mas não há documentação ou acesso a parâmetros internos do Unity.

CBUFFER_START(SpeedTreeWind) float4 _ST_WindVector; float4 _ST_WindGlobal; float4 _ST_WindBranch; float4 _ST_WindBranchTwitch; float4 _ST_WindBranchWhip; float4 _ST_WindBranchAnchor; float4 _ST_WindBranchAdherences; float4 _ST_WindTurbulences; float4 _ST_WindLeaf1Ripple; float4 _ST_WindLeaf1Tumble; float4 _ST_WindLeaf1Twitch; float4 _ST_WindLeaf2Ripple; float4 _ST_WindLeaf2Tumble; float4 _ST_WindLeaf2Twitch; float4 _ST_WindFrondRipple; float4 _ST_WindAnimation; CBUFFER_END 

Como os escolheríamos? Para fazer isso, para cada tipo de árvore, renderizaremos o objeto SpeedTree original em algum lugar invisível (ou melhor, visível no Unity, mas não visível na câmera, caso contrário, os parâmetros não serão atualizados). Isso pode ser alcançado aumentando bastante a caixa delimitadora e colocando o objeto atrás da câmera). Cada quadro é removido do conjunto de valores desejado usando material.GetVector (...).

Então, as árvores tremulam ao vento, mas a vista superior dos outdoors é deprimente:


Com a opção de sombreador BILLBOARD_FACE_CAMERA_POS ainda pior:


Precisamos de outdoors horizontais (de cima para baixo). Esse é um recurso padrão do SpeedTree desde a época de King Pea, mas, a julgar pelos fóruns, ele ainda não foi implementado no Unity. Postagem do fórum oficial do SpeedTree: "A integração do Unity nunca usou o outdoor horizontal". Vamos apertar nossas mãos. A geometria em si é fácil de fazer. Como descobrir as coordenadas UV de um sprite em um atlas para ela?


Obtemos o antigo SpeedTreeRT SDK e encontramos a estrutura na documentação:

 struct SBillboard { bool m_bIsActive; const float* m_pTexCoords; const float* m_pCoords; float m_fAlphaTestValue; }; 

"M_pTexCoords aponta para um conjunto de 4 (s, t) coordenadas de textura que definem as imagens usadas no outdoor. m_pTexCoords contém 8 entradas. ”, diz em um idioma estrangeiro. Bem, procuraremos uma sequência de 4 valores de ponto flutuante em um arquivo spm binário, cada um dos quais está no intervalo [0..1]. Pelo método de cutucada científica, descobrimos que a sequência desejada está na frente de um bloco de 12 flutuadores com sinais correspondentes ao padrão:

 float signs[] = { -1, 1, -1, 1, 1, -1, 1, 1, 1, -1, 1, 1 }; 

Escrevemos um pequeno utilitário de console para os profissionais, que itera sobre todos os arquivos spm e procura coordenadas uv para outdoors horizontais neles. A saída é uma etiqueta CSV:

 Azalea_Desktop.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_1.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Azalea_Desktop_Flowers_2.spm: 0, 1, 0.333333, 1, 0.333333, 0.666667, 0, 0.666667, Leaf_Map_Maker_Desktop_1_Modeler_Use_Only.spm: Pattern not found! Leaf_Map_Maker_Desktop_2_Modeler_Use_Only.spm: Pattern not found! BarrelCactus_Cluster_Desktop_1.spm: 0, 0.592376, 0.407624, 0.592376, 0.407624, 0.184752, 0, 0.184752, BarrelCactus_Cluster_Desktop_2.spm: 0, 1, 0.499988, 1, 0.499988, 0.500012, 0, 0.500012, BarrelCactus_Desktop_1.spm: 0, 0.2208, 0.220748, 0.2208, 0.220748, 5.29885e-05, 0, 5.29885e-05, BarrelCactus_Desktop_2.spm: 0, 1, 0.301392, 1, 0.301392, 0.698608, 0, 0.698608, 

Para atribuir coordenadas de textura à geometria do outdoor horizontal, localizamos o registro desejado e o analisamos.

Agora é assim:


Ainda não muito. Usando o limiar do teste alfa, desbotamos o quadro de avisos vertical, em gravações do ângulo para a câmera:



Sumário

Profiler mostrando estatísticas dinâmicas (quantas coisas estão sendo renderizadas) e estáticas (quantos objetos e seus parâmetros estão em cena):


Bem, o vídeo final bonito (a segunda metade mostra a mudança dos níveis de qualidade):


O que temos no final:

  • O sistema é totalmente independente da CPU.
  • Funciona rápido.
  • Ele usa ativos SpeedTree prontos, que você pode comprar na Internet.
  • É claro que fiz amizade com qualquer grupo de LOD, não apenas com o SpeedTree. Agora, muitas pedras também são possíveis.

Dentre as deficiências, destaca-se a falta de seleção de oclusões e os outdoors ainda não muito expressivos.

Source: https://habr.com/ru/post/pt483946/


All Articles