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) {
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) {

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.