Virtual Reality jetzt im Web verfügbar, Teil II

Frame-Loop

Joe Medley
Joe Medley

Vor Kurzem habe ich den Artikel Virtual Reality kommt ins Web veröffentlicht, in dem die grundlegenden Konzepte der WebXR Device API vorgestellt wurden. Außerdem habe ich eine Anleitung zum Anfordern, Betreten und Beenden einer XR-Sitzung bereitgestellt.

In diesem Artikel wird die Frame-Schleife beschrieben, eine vom User-Agent gesteuerte Endlosschleife, in der Inhalte wiederholt auf dem Bildschirm dargestellt werden. Die Inhalte werden in einzelnen Blöcken gezeichnet, die als Frames bezeichnet werden. Die Abfolge der Frames erzeugt die Illusion von Bewegung.

WebGL und WebGL2 sind die einzigen Möglichkeiten, Inhalte während eines Frame-Loops in einer WebXR-App zu rendern. Glücklicherweise bieten viele Frameworks eine Abstraktionsschicht über WebGL und WebGL2. Zu diesen Frameworks gehören three.js, babylonjs und PlayCanvas. A-Frame und React 360 wurden für die Interaktion mit WebXR entwickelt.

Dieser Artikel ist weder eine WebGL- noch eine Framework-Anleitung. Darin werden die Grundlagen eines Frame-Loops anhand des Beispiels für eine immersive VR-Sitzung der Immersive Web Working Group erläutert (Demo, Quellcode). Wenn Sie sich mit WebGL oder einem der Frameworks vertraut machen möchten, finden Sie im Internet eine wachsende Liste von Artikeln.

Die Spieler und das Spiel

Beim Versuch, die Frame-Schleife zu verstehen, verlor ich mich immer wieder in den Details. Es gibt viele Objekte und einige davon werden nur durch Referenzeigenschaften anderer Objekte benannt. Damit Sie den Überblick behalten, beschreibe ich die Objekte, die ich „Spieler“ nenne. Dann erkläre ich, wie sie miteinander interagieren, was ich als „Spiel“ bezeichne.

Die Spieler

XRViewerPose

Eine Pose ist die Position und Ausrichtung eines Objekts im 3D-Raum. Sowohl Zuschauer als auch Eingabegeräte haben eine Haltung, aber hier geht es um die Haltung der Zuschauer. Sowohl die Pose des Betrachters als auch die des Eingabegeräts haben ein transform-Attribut, das ihre Position als Vektor und ihre Ausrichtung als Quaternion relativ zum Ursprung beschreibt. Der Ursprung wird basierend auf dem angeforderten Referenzraumtyp beim Aufrufen von XRSession.requestReferenceSpace() angegeben.

Referenzbereiche lassen sich nur schwer erklären. Ich gehe ausführlich darauf ein im Artikel Erweiterte Realität. Das Beispiel, das ich als Grundlage für diesen Artikel verwende, verwendet einen 'local'-Referenzbereich. Das bedeutet, dass sich der Ursprung an der Position des Betrachters zum Zeitpunkt der Sitzungserstellung befindet, ohne eine klar definierte Bodenfläche. Die genaue Position kann je nach Plattform variieren.

XRView

Eine Ansicht entspricht einer Kamera, die die virtuelle Szene betrachtet. Eine Ansicht hat außerdem ein transform-Attribut, das ihre Position als Vektor und ihre Ausrichtung beschreibt. Sie werden sowohl als Vektor/Quaternion-Paar als auch als äquivalente Matrix bereitgestellt. Sie können je nach Code die gewünschte Darstellung verwenden. Jede Ansicht entspricht einem Display oder einem Teil eines Displays, das von einem Gerät verwendet wird, um Bilder für den Betrachter zu präsentieren. XRView-Objekte werden vom XRViewerPose-Objekt in einem Array zurückgegeben. Die Anzahl der Ansichten im Array variiert. Auf Mobilgeräten hat eine AR-Szene eine Ansicht, die den Bildschirm des Geräts möglicherweise vollständig bedeckt. Headsets haben in der Regel zwei Ansichten, eine für jedes Auge.

