Es reicht nicht aus, Polygone zu zählen, um 3D-Modelle zu optimieren

Bild

Nachdem Sie die Grundlagen des Mesh-Rendering-Prozesses verstanden haben, können Sie verschiedene Techniken anwenden, um die Rendering-Geschwindigkeit zu optimieren.

Einführung


Wie viele Polygone kann ich verwenden? Dies ist eine sehr häufige Frage, die Künstler beim Erstellen von Modellen für das Echtzeit-Rendering stellen. Diese Frage ist schwer zu beantworten, da es nicht nur um Zahlen geht.

Ich begann meine Karriere als 3D-Künstler in der Ära der ersten PlayStation und wurde später Grafikprogrammierer. Ich möchte diesen Artikel lesen, bevor ich anfing, 3D-Modelle für Spiele zu erstellen. Die darin berücksichtigten grundlegenden Grundlagen sind für viele Künstler nützlich. Obwohl die meisten Informationen in diesem Artikel die Produktivität Ihrer täglichen Arbeit nicht wesentlich beeinflussen, erhalten Sie ein grundlegendes Verständnis dafür, wie die Grafikverarbeitungseinheit (GPU) die von Ihnen erstellten Netze rendert.

Die Geschwindigkeit des Renderns hängt normalerweise von der Anzahl der Polygone im Netz ab. Obwohl die Anzahl der Polygone häufig mit der Bildrate pro Sekunde (FPS) korreliert, können Sie feststellen, dass das Netz auch nach dem Reduzieren der Anzahl der Polygone immer noch langsam gerendert wird. Wenn Sie jedoch verstehen, wie Netze im Allgemeinen gerendert werden, können Sie eine Reihe von Techniken anwenden, um die Rendergeschwindigkeit zu erhöhen.

Wie Polygondaten dargestellt werden


Um zu verstehen, wie die GPU Polygone zeichnet, müssen Sie zunächst die Datenstruktur berücksichtigen, die zur Beschreibung der Polygone verwendet wird. Ein Polygon besteht aus einer Reihe von Punkten, die als Eckpunkte und Verknüpfungen bezeichnet werden. Scheitelpunkte werden häufig wie beispielsweise in Abbildung 1 als Arrays von Werten gespeichert.


Abbildung 1. Ein Array einfacher Polygonwerte.

In diesem Fall ergeben vier Eckpunkte in drei Dimensionen (x, y und z) 12 Werte. Um Polygone zu erstellen, beschreibt das zweite Wertearray die Scheitelpunkte selbst, wie in Abbildung 2 dargestellt.


Abbildung 2. Eine Reihe von Verknüpfungen zu den Scheitelpunkten.

Diese miteinander verbundenen Eckpunkte bilden zwei Polygone. Beachten Sie, dass zwei Dreiecke mit jeweils drei Winkeln durch vier Eckpunkte beschrieben werden können, da die Eckpunkte 1 und 2 in beiden Dreiecken verwendet werden. Damit die GPU diese Daten verarbeiten kann, wird angenommen, dass jedes Polygon dreieckig ist. GPUs erwarten, dass Sie mit Dreiecken arbeiten, da diese speziell zum Zeichnen entwickelt wurden. Wenn Sie Polygone mit einer anderen Anzahl von Scheitelpunkten zeichnen müssen, benötigen Sie eine Anwendung, die sie vor dem Rendern auf der GPU in Dreiecke unterteilt. Wenn Sie beispielsweise einen Würfel mit sechs Polygonen erstellen, von denen jedes vier Seiten hat, ist dies nicht effektiver als das Erstellen eines Würfels mit 12 Polygonen, die aus drei Seiten bestehen. Es sind diese Dreiecke, die die GPU zeichnen wird. Denken Sie an die Regel: Sie müssen nicht Polygone, sondern Dreiecke zählen.

Die im vorherigen Beispiel verwendeten Scheitelpunktdaten sind dreidimensional, dies ist jedoch nicht erforderlich. Zwei Dimensionen mögen für Sie ausreichend sein, aber häufig müssen Sie andere Daten speichern, z. B. UV-Koordinaten für Texturen und Normal für die Beleuchtung.

