In dem folgenden Beitrag möchten wir Ihnen einen kurzen Einstieg in die Arbeit mit OpenVR bieten und auf einige technische Details eingehen. Zum englischen Originalbeitrag von Matias Nassi gelangen Sie über den folgenden Link.

OpenVR 101 Serie

Dies ist unser erster Beitrag in einer Reihe von technischen Beiträgen, in denen wir die Virtual Reality (VR) API behandeln und einige Beispiele dafür coden werden. In diesem ersten Beitrag werden wir beschreiben, was OpenVR ist, wofür es nützlich sein kann und eine kleine OpenVR-Anwendung durchgehen, um einige der Kernfunktionen der API in Bezug auf die Interaktion mit einem VR-System zu demonstrieren (Geräte verbinden, Tracking-Daten erhalten etc.).

Wir werden diese einfache Anwendung als Grundlage für die folgenden Beiträge verwenden, in denen wir anfangen werden, interessante Dinge hinzuzufügen, wie das Rendern von Motion Controllern, das Rendern einfacher Objekte in eine stereoskopische Ansicht, damit sie in VR richtig angezeigt werden können, und das Hinzufügen einiger einfacher Interaktionen ermöglichen.

Warum dieser Beitrag?

Während wir auf die Veröffentlichung der OpenXR-Initiative der Khronos Group warten, haben wir Entwickler nur sehr wenige Möglichkeiten, plattformübergreifende VR-Anwendungen auf dem PC zu entwickeln. Sie mögen vielleicht sagen: „Wir können auf einer Vielzahl von Plattformen eingesetzt werden, die Spiele-Engines wie Unity, Unreal Engine, die neuere Godot-Engine oder eine der anderen Engines da draußen verwenden, die alle damit begonnen haben, VR-Funktionen hinzuzufügen“. Diese Aussage kann man so nicht mehr zustimmen, da die Serie nicht darauf abzielen wird, Anwendungen/Spiele auf einem hohen Abstraktionsgrad zu erstellen, wie bei der Verwendung einer Game Engine. Stattdessen wird versucht, detailliert darzulegen, wie man eine VR-API auf einer niedrigeren Ebene verwendet, um zu verstehen, wie alle diese Engines sie nutzen, um Multiplattform-VR-Anwendungen zu ermöglichen, indem sie Trackinginformationen von einer HMD und jedem zusätzlichen trackbaren Gerät erhalten, eine Szene aus beiden Augenwinkeln rendern, um Stereo Vision zu erreichen, und so weiter.

Einführung in OpenVR.

Eine der besten VR-APIs, die heute einmal geschrieben und überall eingesetzt werden kann, ist OpenVR, eine von Valve entwickelte Open-Source-Programmierschnittstelle, die die Kommunikation mit einem VR-System ermöglicht. Dank seiner Offenheit und Unterstützung durch seine Entwickler wird OpenVR von fast allen wichtigen High-End-Headsets auf dem Markt unterstützt, darunter HTC Vive, Oculus Rift, eines der Windows Mixed-Reality-Headsets, Pimax 5K/8K und sogar ein mobiles Headset wie NoloVR und Headsets der nächsten Generation wie das Varjo Bionic Display Headset oder das kleine Headset von Kopin. Das bedeutet, dass wir, wenn wir diese API für die Kommunikation mit dem VR-System in unserer VR-Anwendung verwenden, es in jeder der zuvor genannten Plattformen ohne Modifikatoren ausführen können.

An dieser Stelle haben Sie vielleicht schon erraten, dass die meisten Engines mit plattformübergreifender VR-Unterstützung lediglich OpenVR in ihre Pipeline aufnehmen, damit die eingesetzte Anwendung auf allen oben genannten VR-Plattformen laufen kann. So kann die „Your Software“-Komponente eine Software-Lösung wie Unity, Unreal Engine, Godot oder John Does selbstgemachte Engine oder Anwendung sein. Allen gemeinsam ist, dass sie hinter den Kulissen OpenVR-Aufrufe zur Kommunikation mit dem VR-System nutzen, das auch die OpenVR-Sprache versteht und entsprechend reagiert.

