Virtual Reality jetzt im Web verfügbar, Teil II

Frame-Loop

Joe Medley
Joe Medley

Vor Kurzem habe ich den Artikel Virtual Reality kommt im Web veröffentlicht, in dem die grundlegenden Konzepte hinter 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. Inhalte werden in separaten Blöcken gezeichnet, die als Frames bezeichnet werden. Die Abfolge der Frames erzeugt die Illusion von Bewegung.

Was in diesem Artikel nicht behandelt wird

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. Anhand des Beispiels für eine immersive VR-Sitzung der Immersive Web Working Group (Demo, Quelle) werden die Grundlagen einer Frame Loop erläutert. Wenn Sie mehr über WebGL oder eines der Frameworks erfahren möchten, finden Sie im Internet eine stetig wachsende Artikelliste.

Die Spieler und das Spiel

Beim Versuch, die Frame-Schleife zu verstehen, verlor ich mich immer wieder in den Details. Es sind viele Objekte im Spiel, von denen einige nur nach Referenzeigenschaften anderer Objekte benannt sind. Damit Sie das Ganze besser halten, 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 von etwas im 3D-Raum. Sowohl Zuschauer als auch Eingabegeräte haben eine Pose, aber um diese Haltung geht es uns hier. 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.

Es kann etwas dauern, bis Referenzräume erklärt sind. Ich gehe ausführlich darauf ein im Artikel Erweiterte Realität. In dem Beispiel, das ich als Grundlage für diesen Artikel verwende, wird ein 'local'-Referenzraum verwendet. Das bedeutet, dass sich der Ursprung zum Zeitpunkt der Sitzungserstellung an der Position des Betrachters befindet, ohne dass ein klar definierter Mindestpreis festgelegt ist. 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 jeweils am besten geeignete 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 liefern eine Quelle für Bitmapbilder 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. 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 Sie es aus dem XRWebGLLayer abgerufen haben, übergeben Sie es einfach an das aktuelle WebGLRenderingContext. Außer bindFramebuffer() (mehr dazu später) werden Sie nie direkt auf dieses Objekt zugreifen. Sie übergeben sie lediglich vom XRWebGLLayer an den WebGLRenderingContext.

XRViewport

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

WebGLRenderingContext

Ein Renderingkontext ist ein programmatischer Zugangspunkt für ein Canvas (den Bereich, auf den wir uns beziehen). 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 an, welches Spiel sie spielen. Es ist ein Spiel, das mit jedem Frame neu 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. Dein 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. Zeigen Sie die Position des Zuschauers.
    3. Übergib ('bind') die WebGLFramebuffer von XRWebGLLayer an 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.

Auf die Position des Zuschauers eingehen

Das versteht sich wahrscheinlich von selbst. Um in AR oder VR etwas zu zeichnen, muss ich wissen, wo sich der Betrachter befindet und Die Position und Ausrichtung des Betrachters werden durch ein XRViewerPose-Objekt bereitgestellt. Um die Position des Betrachters zu ermitteln, rufe ich XRFrame.getViewerPose() im aktuellen Animationsframe auf. Ich übergebe den Referenzraum, den ich beim Einrichten der Sitzung erworben 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 vielleicht erinnern, muss ich bei der Anforderung der Position 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 weitermache, teste ich, ob die Haltung des Zuschauers zurückgegeben wurde, falls das System die Position verliert oder sie 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 aber 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 der Sprache von 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, von denen jede ein Display oder einen Teil eines Displays darstellt. 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 für zwei Augen zeichne, habe ich zwei Ansichten, durch 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. Um in diese Ansicht zu zeichnen, brauche 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 an der Analyse der Beispiele arbeitete, habe ich immer wieder vergessen, worauf sich gl bezieht. Ich habe sie webGLRenContext genannt, um Sie daran zu erinnern, dass dies eine Instanz von WebGLRenderingContext ist.

Dies liegt daran, dass die Methodennamen durch die Verwendung von gl wie ihre Gegenstücke in der OpenGL ES 2.0 API aussehen können, 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 sehr ehrgeizig sind, können Sie WebGL direkt verwenden. Das empfehle ich jedoch nicht. Es ist viel einfacher, eines der oben aufgeführten Frameworks zu verwenden.

Fazit

Das ist aber nicht das Ende von WebXR-Updates oder -Artikeln. 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 auf Unsplash