← STEAM Lessons Fabrication · Lesson 04 Fabrication

Fabrication · Lesson 04 · Extended Reality

XR: Extended Reality
OpenXR & WebXR

Explore the Reality-Virtuality spectrum from AR to full VR, understand how OpenXR unifies headset hardware under one standard API, and write browser-native XR applications with the WebXR Device API.

OpenXR WebXR AR · VR · MR Khronos Group XR Sessions

Beyond VR — The Reality-Virtuality Continuum

In 1994, researcher Paul Milgram introduced the Reality-Virtuality Continuum — a spectrum stretching from the unmodified real world on one end to a fully computer-generated environment on the other. Extended Reality (XR) is the umbrella term for every point on this spectrum.

This single concept unifies technologies that are often treated as separate: AR glasses, smartphone overlays, passthrough MR headsets, and fully immersive VR are all points along the same line. Click each card to learn where it sits.

🌍

Reality

tap to reveal

Reality

The unmodified physical world. No digital overlays, no computer-generated content — just your senses perceiving the environment directly. This is the left anchor of Milgram's continuum (position 0).

📱

Augmented Reality (AR)

tap to reveal

Augmented Reality (AR)

The real world is primary; digital content is overlaid on top. Examples: Pokémon GO on a phone, AR navigation arrows, QR code popups. The user always sees the real environment — digital elements are additions, not replacements.

🥽

Mixed Reality (MR)

tap to reveal

Mixed Reality (MR)

Digital objects are anchored to real-world surfaces and interact with them. A virtual ball rolls off a real table. Uses spatial mapping to understand room geometry. Examples: HoloLens, Meta Quest 3 passthrough MR apps.

🖥️

Virtual Reality (VR)

tap to reveal

Virtual Reality (VR)

Full replacement of reality. The headset shows only computer-generated imagery; the real world is completely blocked out. Requires 6DoF tracking (3 rotational + 3 translational axes) for convincing presence. Examples: Meta Quest, PlayStation VR2, Valve Index.

Drag to Explore the Reality-Virtuality Spectrum

Move the slider to slide along Milgram's continuum. The canvas shows how much of the view is real-world vs. virtual. Each coloured zone on the spectrum strip lights up as you enter it, and the description below updates to explain that zone.

Mixed Reality

Digital and physical objects coexist and interact. Virtual objects are anchored to real surfaces and respond to real-world geometry. Examples: HoloLens spatial mapping, Meta Quest 3 passthrough MR apps.

One API, Every Headset — How Khronos Unified XR

Before 2019, every XR platform had a completely different API. SteamVR had its own SDK, Oculus had its own SDK, Windows Mixed Reality had its own SDK. A developer who wanted their game on three platforms needed to write and maintain three completely separate rendering and input backends — a massive engineering burden.

OpenXR is a royalty-free, open standard published by the Khronos Group (the same organisation behind OpenGL, Vulkan, WebGL, and OpenCL). Released in version 1.0 in July 2019, it defines a single C API that applications call. Each platform vendor then ships an OpenXR Runtime that implements that standard for their specific hardware.

The result: the same application code compiles and runs on Meta Quest, SteamVR (Valve Index, HTC Vive), PlayStation VR2, Windows Mixed Reality, Varjo, Pico, and more — with no code changes.

🔗

XrInstance

The root OpenXR object. Created once at startup, it initialises the OpenXR loader and establishes a connection to the runtime. Think of it like a Vulkan instance — it's the entry point that everything else hangs off.

XrSession

Represents an active XR experience. It manages the render loop, frame timing, headset lifecycle events (entering/leaving standby), and the list of enabled XR features. You wait for frames, begin them, render, then end them — all through the session.

📐

XrSpace

A coordinate system used to express positions and orientations. STAGE space has its origin at the floor of the play area. LOCAL is seated/standing-scale around the headset's starting position. VIEW follows the headset itself. All tracking results are expressed relative to a space.

Supported platforms (via OpenXR runtime): Meta Quest (2/3/Pro), SteamVR (Valve Index, HTC Vive, Cosmos), PlayStation VR2 (PC tethered), Windows Mixed Reality, Varjo headsets, Pico 4, Magic Leap 2, and Apple visionOS (via third-party OpenXR runtime bridge). The same OpenXR application binary runs on all of them.

How Your Code Reaches the Hardware

Click each layer in the diagram to learn what it does. The toggle below switches between the unified OpenXR architecture and the fragmented pre-OpenXR world — showing exactly what problem OpenXR solves.

Click a layer in the diagram to see its details.

XR in the Browser — No Install Required

The WebXR Device API is a W3C specification that brings XR sessions directly to the web browser. Users on a compatible device can enter a full VR or AR experience by clicking a button on a webpage — no app download, no SDK, no install. It ships in Chrome 79+, Microsoft Edge, and the Meta Quest Browser.

WebXR defines two immersive session modes and one non-immersive mode:

