Recursos do trabalho com o Mesh in Unity

A computação gráfica, como você sabe, é a base da indústria de jogos. No processo de criação de conteúdo gráfico, inevitavelmente encontramos dificuldades associadas à diferença em sua apresentação no ambiente de criação e no aplicativo. A essas dificuldades são adicionados os riscos do simples descuido humano. Dada a escala do desenvolvimento de jogos, esses problemas surgem com frequência ou em grande número.

Combater essas dificuldades nos levou a pensar em automação e escrever artigos sobre esse tópico. A maior parte do material trata do trabalho com o Unity 3D , pois essa é a principal ferramenta de desenvolvimento do Plarium Krasnodar. A seguir, modelos 3D e texturas serão considerados como conteúdo gráfico.

Neste artigo, falaremos sobre os recursos de acesso a dados que representam objetos 3D no Unity . O material será útil principalmente para iniciantes, bem como para os desenvolvedores que raramente interagem com a representação interna desses modelos.



Sobre modelos 3D no Unity - para os menores




Na abordagem padrão, o Unity usa os componentes MeshFilter e MeshRenderer para renderizar o modelo. MeshFilter refere-se ao ativo Mesh que representa o modelo. Para a maioria dos shaders, as informações de geometria são um componente mínimo obrigatório para renderizar um modelo na tela. Os dados da verificação de textura e os ossos de animação podem não estar disponíveis se não estiverem envolvidos. Como essa classe é implementada internamente e como tudo é armazenado, há um mistério para a enésima quantidade de dinheiro em sete selos.

Lá fora, a malha como um objeto fornece acesso aos seguintes conjuntos de dados:

  • vértices - um conjunto de posições de vértices geométricos no espaço tridimensional com sua própria origem;
  • normais, tangentes - conjuntos de vetores normais e tangentes a vértices que são comumente usados ​​para calcular a iluminação;
  • UV, UV2, UV3, UV4, UV5, UV6, UV7, UV8 - conjuntos de coordenadas para varredura de textura;
  • colors, colors32 - conjuntos de valores de cores dos vértices, um exemplo de livro didático que é misturar textura por máscara;
  • bindposes - conjuntos de matrizes para posicionar vértices em relação aos ossos;
  • boneWeights - coeficientes de influência dos ossos nos topos;
  • triângulos - um conjunto de índices de vértices processados ​​3 por vez; cada um desses triplos representa um polígono (neste caso, um triângulo) do modelo.

O acesso a informações sobre vértices e polígonos é implementado através das propriedades correspondentes, cada uma das quais retorna uma matriz de estruturas. Para uma pessoa que não lê a documentação raramente trabalha com malhas no Unity , pode não ser óbvio que sempre que os dados do vértice são acessados, uma cópia do conjunto correspondente é criada na memória na forma de uma matriz com um comprimento igual ao número de vértices. Essa nuance é considerada em um pequeno bloco de documentação . Comentários sobre as propriedades da classe Mesh mencionadas acima também alertam sobre isso. A razão para esse comportamento é o recurso de arquitetura do Unity no contexto do tempo de execução Mono . Esquematicamente, isso pode ser representado da seguinte maneira:



