Wie Sie die grafische Qualität Ihrer Renderings in Unity-Spielen optimieren können.

In diesem Artikel werden wir Ihnen genau erläutern, was sich im Hintergrund abspielt, wenn Unity einen Frame rendert, welche Art von Performance-Problemen beim Rendern auftreten können und wie man Performance-Probleme im Zusammenhang mit dem Rendern behebt.

innenleben renderings e

Bevor wir beginnen, ist es wichtig zu verstehen, dass es keinen Ansatz zur Verbesserung der Rendering-Performance gibt. Die Rendering-Performance wird von vielen Faktoren innerhalb unseres Spiels beeinflusst und ist zudem stark von der Hardware und dem Betriebssystem abhängig, auf dem unser Spiel läuft. Das Wichtigste ist, dass wir Performanceprobleme lösen, indem wir die Ergebnisse unserer Experimente untersuchen, experimentieren und rigoros profilieren.

Dieser Artikel enthält Informationen über die häufigsten Probleme bezüglich der Render-Performance mit Vorschlägen zur Behebung dieser Probleme. Es ist möglich, dass unser Spiel ein Problem – oder eine Kombination von Problemen – haben könnte, welche hier nicht behandelt werden. Dieser Artikel wird uns jedoch dabei helfen, unser Problem zu verstehen und uns das Wissen und den Wortschatz anzueignen, um effektiv nach einer Lösung zu suchen.

Eine kurze Einführung in das Rendering.

Bevor wir beginnen, lassen Sie uns einen kurzen und etwas vereinfachten Blick darauf werfen, was passiert, wenn Unity einen Frame rendert. Das Verständnis der Event-Flows und der richtigen Begriffe für die Dinge wird uns helfen, unsere Performance-Probleme zu verstehen, zu analysieren und zu lösen.

In diesem Artikel werden wir den Begriff „Objekt“ verwenden, um ein Objekt zu bezeichnen, das in unserem Spiel wiedergegeben werden kann. Jedes Spielobjekt mit einer Renderer-Komponente wird als Objekt bezeichnet.

Auf der grundlegendsten Ebene kann das Rendern wie folgt beschrieben werden:

  • Die zentrale Prozessor-Einheit, die so genannte CPU, ermittelt, was und wie gezeichnet werden muss.
  • Die CPU sendet Anweisungen an die Grafikprozessoreinheit, die so genannte GPU.
  • Der Grafikprozessor zeichnet die Dinge gemäß den Anweisungen der CPU.

Lassen Sie uns einen genaueren Blick auf die Funktionsweise werfen. Wir werden jeden dieser Schritte später im Artikel näher erläutern. Vorher machen wir uns aber zunächst mit den verwendeten Begriffen vertraut und entwickeln ein Verständnis dafür, welche Rolle die CPU und GPU beim Rendern spielen.

Der Begriff, der oft verwendet wird, um das Rendering zu beschreiben, ist die Rendering-Pipeline, und das ist ein nützliches Bild, das man sich merken sollte. Effizientes Rendern bedeutet, dass Informationen fließend bleiben.

Für jeden Frame, der gerendert wird, erledigt die CPU die folgende Arbeit:

  • Die CPU überprüft jedes Objekt in der Szene, um festzustellen, ob es gerendert werden soll. Ein Objekt wird nur dann gerendert, wenn es bestimmte Kriterien erfüllt, z.B. muss sich ein Teil seines Begrenzungsrahmens innerhalb des Blickfeldes einer Kamera befinden. Objekte, die nicht gerendert werden sollen, gelten als geculled.
  • Die CPU sammelt Informationen über jedes zu rendernde Objekt und sortiert diese Daten in Befehle, die als Draw-Aufrufe bezeichnet werden. Ein Draw-Aufruf enthält Daten über ein einzelnes Mesh und wie dieses Mesh dargestellt werden soll, z.B. welche Texturen verwendet werden sollen. Unter bestimmten Umständen können Objekte, die Einstellungen teilen, zu einem einzigen Draw-Aufruf zusammengefasst werden. Das Zusammenführen von Daten für verschiedene Objekte in einem Draw-Aufruf wird als Batching bezeichnet.
  • Die CPU erstellt für jeden Draw-Aufruf ein Datenpaket, das als Batch bezeichnet wird. Chargen können manchmal andere Daten als Draw Calls enthalten, aber diese Situationen werden wahrscheinlich nicht zu gemeinsamen Leistungsproblemen beitragen, und deshalb werden wir diese in diesem Artikel nicht berücksichtigen.

Für jeden Batch, der einen Draw-Aufruf enthält, muss die CPU nun folgendes tun:

  • Die CPU kann einen Befehl an den Grafikprozessor senden, um eine Reihe von Variablen zu ändern, die zusammen als Renderstatus bezeichnet werden. Dieser Befehl wird als SetPass-Aufruf bezeichnet. Ein SetPass-Aufruf teilt dem Grafikprozessor mit, welche Einstellungen dieser verwenden muss, um das nächste Mesh darzustellen. Ein SetPass-Aufruf wird nur gesendet, wenn das nächste zu rendernde Mesh eine Änderung des Renderstatus gegenüber dem vorherigen Mesh erfordert.
  • Die GPU sendet den Draw-Aufruf an die GPU. Der Draw-Aufruf weist den Grafikprozessor an, das angegebene Mesh mit den Einstellungen zu rendern, die im letzten SetPass-Aufruf definiert wurden.
  • Unter bestimmten Umständen kann es vorkommen, dass mehr als ein Durchgang für das Batching erforderlich ist. Ein Pass ist ein Abschnitt von Shader-Code und ein neuer Pass erfordert eine Änderung des Renderstatus. Für jeden Durchlauf im Batch muss die CPU einen neuen SetPass-Aufruf senden und anschließend den Draw-Aufruf erneut senden.