Wie auch immer, es ist erwähnenswert, dass OpenVR nicht die einzige VR-API ist, die es gibt, so dass Spiele-Engines in der Regel andere VR-APIs wie Oculus SDK, GoogleVR SDK etc. unterstützen. Als Entwickler haben wir keine Kontrolle über die Laufzeit, in der unsere Anwendung ausgeführt wird (d.h. wir können OpenVR nicht verwenden und uns dafür entscheiden, direkt in Oculus Runtime auszuführen), da dies durch die API/SDK bestimmt wird, die wir stattdessen verwenden. Wenn wir unsere Anwendung beispielsweise in einem Oculus Rift Headset ausführen und mit OpenVR entwickelt haben, ist der Ausführungs-Workflow zur Laufzeit OpenVR-API > SteamVR Runtime > Oculus API > Oculus Runtime > Oculus Driver > Oculus HMD, im Gegensatz zu dem, was passieren würde, wenn wir mit der Oculus API entwickeln würden, in diesem Fall wird der Workflow der Anwendung zur Laufzeit direkter sein, Oculus API > Oculus Runtime > Oculus Driver > Oculus HMD.

Um dies weiter zu verdeutlichen, können einige weitere bekannte Beispiele für diese Art von plattformübergreifenden APIs OpenGL für die Grafikprogrammierung, OpenCV für die Verwendung von Bildverarbeitungsalgorithmen und OpenCL für die Steuerung der Programmausführung zwischen verschiedenen Prozessoren sein. All dies sind Beispiele für Schnittstellen, die in einer Vielzahl von Systemen unterstützt werden, von Desktop-Computern über mobile Geräte bis hin zu Embedded-Systemen, so dass jede Anwendung, die die Schnittstelle nutzt, in allen unterstützten Systemen ausgeführt werden kann. Um ein konkretes Beispiel zu nennen: Die Unterstützung der Unity-Engine für OpenGL bedeutet, dass jedes in Unity entwickelte Spiel entweder auf einem Windows Desktop Computer, Linux, Mac oder sogar auf einer OpenGL-fähigen Konsole wie der Sony PlayStation mit sehr wenigen bis kleinen Änderungen laufen kann, was für uns Entwickler eine sehr gute Nachricht ist. Und das Gleiche geschieht mit OpenVR. Die Tatsache, dass Unity dies unterstützt, bedeutet, dass jedes in Unity entwickelte Spiel entweder im HTC Vive, im Oculus Rift oder auf jeder anderen OpenVR-fähigen Hardware laufen kann. Ein Vorbehalt dazu ist, dass Unity nativ nur den Rendering- und Tracking-Teil von OpenVR unterstützt und spezifischere Verhaltensweisen an Plugins wie das SteamVR-Plugin delegiert, was notwendig ist, um Controller-Eingaben und komplexere Informationen von der Hardware zu erhalten, aber auch diese Art von Plugins verwenden am Ende die VR-API. Aber in diesem Beitrag geht es nicht um Unity, also lassen Sie uns weitermachen.

Ein weiteres großartiges Beispiel für die Idee von „write once, deplay everywhere“ im Zusammenhang mit VR ist WebVR, eine auf Webstandards basierende API zur Erstellung von VR-Erlebnissen, die über URLs, wie wir sie aus dem eigentlichen Web kennen, verteilt und ausgeführt werden können, unabhängig davon, ob wir ein High-End-VR-Headset wie das Oculus verwenden, das Vive, eines der Microsoft Mixed Reality Headsets, ein mobiles VR-Headset wie das Gear VR oder der Daydream Viewer, eines der in den nächsten Monaten erscheinenden Standalone-Headsets wie das Oculus Go oder das Lenovo Mirage Solo oder sogar Nicht-VR-Geräte, sofern sie einen WebVR-fähigen Browser wie Smartphones und Tablets enthalten. Es kann sogar AR-Anwendungen als Teil derselben API aufnehmen, dank einer Erweiterung mit der Bezeichnung WebXR. Die Zukunft von VR im Web ist also sehr vielversprechend, und in Bezug auf Vertrieb und plattformübergeifende Ebene können diese Art von Web-APIs als noch flexibler angesehen werden als OpenVR, aber sie steckt noch in den Kinderschuhen und wir sind noch nicht sicher, wie gut sie sich entwickeln wird.

