๐ง WebXR Fundamentals
๐ฏ What is WebXR?
WebXR is a W3C standard that brings AR and VR experiences directly to web browsers โ no app installation required. It provides a unified JavaScript API for accessing XR devices (headsets, phones, controllers) and rendering immersive content using WebGL or WebGPU. Supported by Chrome, Edge, Firefox, Safari (visionOS), and Meta Quest Browser.
Why WebXR Matters
Zero friction: Click a link โ enter AR/VR. Cross-platform: Works on phones, headsets, and desktops. Open standard: Not locked to any vendor. Web technologies: HTML, CSS, JavaScript, WebGL, WebAssembly. Use cases: E-commerce, education, training, social, games, data visualization.
// Minimal WebXR AR session
async function startAR() {
if (!navigator.xr) {
console.log("WebXR not supported");
return;
}
const supported = await navigator.xr.isSessionSupported('immersive-ar');
if (!supported) {
console.log("Immersive AR not supported");
return;
}
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test', 'local-floor'],
optionalFeatures: ['dom-overlay', 'hand-tracking']
});
// Set up render loop
session.requestAnimationFrame(onXRFrame);
}๐ฌ XR Session & Render Loop
๐ฏ The heart of WebXR
An XRSession represents an active XR experience. It manages the render loop via requestAnimationFrame, provides the XRFrame object, and handles lifecycle events (start, end, visibility).
Session Lifecycle
1. Check support โ navigator.xr.isSessionSupported(). 2. Request session โ navigator.xr.requestSession(). 3. Set up reference space โ session.requestReferenceSpace(). 4. Render loop โ session.requestAnimationFrame(). 5. Handle end โ session.end() or user exits.
// Full XR session setup
let session, refSpace, renderer, scene, camera;
async function initXR() {
session = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ['local-floor']
});
refSpace = await session.requestReferenceSpace('local-floor');
// Set up WebGL renderer
const canvas = document.createElement('canvas');
const gl = canvas.getContext('webgl2', { xrCompatible: true });
await gl.makeXRCompatible();
session.updateRenderState({ baseLayer: new XRWebGLLayer(session, gl) });
// Render loop
function onFrame(time, frame) {
session.requestAnimationFrame(onFrame);
const pose = frame.getViewerPose(refSpace);
if (!pose) return;
// Render each view (eye)
for (const view of pose.views) {
const viewport = session.renderState.baseLayer.getViewport(view);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
renderScene(view, pose);
}
}
session.requestAnimationFrame(onFrame);
}๐ Reference Spaces
๐ฏ Coordinate systems for XR
Reference spaces define the origin and orientation for tracking. Common types: viewer (head), local (seated), local-floor (standing), unbounded (room-scale), bounded-floor (play area).
Reference Space Types
viewer: Origin at head, no floor. local: Origin at seated position. local-floor: Origin at floor level. bounded-floor: Floor + play area bounds. unbounded: Large-scale tracking (AR). Choose based on your experience.
// Requesting reference spaces
const localSpace = await session.requestReferenceSpace('local');
const floorSpace = await session.requestReferenceSpace('local-floor');
const boundedSpace = await session.requestReferenceSpace('bounded-floor');
const unboundedSpace = await session.requestReferenceSpace('unbounded');
// Get pose in a specific space
const pose = frame.getViewerPose(floorSpace);
if (pose) {
const position = pose.transform.position; // {x, y, z}
const orientation = pose.transform.orientation; // {x, y, z, w}
console.log(`Camera at floor: ${position.y}m`);
}
// For AR, use 'local' or 'unbounded' with hit test
const arSpace = await session.requestReferenceSpace('local');๐๏ธ Views & Stereo Rendering
๐ฏ Rendering for each eye
XRFrame provides XRViewerPose with an array of XRView objects (one per eye). Each view has its own projection matrix, view matrix, and viewport. Render the scene once per view.
Stereo Rendering
XRWebGLLayer manages the framebuffer and viewports. getViewport(view) gives the pixel region for each eye. view.projectionMatrix and view.transform.inverse.matrix are used for rendering. For performance, use single-pass stereo with multiview extension.
// Rendering each view
function onXRFrame(time, frame) {
session.requestAnimationFrame(onXRFrame);
const pose = frame.getViewerPose(refSpace);
if (!pose) return;
const glLayer = session.renderState.baseLayer;
gl.bindFramebuffer(gl.FRAMEBUFFER, glLayer.framebuffer);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
for (const view of pose.views) {
const viewport = glLayer.getViewport(view);
gl.viewport(viewport.x, viewport.y, viewport.width, viewport.height);
// Set up camera for this eye
const projectionMatrix = view.projectionMatrix;
const viewMatrix = view.transform.inverse.matrix;
// Render scene
renderScene(projectionMatrix, viewMatrix);
}
}๐ฎ XR Input Sources
๐ฏ Controllers, hands, and gaze
XR input sources represent controllers, hands, or gaze. Each has a targetRaySpace (pointing direction), gripSpace (hand pose), and gamepad (buttons/axes).
Input Source Types
Tracked controllers: Oculus Touch, Vive wands. Hand tracking: Joint poses (requires 'hand-tracking' feature). Gaze: Head-directed input. Screen input: Touch/mouse for AR on phones. Listen to inputsourceschange to detect connect/disconnect.
// Handling XR input
session.addEventListener('inputsourceschange', (event) => {
for (const inputSource of event.added) {
console.log(`Added: ${inputSource.handedness} (${inputSource.targetRayMode})`);
}
for (const inputSource of event.removed) {
console.log(`Removed: ${inputSource.handedness}`);
}
});
function onXRFrame(time, frame) {
for (const inputSource of session.inputSources) {
// Get controller pose
const pose = frame.getPose(inputSource.targetRaySpace, refSpace);
if (pose) {
const rayOrigin = pose.transform.position;
const rayDir = getForwardDirection(pose.transform.orientation);
// Raycast for interaction
}
// Read gamepad
if (inputSource.gamepad) {
const trigger = inputSource.gamepad.buttons[0].value;
const thumbstick = inputSource.gamepad.axes;
}
}
}๐ฏ Hit Testing & Anchors
๐ฏ Placing content in the real world
Hit testing casts a ray from the device (or controller) into the real world and returns intersection points with detected surfaces. Anchors attach content to real-world positions.
Hit Test & Anchors
Hit test: Request via session.requestHitTestSource(). Returns XRHitTestResult with pose. Anchors: Create via frame.createAnchor() โ content stays fixed even as tracking adjusts. Persistence: Anchors can be saved/loaded for persistent AR (WebXR Anchors module).
// Hit testing in WebXR AR
let hitTestSource, hitTestSourceRequested = false;
function onXRFrame(time, frame) {
session.requestAnimationFrame(onXRFrame);
if (!hitTestSourceRequested) {
session.requestHitTestSource({ space: viewerSpace }).then((source) => {
hitTestSource = source;
});
hitTestSourceRequested = true;
}
if (hitTestSource) {
const hitTestResults = frame.getHitTestResults(hitTestSource);
if (hitTestResults.length > 0) {
const hit = hitTestResults[0];
const pose = hit.getPose(refSpace);
// Place reticle at hit position
reticle.visible = true;
reticle.matrix.fromArray(pose.transform.matrix);
// On tap, create anchor
if (userTapped) {
const anchor = await frame.createAnchor(pose, refSpace);
placeObjectAtAnchor(anchor);
}
}
}
}๐ Plane Detection
๐ฏ Finding surfaces
Plane detection identifies horizontal and vertical surfaces in the real world. Part of the WebXR Plane Detection module. Returns plane geometry, pose, and alignment.
Detected Planes
Plane alignment: horizontal or vertical. Plane polygon: Boundary points. Pose: Center position and orientation. Events: planeadded, planeremoved, planechanged on XRPlaneSet. Use for placing objects on floors/tables.
// Plane detection in WebXR
const planeDetection = await session.requestPlaneDetection({
alignments: ['horizontal', 'vertical']
});
// Or via required features
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['plane-detection']
});
function onXRFrame(time, frame) {
const planes = frame.detectedPlanes;
if (planes) {
planes.forEach((plane) => {
const pose = frame.getPose(plane.planeSpace, refSpace);
if (pose) {
// Create or update plane visualization
updatePlaneMesh(plane, pose);
}
});
}
}โ Hand Tracking
๐ฏ Natural hand interaction in the browser
WebXR Hand Input module provides hand tracking data: 25 joints per hand, pinch strength, and gesture detection. Works on Quest, HoloLens 2, and visionOS.
Hand Joints
25 joints: wrist, palm, and 5 joints per finger. XRHand: Accessed via inputSource.hand. Joint spaces: hand.get('index-finger-tip'). Pinch: Computed from thumb-tip and index-finger-tip distance. Render hand meshes or use joints for interaction.
// Hand tracking in WebXR
session = await navigator.xr.requestSession('immersive-vr', {
requiredFeatures: ['hand-tracking']
});
function onXRFrame(time, frame) {
for (const inputSource of session.inputSources) {
if (inputSource.hand) {
const hand = inputSource.hand;
const indexTip = hand.get('index-finger-tip');
const thumbTip = hand.get('thumb-tip');
if (indexTip && thumbTip) {
const indexPose = frame.getJointPose(indexTip, refSpace);
const thumbPose = frame.getJointPose(thumbTip, refSpace);
const distance = Math.hypot(
indexPose.transform.position.x - thumbPose.transform.position.x,
indexPose.transform.position.y - thumbPose.transform.position.y,
indexPose.transform.position.z - thumbPose.transform.position.z
);
const isPinching = distance < 0.02; // 2cm
console.log(`Pinching: ${isPinching}`);
}
}
}
}๐งฉ WebXR Layers
๐ฏ Optimized rendering with layers
WebXR Layers allow compositing multiple textures (e.g., video, UI, 3D scene) at different resolutions and refresh rates. Improves quality and performance.
Layer Types
XRProjectionLayer: Standard 3D rendering. XRQuadLayer: Flat quad (UI, video). XRCylinderLayer: Curved surface. XRCubeLayer: Skybox. XRDepthInformation: Depth for occlusion. Enables high-quality text and video in XR.
// WebXR Layers example
const projectionLayer = new XRProjectionLayer();
const quadLayer = new XRQuadLayer({
space: refSpace,
viewport: { x: 0, y: 0, width: 1024, height: 1024 },
transform: new XRRigidTransform({ x: 0, y: 1.5, z: -2 }),
width: 1.0,
height: 1.0
});
session.updateRenderState({
layers: [projectionLayer, quadLayer]
});
// Render to quad layer
function onXRFrame(time, frame) {
const quadSubImage = quadLayer.getSubImage();
// Bind framebuffer, render UI/video to quad
}๐ฆ Three.js & Babylon.js
๐ฏ Simplifying WebXR development
Three.js and Babylon.js provide high-level abstractions for WebXR, handling sessions, reference spaces, rendering, and input so you can focus on content.
Three.js vs Babylon.js
Three.js: Lightweight, flexible, huge community. Babylon.js: Batteries-included, great WebXR support, built-in physics and GUI. Both support AR/VR, hand tracking, hit testing, and layers.
// Three.js WebXR (VR)
import * as THREE from 'three';
import { VRButton } from 'three/addons/webxr/VRButton.js';
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.xr.enabled = true;
document.body.appendChild(VRButton.createButton(renderer));
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
// Add content
const geometry = new THREE.BoxGeometry(1, 1, 1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const cube = new THREE.Mesh(geometry, material);
scene.add(cube);
// Render loop
renderer.setAnimationLoop(() => {
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
renderer.render(scene, camera);
});โก WebXR Performance
๐ฏ Smooth 72/90/120 fps in the browser
WebXR must hit high frame rates on mobile hardware. Optimize JavaScript, GPU, and memory. Use WebGL2, WebGPU, and WebAssembly for performance.
Performance Tips
Use WebGL2 or WebGPU. Minimize draw calls (instancing, batching). Optimize shaders (avoid branching). Use compressed textures (KTX2, Basis). Fixed foveated rendering (if supported). Web Workers for heavy computation. Avoid garbage collection in render loop.
// Performance optimization: InstancedMesh (Three.js)
const geometry = new THREE.BoxGeometry(0.1, 0.1, 0.1);
const material = new THREE.MeshStandardMaterial({ color: 0x00ff00 });
const count = 1000;
const mesh = new THREE.InstancedMesh(geometry, material, count);
const matrix = new THREE.Matrix4();
for (let i = 0; i < count; i++) {
matrix.setPosition(
Math.random() * 10 - 5,
Math.random() * 10 - 5,
Math.random() * 10 - 5
);
mesh.setMatrixAt(i, matrix);
}
scene.add(mesh);๐ Deployment & Compatibility
๐ฏ Browser and device support
WebXR support varies by browser and device. HTTPS is required. Test across Chrome (Android/Quest), Safari (visionOS), Edge, and Firefox Reality.
Compatibility Matrix
VR: Quest Browser, Chrome (PC VR), Edge, Firefox Reality. AR: Chrome Android (ARCore), Safari iOS (ARKit), Samsung Internet. visionOS: Safari supports WebXR for Vision Pro. Polyfill: WebXR Polyfill for older browsers. Always check navigator.xr.isSessionSupported().
// Feature detection & fallback
async function checkXRSupport() {
if (!navigator.xr) {
showMessage("WebXR not supported. Please use a compatible browser.");
return;
}
const modes = ['immersive-vr', 'immersive-ar', 'inline'];
for (const mode of modes) {
try {
const supported = await navigator.xr.isSessionSupported(mode);
console.log(`${mode}: ${supported ? 'โ' : 'โ'}`);
} catch (e) {
console.log(`${mode}: error`);
}
}
}
// Polyfill for older browsers
import WebXRPolyfill from 'webxr-polyfill';
new WebXRPolyfill();๐ผ WebXR Use Cases
๐ฏ Where WebXR shines
WebXR excels in scenarios where frictionless access matters: e-commerce, education, training, marketing, data visualization, and social experiences.
Real-World Applications
E-commerce: Try products in AR (furniture, shoes, watches). Education: 3D anatomy, historical sites, science simulations. Training: Step-by-step AR instructions for repairs. Marketing: Interactive 3D ads, virtual showrooms. Data viz: 3D charts in AR. Social: Virtual meeting spaces.
// E-commerce AR product viewer (conceptual)
async function showProductInAR(productModelUrl) {
const session = await navigator.xr.requestSession('immersive-ar', {
requiredFeatures: ['hit-test', 'dom-overlay'],
domOverlay: { root: document.getElementById('overlay') }
});
// Load 3D model
const model = await loadGLTF(productModelUrl);
// Hit test to place on real surface
const hitTestSource = await session.requestHitTestSource({ space: viewerSpace });
// On tap, place product
session.addEventListener('select', async () => {
const results = frame.getHitTestResults(hitTestSource);
if (results.length > 0) {
const pose = results[0].getPose(refSpace);
model.position.copy(pose.transform.position);
scene.add(model);
}
});
}๐ Future: WebXR + AI
๐ฏ AI-powered immersive web
WebXR + AI will enable intelligent, adaptive experiences: generative 3D content, conversational agents, semantic understanding, and personalized immersive web.
WebXR + AI Integration
Generative 3D: Create models from text (via WebGPU + AI models). LLM agents: Conversational NPCs in WebXR. Semantic understanding: Real-time scene interpretation. WebNN: Neural network inference in the browser. NeRF/Gaussian Splatting: Photorealistic scenes on the web.
// WebNN + WebXR: real-time AI in browser
const context = await navigator.ml.createContext();
const builder = new MLGraphBuilder(context);
// Define a simple neural network
const input = builder.input('input', { dataType: 'float32', dimensions: [1, 3, 224, 224] });
const conv = builder.conv2d(input, weights, { padding: [1, 1, 1, 1] });
const output = builder.relu(conv);
const graph = await builder.build({ output });
const results = await context.compute(graph, { input: inputTensor });
// Use in WebXR for semantic understanding
console.log('AI inference complete:', results.output);