XRWebGLLayer

Ebenen enthalten Bitmap-Bilder und Beschreibungen dazu, wie diese Bilder auf dem Gerät gerendert werden sollen. Diese Beschreibung trifft nicht ganz auf diesen Spieler zu. Ich sehe es als Vermittler zwischen einem Gerät und einem WebGLRenderingContext. Die MDN vertritt eine ähnliche Ansicht und erklärt, dass es eine „Verknüpfung“ zwischen den beiden darstellt. So erhalten Sie Zugriff auf die anderen Spieler.

Im Allgemeinen speichern WebGL-Objekte Statusinformationen für das Rendern von 2D- und 3D-Grafiken.

WebGLFramebuffer

Ein Framebuffer stellt Bilddaten für die WebGLRenderingContext bereit. Nachdem du es aus der XRWebGLLayer abgerufen hast, übergibst du es einfach an die aktuelle WebGLRenderingContext. Außer über den Aufruf von bindFramebuffer() (mehr dazu später) greifen Sie nie direkt auf dieses Objekt zu. Sie übergeben ihn lediglich von der XRWebGLLayer an den WebGL-Renderingkontext.

XRViewport

Ein Darstellungsbereich enthält die Koordinaten und Abmessungen eines rechteckigen Bereichs in der WebGLFramebuffer.

WebGLRenderingContext

Ein Rendering-Kontext ist ein programmatischer Zugriffspunkt für einen Canvas (der Bereich, auf dem wir zeichnen). Dazu sind sowohl ein WebGLFramebuffer als auch ein XRViewport erforderlich.

Beachten Sie die Beziehung zwischen XRWebGLLayer und WebGLRenderingContext. Eine entspricht dem Gerät des Betrachters und die andere der Webseite. WebGLFramebuffer und XRViewport werden von der ersten an die zweite übergeben.

Die Beziehung zwischen XRWebGLLayer und WebGLRenderingContext
Die Beziehung zwischen XRWebGLLayer und WebGLRenderingContext

Das Spiel

Nachdem wir nun wissen, wer die Spieler sind, sehen wir uns das Spiel an, das sie spielen. Es ist ein Spiel, das mit jedem Frame von vorn beginnt. Frames sind Teil eines Frame-Loops, der mit einer Geschwindigkeit ausgeführt wird, die von der zugrunde liegenden Hardware abhängt. Bei VR-Anwendungen kann die Bildrate zwischen 60 und 144 Bildern pro Sekunde liegen. AR für Android läuft mit 30 Bildern pro Sekunde. Ihr Code sollte keine bestimmte Framerate annehmen.

Der grundlegende Ablauf der Frame-Schleife sieht so aus:

  1. Rufen Sie einfach XRSession.requestAnimationFrame() an. Als Reaktion ruft der User-Agent den von Ihnen definierten XRFrameRequestCallback auf.
  2. In der Callback-Funktion:
    1. Rufen Sie XRSession.requestAnimationFrame() noch einmal an.
    2. Die Körperhaltung des Betrachters
    3. Übergeben (binden, „bind“) Sie die WebGLFramebuffer von der XRWebGLLayer an die WebGLRenderingContext.
    4. Durchlaufen Sie jedes XRView-Objekt, rufen Sie dessen XRViewport aus dem XRWebGLLayer ab und übergeben Sie es an das WebGLRenderingContext.
    5. Etwas in den Framebuffer zeichnen.

Da die Schritte 1 und 2a im vorherigen Artikel behandelt wurden, beginne ich mit Schritt 2b.

Körperhaltung des Betrachters