In der Zwischenzeit erledigt der Grafikprozessor die folgenden Aufgaben:

  • Die GPU verarbeitet Aufgaben von der CPU in der Reihenfolge, in der sie gesendet wurden.
  • Wenn die aktuelle Aufgabe ein SetPass-Aufruf ist, aktualisiert der Grafikprozessor den Renderstatus.
  • Wenn die aktuelle Aufgabe ein Draw-Aufruf ist, rendert die GPU das Mesh. Dies geschieht in Stufen, die durch separate Abschnitte des Shader-Code definiert sind. Dieser Teil des Renderings ist komplex und wir wenden ihn nicht sehr detailliert behandeln, aber es ist nützlich für uns zu verstehen, dass ein Codeabschnitt mit der Bezeichnung Vertex Shader dem Grafikprozessor kommuniziert, wie dieser die Nodes des Meshes verarbeiten soll, und anschließend ein Codeabschnitt mit der Bezeichnung Fragment-Shader dem Grafikprozessor kommuniziert, wie dieser die einzelnen Pixel zeichnen soll.
  • Dieser Prozess wiederholt sich, bis alle von der CPU gesendeten Aufgaben vom Grafikprozessor verarbeitet wurden.

Da wir nun verstehen, was passiert, wenn Unity einen Frame rendert, lassen Sie uns die Art von Probleme betrachten, die beim Rendern auftreten können.

Arten von Rendering-Problemen.

Das Wichtigste, was man beim Rendern beachten muss, ist folgendes: Sowohl die CPU als auch der Grafikprozessor müssen alle ihre Aufgaben erledigen, um das Frame zu rendern. Wenn eine dieser Aufgaben zu lange dauert, führt dies zu einer Verzögerung beim Rendern des Frames.

Renderingprobleme haben zwei wesentliche Ursachen. Die erste Art von Problem wird durch eine ineffiziente Pipeline verursacht. Eine ineffiziente Pipeline ensteht, wenn einer oder mehrere der Schritte in der Rendering-Pipeline zu lange dauern, wodurch der reibungslose Datenfluss unterbrochen wird. Ineffizienzen innerhalb der Pipeline werden als Engpässe bezeichnet. Die zweite Art von Problem wird dadurch verursacht, dass man einfach versucht, zu viele Daten durch die Pipeline zu schieben. Selbst die effizienteste Pipeline hat eine Grenze, wie viele Daten sie in einem einzigen Frame verarbeiten kann.

Wenn unser Spiel zu lange braucht, um einen Frame zu rendern, weil die CPU zu lange braucht, um ihre Rendering-Aufgaben auszuführen, ist unser Spiel das, was als CPU-gebunden bekannt ist. Wenn unser Spiel zu lange braucht, um einen Frame zu rendern, weil der Grafikprozessor zu lange braucht, um seine Rendering-Aufgaben auszuführen, ist unser Spiel das, was als GPU-gebunden bekannt ist.

Rendering-Probleme verstehen.

Es ist wichtig, dass wir Profilerstellungstools einsetzen, um die Ursache von Performance-Problemen zu verstehen, bevor wir Änderungen vornehmen. Unterschiedliche Probleme erfordern unterschiedliche Lösungen. Es ist auch sehr wichtig, dass wir die Auswirkungen jeder Änderung messen. Die Behebung von Performance-Problemen ist ein Balanceakt, und die Verbesserung eines Leistungsaspekts kann sich negativ auf einen anderen auswirken.

Wir werden zwei Tools verwenden, die uns helfen, unsere Rendering-Performance-Probleme zu verstehen und zu beheben: das Profiler-Window und dem Frame Debugger: Beide Tools sind in Unity integriert.

Profiler Window.

Das Profiler Window ermöglicht es uns, Echtzeitdaten über die Leistung unseres Spiels zu sehen. Wir können das Profiler Window verwenden, um Daten über viele Aspekte unseres Spiels anzuzeigen, einschließlich der Speichernutzung, der Rendering-Pipeline und der Leistung von User Scripts.

Frame Debugger.

Der Frame Debugger ermöglicht es uns, Schritt für Schritt zu sehen, wie ein Frame dargestellt wird. Mit dem Frame Debugger können wir detaillierte Informationen wie z.B. was bei jedem Draw-Aufruf gezeichnet wird, Shader-Eigenschaften für jeden Draw-Aufruf und die Reihenfolge der an die GPU gesendeten Ereignisse anzeigen. Diese Informationen helfen uns zu verstehen, wie unser Spiel dargestellt wird und wo wir die Leistung verbessern können.

Suche nach der Ursache von Performance-Problemen.

Bevor wir versuchen, die Rendering-Leistung unseres Spiels zu verbessern, müssen wir sicher sein, dass unser Spiel aufgrund von Rendering-Problemen langsam läuft. Es macht keinen Sinn, unsere Rendering-Performance zu optimieren, wenn die eigentliche Ursache unseres Problems in übermäßig komplexen User Scripts liegt. Wenn Sie sich nicht sicher sind, ob sich Ihre Performance-Probleme auf das Rendern beziehen, sollten Sie sich vorher damit befassen.

Sobald wir festgestellt haben, dass sich unsere Probleme auf das Rendering beziehen, müssen wir auch verstehen, ob unser Spiel CPU- oder GPU-gebunden ist. Diese verschiedenen Probleme erfordern unterschiedliche Lösungen, daher ist es wichtig, dass wir die Ursache des Problems verstehen, bevor wir versuchen, es zu beheben.

Wir müssen wissen, ob sich unsere Probleme auf das Rendering beziehen und wir müssen wissen, ob unser Spiel CPU- oder GPU-gebunden ist, erst dann macht es Sinn, weiterzulesen.

Wenn unser Spiel CPU-gebunden ist.