Polygonzeichnung


Beim Rendern eines Polygons bestimmt die GPU zunächst, wo das Polygon gezeichnet werden soll. Dazu berechnet er die Position auf dem Bildschirm, an der sich die drei Eckpunkte befinden sollen. Diese Operation wird als Transformation bezeichnet. Diese Berechnungen in der GPU werden von einem kleinen Programm namens Vertex Shader durchgeführt.

Der Vertex-Shader führt häufig andere Arten von Operationen aus, z. B. das Verarbeiten von Animationen. Nach der Berechnung der Positionen aller drei Eckpunkte des Polygons berechnet die GPU, welche Pixel sich in diesem Dreieck befinden, und beginnt dann, diese Pixel mit einem anderen kleinen Programm namens „Fragment Shader“ (Fragment Shader) zu füllen. Ein Fragment-Shader wird normalerweise einmal pro Pixel ausgeführt. In einigen seltenen Fällen kann es jedoch mehrmals pro Pixel ausgeführt werden, um beispielsweise das Anti-Aliasing zu verbessern. Fragment-Shader werden häufig als Pixel-Shader bezeichnet, da Fragmente in den meisten Fällen Pixeln entsprechen (siehe Abbildung 3).


Abbildung 3. Ein auf dem Bildschirm gezeichnetes Polygon.

Abbildung 4 zeigt die Abfolge der Aktionen, die die GPU beim Rendern des Polygons ausführt.


Abbildung 4. Die Reihenfolge der GPU, die das Polygon rendert.

Wenn Sie das Dreieck in zwei Teile teilen und beide Dreiecke zeichnen (siehe Abbildung 5), entspricht das Verfahren Abbildung 6.


Abbildung 5. Aufteilung des Polygons in zwei Teile.


Abbildung 6. Vorgehensweise beim Zeichnen von zwei Polygonen durch die GPU.

In diesem Fall sind doppelt so viele Transformationen und Vorbereitungen erforderlich. Da jedoch die Anzahl der Pixel gleich bleibt, müssen bei der Operation keine zusätzlichen Pixel gerastert werden. Dies zeigt, dass das Verdoppeln der Anzahl der Polygone nicht unbedingt die Renderzeit verdoppelt.

Vertex-Cache verwenden


Wenn Sie sich die beiden Polygone aus dem vorherigen Beispiel ansehen, sehen Sie, dass sie zwei gemeinsame Eckpunkte haben. Es kann davon ausgegangen werden, dass diese Scheitelpunkte zweimal berechnet werden müssen. Mit einem Mechanismus, der als Scheitelpunkt-Cache bezeichnet wird, können Sie die Berechnungsergebnisse jedoch wiederverwenden. Die Ergebnisse der Vertex-Shader-Berechnungen für die Wiederverwendung werden im Cache gespeichert, einem kleinen Speicherbereich, der die letzten Vertices enthält. Das Verfahren zum Zeichnen von zwei Polygonen mithilfe des Vertex-Cache ist in Abbildung 7 dargestellt.


Abbildung 7. Zeichnen von zwei Polygonen mithilfe des Scheitelpunktcaches

Dank des Scheitelpunkt-Cache können Sie zwei Polygone fast so schnell wie eines zeichnen, wenn sie gemeinsame Scheitelpunkte haben.

Wir beschäftigen uns mit den Parametern der Eckpunkte


Damit der Scheitelpunkt wiederverwendbar ist, muss er bei jeder Verwendung unverändert bleiben. Natürlich sollte die Position gleich bleiben, aber auch andere Parameter sollten sich nicht ändern. Die nach oben übergebenen Parameter hängen vom verwendeten Motor ab. Hier sind zwei allgemeine Parameter:

  • Texturkoordinaten
  • Normal