Mit OpenVR arbeiten.

OpenVR wird von Valve über ein Github-Projekt vertrieben, das Sie hier finden. Die Dokumentation umfasst nur wenige Seiten im Rahmen des Github-Projekt-Wikis, das die meisten der von der API bereitgestellten Operationen beschreibt. Die Beschreibungen sind nicht gerade gründlich und es gibt keine Schritt-für-Schritt-Anleitung oder so etwas, um loslegen zu können. Sogar auf diese Weise kann das Rumfuchteln im Code ein paar Stunden dauern, um verstehen zu können, auf was der Code abzielt, umso mehr, wenn man bedenkt, dass der größte Teil des Codes auf einfachem modernen OpenGL/GLSL basiert und es eine Menge Dinge gibt, die nicht nur mit OpenVR selbst zusammenhängen, wie z.B. das Rendern der Szene, um Render-Puffer zu trennen und sie in Stereo zu zeigen, das Anwenden der richtigen perspektivischen Transformationen für jedes Auge, damit alles in Ordnung ist, das Laden der Shader, die für das Rendern verwendet werden sollen, und viele andere Sachen im Bereich der Computergrafik, die wir in kommenden Beiträgen auch noch ansprechen werden.

Eines der Ziele dieser Serie ist es also, schrittweise zu erklären, wie einfach es ist, welche Methoden am nützlichsten sind und wie man sie verwendet, was der Hauptworkflow bei der Arbeit mit der API ist, usw., so dass wir besser verstehen können, wie die Dinge im Hintergrund funktionieren, wenn eine VR-Szene in der HMD mit OpenVR gerendert wird. Wie auch immer, bedenken Sie, dass es eine Menge Dinge geben wird, die die VR-Laufzeit selbst noch hinter den Kulissen tun wird (z.B. korrekte Zylinderverzerrung auf das gerenderte Bild anwenden, um dem natürlichen Linsen-Pinpolstereffekt entgegenzuwirken, eine korrekte Bildrate sicherstellen, indem Rendering-Techniken wie asynchrone Reprojektion angewendet werden, und so weiter), aber zumindest werden wir in einem niedrigeren Grad der Abstraktion im Vergleich zur Arbeit mit einer High-Level-Game-Engine arbeiten, und dies wird es uns ermöglichen, besser zu verstehen, wie es funktioniert.

Im Folgenden präsentiere ich Ihnen einige Argumente, weshalb Sie sich mit dieser Thematik beschäftigen sollten:

  • Wenn Sie ein besseres Verständnis davon haben möchten, was im Hintergrund einer VR-Anwendung vor sich geht, sind Sie hier richtig.
  • Wenn Sie eine Game-Engine haben und OpenVR-Support hinzufügen möchten, sollten Sie weiterlesen.
  • Wenn Sie Ihre VR-Szene von Grund auf neu erstellen wollen, ohne eine Game-Engine zu verwenden, ist sie ziemlich nützlich.
  • Wenn Sie ein VR-Spiel entwickeln, es veröffentlichen und den Prozess überleben möchten, vergessen Sie das alles und lesen Sie besser das Handbuch ihrer Lieblings-Game-Engine. Wie wir bereits angedeutet haben, unterstützen die meisten Game-Engines heutzutage all diese Dinge bereits transparent und mit sehr wenig Aufwand seitens des Entwicklers. Aber wenn Sie es trotzdem von Grund auf neu programmieren möchten, sollten Sie weiterlesen.

Unsere erste OpenVR-Anwendung.

Beginnen wir also mit der Programmierung unserer Hello World Anwendung mit OpenVR, während wir beschreiben, welche die relevantesten Operationen sind, um Daten aus dem VR-System zu erhalten. Die endgültige Anwendung wird diesmal sehr einfach sein und zeigt alle erkannten Geräte (HMD, Controller, Tracker etc.) und ihre 3D-Positionen auf dem Bildschirm an, wie sie erkannt werden. Bei den Controllern färbt er seine Daten auch unterschiedlich ein, je nachdem, ob eine Taste gedrückt wird oder nicht. Im Folgenden finden Sie eine Demo dazu:

