WebXR Device API: Reference Space and Render Loop

Learn about spatial tracking, render loop, and state updates in the WebXR Device API.

So far, we’ve learned how to initialize the WebXR API through a browser. Next, we’ll understand how WebXR performs spatial tracking in the VR headset and recursively renders the XR content.

Reference space

Developers can analyze the mobility needs of the experience by defining an appropriate reference space. WebXR supports several different reference spaces. A reference space is a virtual coordinate system that defines the origin and orientation of a WebXR session. It allows WebXR applications to map the physical world to a virtual space and enables developers to create immersive experiences that interact with the user’s surroundings. We discuss the reference spaces supported by WebXR below:

  • viewer: This is the default reference space in WebXR and doesn’t require any tracking, such as those that use click-and-drag to explore the environment.

  • local: This reference space is used if the XR experience doesn’t require tracking, but the initial position of the experience needs to be initialized on runtime.

  • local-floor: This is similar to the local reference space but also identifies the floor level and sets its own y-axis coordinate for the XR experience.

  • bounded-floor: This reference space is preferred when the user moves around to interact with the environment within a specific boundary fully.

  • unbounded: This reference space is preferred when the user is free to move around their physical environment and travel significant distances.

The following flowchart shows how to analyze reference spaces in your environment:

Press + to interact
Reference spaces flowchart
Reference spaces flowchart

The user can choose the appropriate reference space spacial tracking option based on the requirements of their XR application.

The following code snippet shows how we request the reference space in the API:

var xrRefSpace = await xrSession.requestReferenceSpace('viewer');

The requestReferenceSpace() function returns a promise that resolves to provide us with the reference space.

The need for a render loop

As learned in the WebXR API: Initialization lesson, the XRWebGLLayer interface is hooked with an XRSession object to render content onto the end user’s device via the updateRenderState() method. However, this sets the initial render state on the viewport of the end user’s device to the content that’s inside the XRWebGLLayer interface at the instance the method was invoked. Therefore, the future changes to WebGLRenderingContext (contained inside XRWebGLLayer) won’t be automatically propagated to the end user’s device. The device must be notified when to fetch updates from the frame buffer to update its viewport.

The requestAnimationFrame() method

The XRSession object comes with a handy function, the requestAnimationFrame() method, to update the rendered content inside the end user’s device. It takes a callback method and schedules it to run before the next repaint. Once the callback is executed, the frame inside the end user’s device is updated based on the associated frame buffer initially specified in the XRWebGLLayer object. Here’s the syntax for the function and the associated callback:

requestAnimationFrame(animationFrameCallback);
function animationFrameCallback(timestamp, frame) {
// Update the frame state here
}

The requestAnimationFrame() method requires the animationFrameCallback() callback with two parameters: the timestamp parameter that specifies the time offset at which the updated viewer state was received from the end user’s device and the XRFrame object as the frame parameter.

The XRFrame object provides information on the tracked device in the virtual space, for example, head pose, controller positions, etc. This data can then be used to update the application’s state and the frame buffer (inside the WebGLRenderingContext object) that’s then rendered onto the viewport of the end user’s device.

Because the requestAnimationFrame() method schedules only one repaint job, it needs to be invoked repeatedly to continuously render new content inside the end user’s device based on its tracking information. There are several ways to achieve this. However, the simplest is to recursively call the requestAnimationFrame() method inside the animationFrameCallback() method, effectively establishing a render loop. The following snippet clarifies this process:

function animationFrameCallback(timestamp, frame) {
const session = frame.session;
// Get updated tracking information from the `XRFrame` object
const pose = frame.getViewerPose(referenceSpace);
// Update the application's state and the frame buffer
// ...
// Queue the next repaint callback
session.requestAnimationFrame(animationFrameCallback);
}
Writing the pseudocode of the recursive implementation of a render loop

Next, let’s explore how we can update the frame buffer.

Updating the frame buffer

To update the content inside the end user’s device, we have to explicitly bind the WebGLRenderingContext interface’s frame buffer with the XRWebGLLayer interface’s frame buffer first; only then the changes we’ll make to the frame buffer will get rendered onto the end user’s device. This binding is achieved with the bindFrameBuffer() method defined on the WebGLRenderingContext object. The following snippet clarifies this process:

Press + to interact
function animationFrameCallback(timestamp, frame) {
const session = frame.session;
// Get updated tracking information from the `XRFrame` object
const pose = frame.getViewerPose(referenceSpace);
// Update the application's state and the frame buffer
if (pose) {
// Only proceed if tracking data was successfully retrieved
// Fetch the XRWebGLLayer object
const glLayer = frame.session.renderState.baseLayer;
// Bind WebGLRenderingContext's frame buffer with XRWebGLLayer's frame buffer
gl.bindFrameBuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
// Update the WebGLRenderingContext's frame buffer here to propogate changes to the end user's device
// ...
}
// Queue the next repaint callback
session.requestAnimationFrame(animationFrameCallback);
}

After updating the frame buffer, we’ll determine how to terminate the session.

Terminating the session

Finally, the end() method defined on the XRSession object can be invoked to terminate the session. Any post-processing required can be catered to in a callback, for example, unbinding the frame buffer or clearing global variables, as shown in the snippet below:

function terminateSession() {
// Is an `XRSession` in progress?
if (xrSession) {
// If so, end the session
xrSession.end().then(endSession);
}
}
function endSession() {
// Unbind the frame buffer
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
// Clear global variables
xrSession = null;
xrRefSpace = null;
gl = null;
}
Implementing the high level idea of a render loop

Note: Ending the session terminates all callbacks queued for the XRSession object (e.g., the animationFrameCallback() method).

Let’s take a look at an executable example of an XR application.

Example XR application

The widget below presents an XR application that updates the color of the viewport of the end user’s device on each render call:

<!doctype html>
<html>
  <body>
    <title> WebXR Example Application</title>
    <h1> WebXR Example Application</h1>
    <button
      id="xr-button"
      >Enter XR</button
    >
    <script>
      (function () {
        // Constants
        const XRSessionType = 'immersive-vr';
        const XRRefSpaceType = 'viewer';

        // Global Variables
        let xrButton = document.getElementById('xr-button');
        let xrSession = null;
        let xrRefSpace = null;
        let gl = null;

        function initXR() {
          if (navigator.xr) {
            navigator.xr.isSessionSupported(XRSessionType).then((supported) => {
              if (!supported) {
                xrButton.textContent = 'XR Not Supported';
                xrButton.disabled = true;
              }
            });
          }
        }

        function enterXR() {
          if (!xrSession) {
            navigator.xr.requestSession(XRSessionType).then(startSession);
          }
        }

        function exitXR() {
          if (xrSession) {
            xrSession.end().then(endSession);
          }
        }

        function endSession() {
          // Unbind the frame buffer
          gl.bindFramebuffer(gl.FRAMEBUFFER, null);

          // Clear global variables
          xrSession = null;
          xrRefSpace = null;
          gl = null;
        }

        function startSession(session) {
          xrSession = session;
          xrButton.textContent = 'Exit XR';
          xrButton.onclick = endSession;

          const canvas = document.createElement('canvas');
          gl = canvas.getContext('webgl', { xrCompatible: true });

          session.updateRenderState({
            baseLayer: new XRWebGLLayer(session, gl),
          });

          session.requestReferenceSpace(XRRefSpaceType).then((refSpace) => {
            xrRefSpace = refSpace;
            session.requestAnimationFrame(animationFrameCallback);
          });
        }

        function animationFrameCallback(time, frame) {
          const session = frame.session;

          const pose = frame.getViewerPose(xrRefSpace);

          if (pose) {
            const glLayer = frame.session.renderState.baseLayer;

            gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);

            gl.clearColor(
              Math.cos(time / 2000),
              Math.cos(time / 4000),
              Math.cos(time / 6000),
              1.0
            );

            gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
          }

          session.requestAnimationFrame(animationFrameCallback);
        }

        document.getElementById("xr-button").addEventListener("click", enterXR);
        initXR();
      })();
    </script>
  </body>
</html>

Use the following XR Widget to connect your end user’s device with the above application.

VR Not Connected
Experience in VR
Connect your VR headset to get started.

Congratulations on making it this far!

Conclusion

We now have an understanding of how the WebXR render loop works is a foundational building block in learning how to create XR experiences in WebXR.