O núcleo do mecanismo (UnityEngine (nativo)) é isolado dos scripts do desenvolvedor e o acesso a sua funcionalidade é implementado através da biblioteca UnityEngine (C #). De fato, é um adaptador, pois a maioria dos métodos serve como uma camada para receber dados do kernel. Ao mesmo tempo, o kernel e o restante, incluindo seus scripts, rodam sob diferentes processos e a parte do script conhece apenas a lista de comandos. Portanto, não há acesso direto à memória usada pelo kernel a partir do script.

Sobre o acesso a dados internos ou quão ruins podem ser as coisas


Para demonstrar como as coisas podem ser ruins, vamos analisar a quantidade de memória limpa pelo Garbage Collector usando um exemplo da documentação. Para simplificar a criação de perfil, envolva o mesmo código no método Update.

public class MemoryTest : MonoBehaviour { public Mesh Mesh; private void Update() { for (int i = 0; i < Mesh.vertexCount; i++) { float x = Mesh.vertices[i].x; float y = Mesh.vertices[i].y; float z = Mesh.vertices[i].z; DoSomething(x, y, z); } } private void DoSomething(float x, float y, float z) { //nothing to do } } 

Rodamos esse script com uma primitiva padrão - uma esfera (515 vértices). Usando a ferramenta Profiler , na guia Memória , você pode ver quanta memória foi marcada para coleta de lixo em cada quadro. Na nossa máquina de trabalho, esse valor era de ~ 9,2 Mb.



Isso é bastante, mesmo para um aplicativo carregado, e aqui lançamos uma cena com um objeto no qual o script mais simples está montado.

É importante mencionar os recursos do compilador .Net e a otimização de código. Passando pela cadeia de chamadas, você encontrará que chamar Mesh.vertices implica chamar o método externo do mecanismo. Isso impede que o compilador otimize o código dentro do nosso método Update () , apesar de DoSomething () estar vazio e as variáveis x, y, z não serem utilizadas por esse motivo.

Agora, armazenamos em cache a matriz de posições no início.

 public class MemoryTest : MonoBehaviour { public Mesh Mesh; private Vector3[] _vertices; private void Start() { _vertices = Mesh.vertices; } private void Update() { for (int i = 0; i < _vertices.Length; i++) { float x = _vertices[i].x; float y = _vertices[i].y; float z = _vertices[i].z; DoSomething(x, y, z); } } private void DoSomething(float x, float y, float z) { //nothing to do } } 



Em média 6 Kb. Outra coisa!

Esse recurso se tornou um dos motivos pelos quais tivemos que implementar nossa própria estrutura para armazenar e processar dados de malha.

Como fazemos


Durante o trabalho em grandes projetos, surgiu a idéia de criar uma ferramenta para análise e edição de conteúdo gráfico importado. Discutiremos os métodos de análise e transformação nos seguintes artigos. Agora, vejamos a estrutura de dados que decidimos escrever para a conveniência de implementar algoritmos, levando em consideração os recursos de acesso às informações sobre a malha.

Inicialmente, essa estrutura era assim:



Aqui, a classe CustomMesh representa a própria malha. Separadamente, na forma de Utilitário, implementamos a conversão de UntiyEngine.Mesh e vice-versa. Uma malha é definida por sua matriz de triângulos. Cada triângulo contém exatamente três arestas, que por sua vez são definidas por dois vértices. Decidimos adicionar aos vértices apenas as informações necessárias para análise, a saber: posição, normal, dois canais de varredura de textura ( uv0 para a textura principal, uv2 para iluminação) e cor.

Depois de algum tempo, surgiu a necessidade de subir a hierarquia. Por exemplo, para descobrir a partir de um triângulo a que malha pertence. Além disso, o downgrade do CustomMesh para o Vertex parecia pretensioso, e a quantidade irracional e significativa de valores duplicados me deu nos nervos. Por esses motivos, a estrutura teve que ser redesenhada.



O CustomMeshPool implementa métodos para gerenciamento conveniente e acesso a todos os CustomMesh processados. Devido ao campo MeshId , cada entidade tem acesso às informações de toda a malha. Essa estrutura de dados atende aos requisitos para as tarefas iniciais. É fácil expandir adicionando o conjunto de dados apropriado ao CustomMesh e os métodos necessários ao Vertex .

Vale a pena notar que essa abordagem não é ótima no desempenho. Ao mesmo tempo, a maioria dos algoritmos que implementamos concentra-se na análise de conteúdo no editor do Unity , e é por isso que você não precisa pensar frequentemente na quantidade de memória usada. Por esse motivo, armazenamos literalmente tudo o que é possível. Primeiro testamos o algoritmo implementado, depois refatoramos seus métodos e, em alguns casos, simplificamos as estruturas de dados para otimizar o tempo de execução.

Por enquanto é tudo. No próximo artigo, falaremos sobre como editar modelos 3D já adicionados ao projeto e usaremos a estrutura de dados considerada.

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


All Articles