Mit Unity 2017.2 wurde der Stereo-Support 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 brandneuen Windows Mixed Reality Headsets haben. In diesem Beitrag werden wir die Möglichkeit nutzen, ihnen mehr über die spannende Weiterentwicklung des Renderings berichten und wie Sie diese nutzen können.
Kurze Historie.
Eine 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 tiefer eintauchen, wie wir zwei Standpunkte darstellen könnten, werfen wir einen Blick auf den klassischen Single Viewpoint Fall.
In einer traditionellen Rendering-Umgebung rendern wir unsere Szene aus einer einzigen Ansicht. Wir nehmen unsere Objekte und verwandeln sie in einen Raum, 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 Raum in einen Raum 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 Space in den View Space, mit unserer View Matrix. Nun sind unsere Objekte relativ zu unserem Standpunkt angeordnet. Sobald wir sie im Viewport haben, können wir sie mit unserer Projektionsmatrix auf unsere 2D-Leinwand projizieren und die Objekte in den Clip-Raum legen. Die perspektivische Teilung erfolgt und führt zu einem NDC Space und schließlich wird die Viewport-Transformation angewendet, was zu einem Bildschirmraum 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 rendern.
Diese Reihe 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 gleichzeitige Standpunkte 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.
Zumindest 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 gerendert und diese später manuell zusammengesetzt zu haben.
Betreten Sie XRagon.
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 reprä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. Dies sind nicht unbedingt Standard-Begriffe in der Industrie, da Rendering-Ingenieure 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 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 dem Display präsentiert wird. Wir verwenden auch den Begriff Renderpipeline bei Unity, da er sich auf einige kommende Renderingfunktionen bezieht, die wie 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.
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 Render Loop zweimal auszuführen. Jedes Auge wird seine eigene Iteration des Render Loop konfigurieren und durchlaufen. Am Ende werden wir zwei Bilder haben, die wir an das Display-Gerä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 Headsets-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.
Mehrfaches Durchlaufen (Multi-Pass).
Multi-Pass war der erste Versuch von Unity, den XR-Render-Loop zu optimieren. Die Kernidee war es, Teile des Render Loops 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. Shadows sind nicht explizit abhängig von der Position des Kamerabetrachters. Unity implementiert Schatten tatsächlich in zwei Schritten: Erzeugen Sie kaskadierte Shadow-Maps und ordnen Sie die Shadows dann dem Screen Space 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 Screen Space vom Standort des Betrachters abhängig sind. Aufgrund der Architektur unserer Shadow-Generierung profitieren sie Shadow-Maps des Screen Space von der Lokalität, da die Shadow-Map-Generation-Loop relativ eng gekoppelt ist. Dies kann mit dem verbleibenden Render-Workload 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-Loop-Stufen getrennt).
Der andere Schritt, der zwischen den beiden Augen geteilt werden kann, ist vielleicht zunächst nicht offensichtlich: Wir können ein einziges Cull 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 Frustum schaffen, das zwischen unseren beiden Augen geteilt wird. Dies bedeutet, dass jedes Auge ein wenig mehr leisten wird, als mit einem einzigen Auge, aber wir haben die Vorteile einer einzigen Aussortierung in Betracht gezogen, um die Kosten für einige zusätzliche Vertex-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 bestimmte Abschnitte zweimal.
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 Target Arrays, 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 Renderzielen und weniger invasiv als die Verwendung des Geometrie-Shaders (obwohl Double-Wide seine eigenen Herausforderungen darstellt, insbesondere beim Post Processing). Es gibt auch die zugehörige Option der Verwendung von Viewport-Arrays, aber sie haben das gleiche Problem wie Render Target Arrays, da der Index nur aus einem Geometrie-Shader exportiert werden kann. Es gibt noch eine weitere Technik, die dynamisches Clipping verwendet, welche wir aber hier nicht untersuchen werden.
Jetzt, da wir 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 View- und Projektionsmatrix durch die Matrizen aus dem aktuellen Auge ersetzen. Mit Single Pass wollen wir jedoch nicht unnötig zwischen konstanten Pufferbindungen wechseln. Stattdessen binden wir also die View- und Projektionsmatrizen beider Augen zusammen und indexieren sie mit unity_StereoEyeIndex, den wir zwischen den Zügen umkehren können. Dies ermöglicht es unserer Shader-Infrastruktur, innerhalb des Shader-Passes zu wählen, mit welchem Satz von View- und Projektionsmatrizen sie rendern möchten.
Ein zusätzliches Detail: Um die Viewport- und unity_StereoEyeIndex-Zustandsänderungen zu minimieren, können wir unser Eye Draw Pattern ändern. Anstatt nach links, rechts, links, links, rechts und so weiter zu zeichnen, können wir stattdessen die linke, rechte, rechte, rechte, rechte, linke, linke usw. Kadenz verwenden. Dadurch können wir die Anzahl der State Updates im Vergleich zur alternierenden Trittfrequenz 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 Target Array zu verwenden. Render Target Arrays 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 Target Array. Aber die Verwendung des Geometrie-Shaders zum Exportieren des Array Slice ist ein großer Nachteil. Was wir wirklich wollen, ist die Möglichkeit, den Render Target Array Index aus dem Vertex-Shader zu exportieren, was eine einfachere Integration und bessere Leistung ermöglicht.
Die Möglichkeit, den Render Target Array 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 Target Arrays 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 Layer des Render Target Arrays 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 den Wert unity_StereoEyeIndex im konstanten Puffer vor jedem Draw verwenden könnten, gibt es eine effizientere 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 (wenn es keine Instanznutzung gibt, setzen wir einfach die Instanzanzahl auf 2). Dann 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 Draw-Aufrufe verarbeiten muss, indem wir das Ansichtsfenster zwischen den Draws nicht ändern müssen, wie wir es bei traditionellen Single-Pass-Verfahren tun.
Bitte beachten Sie: Dies ist nur für Benutzer verfügbar, die ihre Desktop-VR-Erfahrungen unter Windows 10 oder HoloLens ausführen.
Single Pass Multi View.
Multi-View ist eine Extension 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-Aufruf explizit zu instanziieren und die Instanz in einen Augen-Index 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 Target Array Layer 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-Extension 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.
Leistungsübersicht 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. Eine korrekte Leistungsanalyse wird in einer späteren Präsentation noch ein wenig näher erörtert werden.
Vielen Dank für ihren Besuch.