๐Ÿง  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.

๐Ÿ“Œ W3C Standard๐Ÿ“Œ No Install๐Ÿ“Œ Cross-platform๐Ÿ“Œ JavaScript
// 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.

๐Ÿ”„ Render Loopโฑ๏ธ XRFrame๐Ÿ“ก Events
// 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.

๐Ÿง Local๐Ÿ“ Local-Floor๐Ÿšถ Bounded๐ŸŒ Unbounded
// 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.

๐Ÿ‘๏ธ Two Views๐Ÿ“ Projection Matrix๐ŸŽฏ Viewportโšก Multiview
// 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.

๐ŸŽฎ Controllersโœ‹ Hands๐Ÿ‘€ Gaze๐Ÿ“ฑ Screen
// 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 Testโš“ Anchors๐Ÿ’พ Persistence
// 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.

๐Ÿ“ Horizontal๐Ÿ“ Vertical๐Ÿ”บ Polygon๐Ÿ”„ Events
// 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.

๐Ÿ–๏ธ 25 Joints๐Ÿค Pinch๐Ÿฆด Skeleton
// 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.

๐Ÿ–ผ๏ธ Quad๐Ÿ“บ Cylinder๐ŸŽฅ Video๐Ÿ“Š Depth
// 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๐ŸŽฎ Babylon.jsโšก Abstraction
// 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.

โšก WebGL2๐ŸŽฏ Instancing๐Ÿ—œ๏ธ KTX2๐Ÿงต Web Workers
// 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().

๐ŸŒ HTTPS๐Ÿ“ฑ ARCore/ARKit๐Ÿฅฝ Quest Browser๐ŸŽ Safari
// 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๐Ÿ“š Education๐Ÿ”ง Training๐Ÿ“Š Data Viz
// 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.

๐ŸŽจ Generative 3D๐Ÿค– LLM Agents๐Ÿง  WebNNโœจ NeRF
// 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);