Every XR session uses a reference space — the coordinate system that defines where "world centre" is:

WebXR and OpenXR are complementary, not competing. On desktop Chrome, when you enter an immersive-vr session, the browser internally talks to the system's OpenXR runtime to reach the headset. WebXR is the browser-layer API; OpenXR is the system-layer API underneath it. On native apps (Unity, Godot, Unreal) you call OpenXR directly — the browser layer does not exist.

What XR Can Your Browser Do Right Now?

This tool uses the real navigator.xr API to probe your browser's XR capabilities — no headset required to run the check. The results update live as soon as the page loads.

Checking…
navigator.xr available
The WebXR Device API is present in this browser. Required for all XR sessions.
Checking…
immersive-vr supported
Full VR sessions available. Entering this session on a desktop browser typically requires a connected OpenXR-compatible headset.
Checking…
immersive-ar supported
AR passthrough sessions available. Requires a device with camera-based passthrough (Quest 3, HoloLens, phone with ARCore/ARKit).
Checking…
inline session supported
Non-immersive inline sessions available. Used for on-page 360° viewers and device-orientation experiences without a headset.
Checking your browser's XR capabilities…

Code Examples

Four ready-to-run patterns covering the full XR development stack — from a browser-native WebXR session to a low-level OpenXR C++ frame loop.

// WebXR Device API — check support, start immersive-vr, run XR frame loop

// 1. Check that the WebXR API exists
if (!navigator.xr) {
  console.log('WebXR not supported in this browser');
}

// 2. Check if immersive-vr is available (async)
const supported = await navigator.xr.isSessionSupported('immersive-vr');
if (!supported) { console.log('No VR headset detected'); return; }

// 3. Request a session (must come from a user gesture — e.g. button click)
const session = await navigator.xr.requestSession('immersive-vr', {
  requiredFeatures: ['local-floor'],
  optionalFeatures: ['bounded-floor', 'hand-tracking']
});

// 4. Set up a WebGL context that is XR-compatible
const gl = canvas.getContext('webgl', { xrCompatible: true });
await session.updateRenderState({
  baseLayer: new XRWebGLLayer(session, gl)
});

// 5. Request a reference space (coordinate system)
const refSpace = await session.requestReferenceSpace('local-floor');

// 6. XR animation loop — called once per frame by the XR runtime
function onXRFrame(time, frame) {
  session.requestAnimationFrame(onXRFrame);       // schedule the next frame
  const pose = frame.getViewerPose(refSpace);
  if (!pose) return;

  gl.bindFramebuffer(gl.FRAMEBUFFER,
    session.renderState.baseLayer.framebuffer);

  for (const view of pose.views) {             // one view per eye
    const vp = session.renderState.baseLayer.getViewport(view);
    gl.viewport(vp.x, vp.y, vp.width, vp.height);
    // view.projectionMatrix  — camera lens matrix
    // view.transform.matrix  — eye world transform
    drawScene(view.projectionMatrix, view.transform.matrix);
  }
}
session.requestAnimationFrame(onXRFrame);

// 7. End the session cleanly
document.getElementById('exit-xr').addEventListener('click', () => session.end());
// OpenXR C++ — create instance, get system, create session, run frame loop
#include <openxr/openxr.h>

// 1. Create an XrInstance — connects the app to the OpenXR loader/runtime
XrInstanceCreateInfo instanceCI = { XR_TYPE_INSTANCE_CREATE_INFO };
strncpy(instanceCI.applicationInfo.applicationName,
        "MyXRApp", XR_MAX_APPLICATION_NAME_SIZE);
instanceCI.applicationInfo.apiVersion = XR_CURRENT_API_VERSION;

const char* extensions[] = { XR_KHR_VULKAN_ENABLE_EXTENSION_NAME };
instanceCI.enabledExtensionCount = 1;
instanceCI.enabledExtensionNames = extensions;

XrInstance instance;
xrCreateInstance(&instanceCI, &instance);    // connects to the runtime

// 2. Get the XrSystemId for the head-mounted display
XrSystemGetInfo systemGI = { XR_TYPE_SYSTEM_GET_INFO };
systemGI.formFactor = XR_FORM_FACTOR_HEAD_MOUNTED_DISPLAY;
XrSystemId systemId;
xrGetSystem(instance, &systemGI, &systemId);

// 3. Create an XrSession — the active XR experience
XrSessionCreateInfo sessionCI = { XR_TYPE_SESSION_CREATE_INFO };
sessionCI.systemId = systemId;
// ... attach your graphics binding (Vulkan/D3D12/OpenGL) here ...

XrSession session;
xrCreateSession(instance, &sessionCI, &session);

// 4. The frame loop
XrFrameWaitInfo  frameWI    = { XR_TYPE_FRAME_WAIT_INFO };
XrFrameState     frameState = { XR_TYPE_FRAME_STATE };