Im Großen und Ganzen wird die Arbeit, die von der CPU geleistet werden muss, um einen Frame zu rendern, in drei Kategorien unterteilt:

  • Bestimmung, was gezeichnet werden muss,
  • Vorbereiten von Befehlen für den Grafikprozessor,
  • Senden von Befehlen an den Grafikprozessor.

Diese großen Kategorien enthalten viele einzelne Aufgaben, und diese Aufgaben können über mehrere Threads hinweg ausgeführt werden. Threads ermöglichen es, dass getrennte Aufgaben gleichzeitig ausgeführt werden, während ein Thread eine Aufgabe erfüllt, kann ein anderer Thread eine völlig separate Aufgabe ausführen. Das bedeutet, dass die Arbeit schneller erledigt werden kann. Wenn Rendering-Aufgaben auf einzelne Threads verteilt sind, spricht man von Multithread-Rendering.

Es gibt drei Arten von Threads, die am Rendering-Prozess von Unity beteiligt sind: den Hauptthread, den Renderthread und Worker-Threads. Der Haupt-Thread ist, wo die Mehrheit der CPU-Aufgaben für unser Spiel stattfindet, einschließlich einiger Rendering-Aufgaben. Der Render-Thread ist ein spezieller Thread, der Befehle an den Grafikprozessor sendet. Worker-Threads führen jeweils eine einzelne Aufgabe aus, wie z.B. Culling oder Mesh Skinning. Welche Aufgaben von welchem Thread ausgeführt werden, hängt von den Einstellungen unseres Spiels und der Hardware ab, auf der unser Spiel läuft. Je mehr CPU-Kerne unsere Zielhardware beispielsweise hat, desto mehr Worker-Threads können erzeugt werden. Aus diesem Grund ist es sehr wichtig, unser Spiel auf der Zielhardware zu profilieren. Unser Spiel kann auf verschiedenen Geräten sehr unterschiedlich funktionieren.

Da Multithread-Rendering komplex und hardwareabhängig ist, müssen wir verstehen, welche Aufgaben dazu führen, dass unser Spiel CPU-gebunden ist, bevor wir versuchen, die Leistung zu verbessern. Wenn unser Spiel langsam läuft, weil das Auslesen von Vorgängen auf dem Thread zu lange dauert, dann wird es uns nicht helfen, die Zeit zu verkürzen, die benötigt wird, um Befehle an den GPU auf einem anderen Thread zu senden.

Hinweis: Nicht alle Plattformen unterstützen Multithread-Rendering. Zum Zeitpunkt der Erstellung unterstützte WebGL diese Funktion nicht. Auf Plattformen, die kein Multithread-Rendering unterstützen, werden alle CPU-Tasks auf dem gleichen Thread ausgeführt. Wenn wir auf einer solchen Plattform CPU-gebunden sind, verbessert die Optimierung der CPU-Arbeit die CPU-Leistung. Wenn dies bei unserem Spiel der Fall ist, sollten wir alle folgenden Abschnitte lesen und überlegen, welche Optimierungen für unser Spiel am besten geeignet sind.

Grafik-Aufträge.

Die Option Grafikaufträge in den Player-Einstellungen bestimmt, ob Unity Worker-Threads verwendet, um Rendering-Aufgaben auszuführen, die ansonsten auf dem Hauptthread und in einigen Fällen auf dem Render-Thread durchgeführt würden. Auf Plattformen, auf denen diese Funktion verfügbar ist, kann sie eine erhebliche Leistungssteigerung bringen. Wenn wir diese Funktion nutzen wollen, sollten wir unser Spiel mit und ohne aktivierte Grafikaufträge profilieren und die Auswirkungen auf die Leistung beobachten.

Herausfinden, welche Aufgaben zu Problemen beitragen.

Wir können im Profiler Window feststellen, welche Aufgaben dazu führen, dass unser Spiel CPU-gebunden ist.

Da wir nun verstehen, welche Aufgaben dazu führen, dass unser Spiel an die CPU gebunden ist, lassen Sie uns einige allgemeine Probleme und deren Lösungen betrachten.

Senden von Befehlen an den Grafikprozessor.

Die Zeit, die benötigt wird, um Befehle an den Grafikprozessor zu senden, ist der häufigste Grund dafür, dass ein Spiel CPU-gebunden ist. Diese Aufgabe wird auf den meisten Plattformen auf dem Render-Thread ausgeführt, wobei dies auf bestimmten Plattformen (z.B. PlayStation 4) von Worker-Threads durchgeführt werden kann.

Wir können sehen, wie viele SetPass-Aufrufe und Batches gesendet werden, wenn wir im Rendering Profiler das Unity-Profiler-Fenster sehen. Die Anzahl der SetPass-Aufrufe, die gesendet werden können, bevor die Performance leidet, hängt stark von der Zielhardware ab. Ein High-End-PC kann viel mehr SetPass-Aufrufe senden, bevor die Performance leidet als ein mobiles Gerät.

Die Anzahl der SetPass-Aufrufe und ihr Verhältnis zur Anzahl der Chargen hängt von mehreren Faktoren ab, auf die wir später im Artikel näher eingehen werden. Allerdings ist es in der Regel so:

  • Wenn Sie die Anzahl der Stapel reduzieren und/oder mehr Objekte den gleichen Renderstatus teilen, wird in den meisten Fällen die Anzahl der SetPass-Aufrufe reduziert.
  • Die Reduzierung der Anzahl der SetPass-Aufrufe wird in den meisten Fällen die CPU-Leistung verbessern.

Wenn die Reduzierung der Anzahl der Chargen die Anzahl der SetPass-Aufrufe nicht reduziert, kann dies dennoch zu eigenständigen Leistungssteigerungen führen. Denn die CPU kann einen einzelnen Batch effizienter verarbeiten als mehrere Batches, auch wenn sie die gleiche Menge an Mesh-Daten enthalten.