Aus datenschutzrechtlichen Gründen benötigt YouTube Ihre Einwilligung um geladen zu werden. Mehr Informationen finden Sie unter Datenschutzerklärung.

Lassen Sie uns zunächst einige Dinge über den Code/das Projekt sagen. Der Code ist einfach C mit ein paar Features aus C++ (allerdings nicht objektorientiert, sondern nur einfache alte prozedurale Programmierung, um die Dinge vorerst so einfach wie möglich zu machen). Es handelt sich um ein Visual Studio-Projekt, das unter Windows 8.1 64 Bit getestet wurde, obwohl alle verwendeten Bibliotheken von Drittanbietern plattformübergreifend sind, sollte es mit wenigen Anpassungen nach Bedarf auf jede andere Plattform portiert werden können. Wenn Sie auf Probleme bei der Ausführung des Codes auf einer anderen Plattform stoßen, hinterlassen Sie uns unten einen Kommentar.

Das Projekt referenziert vorerst nur ein paar Bibliotheken von Drittanbietern (SDL2 für Windows-Management, SDL TTF für das Rendern von 2D-Text und OpenVR aus offensichtlichen Gründen) und sie alle sind Teil des Projekts bezogen auf den Main Solution Path, so dass es hoffentlich keine Probleme bei der Verwendung der externen Bibliotheken geben sollte. Da der Schwerpunkt dieses Beitrags auf OpenVR liegt, werden wir nicht beschreiben, wie man andere externe Bibliotheken verwendet, die sich von ihm unterscheiden.

Wenn man beginnt, sich in OpenVR-Code zu vertiefen, sollte man beachten, dass die API weiter in verschiedene Module unterteilt ist, die jeweils für verschiedene Aufgaben innerhalb der VR-Anwendung zuständig sind:

  • Das IVRSystem ist die Hauptschnittstelle und ermöglicht es uns, zu interagieren und Informationen über die angeschlossenen Geräte und Controller zu sammeln, sowie die bereits erwähnte Linsenverzerrung und andere anlagenbezogene Dinge zu berechnen.
  • Ein weiteres relevantes Modul ist der IVRCompositor, der es der Anwendung ermöglicht, 3D-Inhalte auf dem Display korrekt darzustellen, und der für die Steuerung aller Rendering-Prozesse im VR-Kontexts zuständig ist.
  • Es gibt auch die Module IVRChaperone und IVROverlay, die es ermöglichen, auf alle Informationen über das verwendete Virtual Bonds Systems zuzugreifen und 2D-Inhalte als Teil des VR-Overlays (Menüs, Buttons etc.) zu rendern.

Obwohl es einige zusätzliche Module gibt, sind diese für unsere Aufgabe am wichtigsten. Jedes der Module wird instanziert und bei Bedarf verwendet, so dass wir für diese erste Anwendung nur das IVRSystem verwenden werden, um Basisdaten aus dem VR-System zu erhalten.

Das erste, was wir tun müssen, ist, das IVRSystem-Modul zu initialisieren (init_OpenVR-Funktion im Code überprüfen). Wir können dies tun, indem wir die VR_init-Funktion aufrufen, die die Art der VR-Anwendung empfängt, die wir als Parameter verwenden werden, und falls alles in Ordnung ist, gibt sie einen VR-Kontext zurück, den wir später verwenden können, um Daten aus dem System zu erhalten. Der Anwendungstyp kann eine 3D-Anwendung wie ein Spiel oder eine andere 3D-Anwendung sein, eine Overlay-Anwendung wie eine Utility-Anwendung, auf die wir innerhalb der Laufzeit selbst zugreifen (denken Sie an das Revive Dashboard oder die OpenVR Advanced Settings), oder ein paar andere. Obwohl wir (noch!) keine Grafiken machen werden, werden wir einen 3D-Applikationskontext erstellen, der dem Parameter VRApplication_Scene entspricht.