while (running) {
  xrWaitFrame(session, &frameWI, &frameState);    // block until frame is due
  xrBeginFrame(session, nullptr);

  // ... locate views, render each eye to XrSwapchain images ...

  XrFrameEndInfo frameEI = { XR_TYPE_FRAME_END_INFO };
  frameEI.displayTime           = frameState.predictedDisplayTime;
  frameEI.environmentBlendMode  = XR_ENVIRONMENT_BLEND_MODE_OPAQUE;
  frameEI.layerCount            = layerCount;
  frameEI.layers                = layers;
  xrEndFrame(session, &frameEI);                  // submit to the runtime
}
// Three.js + WebXR — full setup with VRButton helper

import * as THREE from 'three';
import { VRButton } from 'three/addons/webxr/VRButton.js';

// 1. Renderer with XR enabled
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;               // activate WebXR support
renderer.setPixelRatio(window.devicePixelRatio);
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);

// 2. VRButton handles the full session lifecycle for you
document.body.appendChild(VRButton.createButton(renderer));

// 3. Scene
const scene  = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(70,
  window.innerWidth / window.innerHeight, 0.1, 100);

// 4. Environment
const floor = new THREE.Mesh(
  new THREE.PlaneGeometry(10, 10),
  new THREE.MeshStandardMaterial({ color: 0x222222 })
);
floor.rotation.x = -Math.PI / 2;
scene.add(floor);

const box = new THREE.Mesh(
  new THREE.BoxGeometry(0.5, 0.5, 0.5),
  new THREE.MeshStandardMaterial({ color: 0x00c8a0 })
);
box.position.set(0, 1, -2);
scene.add(box);
scene.add(new THREE.HemisphereLight(0xffffff, 0x444444, 1));

// 5. setAnimationLoop handles both flat and XR frames automatically
renderer.setAnimationLoop(function(time) {
  box.rotation.y = time * 0.001;
  renderer.render(scene, camera);
});
// WebXR Hit Testing — place objects on real-world surfaces (immersive-ar)

// Request an AR session with hit-test feature enabled
const session = await navigator.xr.requestSession('immersive-ar', {
  requiredFeatures: ['local', 'hit-test']
});

const refSpace    = await session.requestReferenceSpace('local');
const viewerSpace = await session.requestReferenceSpace('viewer');

// Create a hit test source — tests along the viewer's forward ray
const hitTestSource = await session.requestHitTestSource({
  space: viewerSpace
});

// In the XR frame loop:
function onXRFrame(time, frame) {
  session.requestAnimationFrame(onXRFrame);

  const hits = frame.getHitTestResults(hitTestSource);

  if (hits.length > 0) {
    const pose = hits[0].getPose(refSpace);
    if (pose) {
      // pose.transform.matrix is where the real surface is in world space
      reticle.matrix.fromArray(pose.transform.matrix);
      reticle.visible = true;
    }
  } else {
    reticle.visible = false;   // no surface under the cursor
  }
}

// On tap: permanently place a 3D object at the reticle's pose
session.addEventListener('select', function() {
  if (reticle.visible) {
    placeObjectAt(reticle.matrix);   // your own function to spawn a mesh
  }
});

// Clean up the hit test source when done
session.addEventListener('end', () => hitTestSource.cancel());

The Concepts Behind XR

XR Reference Spaces — choosing the right one
viewer — origin glued to the headset, everything moves with you. Good for on-screen HUDs.
local — origin at your starting head position; for seated or standing experiences.
local-floor — like local, but origin is on the floor below you. Most common for room-scale VR.
bounded-floor — room-scale with a defined physical boundary polygon from the Guardian/Chaperone system.
unbounded — for large walking-scale experiences; positional drift is expected over long distances.
WebXR vs. OpenXR — the two-layer model
WebXR is a browser-layer API defined by the W3C. It lets JavaScript request and run XR sessions inside a webpage. OpenXR is a system-layer API defined by the Khronos Group. It lets native C/C++ applications (and engines like Unity, Godot, Unreal) talk directly to headset hardware via the platform's runtime. On a PC running Chrome, an immersive-vr WebXR session goes: JavaScript → Chrome WebXR implementation → System OpenXR Runtime → Hardware.
WebXR Session Lifecycle
1. Check: navigator.xr.isSessionSupported(mode) — async, no user gesture needed.
2. Request: navigator.xr.requestSession(mode, options) — must be called inside a user gesture (button click) or the browser blocks it.
3. Configure: call session.updateRenderState() with your WebGL layer and session.requestReferenceSpace().
4. Run: start the loop with session.requestAnimationFrame().
5. End: call session.end() to exit cleanly, or listen for the session's own 'end' event (fired when the user removes the headset).

Quick Quiz

1. Which organisation publishes the OpenXR standard?

2. Which WebXR session mode lets you place virtual objects over the real-world camera feed (passthrough AR)?

3. A seated VR experience that doesn't need to track the room boundary should use which reference space?

4. In Paul Milgram's Reality-Virtuality Continuum, where does Mixed Reality (MR) sit?