Start · Sprachen · JavaScript · Referenz · XRHand

XRHand

Schnittstelle

Nur lesbare, geordnete Map mit den 25 Handgelenken einer getrackten Hand: Schlüssel ist der Gelenkname, Wert der zugehörige <code>XRJointSpace</code>.

seit JavaScript WebXR Hand Input Module Level 1 – Meta Quest Brows Kategorie: webapi

Signatur

interface XRHand (maplike&lt;XRHandJoint, XRJointSpace&gt;)

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

Typ
XRHand | null
Beschreibung
Über 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);
Hand erkannt: left, Gelenke: 25 Hand erkannt: right, Gelenke: 25

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);
};
right wrist: 0.21 1.05 -0.32

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!');
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]);
};
Tip-Position: 0.18 1.12 -0.41