Vor der Initialisierung des Kontextes kann es auch sinnvoll sein, zu überprüfen, ob ein HMD angeschlossen ist und die Laufzeit korrekt installiert ist. Dies kann mit den Aufrufen VR_IsHmdPresent und VR_IsRuntimeInstalled geschehen. Bei all dem sollte die OpenVR-Initialisierung die folgende Form haben (Hinweis: Beachten Sie, dass alle folgenden Codeausschnitte nur Pseudocode sind und möglicherweise nicht genau so kompiliert werden, wir sie geschrieben wurden. Es wird empfohlen, die API-Dokumente zu lesen, um die Details der einzelnen Funktionen zu überprüfen):

Copy to Clipboard

Da wir unseren VR-Kontext bereits initialisiert haben, können wir die Gelegenheit nutzen, im Rahmen unserer Initialisierungsfunktion einige Informationen über die vom VR-System verfolgten Geräte zu erhalten. Um dies zu erreichen, müssen wir alle Geräte, die vom System erkannt werden, durchlaufen. Der VR-Kontext zur Laufzeit führt eine Liste aller zu trackenden Geräte, und wir können einige Variablen und Funktionen verwenden, um diese Liste zu durchlaufen. Die OpenVR-Variable k_unTrackedDeviceIndex_Hmd verwaltet die ID des ersten erkannten Geräts (das immer das Headset selbst ist), und die Variable k_unMaxTrackedDeviceCount verwaltet die ID des zuletzt möglichen Geräts. Zusätzlich gibt es noch eine nützliche Funktion mit der Bezeichnung IsTrackedDeviceConnected als Teil des VR-Kontextes, mit der wir abfragen können, ob dieses bestimmte Gerät getrackt wird oder nicht.

So können wir diese Variablen und diese Funktion verwenden, um die ganze Liste der möglichen Geräte zu durchlaufen und Daten von denen zu erhalten, die korrekt getrackt werden. Sobald wir wissen, dass ein Gerät getrackt wird, können wir einige grundlegende Informationen abfragen, wie z.B. seinen Typ (HMD, Controller, Basisstation, Standalone Tracker etc.) mit der Funktion GetTrackedDeviceClass, oder seinen Namen und zusätzliche Eigenschaften mit der Funktion GetStringTrackedDeviceProperty. Beide Funktionen werden vom VR-Kontext bereitgestellt, aber Sie werden sehen, dass sie im Code in einige Utility-Funktionen eingepackt sind, um eine Zeichenkette aus der zurückgegebenen Datenstruktur zu erhalten. Der Codeausschnitt, der diese einfache Informationsbeschaffung aus dem VR-System veranschaulicht, ist unten dargestellt:

Copy to Clipboard

Es ist hier anzumerken, dass, wenn wir den OpenVR-API-Code überprüfen, wir feststellen, dass alle diese Funktionen keine Implementierung haben, da sie als reine virtuelle Funktionen in der Header-Datei enthalten sind. Denn derjenige, der für die Implementierung verantwortlich ist, ist die Laufzeit, die am Ende über den Treiber mit der zugrunde liegenden Hardware kommuniziert. So wird jeder Hardware-Hersteller, der sein Headset-Gespräch mit OpenVR führen möchte, die OpenVR-Treiber-API mit den richtigen hardware-spezifischen Anweisungen implementieren, um auf die benötigten Informationen zuzugreifen und diese abzurufen. Wichtig ist, dass die zurückkehrenden Strukturen bei der Implementierung der API die gleichen Typen und Datenstrukturen teilen, unabhängig vom Anbieter, und das ist der Hauptgrund, warum diese Art von APIs plattformübergreifend sind.

Da wir nun OpenVR initialisiert und einige grundlegende Informationen von den angeschlossenen Geräten abgerufen haben, sind wir in der Lage, die Hauptschleife der Anwendung zu kodieren, in der wir die Position aller angeschlossenen Geräte auf dem Bildschirm in jedem Frame erhalten und drucken. Sie werden im Code sehen, dass die PollNextEvent-Funktion verwendet wird, um nach Ereignissen zu suchen, aber das sind nur Ereignisse, die verwendet werden, um relevante Informationen auszudrucken, wie z.B. wenn ein neues Gerät erkannt wird oder das Tracking verloren geht, wenn Tasten auf den Geräten gedrückt werden usw., so dass es für die Aufgabe, Positionen abzurufen, nicht unbedingt notwendig ist und wir werden es hier nicht näher erläutern.