Es gibt im Allgemeinen drei Möglichkeiten, die Anzahl der Chargen und SetPass-Aufrufe zu reduzieren. Wir werden uns mit jedem einzelnen von ihnen eingehender befassen:

  • Die Reduzierung der Anzahl der zu rendernden Objekte wird wahrscheinlich sowohl die Chargen als auch die SetPass-Aufrufe reduzieren.
  • Die Reduzierung der Anzahl der Renderzeiten für jedes Objekt reduziert in der Regel die Anzahl der SetPass-Aufrufe.
  • Die Kombination der Daten von Objekten, die zu weniger Chargen gerendert werden müssen, reduziert die Anzahl der Chargen.

Verschiedene Techniken werden für verschiedene Spiele geeignet sein, also sollten wir alle diese Optionen in Betracht ziehen, entscheiden, welche in unserem Spiel funktionieren könnten und experimentieren.

Reduzierung der Anzahl der zu rendernden Objekte.

Die Reduzierung der Anzahl der zu rendernden Objekte ist der einfachste Weg, um die Anzahl der Chargen und SetPass-Aufrufe zu reduzieren. Es gibt mehrere Techniken, mit denen wir die Anzahl der gerenderten Objekte reduzieren können.

  • Die einfachste Reduzierung der Anzahl der sichtbaren Objekte in unserer Szene kann eine effektive Lösung sein. Wenn wir zum Beispiel eine große Anzahl verschiedener Charaktere in einer Menge darstellen, können wir damit experimentieren, einfach weniger diese Charaktere in der Szene zu haben. Wenn die Szene immer noch gut aussieht und sich die Leistung verbessert, wird dies wahrscheinlich eine viel schnellere Lösung sein als die Anwendung ausgefeilter Techniken.
  • Wir können den Zeichenabstand unserer Kamera mithilfe der Eigenschaft Far Clip Plane der Kamera reduzieren. Diese Eigenschaft ist die Entfernung, ab der Objekte von der Kamera nicht mehr dargestellt werden. Wenn wir die Tatsache verschleiern wollen, dass entfernte Objekte nicht mehr sichtbar sind, können wir versuchen, mit Nebel den Mangel an entfernten Objekten zu verbergen.
  • Für einen feinkörnigeren Ansatz zum Ausblenden von Objekten basierend auf der Entfernung können wir die Eigenschaft Layer Cull Distances unserer Kamera verwenden, um benutzerdefinierte Entfernungen für Objekte bereitzustellen, die sich auf separaten Ebenen befinden. Dieser Ansatz kann nützlich sein, wenn wir viele kleine dekorative Details im Vordergrund haben. Wir könnten diese Details in einem viel kürzeren Abstand ausblenden als große bei großen Terrain Features.
  • Wir können eine Technik mit der Bezeichnung Occlusion Culling verwenden, um die Darstellung von Objekten zu deaktivieren, die durch andere Objekte verborgen sind. Wenn es zum Beispiel ein großes Gebäude in unserer Szene gibt, können wir die Abschaffung von Occlusions verwenden, um die Darstellung von Objekten dahinter zu deaktivieren. Unity`s Occlusion Culling ist nicht für alle Szenen geeignet, kann zu zusätzlichem CPU-Overhead führen und kann komplex eingerichtet werden, kann aber in einigen Szenen die Performance deutlich verbessern. Zusätzlich zur Verwendung von Unity`s Occlusion Culling können wir auch unsere eigene Form der Occlusion Culling implementieren, indem wir manuell Objekte deaktivieren, von denen wir wissen, dass sie für den Spieler nicht sichtbar sind. Wenn unsere Szene beispielsweise Objekte enthält, die für eine Cutszene verwendet werden, aber vorher oder nachher nicht sichtbar sind, sollten wir sie deaktivieren. Unser Wissen über unser eigenes Spiel zu nutzen, ist immer effizienter, als Unity zu bitten, die Dinge dynamisch auszuarbeiten.

Reduzierung der Häufigkeit, mit der jedes Objekt gerendert werden muss.

Echtzeitbeleuchtung, Schatten und Reflexionen verleihen den Spielern viel Realismus, können aber sehr teuer sein. Die Verwendung dieser Funktionen kann dazu führen, dass Objekte mehrfach gerendert werden, was die Leistung erheblich beeinträchtigen kann.

Die genaue Auswirkung dieser Funktionen hängt von dem Rendering-Pfad ab, den wir für unser Spiel wählen. Der Rendering-Pfad ist der Begriff für die Reihenfolge, in der Berechnungen beim Zeichnen der Szene durchgeführt werden, und der Hauptunterschied zwischen den Rendering-Pfaden besteht darin, wie sie mit Echtzeit-Licht, Schatten und Reflexionen umgehen. In der Regel ist Deferred Rendering wahrscheinlich eine bessere Wahl, wenn unser Spiel auf High-End-Hardware läuft und viele Echtzeit-Lichter, Schatten und Reflexionen verwendet. Forward Rendering ist wahrscheinlich besser geeignet, wenn unser Spiel auf Low-End-Hardware läuft und diese Funktionen nicht nutzt. Dies ist jedoch ein sehr komplexes Thema, und wenn wir Echtzeitlicht, Schatten und Reflexionen nutzen wollen, ist es am besten, das Thema zu erforschen und zu experimentieren.