Wenn UV-Strahlung auf ein 3D-Objekt angewendet wird, bedeutet jede erstellte Naht, dass die Scheitelpunkte entlang der Naht nicht gemeinsam genutzt werden können. Daher sollten im allgemeinen Fall Nähte vermieden werden (siehe Abbildung 8).


Abbildung 8. UV-Nahttextur.

Für eine ordnungsgemäße Beleuchtung der Oberfläche speichert jeder Scheitelpunkt normalerweise eine Normale - einen von der Oberfläche gerichteten Vektor. Aufgrund der Tatsache, dass alle Polygone mit einem gemeinsamen Scheitelpunkt durch eine Normale definiert sind, scheint ihre Form glatt zu sein. Dies wird als glatte Schattierung bezeichnet. Wenn jedes Dreieck seine eigenen Normalen hat, werden die Kanten zwischen den Polygonen ausgeprägt und die Oberfläche erscheint flach. Daher wird dies als flach schattiert bezeichnet. Abbildung 9 zeigt zwei identische Netze, eines mit glatter Schattierung und das andere mit flacher Schattierung.


Abbildung 9. Vergleich von glatter mit flacher Schattierung.

Diese glatt schattierte Geometrie besteht aus 18 Dreiecken und hat 16 gemeinsame Eckpunkte. Für die flache Schattierung von 18 Dreiecken sind 54 (18 x 3) Scheitelpunkte erforderlich, da keiner der Scheitelpunkte gemeinsam genutzt wird. Selbst wenn zwei Netze die gleiche Anzahl von Polygonen haben, ist ihre Rendergeschwindigkeit immer noch unterschiedlich.

Bedeutung der Form


GPUs arbeiten schnell, hauptsächlich weil sie viele Operationen parallel ausführen können. GPU-Marketingmaterialien konzentrieren sich häufig auf die Anzahl der Pipelines, die bestimmen, wie viele GPUs gleichzeitig arbeiten können. Wenn die GPU das Polygon zeichnet, haben viele Pipelines die Aufgabe, die Pixelquadrate zu füllen. Dies ist normalerweise ein Quadrat von acht mal acht Pixeln. Die GPU tut dies so lange, bis alle Pixel voll sind. Offensichtlich sind die Dreiecke keine Quadrate, daher befinden sich einige Pixel des Quadrats innerhalb des Dreiecks und andere außerhalb. Die Ausrüstung funktioniert mit allen Pixeln in einem Quadrat, auch mit Pixeln außerhalb des Dreiecks. Nach der Berechnung aller Eckpunkte im Quadrat verwirft das Gerät die Pixel außerhalb des Dreiecks.

Abbildung 10 zeigt ein Dreieck, für dessen Zeichnen drei Quadrate (Kacheln) erforderlich sind. Die meisten berechneten Pixel (Cyan) werden verwendet, und die rot dargestellten Pixel überschreiten die Grenzen des Dreiecks und werden verworfen.


Abbildung 10. Drei Kacheln zum Zeichnen eines Dreiecks.

Das Polygon in Abbildung 11 mit genau der gleichen Anzahl von Pixeln, jedoch gestreckt, erfordert mehr Kacheln zum Füllen. Die meisten Ergebnisse in jeder Kachel (roter Bereich) werden verworfen.


Abbildung 11. Füllen von Kacheln in einem gestreckten Bild.

Die Anzahl der gerenderten Pixel ist nur einer der Faktoren. Die Form des Polygons ist ebenfalls wichtig. Vermeiden Sie zur Steigerung der Effizienz lange, schmale Polygone und bevorzugen Sie Dreiecke mit ungefähr gleichen Seitenlängen, deren Winkel nahe bei 60 Grad liegen. Die beiden flachen Flächen in Abbildung 12 sind auf zwei verschiedene Arten trianguliert, sehen jedoch beim Rendern gleich aus.


Abbildung 12. Auf zwei verschiedene Arten triangulierte Oberflächen.

Sie haben genau die gleiche Anzahl von Polygonen und Pixeln, aber da die Oberfläche der linken Seite längere, schmalere Polygone als die rechte hat, wird sie langsamer gerendert.

