Alles über den Frame-Loop
Vor Kurzem habe ich den Artikel Virtual reality comes to the web veröffentlicht, in dem die grundlegenden Konzepte der WebXR Device API vorgestellt werden. Ich habe auch eine Anleitung zum Anfordern, Starten und Beenden einer XR-Sitzung bereitgestellt.
In diesem Artikel wird die Frame-Schleife beschrieben. Das ist eine vom User-Agent gesteuerte Endlosschleife, in der Inhalte wiederholt auf dem Bildschirm dargestellt werden. Inhalte werden in diskreten Blöcken, sogenannten Frames, dargestellt. Die Abfolge der Frames erzeugt die Illusion von Bewegung.
Was dieser Artikel nicht ist
WebGL und WebGL2 sind die einzigen Möglichkeiten, Inhalte während einer Frame-Schleife in einer WebXR-App zu rendern. Glücklicherweise bieten viele Frameworks eine Abstraktionsebene ü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.
In diesem Artikel werden die Grundlagen einer Frame-Schleife anhand des Beispiels für immersive VR-Sitzungen der Immersive Web Working Group (Demo, Quelle) erläutert. Wenn Sie sich näher mit WebGL oder einem der Frameworks beschäftigen möchten, finden Sie online eine wachsende Liste von Ressourcen.
Die Spieler und das Spiel
Beim Versuch, die Frame-Schleife zu verstehen, habe ich mich immer wieder in den Details verloren. Es gibt viele Objekte und einige von ihnen werden nur über Referenzeigenschaften anderer Objekte benannt. Damit du den Überblick behältst, beschreibe ich die Objekte, die ich als „Akteure“ bezeichne. Dann beschreibe ich, wie sie interagieren. Das nenne ich „das Spiel“.
Die Spieler
XRViewerPose
Eine Pose ist die Position und Ausrichtung eines Objekts im 3D-Raum. Sowohl Zuschauer als auch Eingabegeräte haben eine Position, aber hier geht es um die Position des Zuschauers. Sowohl die Position des Zuschauers als auch die des Eingabegeräts haben ein transform-Attribut, das die Position als Vektor und die Ausrichtung als Quaternion relativ zum Ursprung beschreibt. Der Ursprung wird basierend auf dem angeforderten Referenzraumtyp beim Aufrufen von XRSession.requestReferenceSpace() angegeben.
Referenzräume sind etwas erklärungsbedürftig. Ich gehe in Augmented Reality ausführlich darauf ein. Das Beispiel, das ich als Grundlage für diesen Artikel verwende, nutzt einen 'local'-Referenzraum. Das bedeutet, dass der Ursprung an der Position des Betrachters zum Zeitpunkt der Sitzungserstellung liegt. Es gibt keinen genau definierten Boden und die genaue Position kann je nach Plattform variieren.
XRView
Eine Ansicht entspricht einer Kamera, die die virtuelle Szene aufnimmt. Eine Ansicht hat auch ein transform-Attribut, das ihre Position als Vektor und ihre Ausrichtung beschreibt.
Sie werden sowohl als Vektor-/Quaternionenpaar als auch als entsprechende Matrix bereitgestellt. Sie können je nach Bedarf die für Ihren Code am besten geeignete Darstellung verwenden. Jede Ansicht entspricht einem Display oder einem Teil eines Displays, das von einem Gerät verwendet wird, um dem Betrachter Bilder zu präsentieren. XRView-Objekte werden in einem Array aus dem XRViewerPose-Objekt zurückgegeben. Die Anzahl der Aufrufe im Array variiert. Auf Mobilgeräten hat eine AR‑Szene eine Ansicht, die den Gerätebildschirm abdecken kann oder nicht.
Headsets haben in der Regel zwei Ansichten, eine für jedes Auge.
XRWebGLLayer
Ebenen enthalten eine Quelle für Bitmap-Bilder und Beschreibungen, wie diese Bilder auf dem Gerät gerendert werden sollen. Diese Beschreibung gibt nicht genau wieder, was dieser Player macht. Ich sehe es als Vermittler zwischen einem Gerät und einem WebGLRenderingContext. MDN vertritt eine ähnliche Ansicht und erklärt, dass es eine Verbindung zwischen den beiden herstellt. Dadurch erhalten Sie Zugriff auf die anderen Spieler.
Im Allgemeinen werden in WebGL-Objekten Statusinformationen zum Rendern von 2D- und 3D-Grafiken gespeichert.
WebGLFramebuffer
Ein Framebuffer stellt Bilddaten für WebGLRenderingContext bereit. Nachdem Sie es aus dem XRWebGLLayer abgerufen haben, übergeben Sie es an das aktuelle WebGLRenderingContext. Außer durch den Aufruf von bindFramebuffer() (mehr dazu später) greifen Sie nie direkt auf dieses Objekt zu. Sie übergeben sie lediglich von XRWebGLLayer an WebGLRenderingContext.
XRViewport
Ein Darstellungsbereich enthält die Koordinaten und Abmessungen eines rechteckigen Bereichs im WebGLFramebuffer.
WebGLRenderingContext
Ein Rendering-Kontext ist ein programmatischer Zugriffspunkt für einen Canvas (den Bereich, auf dem wir zeichnen). Dazu sind sowohl ein WebGLFramebuffer als auch ein XRViewport erforderlich.
Achten Sie auf den Zusammenhang zwischen XRWebGLLayer und WebGLRenderingContext. Einer entspricht dem Gerät des Betrachters und der andere der Webseite.
WebGLFramebuffer und XRViewport werden vom Ersteren an den Letzteren übergeben.
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 neu beginnt. Frames sind Teil eines Frame-Loops, der mit einer Rate 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 wird mit 30 Bildern pro Sekunde ausgeführt. In Ihrem Code sollte keine bestimmte Framerate vorausgesetzt werden.
Der grundlegende Prozess für die Frame-Schleife sieht so aus:
- Rufen Sie einfach
XRSession.requestAnimationFrame()an. Als Reaktion darauf ruft der User-Agent die von Ihnen definierteXRFrameRequestCallbackauf. - In Ihrer Callback-Funktion:
- Rufen Sie
XRSession.requestAnimationFrame()noch einmal an. - Die Pose des Zuschauers abrufen
- Binden Sie die
WebGLFramebufferaus derXRWebGLLayeran dieWebGLRenderingContext. - Iterieren Sie über jedes
XRView-Objekt, rufen Sie das zugehörigeXRViewportaus demXRWebGLLayerab und übergeben Sie es an dasWebGLRenderingContext. - Etwas in den Framebuffer zeichnen.
- Rufen Sie
Da die Schritte 1 und 2a im vorherigen Artikel behandelt wurden, beginne ich mit Schritt 2b.
Pose des Zuschauers abrufen
Das versteht sich eigentlich von selbst. Damit ich etwas in AR oder VR zeichnen kann, muss ich wissen, wo sich der Betrachter befindet und wohin er schaut. Die Position und Ausrichtung des Betrachters werden durch ein XRViewerPose-Objekt angegeben. Ich rufe die Position des Zuschauers ab, indem ich XRFrame.getViewerPose() für den aktuellen Animationsframe aufrufe. Ich übergebe den Referenz-Space, den ich beim Einrichten der Sitzung erhalten habe. Die von diesem Objekt zurückgegebenen Werte sind immer relativ zum Referenzbereich, den ich beim Aufrufen der aktuellen Sitzung angefordert habe. Wie du dich vielleicht erinnerst, muss ich den aktuellen Referenzraum übergeben, wenn ich die Pose anfordere.
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 Zuschauerposition, die die Gesamtposition des Nutzers repräsentiert, d. h. entweder den Kopf des Zuschauers oder die Smartphone-Kamera.
Die Position gibt Ihrer Anwendung an, wo sich der Betrachter befindet. Für das eigentliche Rendern von Bildern werden XRView-Objekte verwendet, auf die ich gleich eingehen werde.
Bevor ich fortfahre, teste ich, ob die Zuschauerposition zurückgegeben wurde, falls das System die Position aus Datenschutzgründen nicht mehr verfolgt oder blockiert. Tracking ist die Fähigkeit des XR-Geräts, zu erkennen, wo es sich und seine Eingabegeräte relativ zur Umgebung befinden. Das Tracking kann auf verschiedene Arten verloren gehen, je nach verwendeter Methode. Wenn beispielsweise Kameras am Headset oder Smartphone zum Tracken des Geräts verwendet werden, kann das Gerät möglicherweise nicht mehr feststellen, wo es sich befindet, wenn wenig oder kein Licht vorhanden ist oder die Kameras abgedeckt sind.
Ein Beispiel für das Blockieren der Pose aus Datenschutzgründen ist, wenn auf dem Headset ein Sicherheitsdialogfeld wie eine Berechtigungsaufforderung angezeigt wird. In diesem Fall kann der Browser die Bereitstellung von Posen für die Anwendung beenden. Ich habe XRSession.requestAnimationFrame() jedoch bereits aufgerufen, damit die Frame-Schleife fortgesetzt wird, wenn das System wiederhergestellt werden kann. Andernfalls beendet der User-Agent die Sitzung und ruft den end-Ereignishandler 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-Renderingkontext zu erstellen, den ich durch Aufrufen von canvas.getContext() erhalten habe. Alle Zeichnungen werden mit der WebGL API, der WebGL2 API oder einem WebGL-basierten Framework wie Three.js erstellt. Dieser Kontext wurde mit updateRenderState() an das Sitzungsobjekt übergeben, zusätzlich zu einer neuen Instanz von XRWebGLLayer.
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)
});
Übergeben Sie das WebGLFramebuffer (bind).
Der XRWebGLLayer stellt einen Framebuffer für den WebGLRenderingContext bereit, der speziell für die Verwendung mit WebXR vorgesehen ist und den Standard-Framebuffer der Rendering-Kontexte 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
}
}
Jedes XRView-Objekt durchlaufen
Nachdem Sie die Pose erhalten und den Framebuffer gebunden haben, ist es an der Zeit, die Viewports abzurufen. Das XRViewerPose enthält ein Array von XRView-Schnittstellen, die jeweils ein Display oder einen Teil eines Displays darstellen. Sie enthalten Informationen, die zum Rendern von Inhalten benötigt werden, 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, die ich durchlaufe und für die ich jeweils ein separates Bild zeichne.
Bei der Implementierung für Smartphone-basierte Augmented Reality hätte ich nur eine Ansicht, würde aber trotzdem eine Schleife verwenden. Auch wenn es sinnlos erscheinen mag, eine Ansicht zu durchlaufen, ermöglicht dies einen einzigen Rendering-Pfad für ein Spektrum immersiver Erlebnisse. 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 WebGLRenderingContext übergeben
Ein XRView-Objekt bezieht sich auf das, was auf einem Bildschirm zu sehen ist. Um diese Ansicht zu zeichnen, benötige ich jedoch Koordinaten und Dimensionen, die für mein Gerät spezifisch sind. Wie beim Framebuffer fordere ich sie vom XRWebGLLayer an und übergebe sie an den WebGLRenderingContext.
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
}
}
}
webGLRenContext
Ich hatte eine Diskussion mit einigen Kollegen über die Benennung des webGLRenContext-Objekts. In den Beispielskripts und im meisten WebXR-Code wird diese Variable als gl bezeichnet. Als ich versuchte, die Beispiele zu verstehen, vergaß ich immer wieder, worauf sich gl bezog. Ich habe sie webGLRenContext genannt, damit du dich daran erinnerst, dass es sich um eine Instanz von WebGLRenderingContext handelt.
Der Grund dafür ist, dass durch die Verwendung von gl Methodennamen wie ihre Pendants in der OpenGL ES 2.0-API aussehen, die zum Erstellen von VR in kompilierten Sprachen verwendet wird. Wenn Sie bereits VR-Apps mit OpenGL geschrieben haben, ist das offensichtlich. Wenn Sie jedoch noch nie mit dieser Technologie gearbeitet haben, kann es verwirrend sein.
Etwas in den Framebuffer zeichnen
Wenn Sie wirklich ehrgeizig sind, können Sie WebGL direkt verwenden, aber das ist nicht empfehlenswert. Es ist viel einfacher, eines der oben aufgeführten Frameworks zu verwenden.
Fazit
Dies ist nicht das Ende der WebXR-Updates oder -Artikel. Eine Referenz für alle WebXR-Schnittstellen und -Mitglieder finden Sie auf MDN. Informationen zu bevorstehenden Verbesserungen der Schnittstellen selbst finden Sie unter Chrome Status.