Unabhängig vom gewählten Rendering-Pfad kann die Verwendung von Echtzeit-Lichtern, -Schatten und -Reflexionen die Leistung unseres Spiels beeinflussen, und es ist wichtig zu verstehen, wie man sie optimiert.

  • Dynamische Beleuchtung in Unity ist ein sehr komplexes Thema und eine eingehende Diskussion darüber geht über den Rahmen dieses Artikels hinaus. Deshalb möchten wir dieses Thema hier nicht näher vertiefen.
  • Dynamische Beleuchtung ist teuer. Wenn unsere Szene Objekte enthält, die sich nicht bewegen, wie z.B. die Landschaft, können wir eine Technik mit der Bezeichnung Baken verwenden, um die Beleuchtung für die Szene vorzuberechnen, so dass keine Berechnungen der Laufzeitbeleuchtung erforderlich sind.
  • Wenn wir Echtzeit-Schatten in unserem Spiel verwenden wollen, ist dies wahrscheinlich ein Bereich, in dem wir die Leistung verbessern können. Beispielsweise können wir die Eigenschaft Shadow Distance verwenden, um sicherzustellen, dass nur Objekte in der Nähe Schatten werfen.
  • Reflexion Probes erzeugen realistische Reflexionen, können aber in Bezug auf die Chargen sehr kostspielig sein. Es ist am besten, den Einsatz von Reflexionen dort, wo Leistung gefragt ist, auf ein Minimum zu beschränken und sie dort, wo sie eingesetzt werden, so weit wie möglich zu optimieren.

Zusammenführung von Objekten zu weniger Chargen.

Eine Charge kann die Daten für mehrere Objekte enthalten, wenn bestimmte Bedingungen erfüllt sind. Um für die Dosierung in Frage zu kommen, müssen Objekte:

  • Teilen Sie die gleiche Instanz des gleichen Materials.
  • Identische Materialeinstellungen haben (z.B. Textur- oder Shader-Parameter).

Das Batching geeigneter Objekte kann die Leistung verbessern, obwohl wir, wie bei allen Optimierungstechniken, sorgfältig profilieren müssen, um sicherzustellen, dass die Kosten für das Batching die Leistungssteigerungen nicht übersteigen.

Es gibt einige verschiedene Techniken, um berechtigte Objekte zu batchen:

  • Static Batching ist eine Technik, die es Unity ermöglicht, in der Nähe von geeigneten Objekten, die sich nicht bewegen, zu batchen. Ein gutes Beispiel für etwas, das vom Static Batching profitieren könnte, ist das Batching ähnlicher Objekte, wie z.B. Felsbrocken. Static Batching kann zu einem höheren Speicherverbrauch führen, daher sollten wir diese Kosten bei der Erstellung unseres Spiels berücksichtigen.
  • Dynamic Batching ist eine weitere Technik, die es Unity ermöglicht, geeignete Objekte zu batchen, unabhängig davon, ob sie sich bewegen oder nicht. Es gibt einige Einschränkungen für die Objekte, die mit dieser Technik chargenpflichtig sind. Diese Einschränkungen können Sie im Unity-Handbuch nachlesen. Dynamic Batching hat einen Einfluss auf die CPU-Auslastung, was dazu führen kann, dass es mehr CPU-Zeit kostet, als es einspart. Wir sollten diese Kosten im Hinterkopf behalten, wenn wir mit dieser Technik experimentieren, und vorsichtig mit ihrer Anwendung sein.
  • Die Oberflächenelemente von Batching in Unity sind etwas komplexer, da sie vom Layout unserer Benutzeroberfläche beeinflusst werden können.
  • GPU Instancing ist eine Technik, die es ermöglicht, eine große Anzahl identischer Objekte sehr effizient zu batchen. Es gibt Einschränkungen bei der Verwendung und es wird nicht von jeder Hardware unterstützt, aber wenn unser Spiel viele identische Objekte auf einmal auf dem Bildschirm hat, können wir vielleicht von dieser Technik profitieren.
  • Textur Atlasing ist eine Technik, bei der mehrere Texturen zu einer größeren Textur kombiniert werden. Es wird häufig in 2D-Spielen und UI-Systemen verwendet, kann aber auch in 3D-Spielen verwendet werden. Wenn wir diese Technik bei der Erstellung von Kunstwerken für unser Spiel verwenden, können wir sicherstellen, dass Objekte Texturen teilen und somit für das Batching geeignet sind. Unity hat ein integriertes Textur-Atlas-Tool mit der Bezeichnung Sprite Packer für die Verwendung mit 2D-Spielen.
  • Es ist möglich, Meshes, die das gleiche Material und die gleiche Textur haben, manuell zu kombinieren, entweder im Unity-Editor oder über Code zur Laufzeit. Bei dieser Kombination von Meshes müssen wir uns darüber im Klaren sein, dass Schatten, Beleuchtung und Aussortierung immer noch auf objektbezogener Ebene arbeiten. Das bedeutet, dass einer Leistungssteigerung durch die Kombination von Meshes entgegengewirkt werden könnte, indem man diese Objekte nicht mehr aussortieren kann, obwohl sie sonst nicht gerendert worden wären. Wenn wir diesen Ansatz untersuchen wollen, sollten wir die Funktion Mesh-CombineMeshes untersuchen. Das CombineChildren-Skript im Paket Standard Assets von Unity ist ein Beispiel für diese Technik.
  • Beim Zugriff auf Renderer.material in Skripten müssen wir sehr vorsichtig sein. Dadurch wird das Material dupliziert und eine Referenz auf die neue Kopie zurückgegeben. Andernfalls wird das Batching abgebrochen, wenn der Renderer Teil einer Charge war, weil der Renderer keine Referenz mehr auf die gleiche Instanz des Materials hat. Wenn wir in einem Skript auf das Material eines gebatchten Objekts zugreifen möchten, sollten wir Renderer.sharedMaterial verwenden.

Culling, Sorting und Batching.

