Otimização de modelos 3D para a cena do jogo

Este artigo conclui uma série de publicações do estúdio Krasnodar Plarium sobre vários aspectos do trabalho com modelos 3D no Unity. Artigos anteriores: “Recursos de trabalho com o Mesh in Unity” , “Unity: edição procedural do Mesh” , “Importação de modelos 3D para o Unity e armadilhas” , “Recuo de pixel na digitalização de texturas” .

Há quase dois anos, escrevemos um artigo no qual falamos sobre a opção de otimizar a geometria 3D em uma cena com restrições no ângulo da câmera e na rotação dos objetos correspondentes. Não houve muito fluxo desde então, mas a oportunidade de melhorar a solução, considerar diferentes abordagens e espionar os outros está assombrando as mentes dos desenvolvedores. Neste artigo, descreveremos uma versão aprimorada do algoritmo com base na pintura de polígonos e também falaremos sobre tentativas de transferir parte deste trabalho para um pacote 3D.



Cortar em cena


Já consideramos o princípio básico desse algoritmo no artigo acima: extinguimos todos os efeitos e objetos transparentes, pintamos polígonos não processados ​​com uma cor e processados ​​com diferentes cores, renderizamos e extraímos o resultado. Na versão antiga, eles pintavam para que tudo fosse preto redundante e apenas um triângulo fosse marcado em vermelho.

Nos comentários a esse artigo, um dos leitores apontou a possibilidade de otimizar o algoritmo estabelecendo uma correspondência individual entre o conjunto de polígonos e algum conjunto de números únicos. Então será possível processar mais de um triângulo da mesma maneira. Considere esta opção.

Nesse caso, assim como na última vez, é suposto que algum pré-treinamento esteja conectado à desativação de todos os objetos assobiados no palco e objetos que garantem não afetar a visibilidade do modelo de destino. As visualizações da câmera são processadas quase de forma independente; elas são conectadas apenas por um buffer de índice comum de polígonos visíveis. Além disso, é realizado um pré-processamento de geometria para cada ângulo, durante o qual os polígonos são girados e voltados para a câmera ( face posterior). Isso é feito porque, em um determinado estágio do algoritmo, uma malha temporária é criada com um número significativamente maior de vértices que o original. Esse número pode facilmente exceder o limite de 65.535, o que exigirá gestos adicionais nos cálculos e levará a um desempenho reduzido. De qualquer forma, esses polígonos serão excluídos, pois sua cor não cairá no quadro. No entanto, devido ao fato de que cada triângulo gera três vértices de lixo, a eliminação antecipada de polígonos desnecessários facilita o estágio principal do algoritmo e reduz os custos de memória.

Haja algum modelo 3D, cuja geometria é representada por uma malha. Para pintar um polígono específico em uma cor única, você precisa pintar todos os seus vértices nessa cor. Como no caso geral um vértice pode pertencer a polígonos diferentes, não será possível resolver o problema de frente. Não importa como pintamos qualquer vértice, ao renderizar, sua cor se espalhará por todos os triângulos que o possuem, de acordo com o algoritmo de interpolação na lateral da placa de vídeo.


Um exemplo de interpolação de cores ao exibir polígonos com vértices comuns

Portanto, é necessário, de alguma forma, dividir a malha em polígonos independentes separados, preservando a topologia e a geometria do objeto. Dictum factum. Transformamos as matrizes de triângulos e vértices de forma que, para cada triângulo, sejam criados 3 vértices únicos, cuja posição é determinada pelos vértices correspondentes da malha original. Vale a pena notar que, no caso geral, essa malha terá um número significativamente maior de vértices em comparação com o original. E se esse número exceder 65 535, ao criar a malha, você deverá especificar o formato de indexação apropriado.

Converter a malha original em uma malha com vértices exclusivos para cada polígono
private static Mesh GetNotSmoothMesh(Mesh origin) { var oVertices = origin.vertices; var oTriangles = origin.triangles; var vertices = new Vector3[oTriangles.Length]; var triangles = new int[oTriangles.Length]; for (int i = 0; i < triangles.Length; i++) { vertices[i] = oVertices[oTriangles[i]]; triangles[i] = i; } return new Mesh() { indexFormat = vertices.Length > 65535 ? IndexFormat.UInt32 : IndexFormat.UInt16, vertices = vertices, triangles = triangles }; } 