Neu zeichnen


Um einen sechszackigen Stern zu zeichnen, können Sie ein Netz aus 10 Polygonen erstellen oder dieselbe Form aus nur zwei Polygonen zeichnen, wie in Abbildung 13 dargestellt.


Abbildung 13. Zwei verschiedene Möglichkeiten zum Rendern eines sechszackigen Sterns.

Sie können entscheiden, dass das Zeichnen von zwei Polygonen schneller als 10 ist. In diesem Fall ist dies jedoch höchstwahrscheinlich falsch, da die Pixel in der Mitte des Sterns zweimal gezeichnet werden. Dieses Phänomen wird als Überziehung bezeichnet. Im Wesentlichen bedeutet dies, dass Pixel mehr als einmal neu gezeichnet werden. Das Neuzeichnen erfolgt natürlich während des gesamten Rendervorgangs. Wenn ein Zeichen beispielsweise teilweise von einer Spalte ausgeblendet wird, wird es vollständig gezeichnet, obwohl die Spalte einen Teil des Zeichens überlappt. Einige Engines verwenden komplexe Algorithmen, um das Rendern von Objekten zu vermeiden, die im endgültigen Bild nicht sichtbar sind. Dies ist jedoch eine schwierige Aufgabe. Die CPU ist oft schwieriger herauszufinden, was nicht gerendert werden muss, als die GPU, um es zu zeichnen.

Als Künstler müssen Sie sich damit abfinden, dass Sie das Neulackieren nicht loswerden können. Es empfiehlt sich jedoch, nicht sichtbare Oberflächen zu entfernen. Wenn Sie mit einem Entwicklungsteam zusammenarbeiten, bitten Sie darum, der Spiel-Engine einen Debugging-Modus hinzuzufügen, in dem alles transparent wird. Dies erleichtert das Auffinden versteckter Polygone, die gelöscht werden können.

Implementierung einer Schublade auf dem Boden


Abbildung 14 zeigt eine einfache Szene: eine Kiste, die auf dem Boden steht. Der Boden besteht nur aus zwei Dreiecken, und die Box besteht aus 10 Dreiecken. Das Neuzeichnen in dieser Szene wird rot angezeigt.


Abbildung 14. Eine Schublade auf dem Boden.

In diesem Fall zieht die GPU einen Teil des Bodens mit einer Schublade auf den Boden, obwohl er nicht sichtbar ist. Wenn wir stattdessen ein Loch im Boden unter der Box erzeugt hätten, hätten wir mehr Polygone erhalten, aber viel weniger neu gezeichnet, wie aus Abbildung 15 ersichtlich ist.


Abbildung 15. Ein Loch unter der Schublade, um ein erneutes Zeichnen zu vermeiden.

In solchen Fällen hängt alles von Ihrer Wahl ab. Manchmal lohnt es sich, die Anzahl der Polygone zu reduzieren und dafür eine Neuzeichnung zu erhalten. In anderen Situationen lohnt es sich, Polygone hinzuzufügen, um ein erneutes Zeichnen zu vermeiden. Ein weiteres Beispiel: Die beiden unten gezeigten Abbildungen sind gleich aussehende Oberflächennetze, aus denen Punkte herausragen. Im ersten Netz (Abbildung 16) befinden sich die Spitzen auf der Oberfläche.


Abbildung 16. Die Spitzen befinden sich auf der Oberfläche.

Im zweiten Netz in Abbildung 17 werden unter den Spitzen Löcher in die Oberfläche geschnitten, um das Nachzeichnen zu verringern.


Abbildung 17. Löcher werden unter den Spitzen ausgeschnitten.

In diesem Fall wurden viele Polygone hinzugefügt, um Löcher zu schneiden, von denen einige eine schmale Form haben. Außerdem ist die Oberfläche der Neuzeichnung, die wir entfernt haben, nicht sehr groß, so dass diese Technik in diesem Fall unwirksam ist.