Culling, Sammeln von Daten über Objekte, die gezeichnet werden sollen, das Sortieren dieser Daten im Batching und das Erzeugen von GPU-Befehlen kann dazu beitragen, dass sie CPU-gebunden sind. Diese Aufgaben werden entweder auf dem Haupt-Thread oder auf einzelnen Worker-Threads ausgeführt, je nach den Einstellungen unseres Spiels und der Zielhardware.

  • Culling allein ist wahrscheinlich nicht sehr kostspielig, aber die Reduzierung unnötiger Cullings kann die Leistung verbessern. Es gibt einen Overhead pro Objekt und Kamera für alle aktiven Szenenobjekte, auch für solche, die sich auf Ebenen befinden, die nicht gerendert werden. Um dies zu reduzieren, sollten wir Kameras und Renderer deaktivieren, die derzeit nicht in Gebrauch sind.
  • Das Batching kann die Geschwindigkeit beim Senden von Befehlen an den Grafikprozessor erheblich verbessern, kann aber manchmal auch unerwünschte Overheads an anderer Stelle verursachen. Wenn Batching-Vorgänge dazu beitragen, dass unser Spiel CPU-gebunden ist, möchten wir vielleicht die Anzahl der manuellen oder automatischen Batching-Vorgänge in unserem Spiel begrenzen.

Skinned Meshes.

SkinnedMeshRenderer werden verwendet, wenn wir ein Mesh animieren, indem wir es mit einer Technik mit der Bezeichnung Bone Animation verformen. Es wird am häufigsten in animierten Charakteren verwendet. Aufgaben im Zusammenhang mit dem Rendern von Skinned Meshes werden in der Regel auf dem Hauptthread oder auf einzelnen Worker-Threads ausgeführt, abhängig von den Einstellungen unseres Spiels und der Zielhardware.

Das Rendern von Skinned Meshes kann eine kostspielige Angelegenheit sein. Wenn wir im Profiler Window sehen können, dass das Rendern von Skinned Meshes dazu beiträgt, dass unser Spiel CPU-gebunden ist, gibt es ein paar Dinge, die wir versuchen können, die Leistung zu verbessern:

  • Wir sollten überlegen, ob wir SkinnedMeshRenderer-Komponenten für jedes Objekt verwenden müssen, das derzeit eine verwendet. Es kann sein, dass wir ein Modell importiert haben, das eine SkinnedMeshRenderer-Komponente verwendet, aber wir animieren es nicht wirklich. In einem solchen Fall wird das Ersetzen der SkinnedMeshRenderer-Komponente durch eine MeshRenderer-Komponente die Performance verbessern. Wenn wir beim Import von Modellen in Unity keine Animationen in den Importeinstellungen des Modells importieren, verfügt das Modell über einen MeshRenderer anstelle eines SkinnedMeshRenderers.
  • Wenn wir an unserem Objekt nur einen Teil der Zeit animieren (z.B. nur beim Hochfahren oder nur, wenn es sich in einem bestimmten Abstand zur Kamera befindet), können wir sein Mesh gegen eine weniger detaillierte Version oder seine SkinnedMeshRenderer-Komponente gegen eine MeshRenderer-Komponente austauschen. Die SkinnedMeshRenderer-Komponente verfügt über eine BakeMesh-Funktion, die ein Mesh in einer passenden Pose erstellen kann, was nützlich ist, um zwischen verschiedenen Meshes oder Renderern zu wechseln, ohne dass das Objekt sichtbar verändert wird.
  • Sie sollten bedenken, dass die Kosten für das Mesh Skinning pro Node steigen. Daher verwenden wir weniger Nodes in unseren Modellen, um den Arbeitsaufwand zu reduzieren.
  • Auf bestimmten Plattformen kann das Skinning nicht von der CPU, sondern von der GPU durchgeführt werden. Diese Option macht Sinn, zu experimentieren, wenn wir Kapazität auf der GPU haben. Wir können GPU-Skinning für die aktuelle Plattform und das Qualitätsziel in den Player Settings aktivieren.

Haupt-Thread-Operationen, die nichts mit dem Rendering zu tun haben.

Es ist wichtig zu verstehen, dass viele CPU-Aufgaben, die nichts mit dem Rendern zu tun haben, auf dem Hauptthread stattfinden. Das bedeutet, dass wir, wenn wir auf dem Hauptthread CPU-gebunden sind, die Leistung verbessern können, indem wir die CPU-Zeit reduzieren, die für Aufgaben aufgewendet wird, die nicht mit dem Rendern zusammenhängen.

Als Beispiel kann unser Spiel teure Rendering-Operationen und teure User-Skript-Operationen auf dem Hauptthread an einem bestimmten Punkt in unserem Spiel durchführen, wodurch wie CPU-gebunden sind. Wenn wir die Renderingoperationen so weit wie möglich optimiert haben, ohne die visuelle Qualität zu verlieren, ist es möglich, dass wir die CPU-Kosten unserer eigenen Skripte senken können, um die Leistung zu verbessern.

Wenn unser Spiel GPU-gebunden ist.

Das erste, was wir tun müssen, wenn unser Spiel GPU-gebunden ist, ist herauszufinden, was den GPU-Engpass verursacht. Die GPU-Leistung ist meist durch die Fill Rate begrenzt, insbesondere bei mobilen Geräten, aber auch Speicherbandbreite und Vertex-Verarbeitung können von Bedeutung sein. Lassen Sie uns jedes dieser Probleme untersuchen und schauen, was es verursacht, wie man es diagnostiziert und behebt.

Fill Rate.

Die Fill Rate bezieht sich auf die Anzahl der Pixel, die der Grafikprozessor jede Sekunde auf dem Bildschirm darstellen kann. Wenn unser Spiel durch die Fill Rate begrenzt ist, bedeutet das, dass unser Spiel versucht, mehr Pixel pro Frame zu zeichnen, als der Grafikprozessor verarbeiten kann.

Es ist einfach zu überprüfen, ob die Fill Rate dazu führt, dass unser Spiel GPU-gebunden ist:

  • Profile das Spiel und notiere die GPU-Zeit.
  • Verringern Sie die Bildschirmauflösung in den Player-Einstellungen.
  • Profile das Spiel noch einmal. Wenn sich die Leistung verbessert hat, ist es wahrscheinlich, dass die Fill Rate das Problem ist.

