Signatur
Beschreibung
XRHand stammt aus dem WebXR Hand Input Module und beschreibt das Skelett einer per Kamera getrackten Hand. Du bekommst ein Objekt über XRInputSource.hand, wenn die XR-Sitzung mit dem Feature "hand-tracking" gestartet wurde und das Eingabegerät eine echte Hand ist (bei klassischen Controllern ist hand null).
Die Schnittstelle verhält sich wie eine schreibgeschützte Map: Die Schlüssel sind Zeichenketten des Aufzählungstyps XRHandJoint (z. B. "wrist", "thumb-tip", "index-finger-tip"), die Werte sind XRJointSpace-Objekte. Verfügbar sind size (immer 25), get(), has(), keys(), values(), entries(), forEach() und [Symbol.iterator]. Die Reihenfolge ist festgelegt: Handgelenk (wrist), dann Daumen (4 Gelenke), danach Zeige-, Mittel-, Ring- und kleiner Finger (je 5 Gelenke, von metacarpal bis tip).
Ein XRJointSpace enthält selbst noch keine Position. Die Pose eines Gelenks erhältst du im Frame-Callback über XRFrame.getJointPose(jointSpace, referenceSpace). Dabei bekommst du ein XRJointPose mit transform und radius (ungefähre Gelenkdicke in Metern). Brauchst du alle Gelenke auf einmal, ist XRFrame.fillPoses() zusammen mit fillJointRadii() deutlich effizienter.
Typische Anwendungen sind das Darstellen virtueller Hände, Gestenerkennung (Pinch, Faust, Zeigen), das direkte Anfassen von Objekten und Benutzeroberflächen, die ganz ohne Controller auskommen.
Parameter
| Name | Typ | Default | Beschreibung |
|---|---|---|---|
| $key Pflicht | XRHandJoint (string) | Parameter von get(key) bzw. has(key): Name des Gelenks, z. B. "wrist", "thumb-tip", "index-finger-phalanx-distal" oder "pinky-finger-metacarpal". |
|
| $callback Pflicht | (value: XRJointSpace, key: XRHandJoint, map: XRHand) => void | Parameter von forEach(): Wird für jedes der 25 Gelenke in fester Reihenfolge aufgerufen. |
Rückgabewert
XRInputSource.hand erhältst du ein XRHand, falls Hand-Tracking aktiv ist, sonst null. get() liefert ein XRJointSpace (oder undefined bei ungültigem Namen), size ist immer 25.Beispiele
Sitzung mit Hand-Tracking anfordern und XRHand prüfen
const startXR = async () => {
if (!navigator.xr || !(await navigator.xr.isSessionSupported('immersive-vr'))) {
console.warn('WebXR nicht verfügbar');
return;
}
// Aufruf muss aus einer Nutzeraktion (z. B. Klick) heraus erfolgen
const session = await navigator.xr.requestSession('immersive-vr', {
optionalFeatures: ['hand-tracking']
});
session.addEventListener('inputsourceschange', (event) => {
for (const source of event.added) {
if (source.hand) {
console.log(`Hand erkannt: ${source.handedness}, Gelenke: ${source.hand.size}`);
}
}
});
};
document.querySelector('#enter-vr').addEventListener('click', startXR);
Alle Gelenke im Frame-Loop durchlaufen
let refSpace;
const onXRFrame = (time, frame) => {
const session = frame.session;
session.requestAnimationFrame(onXRFrame);
for (const source of session.inputSources) {
if (!source.hand) continue;
// XRHand ist iterierbar wie eine Map
for (const [jointName, jointSpace] of source.hand) {
const pose = frame.getJointPose(jointSpace, refSpace);
if (!pose) continue; // Gelenk gerade nicht getrackt
const { x, y, z } = pose.transform.position;
// Hier z. B. eine Kugel mit Radius pose.radius zeichnen
if (jointName === 'wrist') {
console.log(`${source.handedness} wrist:`, x.toFixed(2), y.toFixed(2), z.toFixed(2));
}
}
}
};
const init = async (session) => {
refSpace = await session.requestReferenceSpace('local-floor');
session.requestAnimationFrame(onXRFrame);
};
Pinch-Geste erkennen (Daumen- und Zeigefingerspitze)
const PINCH_THRESHOLD = 0.02; // 2 cm
const isPinching = (frame, hand, refSpace) => {
const thumb = frame.getJointPose(hand.get('thumb-tip'), refSpace);
const index = frame.getJointPose(hand.get('index-finger-tip'), refSpace);
if (!thumb || !index) return false;
const a = thumb.transform.position;
const b = index.transform.position;
const distance = Math.hypot(a.x - b.x, a.y - b.y, a.z - b.z);
// Gelenkradien abziehen, damit die Hautoberfläche zählt
return distance - thumb.radius - index.radius < PINCH_THRESHOLD;
};
// Im Frame-Callback:
// if (source.hand && isPinching(frame, source.hand, refSpace)) console.log('Pinch!');
Effizient: alle Posen mit fillPoses() abfragen
// Puffer einmalig anlegen: 25 Gelenke × 16 Matrixwerte
const transforms = new Float32Array(25 * 16);
const radii = new Float32Array(25);
const updateHand = (frame, hand, refSpace) => {
const joints = [...hand.values()];
const posesOk = frame.fillPoses(joints, refSpace, transforms);
const radiiOk = frame.fillJointRadii(joints, radii);
if (!posesOk || !radiiOk) return;
// Matrix des Zeigefinger-Tips (Index 9 in der festen Reihenfolge)
const tipMatrix = transforms.subarray(9 * 16, 10 * 16);
console.log('Tip-Position:', tipMatrix[12], tipMatrix[13], tipMatrix[14]);
};