Agora você precisa designar os polígonos dessa malha para que, após a operação de renderização, seja possível determinar qual deles apareceu na tela. Como já mencionado, geramos cores exclusivas para polígonos e pintamos cada três vértices na cor correspondente. O resultado é uma nova malha, que chamamos de Malha colorida em bytes .


Malha colorida em bytes

Coloração de malha na qual cada vértice pertence a apenas um polígono
 private static void ColorizePolygons(Mesh mesh) { var pColors = ColorsOfPolygons(mesh); var colors = new Color[mesh.vertexCount]; for (int i = 0; i < colors.Length; i++) { colors[i] = pColors[i / 3]; } mesh.colors = colors; } private static Color[] GetColorsOfPolygons(Mesh mesh) { var colors = new Color[mesh.triangles.Length / 3]; for (int i = 0; i < colors.Length; i++) { var color = Int2Color(i);//         ,     Color2Int //      ,       int     Color32 colors[i] = color; } return colors; } 


Lembre-se da coloração. É hora de renderizar. Realizamos renderização em 3D para todos os ângulos da câmera e, ao processar cada um deles, reabastecemos o buffer de índices de polígonos exclusivos cujas cores foram detectadas no quadro. Para os cálculos da câmera, você precisa desativar o anti-aliasing para evitar o aparecimento de novas cores devido à interpolação dos pixels vizinhos.

Leitura e armazenamento de cores de diferentes ângulos de câmera
 // CameraTransform —         //      SetCameraTransform private static HashSet<Color> GetVisibleColors(Camera camera, CameraTransform[] cameraTransforms) { var renderTexture = new RenderTexture(1920, 1080, 24);//for example var rtRect = new Rect(0, 0, renderTexture.width, renderTexture.height); var frame = new Texture2D(renderTexture.width, renderTexture.height, TextureFormat.RGB24, false);//    -  RGB24   ,    RGBA32 var visibleColorsSet = new HashSet<Color>(); foreach (var cameraTransform in cameraTransforms) { SetCameraTransform(camera, cameraTransform); CreateScreenShot(camera, renderTexture, frame, rtRect); visibleColorsSet.UnionWith(GetTextureColors(frame)); } return visibleColorsSet; } public static void SetCameraTransform(Camera camera, CameraTransform camTransform) { camera.transform.position = camTransform.Position; camera.transform.rotation = camTransform.Rotation; camera.fieldOfView = camTransform.FieldOfView; camera.orthographic = camTransform.IsOrthographic; camera.nearClipPlane = camTransform.NearClippingPlane; camera.farClipPlane = camTransform.FarClippingPlane; } private static HashSet<Color> GetTextureColors(Texture2D texture) { return new HashSet<Color>(texture.GetPixels()); } private static void CreateScreenShot(Camera cam, RenderTexture renderTexture, Texture2D screenShot, Rect renderTextureRect) { cam.targetTexture = renderTexture; cam.Render(); RenderTexture.active = cam.targetTexture; screenShot.ReadPixels(renderTextureRect, 0, 0); RenderTexture.active = null; cam.targetTexture = null; } } 


Vale ressaltar que, devido à discretização, alguns triângulos podem não ser exibidos devido ao tamanho especialmente pequeno de sua projeção na tela, e não porque algo se sobreponha a eles ou eles estejam do lado errado. Implementamos uma versão conservadora do algoritmo. Nesse caso, o AABB da projeção do triângulo na tela é calculado e, se pelo menos um de seus lados for menor que o lado texel da imagem, esse polígono será marcado como visível. Essa abordagem protege contra artefatos ao executar o algoritmo com uma resolução menor que a resolução da tela do dispositivo de destino. Se você ignorar polígonos pequenos, o resultado também será aceitável, desde que a resolução da textura de renderização usada seja superior à resolução das telas dos dispositivos pretendidos.

Implementamos esse algoritmo de corte no Unity e o usamos para otimizar objetos estáticos cujos modelos são encontrados na cena mais de uma vez em várias posições. Este é principalmente o cenário: pedras, árvores, estátuas, vasos, etc., que se refere à pré-fabricada frequentemente usada. Gostaríamos de otimizar esses objetos anteriormente, no estágio de criação em um pacote 3D, mas quem sabe em qual pose fantasmagórica o designer de nível deseja colocar seus candelabros favoritos.

Aparar o conjunto de objetos do mesmo tipo com essa ferramenta reduz o tamanho da cena, pois durante o lote estático, os dados da malha pré-fabricada comum são copiados de qualquer maneira no estágio de construção quantas vezes os objetos desenhados ativos com essa malha são representados na cena. Nosso método também libera espaço em atlas de textura, como o lightmap . Usamos o espaço economizado para aumentar os detalhes das partes dos modelos que sobreviveram à limpeza.

Corte 3D


No entanto, é melhor que o artista possa cortar tudo desnecessário em seu editor, reduzindo assim o número de etapas da preparação do conteúdo. Isso se justifica quando o modelo é usado em uma cena com apenas uma rotação predefinida em relação à câmera. Anteriormente, os objetos que eram exatamente voltados para o usuário por um lado eram frequentemente simplificados manualmente antes da integração no projeto. É importante observar que implementar essa simplificação programaticamente no Unity é muito mais difícil devido à complexidade do empacotamento do desenvolvimento UV ; portanto, a automação no estágio de um pacote 3D às vezes facilita a vida de um artista.

Uma das ferramentas para trabalhar com modelos 3D em nossa empresa é o Blender . Nós subimos nele. Parece que um software "adulto", como o Blender , deve ter uma funcionalidade semelhante. No entanto, descobriu-se que ele não deveria. Eu tive que ver minha própria bicicleta.

A primeira idéia foi usar a ferramenta de seleção familiar - essencialmente repita parte do trabalho manual do artista para um ângulo de câmera: selecione polígonos visíveis, inverta a seleção e exclua. O plano era o seguinte: mover a câmera, determinar a projeção AABB do modelo em cada posição, solicitar o resultado da seleção dos polígonos da área correspondente à AABB , obter a união do conjunto de polígonos da visualização atual com os anteriores e excluir polígonos não selecionados no final.

No entanto, durante a implementação do script, foi encontrada uma desvantagem significativa em termos da tarefa. As ferramentas de seleção no Blender (seleção de retângulo, seleção de círculo) perdem a precisão com o aumento do número de elementos selecionados por unidade de área da tela (alguns polígonos permanecem desmarcados), o que torna impossível o uso em nossas ferramentas de automação. Fato interessante: no mesmo 3ds Max, esse problema não é observado.


Destacando de longe no Blender


Resultado da seleção

A próxima tentativa teve como objetivo resolver o problema de frente: enviamos raios da câmera através de cada pixel da janela de visualização e observamos quais polígonos foram os primeiros a se cruzar com pelo menos um raio. Não esperávamos resultados precisos com essa abordagem, mas valia a pena tentar. O resultado é óbvio: produtividade muito baixa ao processar na CPU ou os mesmos orifícios com um pequeno número de raios.

No entanto, estabelecemos uma base para a implementação de uma abordagem mais avançada. A idéia era selecionar um certo número de pontos aleatórios em cada polígono e enviar raios da câmera em sua direção. Essa abordagem funcionou bem, mas tivemos alguns casos limítrofes: os polígonos também foram cortados, nos quais o ângulo entre a viga e sua normalidade era aproximadamente igual a π / 2. Assim, quando a câmera faz zoom devido a distorções de perspectiva, áreas de corte podem abrir.

Na opinião dos artistas, esse método era muito agressivo, por isso decidimos focar em cortar apenas os backfaces .

Conclusão


Não é segredo que uma atitude cuidadosa com os recursos do dispositivo ao criar jogos é o fator mais importante que afeta a qualidade do produto final. Isto é especialmente verdade para plataformas móveis, sombrias para o uso ativo da RAM. Reduzir o número de polígonos permite preencher com mais eficiência o espaço dos atlas de textura e reduzir um pouco a carga computacional.

Além disso, não se esqueça do custo de horas de trabalho e do custo de erros ao usar as ferramentas descritas acima e similares. A abordagem proposta pressupõe um pipeline que funcione bem para o trabalho do departamento de arte, especialmente os funcionários envolvidos na integração de modelos no projeto.

Assim, tendo as condições e ferramentas discutidas neste artigo, aderimos às seguintes regras. Se for assumido que o modelo criado sempre será virado de um lado para o usuário, e também se, por esses ângulos, a sobreposição de algumas partes do modelo por outras for muito pequena, o artista usará o aparador de backface em um editor 3D, verificará a correção e procederá ao desenvolvimento de UV . Se o modelo for usado frequentemente em posições diferentes ou tiver uma geometria mais complexa, depois de importar para o projeto, executaremos o algoritmo descrito na primeira parte do artigo, processando todos os objetos estáticos na cena com ele.

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


All Articles