Wenn die Fill Rate die Ursache unseres Problems ist, gibt es einige Ansätze, die uns helfen können, das Problem zu lösen.

  • Fragment-Shader sind die Abschnitte des Shader-Codes, die dem Grafikprozessor mitteilen, wie dieser ein einzelnes Pixel zeichnen soll. Dieser Code wird von der GPU für jedes Pixel ausgeführt, das sie zeichnen muss. Wenn der Code also ineffizient ist, können Performance-Probleme leicht auftreten. Komplexere Fragment-Shader sind eine sehr häufige Ursache für Fill Rate-Probleme.
    • Wenn unser Spiel eingebaute Shader verwendet, sollten wir darauf achten, die einfachsten und am besten optimierten Shader für den gewünschten visuellen Effekt zu verwenden. Als Beispiel sind die mobilen Shader, die mit Unity ausgeliefert werden, hochgradig optimiert. Wir sollten mit Ihnen experimentieren und sehen, ob dies die Leistung verbessert, ohne das Aussehen unseres Spiels zu beeinträchtigen. Diese Shader wurden für den Einsatz auf mobilen Plattformen entwickelt, sind aber für jedes Projekt geeignet. Es ist völlig in Ordnung, „mobile Shader“ auf nicht mobilen Plattformen zu verwenden, um die Leistung zu steigern, wenn sie die für das Projekt erforderliche visuelle Fidelity bieten.
    • Wenn Objekte in unserem Spiel den Standard-Shader von Unity verwenden, ist es wichtig zu verstehen, das Unity diesen Shader basierend auf den aktuellen Materialeinstellungen kompiliert. Es werden nur die Features kompiliert, die aktuell verwendet werden. Das bedeutet, dass das Entfernen von Features wie Detail-Maps zu viel weniger komplexem Fragment-Shader-Code führen kann, was die Performance erheblich verbessern kann. Auch hier gilt: Wenn dies in unserem Spiel der Fall ist, sollten wir mit den Einstellungen experimentieren und sehen, ob wir die Leistung verbessern können, ohne die Bildqualität zu beeinträchtigen.
    • Wenn unser Projekt maßgeschneiderte Shader verwendet, sollten wir darauf achten, diese so weit wie möglich zu optimieren. Sie finden dazu viele nützliche Tipps im Handbuch von Unity.
  • Overdraw beschreibt den Fall, dass das gleiche Pixel mehrfach gezeichnet wird. Dies geschieht, wenn Objekte auf andere Objekte gezeichnet werden und trägt wesentlich zur Fill Rate bei. Um Overdraw zu verstehen, müssen wir die Reihenfolge verstehen, in der Unity Objekte in der Szene zeichnet. Der Shader eines Objekts bestimmt seine Zeichenfolge, in der Regel durch die Angabe der Renderwarteschlange, in der sich das Objekt befindet. Unity verwendet diese Informationen, um Objekte in einer strengen Reihenfolge zu zeichnen. Zusätzlich werden die Objekte in verschiedenen Renderwarteschlangen unterschiedlich sortiert, bevor sie gezeichnet werden. Beispielsweise sortiert Unity Elemente in der Geometry-Warteschlange von vorne nach hinten, um Overdraws zu minimieren, aber sortiert Objekte in der transparenten Wartschlange von vorne nach hinten, um den gewünschten visuellen Effekt zu erzielen. Diese Back-to-Frontsortierung hat tatsächlich den Effekt, dass der Overdraw von Objekten in der transparenten Warteschlange maximiert wird. Overdraw ist ein komplexes Thema und es gibt keine einheitliche Größe für alle Ansätze zur Lösung von Overdraw-Problemen, aber die Reduzierung der Anzahl der sich überlappenden Objekte, die Unity nicht automatisch sortieren kann, ist entscheidend. Der beste Ort, um mit der Untersuchung dieses Problems zu beginnen, ist in der Szenenansicht von Unity. Es gibt einen Draw-Modus, der es uns ermöglicht, Overdraws in unserer Szene zu sehen und von dort aus festzustellen, wo wir arbeiten können, um sie zu reduzieren. Die häufigsten Schuldigen für übermäßiges Überziehen sind transparente Materialien, nicht optimierte Partikel und überlappende Oberflächenelemente, daher sollten wir mit der Optimierung oder Reduzierung dieser Elemente experimentieren.
  • Die Verwendung von Bildeffekten kann erheblich zur Auslastung beitragen, insbesondere wenn wir mehr als einen Bildeffekt verwenden. Wenn unser Spiel Bildeffekte nutzt und mit Fill Rate-Problemen zu kämpfen hat, sollten wir vielleicht mit verschiedenen Einstellungen oder optimierten Versionen der Bildeffekte experimentieren. Wenn unser Spiel mehr als einen Bildeffekt auf derselben Kamera verwendet, führt dies zu mehreren Shaderdurchgängen. In diesem Fall kann es vorteilhaft sein, den Shader-Code für unsere Bildeffekte in einem einzigen Durchgang zu kombinieren, wie beispielsweise im PostProcessing-Stack von Unity. Wenn wir unsere Bildeffekte optimiert haben und immer noch Probleme mit der Fill Rate haben, müssen wir möglicherweise darüber nachdenken, Bildeffekte zu deaktivieren, insbesondere bei Geräten der unteren Preisklasse.

Bandbreite des Speichers.

Die Bandbreite des Speichers bezieht sich auf die Geschwindigkeit, mit der die GPU von ihrem dedizierten Speicher lesen und in diesem schreiben kann. Wenn unser Spiel durch die Speicherbandbreite begrenzt ist, bedeutet dies in der Regel, dass wir Texturen verwenden, die zu groß sind, als dass der Grafikprozessor sie schnell verarbeiten könnte.

Um zu überprüfen, ob die Speicherbandbreite ein Problem ist, können wir Folgendes tun:

  • Profile das Spiel und notiere die GPU-Zeit.
  • Reduzieren Sie die Texturqualität für die aktuelle Plattform und das Qualitätsziel in den Qualitätseinstellungen.
  • Profile das Spiel noch einmal und notiere dir die GPU-Zeit. Wenn sich die Leistung verbessert hat, ist es wahrscheinlich, dass die Speicherbandbreite das Problem ist.

