Back openDesk Edu for a sovereign, open-source education — every vote counts.
Vote nowSave products you love by clicking the heart icon.
End-to-End-Leitfaden zum Erstellen einer produktionsreifen WebXR-Anwendung mit Three.js und TypeScript — Projektsetup, Session-Lifecycle, Controller-Input, Teleportation, VR-UI und Deployment.
Ein praxisnaher Deep-Dive in die WebXR Hand Tracking API — Gelenkpositionen, Pinch-Erkennung, benutzerdefinierte Gestenerkennung und Interaktionsmuster für immersive VR-Anwendungen.
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 |