Das versteht sich wahrscheinlich von selbst. Wenn ich etwas in AR oder VR zeichnen möchte, muss ich wissen, wo sich der Betrachter befindet und wo er hinsieht. Die Position und Ausrichtung des Betrachters wird durch ein XRViewerPose-Objekt angegeben. Ich hole mir die Haltung des Betrachters, indem ich XRFrame.getViewerPose() für den aktuellen Animationsframe aufrufe. Ich übergebe ihm den Referenzbereich, den ich beim Einrichten der Sitzung abgerufen habe. Die von diesem Objekt zurückgegebenen Werte beziehen sich immer auf den Referenzbereich, den ich angefordert habe, als ich die aktuelle Sitzung gestartet habe. Wie Sie sich erinnern, muss ich beim Anfordern der Pose den aktuellen Referenzraum übergeben.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
   
// Render based on the pose.
 
}
}

Es gibt eine Zuschauerpose, die die Gesamtposition des Nutzers darstellt, also entweder den Kopf des Zuschauers oder die Kamera des Smartphones. Die Pose gibt Ihrer Anwendung an, wo sich der Betrachter befindet. Für das tatsächliche Bild-Rendering werden XRView-Objekte verwendet, auf die ich gleich näher eingehe.

Bevor ich fortfahre, teste ich, ob die Körperhaltung des Betrachters zurückgegeben wurde, falls das System das Tracking verliert oder die Körperhaltung aus Datenschutzgründen blockiert. Tracking ist die Fähigkeit des XR-Geräts, zu wissen, wo es und/oder seine Eingabegeräte sich im Verhältnis zur Umgebung befinden. Das Tracking kann auf verschiedene Arten verloren gehen und variiert je nach verwendeter Tracking-Methode. Wenn beispielsweise Kameras am Headset oder Smartphone für das Tracking verwendet werden, kann das Gerät seinen Standort in Situationen mit wenig oder gar keinem Licht oder wenn die Kameras abgedeckt sind, möglicherweise nicht mehr bestimmen.

Ein Beispiel für das Blockieren der Pose aus Datenschutzgründen: Wenn auf dem Headset ein Sicherheitsdialogfeld wie eine Berechtigungsanfrage angezeigt wird, stellt der Browser möglicherweise keine Posen mehr für die Anwendung bereit. Ich habe jedoch bereits XRSession.requestAnimationFrame() aufgerufen, damit der Frame-Loop fortgesetzt wird, falls das System wiederhergestellt werden kann. Andernfalls beendet der User-Agent die Sitzung und ruft den end-Ereignis-Handler auf.

Ein kurzer Umweg

Für den nächsten Schritt sind Objekte erforderlich, die während der Sitzungseinrichtung erstellt wurden. Ich habe einen Canvas erstellt und ihn angewiesen, einen XR-kompatiblen WebGL-Rendering-Kontext zu erstellen, den ich durch Aufrufen von canvas.getContext() erhalten habe. Das Zeichnen erfolgt mit der WebGL API, der WebGL2 API oder einem WebGL-basierten Framework wie Three.js. Dieser Kontext wurde über updateRenderState() zusammen mit einer neuen Instanz von XRWebGLLayer an das Sitzungsobjekt übergeben.

let canvas = document.createElement('canvas');
// The rendering context must be based on WebGL or WebGL2
let webGLRenContext
= canvas.getContext('webgl', { xrCompatible: true });
xrSession
.updateRenderState({
    baseLayer
: new XRWebGLLayer(xrSession, webGLRenContext)
 
});

WebGLFramebuffer übergeben ('binden')

Der XRWebGLLayer stellt einen Framebuffer für den WebGLRenderingContext bereit, der speziell für die Verwendung mit WebXR entwickelt wurde und den Standard-Framebuffer des Rendering-Kontexts ersetzt. In WebGL wird dies als „Binding“ bezeichnet.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
// Iterate over the views
 
}
}

Für jedes XRView-Objekt iterieren