Wenn die Speicherbandbreite unser Problem ist, müssen wir den Verbrauch an Texturspeicher in unserem Spiel reduzieren. Auch hier wird die Technik, die für jedes Spiel am besten funktioniert, unterschiedlich sein, aber es gibt ein paar Möglichkeiten, wie wir unsere Texturen optimieren können.

  • Die Texturkompression ist eine Technik, die die Größe von Texturen sowohl auf der Festplatte als auch im Speicher stark reduzieren kann, Wenn die Speicherbandbreite in unserem Spiel ein Problem darstellt, kann die Verwendung von Texturkompression zur Reduzierung der Größe von Texturen im Speicher die Leistung verbessern. Es gibt viele verschiedene Texturkompressionsformate und -einstellungen in Unity, und jede Textur kann eigene Einstellungen haben. In der Regel sollte, wann immer möglich, eine Form der Texturkompression verwendet werden. Jedoch funktioniert ein Trial-und-Error-Ansatz, um die beste Einstellung für jede Textur zu finden, am besten.
  • Mipmaps sind Versionen von Texturen mit geringerer Auflösung, die Unity auf entfernten Objekten verwenden kann. Wenn unsere Szene Objekte enthält, die weit von der Kamera entfernt sind, können wir vielleicht Mipmaps verwenden, um Probleme mit der Speicherbandbreite zu lösen. Der Mipmaps Draw Mode in der Szenenansicht ermöglicht es uns zu sehen, welche Objekte in unserer Szene von Mipmaps profitieren könnten.

Vertex-Processing.

Das Vertex-Processing bezieht sich auf die Arbeit, die der Grafikprozessor leisten muss, um jeden Vertex in einem Mesh darzustellen. Die Kosten der Node-Verarbeitung werden durch zwei Dinge beeinflusst: die Anzahl der Nodes, die gerendert werden müssen, und die Anzahl der Operationen, die an jedem Node durchgeführt werden müssen.

Wenn unser Spiel GPU-gebunden ist und wir festgestellt haben, dass es nicht durch die Fill Rate oder die Speicherbandbreite begrenzt ist, dann ist es wahrscheinlich, dass die Node-Verarbeitung die Ursache des Problems ist. Wenn dies der Fall ist, wird das Experimentieren mit der Reduzierung der Anzahl der Vertex-Processings, die der Grafikprozessor durchführen muss, wahrscheinlich zu Leistungssteigerungen führen.

Es gibt einige Ansätze, die wir in Betracht ziehen könnten, um die Anzahl der Nodes oder die Anzahl der Operationen, die wir an jedem Node durchführen, zu reduzieren.

  • Erstens sollten wir darauf hinarbeiten, unnötige Meshkomplexität zu reduzieren. Wenn wir Meshes verwenden, die einen Detaillierungsgrad haben, der im Spiel nicht zu sehen ist, oder ineffiziente Meshes, die aufgrund von Fehlern bei der Erstellung zu viele Nodes haben, ist dies eine Verschwendung für die GPU. Der einfachste Weg, die Kosten für die Node-Verarbeitung zu senken, ist die Erstellung von Meshes nur einer geringeren Anzahl von Nodes in unserer 3D-Software.
  • Wir können mit einer Technik mit der Bezeichnung Normal Mapping experimentieren, bei der Texturen verwendet werden, um die Illusion einer größeren geometrischen Komplexität auf einem Mesh zu erzeugen. Obwohl diese Technik einen gewissen GPU-Overhead mit sich bringt, wird sie in vielen Fällen zu einem Leistungsgewinn führen.
  • Wenn ein Mesh in unserem Spiel kein normales Mapping verwendet, können wir oft die Verwendung von Vertex-Tangenten für dieses Mesh in den Importeinstellungen des Mesh deaktivieren. Dies reduziert die Datenmenge, die für jedes Node an die GPU gesendet wird.
  • Der Detaillierungsgrad, auch bekannt als LOD, ist eine Optimierungstechnik, bei der Meshes, die weit von der Kamera entfernt sind, in ihrer Komplexität reduziert werden. Dies reduziert die Anzahl der Nodes, die der Grafikprozessor darstellen muss, ohne die visuelle Qualität des Spiels zu beeinträchtigen.
  • Vertex-Shader sind Blöcke von Shader-Code, die dem Grafikprozessor mitteilen, wie er jedes Vertex zeichnen soll. Wenn unser Spiel durch das Vertex-Processing eingeschränkt ist, dann kann es helfen, die Komplexität unserer Vertex-Shader zu reduzieren.
    • Wenn unser Spiel eingebaute Shader verwendet, sollten wir darauf achten, die einfachsten und am besten optimierten Shader für den gewünschten visuellen Effekt zu verwenden. Als Beispiel sind die mobilen Shader, die mit Unity ausgeliefert werden, hochgradig optimiert. Wir sollten mit Ihnen experimentieren und sehen, ob dies die Leistung verbessert, ohne das Aussehen unseres Spiels zu beeinträchtigen.
    • Wenn unser Projekt maßgeschneiderte Shader verwendet, sollten wir darauf achten, diese so weit wir möglich zu optimieren.

Schlußfolgerungen.

Wir haben in diesem Artikel gelernt, wie Rendering in Unity funktioniert, welche Art von Problemen beim Rendern auftreten können und wie man die Renderleistung in unserem Spiel verbessert. Mit diesem Wissen und unseren Profiling-Tools können wir Performance-Probleme im Zusammenhang mit dem Rendern beheben und unsere Spiele so strukturieren, dass sie eine reibungslose und effiziente Rendering-Pipeline haben.

Vielen Dank für Ihren Besuch.