Stellen Sie sich vor, Sie modellieren ein Haus, das auf dem Boden steht. Um es zu schaffen, können Sie entweder die Erde unverändert lassen oder ein Loch in den Boden unter dem Haus schneiden. Neu zeichnen ist mehr, wenn das Loch nicht unter dem Haus ausgeschnitten ist. Die Wahl hängt jedoch von der Geometrie und dem Blickwinkel ab, von dem aus der Spieler das Haus sehen wird. Wenn Sie Erde unter die Basis des Hauses zeichnen, wird dies zu einer großen Menge an Neuzeichnungen führen, wenn Sie in das Haus gehen und nach unten schauen. Der Unterschied wird jedoch nicht besonders groß sein, wenn Sie das Haus von einem Flugzeug aus betrachten. In diesem Fall ist es am besten, einen Debugging-Modus in der Spiel-Engine zu haben, der die Oberflächen transparent macht, damit Sie sehen können, was unter den für den Spieler sichtbaren Oberflächen gezeichnet wird.

Wenn Z-Puffer einen Z-Konflikt haben


Wie bestimmt die GPU, wenn zwei überlappende Polygone gezeichnet werden, welches übereinander liegt? Die ersten Computergrafikforscher haben viel Zeit damit verbracht, dieses Problem zu untersuchen. Ed Catmell (der später Präsident von Pixar und Walt Disney Animation Studios wurde) schrieb einen Artikel, in dem zehn verschiedene Ansätze für diese Aufgabe beschrieben wurden. In einem Teil des Artikels stellt er fest, dass die Lösung dieses Problems trivial sein wird, wenn Computer über genügend Speicher verfügen, um einen Tiefenwert pro Pixel zu speichern. In den 1970er und 1980er Jahren war es eine sehr große Menge an Speicher. Heutzutage funktionieren die meisten GPUs jedoch so: Ein solches System wird als Z-Puffer bezeichnet.

Der Z-Puffer (auch als Tiefenpuffer bezeichnet) funktioniert wie folgt: Jedem Pixel ist sein Tiefenwert zugeordnet. Wenn ein Gerät ein Objekt zeichnet, berechnet es, wie weit ein Pixel von der Kamera entfernt ist. Anschließend wird der Tiefenwert eines vorhandenen Pixels überprüft. Wenn es weiter von der Kamera entfernt ist als das neue Pixel, wird das neue Pixel gezeichnet. Befindet sich ein vorhandenes Pixel näher an der Kamera als ein neues, wird das neue Pixel nicht gezeichnet. Dieser Ansatz löst viele Probleme und funktioniert auch dann, wenn sich die Polygone schneiden.


Abbildung 18. Durchschneidende Polygone, die vom Tiefenpuffer verarbeitet werden.

Der Z-Puffer hat jedoch keine unendliche Genauigkeit. Wenn sich zwei Oberflächen fast im gleichen Abstand von der Kamera befinden, verwirrt dies die GPU und sie kann zufällig eine der Oberflächen auswählen, wie in Abbildung 19 gezeigt.


Abbildung 19. Oberflächen mit derselben Tiefe weisen Anzeigeprobleme auf.

Dies nennt man Z-Fighting und sieht sehr fehlerhaft aus. Oft verschlimmern sich Z-Konflikte, je weiter die Oberfläche von der Kamera entfernt ist. Engine-Entwickler können Korrekturen einbauen, um dieses Problem zu beheben. Wenn ein Künstler jedoch Polygone erstellt, die nahe genug sind und sich überlappen, kann dennoch ein Problem auftreten. Ein anderes Beispiel ist eine Wand mit einem Poster. Das Poster befindet sich fast in der gleichen Tiefe von der Kamera wie die Wand dahinter, sodass das Risiko von Z-Konflikten sehr hoch ist. Die Lösung besteht darin, ein Loch in die Wand unter dem Poster zu schneiden. Dadurch wird auch das Neuzeichnen verringert.


Abbildung 20. Ein Beispiel für einen Z-Konflikt überlappender Polygone.