Nachdem Sie die Pose abgerufen und den Framebuffer gebunden haben, ist es an der Zeit, die Ansichten abzurufen. Die XRViewerPose enthält ein Array von XRView-Benutzeroberflächen, die jeweils ein Display oder einen Teil eines Displays darstellen. Sie enthalten Informationen, die zum Rendern von Inhalten erforderlich sind, die für das Gerät und den Betrachter korrekt positioniert sind, z. B. das Sichtfeld, den Augenabstand und andere optische Eigenschaften. Da ich zwei Augen zeichne, habe ich zwei Ansichten, die ich durchlaufe und für die ich jeweils ein separates Bild zeichne.

Bei der Implementierung für smartphonebasierte Augmented Reality würde ich nur eine Ansicht verwenden, aber trotzdem eine Schleife. Es mag zwar sinnlos erscheinen, eine Ansicht zu iterieren, aber so haben Sie einen einzigen Renderingpfad für ein breites Spektrum an immersiven Erlebnissen. Das ist ein wichtiger Unterschied zwischen WebXR und anderen immersiven Systemen.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
for (let xrView of xrViewerPose.views) {
     
// Pass viewports to the context
   
}
 
}
}

XRViewport-Objekt an den WebGLRenderingContext übergeben

Ein XRView-Objekt bezieht sich auf das, was auf einem Bildschirm zu sehen ist. Aber um in dieser Ansicht zu zeichnen, benötige ich Koordinaten und Abmessungen, die für mein Gerät spezifisch sind. Wie beim Framebuffer fordere ich sie vom XRWebGLLayer an und leite sie an den WebGLRenderingContext weiter.

function onXRFrame(hrTime, xrFrame) {
  let xrSession
= xrFrame.session;
  xrSession
.requestAnimationFrame(onXRFrame);
  let xrViewerPose
= xrFrame.getViewerPose(xrRefSpace);
 
if (xrViewerPose) {
    let glLayer
= xrSession.renderState.baseLayer;
    webGLRenContext
.bindFramebuffer(webGLRenContext.FRAMEBUFFER, glLayer.framebuffer);
   
for (let xrView of xrViewerPose.views) {
      let viewport
= glLayer.getViewport(xrView);
      webGLRenContext
.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
     
// Draw something to the framebuffer
   
}
 
}
}

Der WebGLRenContext

Beim Schreiben dieses Artikels habe ich mit einigen Kollegen über die Benennung des webGLRenContext-Objekts diskutiert. In den Beispielscripts und im Großteil des WebXR-Codes wird diese Variable einfach gl genannt. Als ich die Samples untersuchte, vergaß ich immer wieder, worauf sich gl bezog. Ich habe sie webGLRenContext genannt, damit Sie sich beim Lernen daran erinnern, dass dies eine Instanz von WebGLRenderingContext ist.

Der Grund dafür ist, dass Methodenamen mit gl wie ihre Entsprechungen in der OpenGL ES 2.0 API aussehen, die zum Erstellen von VR in kompilierten Sprachen verwendet wird. Diese Tatsache ist offensichtlich, wenn Sie bereits VR-Apps mit OpenGL erstellt haben, aber verwirrend, wenn Sie mit dieser Technologie noch nicht vertraut sind.

Etwas in den Framebuffer zeichnen

Wenn Sie wirklich ehrgeizig sind, können Sie WebGL direkt verwenden. Ich empfehle das jedoch nicht. Es ist viel einfacher, eines der oben aufgeführten Frameworks zu verwenden.

Fazit

Es werden aber weiterhin WebXR-Updates und -Artikel veröffentlicht. Eine Referenz für alle WebXR-Schnittstellen und ‑Mitglieder finden Sie im MDN. Informationen zu bevorstehenden Verbesserungen an den Benutzeroberflächen selbst finden Sie unter ChromeStatus.

Foto von JESHOOTS.COM bei Unsplash