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.
Comprehensive guide to WebXR hit testing and depth sensing â ray intersection, transient hit tests, reading the depth buffer, occlusion rendering, and mesh reconstruction for real-world interaction.
Eine der zentralen Herausforderungen in XR besteht darin, virtuelle Objekte genau dort zu halten, wo man sie platziert hat. Man geht weg, kommt zurĂŒck â ist das Objekt immer noch da? Hier kommen WebXR Anchors ins Spiel. In Kombination mit der Plane Detection bilden sie das Fundament fĂŒr das rĂ€umliche VerstĂ€ndnis in immersiven Webanwendungen.
Ohne Anchors werden Objekte relativ zu einem Referenzraum positioniert (typischerweise local-floor oder bounded-floor). Bewegt man sich durch den Raum, bewegen sich die Objekte mit â sie sind nicht wirklich in der Welt platziert.
Ein XRAnchor reprĂ€sentiert einen festen Punkt in der Umgebung des Nutzers. Die Runtime trackt diesen Punkt ĂŒber mehrere Frames hinweg und passt seinen Transform an, sobald sich das VerstĂ€ndnis der Umgebung verbessert. Das bedeutet:
Anchors können aus verschiedenen Quellen erstellt werden:
Das gĂ€ngigste Muster â der Nutzer zeigt auf eine OberflĂ€che, und an dem Schnittpunkt wird ein Anchor erstellt:
const session = await navigator.xr.requestSession("immersive-ar", {
requiredFeatures: ["hit-test", "anchors"],
});
// Create hit test source
const hitTestSource = await session.requestHitTestSource({
space: viewerReferenceSpace,
});
// In frame loop
function onXRFrame(time: number, frame: XRFrame) {
const hitResults = frame.getHitTestResults(hitTestSource);
if (hitResults.length > 0) {
const pose = hitResults[0].getPose(referenceSpace);
if (pose) {
const anchor = await frame.createAnchor(pose.transform, referenceSpace);
// attachObjectToAnchor(anchor);
}
}
}
Wenn die Plane Detection aktiv ist, können Anchors direkt aus der Geometrie einer Ebene erstellt werden:
// Get all detected planes
const detectedPlanes = frame.detectedPlanes;
for (const plane of detectedPlanes) {
const centerPose = frame.getPose(plane.planeSpace, referenceSpace);
if (centerPose && plane.semanticLabel === "floor") {
const anchor = await frame.createAnchor(centerPose.transform, referenceSpace);
break;
}
}
Sie können Anchors auch an beliebigen Positionen im Raum erstellen:
const arbitraryTransform = new XRRigidTransform({
x: 1.5,
y: 0.5,
z: -1.0, // 1.5m right, 0.5m up, 1m in front
});
const anchor = await frame.createAnchor(arbitraryTransform, referenceSpace);
Das VerstĂ€ndnis der Anchor-ZustĂ€nde ist entscheidend fĂŒr robuste Anwendungen:
Creation â Tracking â (Loss) â (Recovery) â Deletion
// Check anchor tracking state each frame
function updateAnchor(anchor: XRAnchor, frame: XRFrame) {
const anchorPose = frame.getPose(anchor.anchorSpace, referenceSpace);
if (!anchorPose) {
// Anchor is temporarily lost â occluded or out of tracking area
// Option 1: Hide attached object
anchorObject.visible = false;
// Option 2: Use last known position as fallback
anchorObject.position.copy(lastKnownPosition);
return;
}
// Anchor is tracking â update object transform
anchorObject.position.copy(anchorPose.transform.position);
anchorObject.quaternion.copy(anchorPose.transform.orientation);
lastKnownPosition.copy(anchorPose.transform.position);
// Anchor is fully tracking
anchorObject.visible = true;
}
Ein Anchor-Verlust tritt auf, wenn:
Einige Anchors stellen sich automatisch wieder her, sobald das Tracking wiederhergestellt ist. Andere bleiben dauerhaft verloren â dies ist abhĂ€ngig von der jeweiligen Runtime.
Die Plane Detection identifiziert reale OberflÀchen (Böden, WÀnde, Tische, Decken) und stellt diese als XRPlane Objekte bereit.
const session = await navigator.xr.requestSession("immersive-ar", {
requiredFeatures: ["plane-detection"],
});
Die Erkennung von Ebenen (Planes) erfolgt inkrementell â Ebenen werden im Laufe der Zeit hinzugefĂŒgt, entfernt und verfeinert:
session.addEventListener("plane-detection", (event: XRPlaneEvent) => {
// New planes detected
for (const plane of event.added) {
visualizePlane(plane);
}
// Existing planes changed (polygon refined, classification updated)
for (const plane of event.changed) {
updatePlaneVisualization(plane);
}
// Planes removed (no longer visible or merged)
for (const plane of event.removed) {
removePlaneVisualization(plane);
}
});
Jedes XRPlane stellt Folgendes bereit:
interface XRPlane {
planeSpace: XRSpace; // Origin at plane center
polygon: DOMPointReadOnly[]; // Boundary vertices
lastChangedTime: number; // Timestamp of last modification
semanticLabel?: string; // 'floor', 'wall', 'ceiling', 'table', etc.
orientation: string; // 'horizontal' | 'vertical'
}
Erstellen Sie ein Mesh aus den Polygon-Vertices:
function createPlaneMesh(plane: XRPlane): THREE.Mesh {
const shape = new THREE.Shape();
const points = plane.polygon;
shape.moveTo(points[0].x, points[0].z);
for (let i = 1; i < points.length; i++) {
shape.lineTo(points[i].x, points[i].z);
}
shape.closePath();
const geometry = new THREE.ShapeGeometry(shape);
const material = new THREE.MeshBasicMaterial({
color: plane.semanticLabel === "floor" ? 0x4488ff : 0xff8844,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geometry, material);
// Rotate from XZ to XY (Three.js convention)
mesh.rotation.x = -Math.PI / 2;
return mesh;
}
Hinweis: Die Polygon-Vertices befinden sich im lokalen Koordinatensystem der Ebene (XZ-Ebene), nicht im World Space. Heften Sie das Mesh an ein
THREE.Object3Dan, das demplaneSpaceTransform untergeordnet ist.
Die Eigenschaft semanticLabel bietet eine menschenlesbare Klassifizierung:
| Label | Bedeutung | Typische Verwendung |
|---|---|---|
floor | BodenflÀche | Teleport-Ziele, Platzierung von Objekten |
ceiling | DeckenflÀche | Beleuchtung, Ambient Occlusion Probes |
wall | Vertikale FlÀche | Portal-Fenster, Kunstdisplays, Occlusion |
table | Horizontale FlĂ€che auf HĂŒfthöhe | Miniaturmodelle, Brettspiele |
door | TĂŒröffnung | RaumĂŒbergĂ€nge (experimentell) |
window | Fensteröffnung | Portal zu AuĂenszenen (experimentell) |
Nicht alle Runtimes stellen semantische Labels bereit. PrĂŒfen Sie immer, ob das Label existiert, bevor Sie sich darauf verlassen.
Anchors können mithilfe ihrer UUID auch Neustarts der Anwendung ĂŒberdauern:
// When anchor is created
const anchor = await frame.createAnchor(transform, referenceSpace);
const anchorId = anchor.uuid;
localStorage.setItem("mySavedAnchor", anchorId);
// On subsequent session â restore anchors
const savedAnchorId = localStorage.getItem("mySavedAnchor");
if (savedAnchorId) {
// Session.requestAnchorInformation() returns anchor info by UUID
const anchorInformation = await session.requestAnchorInformation(savedAnchorId);
if (anchorInformation) {
// Re-create scene objects at this anchor
}
}
Wichtige EinschrÀnkungen:
session.enabledFeaturesDas gĂ€ngigste UX-Pattern fĂŒr die Platzierung von Objekten:
class PlacementController {
private previewObject: THREE.Mesh;
private placedAnchors: XRAnchor[] = [];
private hitTestSource: XRHitTestSource | null = null;
async initialize(session: XRSession, refSpace: XRReferenceSpace) {
this.hitTestSource = await session.requestHitTestSource({
space: refSpace,
});
}
update(frame: XRFrame) {
const results = frame.getHitTestResults(this.hitTestSource!);
if (results.length > 0) {
const pose = results[0].getPose(referenceSpace);
if (pose) {
// Move preview to hit surface
this.previewObject.position.copy(pose.transform.position);
this.previewObject.visible = true;
}
} else {
this.previewObject.visible = false;
}
}
async placeObject(frame: XRFrame) {
const results = frame.getHitTestResults(this.hitTestSource!);
if (results.length > 0) {
const pose = results[0].getPose(referenceSpace);
if (pose) {
const anchor = await frame.createAnchor(pose.transform, referenceSpace);
this.placedAnchors.push(anchor);
// Instantiate final object at anchor position
}
}
}
}
| Faktor | Auswirkung | GegenmaĂnahme |
|---|---|---|
| Anchor-Anzahl | Jeder Anchor erhöht den Tracking-Overhead | Auf 32 gleichzeitige Anchors begrenzen |
| KomplexitÀt der Plane-Polygone | Komplexe Planes = mehr Vertices | Polygon auf Convex Hull vereinfachen |
| HĂ€ufigkeit der Plane-Erkennung | CPU-Kosten pro Frame | plane-detection Event nutzen, kein Polling |
| Wiederherstellung persistenter Anchors | Kann vollstÀndigen Environment-Scan auslösen | Anchors lazy nach Sessionsstart wiederherstellen |