Wie Sie wissen, ist Computergrafik die Basis der Spielebranche. Bei der Erstellung von Grafikinhalten stoßen wir unweigerlich auf Schwierigkeiten, die mit der unterschiedlichen Darstellung in der Erstellungsumgebung und in der Anwendung verbunden sind. Zu diesen Schwierigkeiten kommen die Risiken einer einfachen menschlichen Nachlässigkeit hinzu. Angesichts des Umfangs der Spieleentwicklung treten solche Probleme entweder häufig oder in großer Zahl auf.
Der Kampf gegen solche Schwierigkeiten veranlasste uns, über Automatisierung nachzudenken und Artikel zu diesem Thema zu schreiben. Der größte Teil des Materials befasst sich mit der Arbeit mit
Unity 3D , da dies das Hauptentwicklungswerkzeug in Plarium Krasnodar ist. Im Folgenden werden 3D-Modelle und Texturen als grafischer Inhalt betrachtet.
In diesem Artikel werden wir über die Funktionen des Zugriffs auf Daten sprechen, die 3D-Objekte in
Unity darstellen . Das Material ist in erster Linie für Anfänger sowie für Entwickler nützlich, die selten mit der internen Darstellung solcher Modelle interagieren.

Über 3D-Modelle in Unity - für die Kleinsten

Im Standardansatz verwendet
Unity die Komponenten
MeshFilter und
MeshRenderer , um das Modell zu rendern. MeshFilter bezieht sich auf das
Mesh- Asset, das das Modell darstellt. Für die meisten Shader sind Geometrieinformationen eine obligatorische Mindestkomponente zum Rendern eines Modells auf dem Bildschirm. Textur-Scan-Daten und Animationsknochen sind möglicherweise nicht verfügbar, wenn sie nicht beteiligt sind. Wie diese Klasse im Inneren implementiert ist und wie alles dort gespeichert ist, ist ein Rätsel für den
n-ten Geldbetrag in sieben Siegeln.
Außerhalb bietet das Netz als Objekt Zugriff auf die folgenden Datensätze:
- Scheitelpunkte - eine Reihe von Positionen von Geometriescheitelpunkten im dreidimensionalen Raum mit eigenem Ursprung;
- Normalen, Tangenten - Sätze von Normal- und Tangentenvektoren zu Eckpunkten, die üblicherweise zur Berechnung der Beleuchtung verwendet werden;
- uv, uv2, uv3, uv4, uv5, uv6, uv7, uv8 - Koordinatensätze für das Scannen von Texturen;
- Farben, Farben32 - Sätze von Farbwerten von Eckpunkten, von denen ein Lehrbuchbeispiel darin besteht, Textur nach Maske zu mischen;
- bindposes - Sätze von Matrizen zum Positionieren von Eckpunkten relativ zu Knochen;
- Knochengewichte - Einflusskoeffizienten von Knochen auf Spitzen;
- Dreiecke - eine Reihe von Scheitelpunktindizes, die jeweils 3 Mal verarbeitet werden; Jedes dieser Tripel repräsentiert ein Polygon (in diesem Fall ein Dreieck) des Modells.
Der Zugriff auf Informationen zu Scheitelpunkten und Polygonen wird über die entsprechenden Eigenschaften implementiert, von denen jede ein Array von Strukturen zurückgibt. Für eine Person,
die keine Dokumentation liest, die selten mit Netzen in
Unity arbeitet , ist es möglicherweise nicht offensichtlich, dass bei jedem Zugriff auf einen Scheitelpunkt im Speicher eine Kopie der entsprechenden Menge in Form eines Arrays mit einer Länge erstellt wird, die der Anzahl der Scheitelpunkte entspricht. Diese Nuance wird in einem kleinen
Dokumentationsblock betrachtet . Auch Kommentare zu den oben genannten Eigenschaften der
Mesh- Klasse warnen davor. Der Grund für dieses Verhalten ist das
Unity- Architekturmerkmal im Kontext der
Mono- Laufzeit. Schematisch kann dies wie folgt dargestellt werden:

