Back openDesk Edu for a sovereign, open-source education â every vote counts.
Vote nowSave products you love by clicking the heart icon.
A performance deep-dive into WebXR Composition Layers â projection, quad, cylinder, and equirectangular layers, video playback via XRMediaBinding, and GPU-composited overlay techniques.
A performance deep-dive into WebXR Composition Layers â projection, quad, cylinder, and equirectangular layers, video playback via XRMediaBinding, and GPU-composited overlay techniques.
Hand-Tracking ist eines der transformativsten Features moderner XR. Anstatt nach einem Controller zu greifen, greifen Sie direkt nach dem Objekt. In diesem Artikel untersuchen wir die WebXR Hand Tracking API von Grund auf â wie man auf Joint-Daten zugreift, HĂ€nde visuell rendert, Gesten erkennt und natĂŒrliche Interaktionsmuster aufbaut.
hand-tracking Feature unterstĂŒtzt (Chrome fĂŒr Android, Meta Quest Browser)| GerĂ€t | Hand-Tracking | Joint-Anzahl | Max. HĂ€nde |
|---|---|---|---|
| Meta Quest 2 | â | 25 pro Hand | 2 |
| Meta Quest 3 | â | 26 (inkl. Handgelenk) | 2 |
| Meta Quest Pro | â | 26 | 2 |
| PICO 4 | â | 25 | 2 |
| Magic Leap 2 | â controller-basiert | 25 | 2 |
Alle GerĂ€te verwenden dasselbe XRHand Interface â der Unterschied liegt in der Tracking-QualitĂ€t, der Anzahl der Joints und dem Umgang mit Okklusionen.
Hand-Tracking ist ein optionales Feature â Sie mĂŒssen es bei der Erstellung der Session anfordern:
const session = await navigator.xr.requestSession("immersive-vr", {
requiredFeatures: ["hand-tracking"],
});
Sobald die Session aktiv ist, besitzt jeder XRInputSource (der eine Hand oder einen Controller reprÀsentiert) eine hand Eigenschaft, die ein XRHand Objekt enthÀlt. Das XRHand ist eine Map-Àhnliche Sammlung von XRHandJoint XRJointSpace EintrÀgen.
// In the animation frame callback:
function onXRFrame(time: number, frame: XRFrame) {
for (const inputSource of session.inputSources) {
const hand = inputSource.hand;
if (!hand) continue; // Not a hand-tracking input source
for (const [jointName, jointSpace] of hand) {
const pose = frame.getPose(jointSpace, referenceSpace);
if (pose) {
const pos = pose.transform.position;
const rot = pose.transform.orientation;
// pos.x, pos.y, pos.z are in meters relative to referenceSpace
}
}
}
}
Die WebXR-Spezifikation definiert 25 Joints pro Hand, die ĂŒber das XRHandJoint Enum zugĂ€nglich sind:
thumb-tip, index-finger-tip, middle-finger-tip, ring-finger-tip, pinky-finger-tipthumb-phalanx-proximal, index-finger-phalanx-proximal, etc.index-finger-phalanx-distal, etc.index-finger-metacarpal, etc.wristBei Quest 3 und Quest Pro bringt ein zusÀtzlicher thumb-metacarpal Joint die Gesamtzahl auf 26.
Das Abfragen von Joints einzeln mit getPose() kann langsam sein â 25 Joints 2 HĂ€nde = 50 getPose() Aufrufe pro Frame. Verwenden Sie XRFrame.fillPoses() fĂŒr Bulk-Abfragen:
// Set up once
const jointSpaces: XRSpace[] = [];
for (const [, space] of hand) {
jointSpaces.push(space);
}
const jointPoses = new Float32Array(jointSpaces.length * 16); // 4x4 matrices
// Each frame
frame.fillPoses(jointSpaces, referenceSpace, jointPoses);
Dies ist deutlich schneller, da die Runtime alle Joints in einer einzigen Operation verarbeitet.
Sobald Sie die Joint-Positionen haben, ist die einfachste Visualisierung eine Kugel pro Joint mit verbindenden Linien:
const jointGeometry = new THREE.SphereGeometry(0.01, 8, 8);
const jointMaterial = new THREE.MeshBasicMaterial({ color: 0x00aaff });
const jointMeshes: THREE.Mesh[] = [];
for (const [, jointSpace] of hand) {
const mesh = new THREE.Mesh(jointGeometry, jointMaterial);
scene.add(mesh);
jointMeshes.push(mesh);
}
// Each frame â update positions
let i = 0;
for (const [, jointSpace] of hand) {
const pose = frame.getPose(jointSpace, referenceSpace);
if (pose) {
jointMeshes[i].position.set(
pose.transform.position.x,
pose.transform.position.y,
pose.transform.position.z,
);
}
i++;
}
FĂŒr einen realistischeren Look verbinden Sie die Joints mit Zylindern oder Liniensegmenten, um ein Skelett zu bilden:
// Bone connections
const boneConnections: [XRHandJoint, XRHandJoint][] = [
["wrist", "thumb-metacarpal"],
["thumb-metacarpal", "thumb-phalanx-proximal"],
["thumb-phalanx-proximal", "thumb-phalanx-distal"],
["thumb-phalanx-distal", "thumb-tip"],
["wrist", "index-finger-metacarpal"],
// ... repeat for all fingers
];
FĂŒr Produktionsanwendungen sollten Sie Skinned Meshes in Betracht ziehen â ein einzelnes Handmodell mit Bones, die auf die WebXR-Joints gemappt werden. Bibliotheken wie three-mesh-bvh können bei der Kollisionserkennung auf dem Hand-Mesh helfen.
Die eigentliche StÀrke des Hand-Trackings liegt in der Gestenerkennung. Hier sind die wichtigsten Muster:
Ein Pinch wird erkannt, indem der Abstand zwischen der Daumenspitze und der Zeigefingerspitze gemessen wird:
function getPinchStrength(hand: XRHand, frame: XRFrame, referenceSpace: XRReferenceSpace): number {
const thumbPose = frame.getPose(hand.get("thumb-tip")!, referenceSpace);
const indexPose = frame.getPose(hand.get("index-finger-tip")!, referenceSpace);
if (!thumbPose || !indexPose) return 0;
const dx = thumbPose.transform.position.x - indexPose.transform.position.x;
const dy = thumbPose.transform.position.y - indexPose.transform.position.y;
const dz = thumbPose.transform.position.z - indexPose.transform.position.z;
const distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
// 0.05 meters (~2 inches) = fully open, 0.01 meters = fully pinched
return Math.max(0, Math.min(1, 1 - (distance - 0.01) / 0.04));
}
Ein Wert von 1,0 bedeutet einen vollstÀndigen Pinch. Verwenden Sie einen Schwellenwert (z. B. 0,8), um Pinch-Events auszulösen.
Erkennen Sie, wenn der Zeigefinger ausgestreckt und die anderen Finger gebeugt sind:
function isPointing(hand: XRHand, frame: XRFrame, refSpace: XRReferenceSpace): boolean {
const indexTip = frame.getPose(hand.get("index-finger-tip")!, refSpace)?.transform.position;
const indexBase = frame.getPose(hand.get("index-finger-metacarpal")!, refSpace)?.transform
.position;
const middleTip = frame.getPose(hand.get("middle-finger-tip")!, refSpace)?.transform.position;
const middleBase = frame.getPose(hand.get("middle-finger-metacarpal")!, refSpace)?.transform
.position;
if (!indexTip || !indexBase || !middleTip || !middleBase) return false;
// Index is extended (tip further from base than middle)
const indexLength = indexTip.distanceTo(indexBase);
const middleLength = middleTip.distanceTo(middleBase);
return indexLength > middleLength * 1.3;
}
Die rohe Gestenerkennung ist oft verrauscht. Implementieren Sie eine State Machine, um ĂbergĂ€nge zu glĂ€tten:
enum GestureState {
Idle,
Pinching,
Pointing,
Grab,
}
class GestureDetector {
private state: GestureState = GestureState.Idle;
private pinchHeld = false;
update(hand: XRHand, frame: XRFrame, refSpace: XRReferenceSpace) {
const pinch = getPinchStrength(hand, frame, refSpace);
const pointing = isPointing(hand, frame, refSpace);
switch (this.state) {
case GestureState.Idle:
if (pinch > 0.8) this.transitionTo(GestureState.Pinching);
else if (pointing) this.transitionTo(GestureState.Pointing);
break;
case GestureState.Pinching:
if (pinch < 0.3) this.transitionTo(GestureState.Idle);
break;
// ...
}
}
private transitionTo(newState: GestureState) {
// Fire onEnter/onExit callbacks
this.state = newState;
}
}
Sobald Sie eine zuverlÀssige Gestenerkennung haben, sind dies die drei gÀngigsten Interaktionsmuster:
Die Hand fungiert als 3D-Cursor. Ein Collider (Kugel oder Kapsel) an der Zeigefingerspitze oder der HandflĂ€che prĂŒft auf Ăberschneidungen mit interaktiven Objekten:
const handCollider = new THREE.Mesh(
new THREE.SphereGeometry(0.02),
new THREE.MeshBasicMaterial({ visible: false }),
);
scene.add(handCollider);
// In frame loop â position collider at index tip
const indexTip = frame.getPose(hand.get("index-finger-tip")!, refSpace);
if (indexTip) {
handCollider.position.copy(indexTip.transform.position);
// Check overlap with interactable objects
}
Bestens geeignet fĂŒr: Greifen, DrĂŒcken, BetĂ€tigen von Knöpfen, Manipulieren kleiner Objekte.
Ein Strahl (Ray) geht zwischen Daumen und Zeigefinger (oder aus der HandflÀche) aus. Pinch zum AuswÀhlen, Bewegen und Loslassen:
const rayOrigin = new THREE.Vector3();
const rayDirection = new THREE.Vector3();
// Compute ray from midpoint of thumb-index web
const thumbBase = frame.getPose(hand.get("thumb-phalanx-proximal")!, refSpace);
const indexBase = frame.getPose(hand.get("index-finger-metacarpal")!, refSpace);
if (thumbBase && indexBase) {
rayOrigin.lerpVectors(thumbBase.transform.position, indexBase.transform.position, 0.5);
// Direction = forward from palm, computed from wrist/knuckle orientation
}
Bestens geeignet fĂŒr: Auswahl entfernter Objekte, MenĂŒnavigation, Zeigen.
Binden Sie ein 2D- oder 3D-UI-Element an den Bereich der HandflÀche:
const palmSpace = hand.get("wrist"); // or 'palm' on supported devices
// The UI element follows the palm position and orientation
Bestens geeignet fĂŒr: SchnellzugriffsmenĂŒs, Statusanzeigen, Werkzeugauswahl.
| Optimierung | Auswirkung |
|---|---|
fillPoses() statt einzelner getPose() pro Joint verwenden | 3-5x schneller bei 25 Joints |
| HĂ€nde nur rendern, wenn sie im Sichtfeld sind (Frustum Culling) | Spart GPU-Zeit |
| Joint-Detailgrad basierend auf Distanz zum Kopf reduzieren | LOD: 25 12 5 Joints |
| Okklusions-behandelte Joints (innerhalb der Geometrie) ĂŒberspringen | Weniger Draw Calls |
| Joint-Meshes poolen statt pro Frame neu zu allozieren | Null GC-Druck |
Tipp zum Frame-Budget: Hand-Tracking-Daten kommen je nach GerĂ€t mit 30-60 Hz an. Wenn Ihr Render-Loop mit 90 Hz lĂ€uft, mĂŒssen Sie die Hand-Visuals nicht in jedem Frame aktualisieren â eine Interpolation zwischen den Samples ist völlig ausreichend.