In extremen Fällen kann ein Z-Konflikt auftreten, selbst wenn sich die Objekte berühren. Abbildung 20 zeigt die Schublade auf dem Boden. Da wir unter der Schublade kein Loch in den Boden geschnitten haben, kann der Z-Puffer neben der Kante verwechselt werden, an der der Boden auf die Schublade trifft.

Draw Calls verwenden


GPUs sind extrem schnell geworden - so schnell, dass die CPUs möglicherweise nicht mit ihnen Schritt halten. Da GPUs im Wesentlichen für die Ausführung einer Aufgabe ausgelegt sind, ist es viel einfacher, schnell zur Arbeit zu kommen. Grafiken hängen von Natur aus mit der Berechnung mehrerer Pixel zusammen, sodass Sie Geräte erstellen können, die mehrere Pixel parallel berechnen. Die GPU rendert jedoch nur das, was sie zum Zeichnen der CPU befiehlt. Wenn die CPU die GPU nicht schnell mit Daten „versorgen“ kann, befindet sich die Grafikkarte im Leerlauf. Jedes Mal, wenn die CPU der GPU befiehlt, etwas zu zeichnen, wird dies als Zeichenaufruf bezeichnet. Der einfachste Zeichenaufruf besteht aus dem Rendern eines Netzes, einschließlich eines Shaders und eines Satzes von Texturen.

Stellen Sie sich einen langsamen Prozessor vor, der 100 Zeichenaufrufe pro Frame übertragen kann, und eine schnelle GPU, die eine Million Polygone pro Frame zeichnen kann. In diesem Fall kann ein idealer Zeichenaufruf 10.000 Polygone zeichnen. Wenn Ihre Netze nur aus 100 Polygonen bestehen, kann die GPU nur 10.000 Polygone pro Frame zeichnen. Das heißt, 99% der Zeit ist die GPU inaktiv. In diesem Fall können wir die Anzahl der Polygone in den Netzen leicht erhöhen, ohne etwas zu verlieren.

Woraus der Draw Call besteht und welche Kosten er verursacht, hängt stark von bestimmten Engines und Architekturen ab. Einige Engines können viele Netze in einem Draw-Aufruf kombinieren (Batching, Batch ausführen), aber alle Netze müssen denselben Shader haben oder andere Einschränkungen haben. Neue APIs wie Vulkan und DirectX 12 wurden speziell entwickelt, um dieses Problem zu lösen, indem die Kommunikation des Programms mit dem Grafiktreiber optimiert wird, wodurch die Anzahl der Draw-Aufrufe erhöht wird, die in einem einzelnen Frame übertragen werden können.

Wenn Ihr Team eine eigene Engine schreibt, fragen Sie die Entwickler der Engine, welche Einschränkungen Draw Calls haben. Wenn Sie eine vorgefertigte Engine wie Unreal oder Unity verwenden, führen Sie Leistungsbenchmarks aus, um die Grenzen der Funktionen der Engine zu bestimmen. Möglicherweise können Sie die Anzahl der Polygone erhöhen, ohne die Geschwindigkeit zu verringern.

Fazit


Ich hoffe, dieser Artikel dient als gute Einführung, um Ihnen zu helfen, die verschiedenen Aspekte der Renderleistung zu verstehen. In GPUs verschiedener Hersteller wird alles ein wenig auf seine Weise implementiert. Es gibt viele Vorbehalte und besondere Bedingungen in Bezug auf bestimmte Engines und Hardwareplattformen. Führen Sie immer einen offenen Dialog mit Rendering-Programmierern, um deren Empfehlungen in Ihrem Projekt zu verwenden.

Über den Autor


Eskil Steenberg ist ein unabhängiger Entwickler von Spielen und Tools. Er arbeitet als Berater und an unabhängigen Projekten. Alle Screenshots wurden in aktiven Projekten mit von Esquil entwickelten Tools aufgenommen. Weitere Informationen zu seiner Arbeit finden Sie auf der Website von Quel Solaar und auf seinem Twitter-Konto bei @quelsolaar.

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


All Articles