In dieser Zeile ist der relevanteste Aufruf der Aufruf von GetDeviceToAbsoluteTrackingPose, der das Koordinatensystem, in dem die Positionen benachrichtigt werden sollen, ein leeres Array von TrackedDevicePose_t Datenstrukturelementen, die als Ergebnis gefüllt werden sollen, und die Länge dieses Arrays empfängt. Das TrackedDevicePose_t Array von Elementen wird alle Informationen über die Position, Ausrichtung und andere Eigenschaften jedes Tracking-Gerätes (Pose genannt) enthalten, also ist dies das Array, das wir abfragen müssen, um die Position jedes verfolgten Gerätes zu erhalten. Genauer gesagt, ist das Attribut mDeviceToAbsoluteTracking dieser Struktur das Attribut, das zur Aufrechterhaltung der Geräteposition und -orientierung verwendet wird. Wie üblich werden Positionen und Orientierungen durch Matrizen dargestellt. In diesem Fall sind beide in einer einzigen 3×4-Matrix der Form enthalten:

Copy to Clipboard

wobei die r-Werte der Drehung der Vorrichtung entsprechen (d.h.: ihrer Ausrichtung um jede der drei Achsen), und die t-Werte der Translation (d.h. ihrer Position) entsprechen. Wir werden vorerst nur den Übersetzungsteil verwenden, so dass in Anbetracht dessen der Codeausschnitt für das Abrufen der Position jedes Geräts in jedem Frame wie folgt aussieht:

Copy to Clipboard

Einige zusätzliche Details über den obigen Code-Ausschnitt:

  • Wie bereits erwähnt, ist der erste Parameter der Funktion GetDeviceToAbsoluteTrackingPose das Koordinatensystem, in dem die Positionen ausgegeben werden. In diesem Fall verwenden wir das Koordinatensystem, das durch den Wert TrackingUniverseStanding vorgegeben ist, d.h. wir verwenden den Ursprung und die Ausrichtung aus dem konfigurierten Spielbereich für Steherlebnisse (wir hätten auch das Koordinatensystem verwenden können, das den sitzenden Erfahrungen entspricht oder sogar ein generisches unkalibriertes).
  • Die Attribute bDevicelsConnected und bPoseIsValid sind nur Flags, die in der Datenstruktur TrackedDevicePose_t enthalten sind und angeben, ob diese bestimmte Pose einem angeschlossenen Gerät entspricht und ob es sich um eine gültige Pose handelt oder nicht.
  • Die GetControllerState Funktion ermöglicht es uns, zu jedem bestimmten Zeitpunkt auf den Reglerzustand zuzugreifen, so dass es sinnvoll ist, in Echtzeit zu überprüfen, ob eine Taste gedrückt ist oder nicht. Wir können auch die Hilfsfunktion ButtonMaskFromld für diese Aufgabe verwenden, um eine Bitmap zu erhalten, in der jedes Bit Null ist, außer demjenigen, das dem als Parameter angegebenen Button entspricht. Dann können wir bitweise- oder diese Bitmap mit dem ulButtonPressed-Attribut, das von GetControllerState zurückgegeben wird, um zu überprüfen, ob tatsächlich etwas gedrückt ist.

Schließlich ist es immer wichtig, den von uns erstellten VR-Kontext ordnungsgemäß zu zerstören, um den gesamten zugewiesenen Speicher freizugeben und das System sauber und bereit für zukünftige Ausführungen zu machen. Dies geschieht durch einen Aufruf der von OpenVR bereitgestellten Ausführungen zu machen. Dies geschieht durch einen Aufruf der von OpenVR bereitgestellten VR_Shutdown-Funktion, bevor Sie unsere Abwendung verlassen.

Letzte Gedanken und was als nächstes kommt.

Wir hoffen, dass ihnen der Beitrag gefallen hat und dass es nützlich war, zu verstehen, wie VR auf mittlerer Ebene direkt zwischen einer generischen VR-Software und der zugrunde liegenden Hardware funktioniert.

Sofern Sie noch Fragen oder Anregungen haben sollten, hinterlassen Sie uns unten einen Kommentar.

Vielen Dank für ihren Besuch.

Quelle: skarredghost.com, Matias Nassi