AR session types are here

Chrome 81 adds two long-awaited pieces to the WebXR Device API: augmented reality session types and hit testing. For developers who already work with the API for VR, the learning curve for AR is minimal. Entering a session and running a frame loop are the same. The real differences live in the configuration — making sure content renders correctly against a real-world view instead of a fully synthetic one.

If you're new to WebXR, you'll want to be comfortable with requesting and entering sessions and running a frame loop before reading on. The code in this article follows the Immersive AR Session demo (source) from the Immersive Web Working Group's WebXR Device API samples. To try it yourself you'll need a recent Android device running Chrome 81 or later.

Why AR in the browser matters

Embedding AR directly into a web page makes some compelling interactions possible without a native app. Education pages could add spatial learning aids; e-commerce sites could let shoppers place a life-size product model in their own room. Once a virtual object is anchored to a real surface, the user can move around it, approach it, or step back, getting far more context than a flat product photo can convey.

That scenario requires two things: the ability to run an immersive AR session, and a way to detect surfaces in the real world. This article explains the former. The companion piece on the WebXR Hit Test API covers surface detection.

Starting an AR session

Session setup follows the pattern you already know. Use xr.isSessionSupported() to test availability, this time checking for 'immersive-ar' instead of 'immersive-vr'.

if (navigator.xr) {
  const supported = await navigator.xr.isSessionSupported('immersive-ar');
  if (supported) {
    xrButton.addEventListener('click', onButtonClicked);
    xrButton.textContent = 'Enter AR';
    xrButton.enabled = supported; // supported is Boolean
  }
}

If the session type is supported, an "Enter AR" button becomes available. Clicking that triggers xr.requestSession() with the same 'immersive-ar' argument.

let xrSession = null;
function onButtonClicked() {
  if (!xrSession) {
    navigator.xr.requestSession('immersive-ar')
    .then((session) => {
      xrSession = session;
      xrSession.isImmersive = true;
      xrButton.textContent = 'Exit AR';
      onSessionStarted(xrSession);
    });
  } else {
    xrSession.end();
  }
}

The sample code references an XRSession property called isImmersive. That property is a convenience added for the sample itself, not a part of the WebXR spec. The API keeps this kind of state out because applications need to track it in ways that suit their own logic.

Session setup differs in three places

In the earlier VR article, onSessionStarted() had a straightforward beginning:

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);

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

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

  xrSession.requestReferenceSpace('local-floor')
  .then((refSpace) => {
    xrRefSpace = refSpace;
    xrSession.requestAnimationFrame(onXRFrame);
  });
}

An AR session needs a few adjustments. Content renders against a real-world camera feed, so a solid background color would block the view. The sample checks the isImmersive convenience property before configuring the scene to clear without a background.

function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);

  if (session.isImmersive) {
    removeBackground();
  }

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

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

  refSpaceType = xrSession.isImmersive ? 'local' : 'viewer';
  xrSession.requestReferenceSpace(refSpaceType).then((refSpace) => {
    xrSession.requestAnimationFrame(onXRFrame);
  });

}

Reference spaces are next. Earlier articles didn't dig into them because the default worked well enough for VR. That simplification only goes so far. A reference space defines the relationship of the virtual coordinate system to the physical world: where the origin sits, whether the user can move within it, and whether a boundary is predefined. In every reference space the X axis runs left/right, Y runs up/down, and Z runs forward/backward, with positive values to the right, up, and backward respectively.

The coordinates reported by XRFrame.getViewerPose() depend on which reference space type was requested at session start. Getting that choice right matters most when a single page supplies both a page-level preview and an immersive scene. The sample uses the isImmersive convenience property to pick the right space:

let refSpaceType
function onSessionStarted(xrSession) {
  xrSession.addEventListener('end', onSessionEnded);

  if (session.isImmersive) {
    removeBackground();
  }

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

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

  refSpaceType = xrSession.isImmersive ? 'local' : 'viewer';
  xrSession.requestReferenceSpace(refSpaceType).then((refSpace) => {
    xrSession.requestAnimationFrame(onXRFrame);
  });
}

If you open the demo before starting AR, you're looking at an ordinary WebGL scene that you can pan around with drag gestures. The AR mode is what swaps in the device camera and maps motion to the viewer's physical movement. That switch a different reference space for each mode.

  • local: The origin starts at the viewer's position from session creation. The floor is not well-defined, and the origin may shift across platforms. The expectation is that rotation will suffice for viewing, though some positional movement is possible — as the sample shows.
  • viewer: This space stays glued to the viewing device. It is common for inline page content. Passed to getViewerPose(), it returns no tracking and always reports a pose at the origin until an app repositions it with XRReferenceSpace.getOffsetReferenceSpace(). The sample uses the viewer space to allow the touch-drag camera motion you see before entering AR.

The frame loop stays familiar

Frame loop mechanics do not change from the VR case. Pass the active reference space type to XRFrame.getViewerPose(), and the returned XRViewerPose reflects that space. Using the viewer space by default means the same frame loop handles both the inline preview and the later immersive AR experience, which reduces the amount of code that needs to be maintained.

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

Beyond the basics

This covers only the most fundamental steps for exposing immersive AR content. The Immersive Web Working Group's WebXR Device API samples include a wider array of scenarios, and the new hit test article shows how to anchor virtual items to real-world surfaces. Watch the web.dev blog for more material in the coming months.