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.
00 — The XR Spectrum
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 revealReality
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 revealAugmented 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 revealMixed 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 revealVirtual 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.
01 — Milgram's Continuum
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.
02 — OpenXR Architecture
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.
03 — OpenXR Layer Stack
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.
04 — WebXR Device API
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:
- immersive-vr — full headset VR; real world is replaced entirely by the rendered scene.
- immersive-ar — passthrough AR; the rendered scene blends over the camera feed.
- inline — non-immersive, displayed inside the web page; used for 360° viewers and gyroscope-driven experiences without a headset.
Every XR session uses a reference space — the coordinate system that defines where "world centre" is:
viewer— origin stays glued to the headset; everything moves with you.local— origin at the headset's starting position; for seated/standing experiences.local-floor— like local, but the origin is at the estimated floor level.bounded-floor— room-scale with a defined physical boundary polygon.unbounded— for large walking-scale experiences with no boundary.
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.
05 — WebXR Feature Detector
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.
06 — Write Your First XR App
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());
07 — Key Concepts Recap
The Concepts Behind XR
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 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.
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).
08 — Check Your Understanding
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?