Der Engine-Core (UnityEngine (native)) ist von den Skripten des Entwicklers isoliert, und der Zugriff auf seine Funktionalität wird über die UnityEngine-Bibliothek (C #) implementiert. Tatsächlich handelt es sich um einen Adapter, da die meisten Methoden als Schicht zum Empfangen von Daten vom Kernel dienen. Gleichzeitig drehen sich der Kernel und der Rest davon, einschließlich Ihrer Skripte, unter verschiedenen Prozessen, und der Skriptteil kennt nur die Liste der Befehle. Daher gibt es keinen direkten Zugriff auf den vom Kernel verwendeten Speicher aus dem Skript.
Über den Zugriff auf interne Daten oder wie schlimm Dinge sein können
Um zu demonstrieren, wie schlimm Dinge sein können, analysieren wir die Menge des von Garbage Collector gelöschten Speichers anhand eines Beispiels aus der Dokumentation. Um die Profilerstellung zu vereinfachen, schließen Sie denselben Code in die Update-Methode ein.
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) {
Wir haben dieses Skript mit einem Standardprimitiv ausgeführt - einer Kugel (515 Eckpunkte). Mit dem
Profiler- Tool können Sie auf der Registerkarte Speicher sehen, wie viel Speicher für die Speicherbereinigung in jedem Frame markiert wurde. Auf unserer Arbeitsmaschine betrug dieser Wert ~ 9,2 MB.

Dies ist selbst für eine geladene Anwendung ziemlich viel, und hier haben wir eine Szene mit einem Objekt gestartet, auf dem das einfachste Skript bereitgestellt ist.
Es ist wichtig, die Funktionen des
.Net- Compilers und die Codeoptimierung zu erwähnen. Wenn Sie die Aufrufkette durchlaufen, werden Sie feststellen, dass beim Aufrufen von
Mesh.vertices die
externe Methode der Engine
aufgerufen wird . Dies verhindert, dass der Compiler den Code in unserer
Update () -Methode optimiert, obwohl
DoSomething () leer ist und die Variablen
x, y, z aus diesem Grund nicht verwendet werden.
Jetzt zwischenspeichern wir das Array von Positionen am Anfang.
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) {

Im Durchschnitt 6 Kb. Eine andere Sache!
Diese Funktion wurde zu einem der Gründe, warum wir unsere eigene Struktur zum Speichern und Verarbeiten von Netzdaten implementieren mussten.
Wie machen wir das?
Während der Arbeit an großen Projekten entstand die Idee, ein Tool zur Analyse und Bearbeitung importierter Grafikinhalte zu erstellen. Wir werden die Methoden der Analyse und Transformation in den folgenden Artikeln diskutieren. Schauen wir uns nun die Datenstruktur an, die wir zur Vereinfachung der Implementierung von Algorithmen geschrieben haben, wobei die Merkmale des Zugriffs auf Informationen über das Netz berücksichtigt werden.
Anfangs sah diese Struktur so aus:

Hier repräsentiert die
CustomMesh- Klasse das Netz selbst. Separat haben wir in Form von
Utility die Konvertierung von
UntiyEngine.Mesh implementiert und umgekehrt. Ein Netz wird durch seine Anordnung von Dreiecken definiert. Jedes Dreieck enthält genau drei Kanten, die wiederum durch zwei Eckpunkte definiert sind. Wir haben beschlossen, den Scheitelpunkten nur die Informationen hinzuzufügen, die wir für die Analyse benötigen, nämlich: Position, Normal, zwei Textur-Scan-Kanäle (
uv0 für die
Haupttextur ,
uv2 für die Beleuchtung) und Farbe.
Nach einiger Zeit entstand die Notwendigkeit, die Hierarchie nach oben zu verschieben. Zum Beispiel, um aus einem Dreieck herauszufinden, zu welchem Netz es gehört. Darüber hinaus sah ein
Downgrade von
CustomMesh auf
Vertex prätentiös aus, und die unvernünftige und erhebliche Menge an doppelten Werten ging mir auf die Nerven. Aus diesen Gründen musste die Struktur neu gestaltet werden.
CustomMeshPool implementiert Methoden für die bequeme Verwaltung und den Zugriff auf alle verarbeiteten
CustomMesh . Aufgrund des
Felds MeshId hat jede Entität Zugriff auf die Informationen des gesamten Netzes. Diese Datenstruktur erfüllt die Anforderungen für die ersten Aufgaben. Es ist einfach zu erweitern, indem Sie
CustomMesh den entsprechenden Datensatz und
Vertex die erforderlichen Methoden hinzufügen.
Es ist anzumerken, dass dieser Ansatz in der Leistung nicht optimal ist. Gleichzeitig konzentrieren sich die meisten von uns implementierten Algorithmen auf die Analyse von Inhalten im
Unity- Editor, weshalb Sie nicht oft über die verwendete Speichermenge nachdenken müssen. Aus diesem Grund zwischenspeichern wir buchstäblich alles, was möglich ist. Wir testen zuerst den implementierten Algorithmus, überarbeiten dann seine Methoden und vereinfachen in einigen Fällen Datenstrukturen, um die Laufzeit zu optimieren.
Das ist alles für jetzt. Im nächsten Artikel werden wir darüber sprechen, wie 3D-Modelle bearbeitet werden, die bereits zum Projekt hinzugefügt wurden, und wir werden die berücksichtigte Datenstruktur verwenden.