Mit Version 2017.2 hat Unity den Support für Stereo Instancing für XR-Geräte auf DX11 freigegeben, so dass Entwickler Zugriff auf noch mehr Leistungsoptimierungen für HTC Vive, Oculus Rift und die neuen Windows Mixed Reality immersiven Headsets haben. In dem folgenden Beitrag werden wir die Gelegenheit nutzen, um etwas vertiefter auf diese spannende Weiterentwicklung des Renderings einzugehen und ihnen aufzuzeigen, wie Sie diese nutzen können.
Einer der einzigartigen und offensichtlichsten Aspekte des XR-Renderings ist die Notwendigkeit, zwei Ansichten zu erzeugen, eine pro Auge. Wir benötigen diese beiden Ansichten, um den stereoskopischen 3D-Effekt für den Betrachter zu erzeugen. Aber bevor wir eintauchen, wie wir zwei Standpunkte darstellen können, werfen wir einen Blick auf den klassischen Single Viewport Fall.
In einer traditionellen Rendering-Umgebung rendern wir unsere Szene aus einer einzigen Ansicht. Wir nehmen unsere Objekte und verwandeln sie in einen Space, der für das Rendern geeignet ist. Wir tun dies, indem wir eine Reihe von Transformationen auf unsere Objekte anwenden, wo wir sie aus einem lokal definierten Space zu einen Space bringen, den wir auf unserem Bildschirm zeichnen können.
Die klassische Transformations-Pipeline beginnt mit Objekten in ihrem eigenen, lokalen/objektiven Raum. Wir transformieren dann die Objekte mit unserem Modell oder unserer World Matrix, um die Objekte in den World Space zu bringen. Der World Space ist ein gemeinsamer Raum für die anfängliche, relative Platzierung von Objekten. Als nächstes transformieren wir unsere Objekte vom World in den View Space, mit unserer View Matrix. Nun sind unsere Objekte relativ zu unserem Standpunkt angeordnet. Sobald wir sie im Sichtbereich haben, können wir sie mit unserer Projektionsmatrix auf unser 2D-Canvas projizieren und die Objekte in den Clip-Space legen. Die perspektivische Teilung folgt, führt zu einem NDC-Space und schließlich wird die Viewport-Transformation angewendet, was zu einem Screen Space führt. Sobald wir im Screen Space sind, können wir Fragmente für unser Renderziel generieren. Für die Zwecke unserer Diskussion werden wir nur zu einem einzigen Renderziel hin rendern.
Diese Serie von Transformationen wird manchmal als „Graphic Transformation Pipeline“ bezeichnet und ist eine klassische Technik des Renderings.
Neben dem aktuellen XR-Rendering gab es Szenarien, in denen wir simultane Viewports darstellen wollten. Vielleicht hatten wir Split-Screen-Rendering für lokale Multiplayer. Wir hatten vielleicht einen separaten Mini-Viewport, den wir für eine In-Game-Map oder eine Sicherheitskamera verwenden würden. Diese alternativen Ansichten können Szenendaten miteinander teilen, aber sie teilen sich oft wenig anderes als das endgültige Ziel des Renderings.
Zumindestens besitzt jede Ansicht oft eigene Ansichten und Projektionsmatrizen. Um das endgültige Renderziel zusammenzusetzen, müssen wir auch andere Eigenschaften der Grafiktransformationspipeline manipulieren. In den „frühen“ Tagen, in denen wir nur ein Renderziel hatten, konnten wir Ansichtsfenster verwenden, um Teilreaktionen auf dem Bildschirm zu diktieren, in die wir rendern konnten. Als sich GPUs und die dazugehörigen APIs weiterentwickelten, waren wir in der Lage, in separate Renderziele zu rendern und diese später manuell zusammenzusetzen.
Geben Sie XRagon ein.
Moderne XR-Geräte führten die Anforderung ein, zwei Ansichten zu steuern, um den stereoskopischen 3D-Effekt zu erzeugen, der dem Geräteträger Tiefe verleiht. Jede Ansicht repäsentiert ein Auge. Während die beiden Augen die gleiche Szene aus einem ähnlichen Blickwinkel betrachten, verfügt jede Ansicht über einen einzigartigen Satz von Sicht- und Projektionsmatrizen.
Bevor Sie fortfahren, sollten Sie sich kurz mit der Definition einer Terminologie befassen. Diese sind nicht unbedingt Standardbegriffe in der Industrie, da Renderingingenieure in der Regel unterschiedliche Begriffe und Definitionen für verschiedene Engines und Anwendungsfälle verwenden. Behandeln Sie diese Begriffe als eine lokale Annehmlichkeit.
Szenendiagramm – Ein Szenendiagramm ist ein Begriff, der verwendet wird, um eine Datenstruktur zu beschreiben, die die Informationen organisiert, die für das Rendern unserer Szene benötigt werden und vom Renderer verbraucht werden. Das Szenendiagramm kann sich entweder auf die Szene in ihrer Gesamtheit oder auf den für die Ansicht sichtbaren Teil beziehen, den wir das „ausgewählte Szenendiagramm“ nennen werden.
Render Loop/Pipeline – Der Render Loop bezieht sich auf die logische Architektur, wie wir den gerenderten Frame zusammensetzen. Ein High-Level-Beispiel für einen Render-Loop könnte dies sein:
Culling > Shadows > Opaque > Transparent > Post Processing > Present
Wir durchlaufen diese Schritte in jedem Einzelbild, um ein Bild zu erzeugen, das der Anzeige präsentiert wird. Wir verwenden auch den Begriff Renderpipeline bei Unity, da er sich auf einige kommende Renderingfunktionen bezieht, die wir bereitstellen (z.B. Scriptable Render Pipeline). Render-Pipeline kann mit anderen Begriffen verwechselt werden, wie beispielsweise der Grafik-Pipeline, die sich auf die GPU-Pipeline bezieht, um Zeichenbefehle zu verarbeiten.
Ok, mit diesen Definitionen können wir zum VR-Rendering zurückkehren.
Multi-Kamera.
Um die Ansicht für jedes Auge darzustellen, ist die einfachste Methode, den Renderloop zweimal auszuführen. Jedes Auge wird seine eigene Iteration des Renderloops konfigurieren und durchlaufen. Am Ende werden wir zwei Bilder haben, die wir an das Anzeigegerät senden können. Die zugrunde liegende Implementierung verwendet zwei Unity-Kameras, eine für jedes Auge und sie durchlaufen den Prozess der Erzeugung der Stereobilder. Dies war die erste Methode der XR-Unterstützung in Unity und wird immer noch von Headset-Plugins von Drittanbietern angeboten.
Obwohl diese Methode sicherlich funktioniert, verlässt sich die Multi-Kamera auf Brute-Force und ist in Bezug auf CPU und GPU am wenigsten effizient. Die CPU muss zweimal durch den Render-Loop vollständig iterieren und die GPU ist wahrscheinlich nicht in der Lage, das Caching von Objekten zu nutzen, die zweimal über die Augen gezogen wurden.
Multi-Pass.
Multi Pass war der erste Versuch von Unity, den XR-Render-Loop zu optimieren. Die Kernidee war es, Teile des Renderloops zu extrahieren, die view-unabhängig waren. Das bedeutet, dass alle Arbeiten, die nicht explizit auf die XR-Sichtweisen angewiesen sind, nicht pro Auge durchgeführt werden müssen.
Der offensichtlichste Kandidat für diese Optimierung ist das Shadow-Rendering. Schatten sind nicht explizit unabhängig von der Position des Kamerabetrachters. Unity implementiert Schatten tatsächlich in zwei Schritten: Erzeugen Sie kaskadierte Shadow-Maps und ordnen Sie die Schatten dann dem Bildschirmraum zu. Für Multi-Pass können wir einen Satz kaskadierter Shadow Maps erzeugen und dann zwei Shadow Maps für den Bildschirm erstellen, da die Shadow Maps für den Bildschirmraum vom Standort des Betrachters abhängig sind. Aufgrund der Architektur unserer Shadow-Generierung profitieren die Shadow Maps des Bildschirmraums von der Lokalität, da der Shadow-Map-Generierungs-Loop relativ eng gekoppelt ist. Dies kann mit der verbleibenden Render-Workflow verglichen werden, die eine vollständige Iteration über den Render-Loop erfordert, bevor sie zu einer ähnlichen Stufe zurückkehrt (z.B. werden die augenspezifischen opaken Durchgänge durch die restlichen Render-Loopstufen getrennt).
Der andere Schritt, der zwischen den beiden Augen geteilt werden kann, ist vielleicht zunächst nicht offensichtlich: Wir können ein einziges Culling zwischen den beiden Augen durchführen. Bei unserer ersten Implementierung haben wir mit Hilfe von Frustum Culling zwei Listen von Objekten generiert, eine pro Auge. Wir könnten jedoch ein einheitliches Culling-Face schaffen, das zwischen unseren beiden Augen geteilt wird. Dies bedeutet, dass jedes Auge ein wenig mehr leisten wird, als mit einem Single Eye Culling Frustum, aber wir haben die Vorteile einer einzigen Aussortierung in Betracht gezogen, um die Kosten für einige zusätzliche Scheitelpunkt-Shader, Clipping und Rasterung zu überwiegen.
Multi-Pass bot uns einige schöne Einsparungen gegenüber der Multi-Kamera, aber es gibt noch mehr zu tun.
Single-Pass.
Single-Pass Stereo Rendering bedeutet, dass wir eine einzige Überquerung des gesamten Render-Loops vornehmen, anstatt zweimal oder bestimmte Abschnitte zu durchlaufen.
Um beide Draws durchführen zu können, müssen wir sicherstellen, dass wir alle konstanten Daten und einen Index gebunden haben.
Was ist mit den Draws selbst? Wie können wir jeden Draw durchführen? In Multi-Pass haben die beiden Augen jeweils ihr eigenes Renderziel, aber das können wir für Single-Pass nicht tun, da die Kosten für das Umschalten der Renderziele für aufeinanderfolgende Draw-Aufrufe unerschwinglich wären. Eine ähnliche Option wäre die Verwendung von Render-Zielarrays, aber wir müssten den Slice-Index auf den meisten Plattformen aus dem Geometrie-Shader exportieren, was auch auf dem Grafikprozessor teuer sein kann und für bestehende Shader invasiv.
Die Lösung, auf die wir uns einigten, war die Verwendung eines Double-Wide Renderziels und die Umschaltung des Viewports zwischen Draw-Aufrufen, so dass jedes Auge in die Hälfte des Double-Wide Renderziels rendern konnte. Das Wechseln von Ansichtsfenstern ist zwar kostenintensiv, aber weniger aufwändig als das Wechseln von Renderingzielen und weniger invasiv als die Verwendung des Geometrie-Shaders. Es gibt auch die zugehörige Option der Verwendung von Viewport-Arrays, aber sie haben das gleiche Problem wie Render-Ziel-Arrays, da der Index nur aus einem Geometrie-Shader exportiert werden kann. Es gibt noch eine weitere Technik, die dynamisches Clipping verwendet, das wir hier nicht untersuchen werden.
Da wir nun eine Lösung haben, um zwei aufeinander folgende Draws zu beginnen, um beide Augen zu sehen, müssen wir unsere unterstützende Infrastruktur konfigurieren. Im Multi-Pass konnten wir, da es dem monoskopischen Rendering ähnlich war, unsere bestehende View- und Projektionsmatrix-Infrastruktur nutzen. Wir mussten lediglich die Sicht- und Projektionsmatrix durch die Matrizen aus dem aktuellen Auge ersetzen. Mit Single-Pass wollen wir jedoch nicht unnötig zwischen konstanten Pufferbindungen umschalten. Stattdessen binden wir also die Sicht- und Projektionsmatrizen beider Augen zusammen und indexieren sie mit unity_StereoEyeIndex, den wir zwischen den Draws spiegeln können. Dies ermöglicht es unserer Shader-Infrastruktur, inerhalb des Shader-Passes zu wählen, mit welchem Satz von Sicht- und Projektionsmatrizen sie gerendert werden sollen.
Ein zusätzliches Detail: Um unsere Viewport- und unity_StereoEyeIndex-Zustandsänderungen zu minimieren, können wir unser Augenziehmuster ändern. Anstatt nach links, rechts, links, rechts usw. zu zeichnen, können wir stattdessen die links, rechts, rechts, links, links usw. Rhythmus verwenden. Dadurch können wir die Anzahl der Zustandsaktualisierungen im Vergleich zur alternierenden Kadenz halbieren.
Dies ist nicht genau doppelt so schnell wie Multi-Pass. Der Grund dafür ist, dass wir bereits für Culling und Shadows optimiert wurden, ebenso wie die Tatsache, dass wir immer noch ein Draw per Auge senden und Viewports wechseln, was einige CPU- und GPU-Kosten verursacht.
Stereo Instancing (Single-Pass Instanced).
Zuvor haben wir die Möglichkeit erwähnt, ein Render-Zielarray zu verwenden. Render-Zielarrays sind eine natürliche Lösung für das Stereo-Rendering. Die Augentexturen teilen Format und Größe und qualifizieren sie für die Verwendung in einem Render-Zielarray. Aber die Verwendung des Geometrie-Shaders zum Exportieren der Array-Slice ist ein großer Nachteil. Was wir wirklich wollen, ist die Möglichkeit, den Render-Zielarray-Index aus dem Vertex-Shader zu exportieren, was eine einfachere Integration und bessere Leistung ermöglicht.
Die Möglichkeit, den Render-Zielarray-Index aus dem Vertex-Shader zu exportieren, existiert tatsächlich bei einigen GPUs und APIs und wird immer häufiger. Auf DX11 wird diese Funktionalität als Feature Option VPAndRTArrayIndexFromAnyShaderFeedingRasterizer bereitgestellt.
Da wir nun bestimmen können, auf welchen Slice unseres Render-Zielarrays wir rendern werden, wie können wir den Slice auswählen? Wir nutzen die bestehende Infrastruktur von Single-Pass Double-Wide. Wir können unity_StereoEyeIndex verwenden, um die SV_RenderTargetArrayIndex-Semantik im Shader zu füllen. Auf der API-Seite müssen wir das Ansichtsfenster nicht mehr umschalten, da das gleiche Ansichtsfenster für beide Slices des Renderzielarrays verwendet werden kann. Und wir haben unsere Matrizen bereits so konfiguriert, dass sie aus dem Vertex-Shader indiziert werden können.
Obwohl wir weiterhin die bestehende Technik der Ausgabe von zwei Draws und des Umschaltens des Wertes unity_StereoEyeIndex im konstanten Puffer vor jedem Draw verwenden könnten, gibt es eine effiziente Technik. Wir können GPU-Instancing verwenden, um einen einzigen Draw-Aufruf zu senden und es der GPU zu ermöglichen, unsere Draws über beide Augen zu multiplizieren. Wir können die vorhandene Instanzanzahl eines Draws verdoppeln. Anschließend können wir im Vertex-Shader die Instanz-ID dekodieren, um festzustellen, auf welches Auge wir rendern.
Die größte Auswirkung der Verwendung dieser Technik ist, dass wir die Anzahl der Draw-Aufrufe, die wir auf der API-Seite generieren, buchstäblich halbieren und so einen Teil der CPU-Zeit sparen. Darüber hinaus ist die GPU selbst in der Lage, die Draws effizienter zu verarbeiten, obwohl der gleiche Arbeitsaufwand erzeugt wird, da sie nicht zwei einzelne Drawaufrufe verarbeiten muss. Wir minimieren auch Zustandsaktualisierungen, indem wir das Ansichtsfenster zwischen den Draws nicht ändern müssen, wie wir es bei traditionellen Single-Pass-Verfahren.
Single-Pass Multi-View.
Multi-View ist eine Erweiterung für bestimmte OpenGL/OpenGL ES-Implementierungen, bei denen der Treiber selbst das Multiplexing einzelner Draw Calls über beide Augen hinweg übernimmt. Anstatt den Draw-Call explizit zu instanziieren und die Instanz in einen Augenindex im Shader zu dekodieren, ist der Treiber dafür verantwortlich, die Draws zu duplizieren und den Array-Index (über gl_ViewID) im Shader zu erzeugen.
Es gibt ein zugrunde liegendes Implementierungsdetail, das sich von der Stereo-Instanz unterscheidet: Anstelle des Vertex-Shaders, der explizit die zu rasternde Render-Zielarray-Schicht auswählt, bestimmt der Treiber selbst das Renderziel. gl_ViewID wird verwendet, um den viewabhängigen Zustand zu berechnen, nicht aber, um das Renderziel auszuwählen. Im Gebrauch spielt es für den Entwickler keine große Rolle, ist aber ein interessantes Detail.
Aufgrund der Art und Weise, wie wir die Multi-View-Erweiterung verwenden, können wir die gleiche Infrastruktur nutzen, die wir für Single-Pass-Instancing aufgebaut haben. Entwickler sind in der Lage, das gleiche Gerüst zu verwenden, um beide Single-Pass-Techniken zu unterstützen.
Performance auf hohem Niveau.
Auf der Unite Austin 2017 präsentierte das XR Graphics Team einen Teil der XR Graphics Infrastruktur und führte eine kurze Diskussion über die Auswirkungen der verschiedenen Stereo-Rendering-Modi auf die Leistung. Über die gesamte Leistungsanalyse könnten wir einen eigenen Blogbeitrag schreiben, werden dies aber hier recht kurz abarbeiten.
- Single-Pass und Single-Pass-Instancing stellen einen erheblichen CPU-Vorteil gegenüber Multi-Pass dar. Das liegt daran, dass der Großteil des CPU-Overheads bereits durch die Umstellung auf Singe-Pass eingespart wird.
- Single-Pass-Instancing reduziert zwar die Anzahl der Draw-Aufrufe, aber diese Kosten sind im Vergleich zur Verarbeitung des Szenengraphen recht gering.
- Wenn man bedenkt, dass die meisten modernen Grafiktreiber multi-threaded sind, kann die Ausgabe von Draw-Aufrufen ziemlich schnell auf dem Dispatching CPU-Thread erfolgen.
Wir hoffen, dass wir ihnen einen kurzen Überblick über die Thematik verschaffen konnten. Falls Sie noch Fragen haben sollten, können Sie sich gerne an unsere Fachexperten in unserem Forum wenden.
Vielen Dank für ihren Besuch.