Back openDesk Edu for a sovereign, open-source education â every vote counts.
Vote nowSave products you love by clicking the heart icon.
Ein praxisnaher Deep-Dive in die WebXR Hand Tracking API â Gelenkpositionen, Pinch-Erkennung, benutzerdefinierte Gestenerkennung und Interaktionsmuster fĂŒr immersive VR-Anwendungen.
A practical deep-dive into the WebXR Hand Tracking API â joint positions, pinch detection, custom gesture recognition, and interaction patterns for immersive VR applications.
Das Standard-WebXR-Rendering funktioniert so, dass alles in einen WebGL-Framebuffer gezeichnet wird, den die Runtime anschlieĂend auf das Headset-Display kopiert. Das funktioniert zwar, ist aber fĂŒr viele gĂ€ngige AnwendungsfĂ€lle ineffizient â insbesondere bei Videos, 2D-UIs und Hintergrundumgebungen.
WebXR Composition Layers ermöglichen es, den App-Framebuffer fĂŒr bestimmte Inhalte zu umgehen und diese direkt an den GPU-Compositor zu ĂŒbergeben. Das Ergebnis: geringerer Stromverbrauch, höhere Bildwiederholraten und eine bessere visuelle QualitĂ€t fĂŒr ĂŒberlagerte Inhalte.
| Aspekt | WebGL Framebuffer | Composition Layer |
|---|---|---|
| Render-Kosten | VollstÀndiger App-Frame bei jedem Vsync | Direkt durch GPU composited |
| Videowiedergabe | Dekodieren â Upload â Zeichnen jedes Frames | Direkter Scanout vom Videodecoder |
| Auflösung | Durch App-Framebuffer-GröĂe begrenzt | UnabhĂ€ngig pro Layer |
| Bildwiederholrate | An App-Framerate gebunden (meist 72-90 Hz) | Kann mit nativer Displayrate laufen |
| Passthrough | Passthrough hinter WebGL nicht möglich | AR-transparent zwischen Layern |
Layer spielen ihre StÀrken aus bei:
Der Kompromiss: Sie können keine pro-Pixel Three.js-Effekte auf Layer-Inhalte anwenden. Layer sind fĂŒr Inhalte gedacht, die keine Shader-Verarbeitung auf App-Ebene benötigen.
WebXR definiert vier Layer-Typen, die jeweils einem anderen Zweck dienen:
Dies ist der Standard-Layer â derjenige, der den herkömmlichen WebGL-Framebuffer ersetzt. Erstellen Sie ihn wĂ€hrend des Session-Setups:
const glBinding = new XRWebGLBinding(session, gl);
const projectionLayer = glBinding.createProjectionLayer({
textureType: "texture", // 'texture' or 'texture-array'
colorFormat: gl.RGBA8,
depthFormat: gl.DEPTH_COMPONENT24,
scaleFactor: 1.0, // Resolution scale (0.5 = half res)
});
session.updateRenderState({
layers: [projectionLayer], // Must always be the first layer
});
Die Eigenschaft scaleFactor ist entscheidend fĂŒr die Performance:
1.0 = volle GerĂ€teauflösung0.75 = gute Balance auf der Quest 30.5 = Performance-Modus fĂŒr komplexe SzenenEine flache, rechteckige OberflĂ€che, die im 3D-Raum positioniert wird. Perfekt fĂŒr 2D-Inhalte:
const quadLayer = glBinding.createQuadLayer({
space: referenceSpace,
layout: "mono",
width: 2, // meters
height: 1.5, // meters
pixelWidth: 1024,
pixelHeight: 768,
blendTextureSourceAlpha: true,
});
// Position the quad
quadLayer.transform = new XRRigidTransform({
x: 0,
y: 1.5,
z: -2, // 2m in front, 1.5m up
});
session.updateRenderState({
layers: [projectionLayer, quadLayer],
});
AnwendungsfÀlle: 2D-Dashboards in VR, Video-Player-UI, Datenvisualisierungs-Panels.
Eine gekrĂŒmmte DisplayflĂ€che, nĂŒtzlich fĂŒr immersive Dashboards und MenĂŒs:
const cylinderLayer = glBinding.createCylinderLayer({
space: referenceSpace,
layout: "stereo-left-right",
radius: 3, // meters
centralAngle: Math.PI * 0.5, // 90 degree arc
aspectRatio: 1.5, // height = radius * aspect
pixelWidth: 2048,
pixelHeight: 2048,
});
AnwendungsfĂ€lle: Auto-Dashboards, Cockpit-UIs, Panorama-MenĂŒs, MonitorwĂ€nde.
Ein sphĂ€rischer (360°) Layer fĂŒr UmgebungshintergrĂŒnde und 360°-Medien:
const equiLayer = glBinding.createEquirectangularLayer({
space: referenceSpace,
layout: "stereo-left-right",
centralAngle: Math.PI * 2, // Full sphere
radius: 0,
pixelWidth: 4096,
pixelHeight: 2048,
});
AnwendungsfĂ€lle: 360°-Fotos als Umgebungen, 360°-VideohintergrĂŒnde, Skyboxes.
Layer werden in der Reihenfolge von hinten nach vorne (back-to-front) basierend auf ihrer Position im Array zusammengesetzt:
// Hintergrundebene (equirektangular)
// â dahinter komponiert
// Quad-Ebene (Dashboard)
// â dahinter komponiert
// Projektionsebene (Haupt-3D-Szene)
session.updateRenderState({
layers: [projectionLayer, quadLayer, equiLayer],
});
Wait â this is actually front-to-back in the array order. The first layer is rendered first (closest to the viewer). The runtime composits them in the natural over-draw order: layers later in the array appear on top of earlier ones.
Important: Not all runtimes support more than 2-3 simultaneous layers. Check session.supportedLayers to determine the maximum.
Video is where composition layers truly shine. Instead of decoding video to a canvas and uploading to a WebGL texture each frame, XRMediaBinding hands the video decoder output directly to the compositor.
const mediaBinding = new XRMediaBinding(session);
// Erstellen eines versteckten Video-Elements
const video = document.createElement("video");
video.src = "/videos/my-360-video.mp4";
video.loop = true;
video.muted = true; // Erforderlich fĂŒr Autoplay-Richtlinien
video.crossOrigin = "anonymous";
await video.play();
// Erstellen einer an das Video gebundenen Quad-Ebene
const videoLayer = mediaBinding.createQuadLayer(video, {
space: referenceSpace,
layout: "mono",
width: 4,
height: 2.25, // 16:9 SeitenverhÀltnis
invertStereo: false,
});
// Positionieren des Videobildschirms
videoLayer.transform = new XRRigidTransform({
x: 0,
y: 1.5,
z: -3,
y: 0,
z: 0,
w: 1, // Quaternion (zum Betrachter ausgerichtet)
});
session.updateRenderState({
layers: [projectionLayer, videoLayer],
});
For 360° video, use an equirectangular layer instead:
const videoLayer = mediaBinding.createEquirectangularLayer(video, {
space: referenceSpace,
layout: "stereo-left-right", // Die meisten 360°-Videos sind stereoskopisch
centralAngle: Math.PI * 2,
radius: 0,
pixelWidth: 3840,
pixelHeight: 1920,
});
Since the video element lives in the DOM, you can control it programmatically:
// Wiedergabe/Pause via Geste
function togglePlayback() {
if (video.paused) {
video.play();
} else {
video.pause();
}
}
// LautstÀrkeregelung
function setVolume(level: number) {
video.volume = Math.max(0, Math.min(1, level));
}
The video layer automatically reflects changes to playback state, volume, and looping.
Compared to canvas-based video rendering:
| Metric | Canvas (WebGL) | Composition Layer |
|---|---|---|
| CPU cost per frame | ~3-5 ms (decode + upload) | ~0 ms (direct scanout) |
| GPU memory | Double-buffered RGBA texture | Single shared texture |
| Power consumption | High (full app frame) | Low (compositor only) |
| Frame drops | App-wide (video stutters everything) | Video-only (rest of scene unaffected) |
For non-video content, you need to render to the layer's color texture:
// In jedem Frame das Sub-Image der Ebene abrufen
const layer = quadLayer;
const subImage = glBinding.getSubImage(layer, frame, eye);
// Color-Texture binden
gl.bindTexture(gl.TEXTURE_2D, subImage.colorTexture);
// Depth/Stencil-Texture binden (optional)
gl.bindTexture(gl.TEXTURE_2D, subImage.depthStencilTexture);
// Inhalte in das Sub-Image rendern
gl.viewport(0, 0, subImage.viewport.width, subImage.viewport.height);
// ... draw calls ...
For quad layers, you typically render a fullscreen quad. For cylinder layers, you render a warped projection. For equirectangular, you render a panoramic image.
Not all browsers or devices support composition layers. Always implement a fallback:
function createLayers(session: XRSession, gl: WebGL2RenderingContext) {
if (session.supportedLayers?.length > 1) {
// Use composition layers
return createCompositionLayers(session, gl);
} else {
// Fall back to standard WebGL rendering
session.updateRenderState({
baseLayer: new XRWebGLLayer(session, gl),
});
}
}
alpha-blend for transparent layers, opaque for solid content.session.updateRenderState({
layers: [projectionLayer],
foveationLevel: 0.5, // 0.0 = no foveation, 1.0 = max
});