@babylonjs/core
Version:
Getting started? Play directly with the Babylon.js API using our [playground](https://playground.babylonjs.com/). It also contains a lot of samples to learn how to use it.
975 lines (974 loc) • 44.8 kB
TypeScript
/** This file must only contain pure code and pure imports */
import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
import { type WebXRSessionManager } from "../webXRSessionManager.js";
import { type AbstractMesh } from "../../Meshes/abstractMesh.pure.js";
import { Quaternion } from "../../Maths/math.vector.pure.js";
import { type Nullable } from "../../types.js";
import { type IDisposable, type Scene } from "../../scene.pure.js";
import { Observable } from "../../Misc/observable.pure.js";
import { TransformNode } from "../../Meshes/transformNode.pure.js";
import { type Node } from "../../node.js";
/**
* All 83 body joint names as defined by the WebXR Body Tracking specification.
* @see https://immersive-web.github.io/body-tracking/#xrbody-interface
*/
export declare enum WebXRBodyJoint {
/** The center of the hips / pelvis */
HIPS = "hips",
/** Lower spine (lumbar) */
SPINE_LOWER = "spine-lower",
/** Middle spine (thoracic) */
SPINE_MIDDLE = "spine-middle",
/** Upper spine */
SPINE_UPPER = "spine-upper",
/** Chest */
CHEST = "chest",
/** Neck */
NECK = "neck",
/** Head */
HEAD = "head",
/** Left shoulder */
LEFT_SHOULDER = "left-shoulder",
/** Left scapula */
LEFT_SCAPULA = "left-scapula",
/** Left upper arm */
LEFT_ARM_UPPER = "left-arm-upper",
/** Left forearm (lower arm) */
LEFT_ARM_LOWER = "left-arm-lower",
/** Left hand wrist twist (forearm twist) */
LEFT_HAND_WRIST_TWIST = "left-hand-wrist-twist",
/** Right shoulder */
RIGHT_SHOULDER = "right-shoulder",
/** Right scapula */
RIGHT_SCAPULA = "right-scapula",
/** Right upper arm */
RIGHT_ARM_UPPER = "right-arm-upper",
/** Right forearm (lower arm) */
RIGHT_ARM_LOWER = "right-arm-lower",
/** Right hand wrist twist (forearm twist) */
RIGHT_HAND_WRIST_TWIST = "right-hand-wrist-twist",
/** Left palm center */
LEFT_HAND_PALM = "left-hand-palm",
/** Left wrist */
LEFT_HAND_WRIST = "left-hand-wrist",
/** Left thumb metacarpal */
LEFT_HAND_THUMB_METACARPAL = "left-hand-thumb-metacarpal",
/** Left thumb proximal phalanx */
LEFT_HAND_THUMB_PHALANX_PROXIMAL = "left-hand-thumb-phalanx-proximal",
/** Left thumb distal phalanx */
LEFT_HAND_THUMB_PHALANX_DISTAL = "left-hand-thumb-phalanx-distal",
/** Left thumb tip */
LEFT_HAND_THUMB_TIP = "left-hand-thumb-tip",
/** Left index finger metacarpal */
LEFT_HAND_INDEX_METACARPAL = "left-hand-index-metacarpal",
/** Left index finger proximal phalanx */
LEFT_HAND_INDEX_PHALANX_PROXIMAL = "left-hand-index-phalanx-proximal",
/** Left index finger intermediate phalanx */
LEFT_HAND_INDEX_PHALANX_INTERMEDIATE = "left-hand-index-phalanx-intermediate",
/** Left index finger distal phalanx */
LEFT_HAND_INDEX_PHALANX_DISTAL = "left-hand-index-phalanx-distal",
/** Left index finger tip */
LEFT_HAND_INDEX_TIP = "left-hand-index-tip",
/** Left middle finger metacarpal */
LEFT_HAND_MIDDLE_METACARPAL = "left-hand-middle-metacarpal",
/** Left middle finger proximal phalanx */
LEFT_HAND_MIDDLE_PHALANX_PROXIMAL = "left-hand-middle-phalanx-proximal",
/** Left middle finger intermediate phalanx */
LEFT_HAND_MIDDLE_PHALANX_INTERMEDIATE = "left-hand-middle-phalanx-intermediate",
/** Left middle finger distal phalanx */
LEFT_HAND_MIDDLE_PHALANX_DISTAL = "left-hand-middle-phalanx-distal",
/** Left middle finger tip */
LEFT_HAND_MIDDLE_TIP = "left-hand-middle-tip",
/** Left ring finger metacarpal */
LEFT_HAND_RING_METACARPAL = "left-hand-ring-metacarpal",
/** Left ring finger proximal phalanx */
LEFT_HAND_RING_PHALANX_PROXIMAL = "left-hand-ring-phalanx-proximal",
/** Left ring finger intermediate phalanx */
LEFT_HAND_RING_PHALANX_INTERMEDIATE = "left-hand-ring-phalanx-intermediate",
/** Left ring finger distal phalanx */
LEFT_HAND_RING_PHALANX_DISTAL = "left-hand-ring-phalanx-distal",
/** Left ring finger tip */
LEFT_HAND_RING_TIP = "left-hand-ring-tip",
/** Left little finger metacarpal */
LEFT_HAND_LITTLE_METACARPAL = "left-hand-little-metacarpal",
/** Left little finger proximal phalanx */
LEFT_HAND_LITTLE_PHALANX_PROXIMAL = "left-hand-little-phalanx-proximal",
/** Left little finger intermediate phalanx */
LEFT_HAND_LITTLE_PHALANX_INTERMEDIATE = "left-hand-little-phalanx-intermediate",
/** Left little finger distal phalanx */
LEFT_HAND_LITTLE_PHALANX_DISTAL = "left-hand-little-phalanx-distal",
/** Left little finger tip */
LEFT_HAND_LITTLE_TIP = "left-hand-little-tip",
/** Right palm center */
RIGHT_HAND_PALM = "right-hand-palm",
/** Right wrist */
RIGHT_HAND_WRIST = "right-hand-wrist",
/** Right thumb metacarpal */
RIGHT_HAND_THUMB_METACARPAL = "right-hand-thumb-metacarpal",
/** Right thumb proximal phalanx */
RIGHT_HAND_THUMB_PHALANX_PROXIMAL = "right-hand-thumb-phalanx-proximal",
/** Right thumb distal phalanx */
RIGHT_HAND_THUMB_PHALANX_DISTAL = "right-hand-thumb-phalanx-distal",
/** Right thumb tip */
RIGHT_HAND_THUMB_TIP = "right-hand-thumb-tip",
/** Right index finger metacarpal */
RIGHT_HAND_INDEX_METACARPAL = "right-hand-index-metacarpal",
/** Right index finger proximal phalanx */
RIGHT_HAND_INDEX_PHALANX_PROXIMAL = "right-hand-index-phalanx-proximal",
/** Right index finger intermediate phalanx */
RIGHT_HAND_INDEX_PHALANX_INTERMEDIATE = "right-hand-index-phalanx-intermediate",
/** Right index finger distal phalanx */
RIGHT_HAND_INDEX_PHALANX_DISTAL = "right-hand-index-phalanx-distal",
/** Right index finger tip */
RIGHT_HAND_INDEX_TIP = "right-hand-index-tip",
/** Right middle finger metacarpal */
RIGHT_HAND_MIDDLE_METACARPAL = "right-hand-middle-metacarpal",
/** Right middle finger proximal phalanx */
RIGHT_HAND_MIDDLE_PHALANX_PROXIMAL = "right-hand-middle-phalanx-proximal",
/** Right middle finger intermediate phalanx */
RIGHT_HAND_MIDDLE_PHALANX_INTERMEDIATE = "right-hand-middle-phalanx-intermediate",
/** Right middle finger distal phalanx */
RIGHT_HAND_MIDDLE_PHALANX_DISTAL = "right-hand-middle-phalanx-distal",
/** Right middle finger tip */
RIGHT_HAND_MIDDLE_TIP = "right-hand-middle-tip",
/** Right ring finger metacarpal */
RIGHT_HAND_RING_METACARPAL = "right-hand-ring-metacarpal",
/** Right ring finger proximal phalanx */
RIGHT_HAND_RING_PHALANX_PROXIMAL = "right-hand-ring-phalanx-proximal",
/** Right ring finger intermediate phalanx */
RIGHT_HAND_RING_PHALANX_INTERMEDIATE = "right-hand-ring-phalanx-intermediate",
/** Right ring finger distal phalanx */
RIGHT_HAND_RING_PHALANX_DISTAL = "right-hand-ring-phalanx-distal",
/** Right ring finger tip */
RIGHT_HAND_RING_TIP = "right-hand-ring-tip",
/** Right little finger metacarpal */
RIGHT_HAND_LITTLE_METACARPAL = "right-hand-little-metacarpal",
/** Right little finger proximal phalanx */
RIGHT_HAND_LITTLE_PHALANX_PROXIMAL = "right-hand-little-phalanx-proximal",
/** Right little finger intermediate phalanx */
RIGHT_HAND_LITTLE_PHALANX_INTERMEDIATE = "right-hand-little-phalanx-intermediate",
/** Right little finger distal phalanx */
RIGHT_HAND_LITTLE_PHALANX_DISTAL = "right-hand-little-phalanx-distal",
/** Right little finger tip */
RIGHT_HAND_LITTLE_TIP = "right-hand-little-tip",
/** Left upper leg (thigh) */
LEFT_UPPER_LEG = "left-upper-leg",
/** Left lower leg (shin) */
LEFT_LOWER_LEG = "left-lower-leg",
/** Left foot ankle twist */
LEFT_FOOT_ANKLE_TWIST = "left-foot-ankle-twist",
/** Left foot ankle */
LEFT_FOOT_ANKLE = "left-foot-ankle",
/** Left foot subtalar */
LEFT_FOOT_SUBTALAR = "left-foot-subtalar",
/** Left foot transverse */
LEFT_FOOT_TRANSVERSE = "left-foot-transverse",
/** Left foot ball */
LEFT_FOOT_BALL = "left-foot-ball",
/** Right upper leg (thigh) */
RIGHT_UPPER_LEG = "right-upper-leg",
/** Right lower leg (shin) */
RIGHT_LOWER_LEG = "right-lower-leg",
/** Right foot ankle twist */
RIGHT_FOOT_ANKLE_TWIST = "right-foot-ankle-twist",
/** Right foot ankle */
RIGHT_FOOT_ANKLE = "right-foot-ankle",
/** Right foot subtalar */
RIGHT_FOOT_SUBTALAR = "right-foot-subtalar",
/** Right foot transverse */
RIGHT_FOOT_TRANSVERSE = "right-foot-transverse",
/** Right foot ball */
RIGHT_FOOT_BALL = "right-foot-ball"
}
/**
* Parent index for each joint in {@link BodyJointReferenceArray} order.
* -1 means "root" (no parent). Used to convert world-space XR poses to
* local-space transforms suitable for skeleton bones.
*
* Hierarchy follows the WebXR Body Tracking specification and standard
* humanoid anatomy.
*/
export declare const BodyJointParentIndex: number[];
/**
* Logical body parts for convenient grouping of joints.
*/
export declare enum BodyPart {
/** Torso / spine (hips through head) */
TORSO = "torso",
/** Left arm (shoulder through wrist twist) */
LEFT_ARM = "left-arm",
/** Right arm (shoulder through wrist twist) */
RIGHT_ARM = "right-arm",
/** Left hand (palm through finger tips) */
LEFT_HAND = "left-hand",
/** Right hand (palm through finger tips) */
RIGHT_HAND = "right-hand",
/** Left leg (upper leg through foot ball) */
LEFT_LEG = "left-leg",
/** Right leg (upper leg through foot ball) */
RIGHT_LEG = "right-leg"
}
/**
* A dictionary mapping each {@link WebXRBodyJoint} to a bone name in a rigged body mesh.
*
* When you supply a rigged mesh to {@link WebXRTrackedBody.setBodyMesh}, provide
* a mapping so the feature knows which skeleton bone to drive for each joint.
*
* @example
* ```typescript
* const rigMapping: XRBodyMeshRigMapping = {
* "hips": "Bip01_Pelvis",
* "spine-lower": "Bip01_Spine",
* "head": "Bip01_Head",
* // …remaining joints…
* };
* ```
*/
export type XRBodyMeshRigMapping = {
[jointName in WebXRBodyJoint]?: string;
};
/**
* Represents the XRBodySpace native interface as defined by the spec.
* An XRBodySpace is an XRSpace that additionally exposes a jointName.
* @see https://immersive-web.github.io/body-tracking/#xrjointspace-interface
*/
interface XRBodySpace extends XRSpace {
readonly jointName: string;
}
/**
* Represents the native XRBody interface as defined by the spec.
* An XRBody is an iterable map of XRBodyJoint → XRBodySpace.
* @see https://immersive-web.github.io/body-tracking/#xrbody-interface
*/
interface XRBody {
readonly size: number;
get(key: string): XRBodySpace | undefined;
forEach(callbackfn: (value: XRBodySpace, key: string, map: XRBody) => void): void;
[Symbol.iterator](): IterableIterator<[string, XRBodySpace]>;
entries(): IterableIterator<[string, XRBodySpace]>;
keys(): IterableIterator<string>;
values(): IterableIterator<XRBodySpace>;
}
declare global {
interface XRFrame {
body?: XRBody;
}
}
/**
* Configuration options for the WebXR body tracking feature.
*/
export interface IWebXRBodyTrackingOptions {
/**
* A pre-existing rigged body mesh to drive with tracked joint poses.
* If provided, skeleton bones will be linked to tracked joints automatically.
* The mesh should contain a skeleton whose bones can be mapped via `rigMapping`.
*/
bodyMesh?: AbstractMesh;
/**
* A mapping from {@link WebXRBodyJoint} names to skeleton bone names.
* Required when the skeleton's bone names do not match the WebXR joint names.
* If omitted and a body mesh is provided, the feature assumes bones are
* named identically to the WebXR joint names (e.g. `"hips"`, `"left-arm-upper"`, etc.).
*/
rigMapping?: XRBodyMeshRigMapping;
/**
* Scale factor applied to the local-space position of every joint.
*
* This uniformly scales the distances between parent and child joints,
* allowing you to fit XR body tracking data to meshes that are larger or
* smaller than the tracked user.
*
* - `1.0` (default): no scaling — real-world proportions.
* - `> 1.0`: stretches the skeleton (makes it taller / wider).
* - `< 1.0`: compresses the skeleton.
*
* Only affects local joint offsets, not the root (hips) position.
*/
jointScaleFactor?: number;
/**
* Preserve bind-pose local translations for mapped bones and only retarget rotations.
*
* When `false` (default), mapped bones are translated directly to the tracked joint
* positions. This reproduces the tracked skeleton exactly, but can distort avatars whose
* proportions differ from the tracked user.
*
* When `true`, mapped child bones keep their bind-pose local translation offsets while
* still using the tracked joint rotations. This is the typical retargeting mode for
* driving arbitrary skinned characters without forcing them to the tracked skeleton's
* segment lengths.
*/
preserveBindPoseBonePositions?: boolean;
/**
* Apply a per-bone orientation offset so the avatar bone basis matches the XR joint basis.
*
* When enabled, WebXR joint rotations are corrected using the bone's bind-space child axis.
* This is useful for rigs whose bone local axes do not match the anatomical axes used by
* the WebXR body-tracking joint orientations.
*/
useBoneOrientationOffsets?: boolean;
/**
* Rotation applied in each tracked joint's local frame to re-base the
* XR joint axes. Some runtimes (e.g., some Meta Quest builds) emit body
* joint poses whose +Z axis points along the bone (parent → child), while
* most avatar rigs expect +Y along the bone. Setting this to
* `Quaternion.RotationAxis(Vector3.Right(), -Math.PI / 2)` converts
* "+Z-along-bone" joint data to "+Y-along-bone" before retargeting.
*
* Applied as a pre-multiply on each joint's world matrix:
* `M' = R × M`, which, under Babylon's row-vector convention
* (`v_world = v_local × M`), effectively re-bases the joint-local axes.
*
* Default `undefined` = identity (no re-basing).
*/
jointLocalRotationOffset?: Quaternion;
/**
* Per–XR-joint override for the "aim child" joint used when
* {@link useBoneOrientationOffsets} is enabled.
*
* By default the aim direction for a mapped bone is computed against its
* nearest mapped descendant in the skeleton. For rigs whose XR-mapped
* spine chain has very short segments (e.g. WebXR's `hips`→`spine-lower`
* is typically only ~1 cm), the aim direction becomes noise-dominated and
* can produce a large incorrect rotation for the parent bone.
*
* Use this map to redirect a specific XR joint's aim target to a farther
* XR joint whose relative position is stable. Both the source and target
* joint must be present in {@link rigMapping} (i.e. mapped to a bone on the
* skeleton).
*
* Example for a Mixamo-style rig where hips, spine, spine1, spine2 and
* neck are all mapped:
* ```ts
* aimChildOverrides: {
* [WebXRBodyJoint.HIPS]: WebXRBodyJoint.SPINE_UPPER, // skip spine-lower/middle
* [WebXRBodyJoint.SPINE_LOWER]: WebXRBodyJoint.NECK, // long stable segment
* [WebXRBodyJoint.SPINE_MIDDLE]: WebXRBodyJoint.NECK,
* }
* ```
*/
aimChildOverrides?: Partial<Record<WebXRBodyJoint, WebXRBodyJoint>>;
/**
* Convenience flag for Mixamo-rigged characters.
*
* When set to `true`, the feature automatically applies:
* - {@link MixamoRigMapping} as the `rigMapping` (skipping any explicit
* `rigMapping` you provide).
* - {@link MixamoAimChildOverrides} as `aimChildOverrides` (skipping any
* explicit `aimChildOverrides` you provide).
* - Turns on {@link useBoneOrientationOffsets} by default (you can still
* override to `false`).
*
* The Mixamo `mixamorig:` bone-name prefix is detected automatically by
* inspecting the skeleton, so the same mapping works regardless of whether
* the bones have been renamed to strip the prefix (common when re-exporting).
*/
isMixamoModel?: boolean;
}
/**
* Default rig mapping for Mixamo-rigged humanoid characters.
*
* Maps each supported {@link WebXRBodyJoint} to the corresponding Mixamo bone
* name, **without** the `mixamorig:` prefix. When the feature applies this
* mapping, it auto-detects whether the skeleton uses the `mixamorig:` prefix
* and prepends it as needed, so the same table works for both prefixed and
* unprefixed exports.
*
* @example
* ```ts
* xr.featuresManager.enableFeature(WebXRFeatureName.BODY_TRACKING, "latest", {
* bodyMesh: myMixamoMesh,
* isMixamoModel: true,
* });
* ```
*
* Or, if you want to extend or customize it:
* ```ts
* import { MixamoRigMapping } from "@babylonjs/core";
* const rigMapping: XRBodyMeshRigMapping = { ...MixamoRigMapping, [WebXRBodyJoint.NECK]: "MyNeckBone" };
* ```
*/
export declare const MixamoRigMapping: XRBodyMeshRigMapping;
/**
* Default aim-child overrides for Mixamo-rigged humanoids.
*
* Redirects the short / noisy XR spine segments to longer, stable ones so that
* {@link IWebXRBodyTrackingOptions.useBoneOrientationOffsets} produces clean
* torso rotations. In WebXR data, `hips`→`spine-lower` is typically only ~1 cm
* apart — too short to give a stable aim direction — so we reroute Mixamo's
* Hips/Spine/Spine1 bones to aim at `spine-upper` / `neck` instead.
*/
export declare const MixamoAimChildOverrides: Partial<Record<WebXRBodyJoint, WebXRBodyJoint>>;
/**
* Resolve the Mixamo rig mapping for a given body mesh, auto-detecting the
* `mixamorig:` bone-name prefix. Falls back to the unprefixed names.
* @param bodyMesh The rigged Mixamo body mesh.
* @returns An {@link XRBodyMeshRigMapping} whose bone names include the
* detected prefix (if any).
* @internal
*/
export declare function _ResolveMixamoRigMapping(bodyMesh: AbstractMesh): XRBodyMeshRigMapping;
/**
* Represents a tracked body during a WebXR session.
*
* This class manages the bridge between the WebXR body pose data and the
* Babylon.js scene graph. It creates a set of {@link TransformNode}s — one per
* body joint — whose transforms are updated every frame from the XR runtime.
* When a rigged body mesh is attached, its skeleton bones are linked to these
* transform nodes, causing the mesh to follow the user's body automatically.
*
* Coordinate-system handling:
* - WebXR delivers poses in a right-handed system.
* - By default, Babylon.js uses a left-handed system.
* - The class converts the data in-place (negating the Z components of every
* 4 × 4 joint matrix) before decomposing into Babylon transforms.
* - If the mesh was authored in a right-handed tool (the common case for glTF),
* the bone transforms are un-flipped so the skeleton interprets them correctly.
*/
export declare class WebXRTrackedBody implements IDisposable {
/**
* Fired when the body mesh is changed via {@link setBodyMesh}.
*/
readonly onBodyMeshSetObservable: Observable<WebXRTrackedBody>;
private _scene;
/**
* One {@link TransformNode} per joint. These receive the WebXR matrix data
* every frame and serve as link targets for skeleton bones.
*/
private _jointTransforms;
/**
* Flat Float32Array that receives transform matrices directly from the
* WebXR API (via `fillPoses`). 16 floats per joint × 83 joints = 1 328 floats.
*/
private _jointTransformMatrices;
/**
* Copy of the raw RHS XR matrices (before LHS conversion).
* Used to compute bone-local transforms for glTF skeletons that
* operate in RHS space.
*/
private _jointTransformMatricesRHS;
/**
* Cached array of XRBodySpace objects extracted from the XRBody, kept in the
* same order as {@link BodyJointReferenceArray}.
*/
private _jointSpaces;
/** Temporary matrix: this joint's XR world-space matrix. */
private _tempJointMatrix;
/** Temporary matrix: parent joint's XR world-space matrix. */
private _tempParentMatrix;
/** Temporary matrix: computed bone-local matrix. */
private _tempLocalMatrix;
/** Temporary vector for scale extracted from decompose. */
private _tempScaleVector;
/** Temporary quaternion for decompose. */
private _tempRotQuat;
/** Temporary position vector for decompose. */
private _tempPosVec;
/** Temporary quaternion for alternate rotation calculations. */
private _tempRotQuat2;
/** Temporary vector for desired child direction. */
private _tempDirection;
/** Temporary vector for joint-local child direction. */
private _tempLocalDirection;
/** Cached desired final positions for mapped joints. */
private _desiredFinalPositions;
/**
* For each joint index, the joint index of the nearest mapped SKELETON
* ancestor bone. -1 when the bone has no mapped ancestor (root level).
* Precomputed in {@link setBodyMesh} by walking the skeleton hierarchy.
*/
private _jointParentJointIdx;
/** Tracks which joint indices have a linked bone (for step 4b). */
private _jointHasBone;
/** Bone → XR joint index lookup, built in {@link setBodyMesh}. */
private _boneToJointIdx;
/** Original bind-pose local matrices for mapped bones. */
private _mappedBoneBindLocals;
/** Nearest mapped child bone for each mapped bone. */
private _mappedChildBones;
/** Bind-space local child direction for each mapped bone. */
private _bindLocalAimDirections;
/** Bind-space local hand-plane normal used to correct wrist/hand twist from tracked finger positions. */
private _bindLocalTwistNormals;
/**
* XR joint index to aim each mapped bone at. This can be a mapped joint
* (same as `_boneToJointIdx.get(aimChildBone)`) or an **unmapped** XR
* joint whose tracked position is nonetheless useful for aim correction —
* e.g. `LEFT_HAND_MIDDLE_METACARPAL` for `mixamorig:LeftHand`, which has
* no mapped finger descendant but whose tracked position still defines
* "where the hand is pointing".
*/
private _boneAimTargetJointIdx;
/** Per-bone pair of tracked joints that define the hand plane used for twist correction. */
private _boneTwistReferenceJointIdx;
/**
* Per-mapped-bone bind-pose world rotation in mesh-local space
* (decomposed from `bone.getFinalMatrix()` at bind time). Used by the
* delta-from-bind retarget path (axis-convention-invariant).
*/
private _bindBoneWorldRotMeshLocal;
/**
* Bind-pose tracked joint rotation in mesh-local space per mapped joint.
* Captured on the first tracked frame (or on demand via
* {@link captureTrackedBind}). `null` until captured.
*/
private _trackedBindDesiredFinalRot;
/** Bind-pose tracked joint position (mesh-local), captured alongside rotation. */
private _trackedBindDesiredFinalPos;
/** True once a tracked-bind snapshot has been taken. */
private _hasTrackedBind;
/**
* When `true` (default), the first tracked frame after the feature
* attaches is used as the "rest" pose for delta-from-bind retargeting.
* Set to `false` to require an explicit {@link captureTrackedBind} call.
*/
autoCaptureBindOnFirstFrame: boolean;
/** Scratch quaternion for retarget delta composition. */
private _tempDeltaQuat;
private _tempBoneWorldRot;
private _tempParentNewWorldRotInv;
private _tempTrackedCurRot;
private _tempTrackedCurPos;
private _tempBindLocalScale;
private _tempBindLocalPos;
/**
* Per-bone cache of the current frame's computed world rotation.
* Entries are pooled across frames (values are reused via `copyFrom`)
* to avoid allocating a fresh Quaternion per mapped bone per frame.
* A bone is considered "populated this frame" iff it has been visited
* by the current retarget pass (tracked via `_computedBoneNewWorldRotFrameId`).
*/
private _computedBoneNewWorldRot;
/** Per-bone marker: frame id at which the pooled rotation above was last set. */
private _computedBoneNewWorldRotFrameId;
private _currentRetargetFrameId;
/** Scratch quaternion reused for the parent-world accumulation loop. */
private _tempParentAccumRot;
/** Scratch quaternion reused for the parent-world intermediate product. */
private _tempParentAccumTmp;
/** Scratch vectors reused by hand twist correction. */
private _tempTwistFirst;
private _tempTwistSecond;
private _tempTwistNormal;
private _tempCurrentTwistNormal;
private _tempProjectedTwistNormal;
private _tempProjectedDesiredTwistNormal;
private _tempTwistAimAxis;
private _tempTwistCross;
/** The skeleton reference for iterating bones in parent-first order. */
private _skeleton;
/** Cached inverse of the skeleton mesh's world matrix. */
private _meshWorldMatrixInverse;
/** Cached inverse of the skeleton mesh pose matrix when initial skinning is used. */
private _initialSkinMatrixInverse;
/**
* Pre-allocated desiredFinal matrices (one per joint slot).
* `desiredFinal[i] = strip(xrWorld[i] × inv(meshWorld))` — the bone's
* target skeleton-space final matrix with parasitic scale removed.
*/
private _desiredFinals;
/**
* Standalone TransformNodes created for unmapped skinned bones.
* These TNs are initialized to the bone's bind-pose local and linked
* so that `prepare()` reads deterministic values rather than the
* original glTF scene-graph TNs (which we don't control).
*/
private _unmappedBoneNodes;
/** The mesh that owns the skeleton (used for world-matrix inverse). */
private _skeletonMesh;
/** The body mesh root (topmost parent), used to parent the mesh to the camera. */
private _bodyMeshRoot;
/** The rigged body mesh, if any. */
private _bodyMesh;
/** Scale factor for local joint offsets. */
private _jointScaleFactor;
/** Whether mapped bones should keep their bind-pose local translations. */
private readonly _preserveBindPoseBonePositions;
/** Whether mapped bones should correct WebXR joint rotations using bind-space orientation offsets. */
private readonly _useBoneOrientationOffsets;
/** Per–XR-joint override mapping for aim children (used with _useBoneOrientationOffsets). */
private _aimChildOverrides;
/**
* Runtime-mutable rotation applied in each tracked joint's local frame to
* re-base XR joint axes (e.g., "+Z-along-bone" → "+Y-along-bone").
* `null` = identity / disabled.
*/
jointLocalRotationOffset: Nullable<Quaternion>;
/** Cached 4×4 matrix form of {@link jointLocalRotationOffset} for the fast path. */
private _jointLocalRotationOffsetMatrix;
/** Temporary matrix used when applying {@link jointLocalRotationOffset}. */
private _tempOffsetAppliedMatrix;
/**
* When true, bypass skeleton.prepare() and write skin matrices directly.
* This is a diagnostic flag to help isolate rendering issues. When the
* standard pipeline (TN → bone → prepare → skin matrices) produces
* unexpected results, enabling this writes `absInvBind × final` directly
* into the skeleton's transform matrix buffer.
* @internal
*/
_directSkinWrite: boolean;
/**
* Debug info string from the last `updateFromXRFrame` call.
* Useful for diagnosing tracking failures on-device.
* @internal
*/
_lastDebugInfo: string;
/**
* Get the current body mesh (if any).
*/
get bodyMesh(): Nullable<AbstractMesh>;
/**
* Get or set the scale factor for local joint offsets.
* @see {@link IWebXRBodyTrackingOptions.jointScaleFactor}
*/
get jointScaleFactor(): number;
set jointScaleFactor(value: number);
/**
* Returns the array of transform nodes representing each body joint.
* The order matches {@link WebXRBodyTracking.AllBodyJoints}; use
* {@link getJointTransform} or {@link getBodyPartTransforms} for
* name-based lookup.
*
* Note: when a body mesh is attached, these transform nodes are also
* used as the skeleton's link targets for mapped joints. In that case
* the values held by mapped-joint nodes are skeleton-local (parent bone's
* frame), not XR world-space. Unmapped-joint nodes always hold world-space
* pose. If you need world-space poses for every joint regardless of
* mapping, sample the bone matrices directly via the attached skeleton.
*/
get jointTransforms(): readonly TransformNode[];
/**
* Get the transform node for a specific body joint.
* @param jointName The name of the body joint (from {@link WebXRBodyJoint}).
* @returns The transform node corresponding to that joint, or `undefined` if not found.
*/
getJointTransform(jointName: WebXRBodyJoint): TransformNode | undefined;
/**
* Get all joint transform nodes that belong to a given body part.
* @param part The body part to query.
* @param result Optional pre-allocated array to fill (avoids per-call allocation).
* The array is cleared and populated with the results.
* @returns An array of TransformNodes for that body part.
*/
getBodyPartTransforms(part: BodyPart, result?: TransformNode[]): TransformNode[];
/**
* Construct a new tracked body instance.
* @param scene The Babylon.js scene.
* @param bodyMesh Optional rigged body mesh to attach immediately.
* @param rigMapping Optional mapping from WebXR joint names to skeleton bone names.
* @param jointScaleFactor Scale factor for local joint offsets (default 1.0).
* @param preserveBindPoseBonePositions Whether mapped bones should keep bind-pose local translations.
* @param useBoneOrientationOffsets Whether mapped bones should correct XR joint rotations using bind-space offsets.
* @param aimChildOverrides Per–XR-joint override for the aim child used with `useBoneOrientationOffsets`.
* @param jointLocalRotationOffset Optional rotation re-basing each XR joint's local frame (e.g. Z-along-bone → Y-along-bone).
*/
constructor(scene: Scene, bodyMesh?: AbstractMesh, rigMapping?: XRBodyMeshRigMapping, jointScaleFactor?: number, preserveBindPoseBonePositions?: boolean, useBoneOrientationOffsets?: boolean, aimChildOverrides?: Partial<Record<WebXRBodyJoint, WebXRBodyJoint>>, jointLocalRotationOffset?: Quaternion);
/**
* Attach (or replace) a rigged body mesh.
*
* The mesh's skeleton bones are linked to the internal transform nodes
* that receive WebXR tracking data each frame. If the mesh has a skeleton,
* the `rigMapping` (or a direct name match) is used to bind each bone.
*
* @param bodyMesh The rigged mesh to drive.
* @param rigMapping An optional mapping from {@link WebXRBodyJoint} to bone name.
* If omitted, bones are expected to be named after the WebXR joint names.
*/
setBodyMesh(bodyMesh: AbstractMesh, rigMapping?: XRBodyMeshRigMapping): void;
/**
* Update joint transforms from the current XR frame.
*
* This method is called once per frame by the feature class. Internally it:
* 1. Extracts all XRBodySpaces from the XRBody.
* 2. Fills the transform-matrix buffer via `fillPoses()` (or per-joint fallback).
* 3. Converts from WebXR right-handed to Babylon left-handed coordinates.
* 4. Decomposes each matrix into the corresponding TransformNode.
* 5. Parents the body mesh root to the XR camera so it tracks correctly.
*
* @param xrFrame The current XRFrame.
* @param referenceSpace The XRReferenceSpace to resolve poses against.
* @param xrCameraParent The parent node of the XR camera (used for parenting).
* @returns `true` if valid tracking data was processed, `false` otherwise.
*/
updateFromXRFrame(xrFrame: XRFrame, referenceSpace: XRReferenceSpace, xrCameraParent: Nullable<Node>): boolean;
/**
* Replay a pre-captured joint matrix set through the retargeting
* pipeline as if it had just been delivered by an XR frame.
*
* Useful for headset-less testing: call with a snapshot captured via
* {@link snapshotFrame} (the `jointMatricesRHS` array, or `jointMatricesLHS`
* already flipped). The matrices are assumed to be RHS unless
* `isAlreadyLhs=true`, in which case the RHS→LHS flip step is skipped.
*
* @param rawMatrices Float32Array of BODY_JOINT_COUNT × 16 (= 1328) floats.
* @param isAlreadyLhs Set to `true` if matrices are already LHS-converted.
*/
replayRawJointMatrices(rawMatrices: Float32Array | number[], isAlreadyLhs?: boolean): void;
/**
* Run steps 2.5 → 5 of the retargeting pipeline using whatever is
* currently in `_jointTransformMatrices` (as if the WebXR API just
* filled it for the current frame).
* @param xrCameraParent Parent node of the XR camera (used to parent the body mesh root during live XR).
* @param skipRhsToLhs If true, skip the RHS→LHS flip (matrices are already in the scene's handedness).
*/
private _processTrackedJointMatrices;
/**
* Capture the current tracked-joint desired-final rotations and positions
* as the "rest pose" for delta-from-bind retargeting.
*
* Delta-from-bind is the production retarget path: every subsequent
* frame is interpreted as a rotation delta from this snapshot, which
* makes retargeting invariant to the XR-joint axis convention and to
* any skeletal-proportion differences between the tracked user and the
* avatar.
*
* Call this after the user assumes a known rest pose (e.g. T-pose,
* A-pose, arms-at-sides). By default the feature auto-captures on the
* first tracked frame — disable via {@link autoCaptureBindOnFirstFrame}.
*/
captureTrackedBind(): void;
/**
* Clear any captured bind, reverting subsequent frames to the fallback
* direct-retarget path (or re-triggering auto-capture on the next frame).
*/
clearTrackedBind(): void;
private _computeNormalFromJointPositions;
private _projectOnPlaneToRef;
private _storeBindLocalTwistNormalFromPositions;
/** Internal: copy current desiredFinals into the tracked-bind slots. */
private _captureTrackedBindFromDesiredFinals;
/**
* Delta-from-bind retarget: axis-convention-invariant.
*
* For each mapped bone in skeleton order (parents first):
* let j = joint mapped to this bone
* deltaMeshLocalRot = bindTracked[j]⁻¹ × currentTracked[j] (right-side in row-vector)
* newBoneWorldRot = bindBoneWorldRot × deltaMeshLocalRot
* newBoneLocalRot = newBoneWorldRot × parentNewBoneWorldRot⁻¹
*
* Positions: root bone receives tracked world delta (so the avatar
* translates with the user). All other mapped bones keep their rig's
* bind-pose local translation to preserve segment lengths.
* @param scaleFactor Additional scale applied to joint local positions.
*/
private _retargetDeltaFromBind;
/**
* Legacy direct-retarget path (pre-bind-capture). Kept as fallback.
* @param useInitialSkinMatrix Skeleton needs initial-skin-matrix chaining for the root.
* @param useBoneOrientationOffsets Apply aim-direction correction per mapped bone.
* @param scaleFactor Additional scale applied to joint local positions.
* @param computedWorldRotations Optional map used by the aim-correction path for parent lookups.
*/
private _retargetDirect;
/**
* Capture a snapshot of the current frame's raw XR joint matrices and
* skeleton metadata. Returns a JSON string that can be used offline to
* replay / debug bone-local computation without a headset.
*
* The snapshot includes:
* - `jointMatricesRHS` – 83 × 16 raw RHS matrices (before LHS conversion)
* - `jointMatricesLHS` – 83 × 16 LHS-converted matrices (after step 3)
* - `meshWorldMatrix` – 16 floats, the skeleton mesh's world matrix (if any)
* - `jointHasBone` – boolean[83], which joints are mapped to bones
* - `jointParentJointIdx` – number[83], mapped ancestor for each joint
* - `useRightHandedSystem` – scene handedness setting
* - `jointNames` – the 83 joint names in order
*
* @returns A JSON string with the snapshot data.
*/
snapshotFrame(): string;
/**
* Capture a snapshot and copy it to the system clipboard.
* Logs to the console on success or failure.
* @returns A promise that resolves when the copy completes.
*/
snapshotFrameToClipboardAsync(): Promise<void>;
/**
* Dispose of this tracked body and its resources.
* @param disposeMesh If `true`, the body mesh and its skeleton are disposed as well.
*/
dispose(disposeMesh?: boolean): void;
}
/**
* WebXR Body Tracking feature.
*
* This feature tracks the user's full-body pose using the
* [WebXR Body Tracking Module](https://immersive-web.github.io/body-tracking/),
* which exposes 83 articulated joints covering the torso, arms, hands, legs and feet.
*
* ## Quick Start
*
* ```typescript
* // Enable body tracking when creating the default XR experience:
* const xr = await scene.createDefaultXRExperienceAsync();
* const bodyTracking = xr.baseExperience.featuresManager.enableFeature(
* WebXRFeatureName.BODY_TRACKING,
* "latest",
* {
* bodyMesh: myRiggedBodyMesh,
* rigMapping: {
* "hips": "Bip01_Pelvis",
* "spine-lower": "Bip01_Spine",
* // … one entry per joint you want to drive …
* },
* } as IWebXRBodyTrackingOptions,
* );
*
* // React to tracking changes:
* bodyTracking.onBodyTrackingStartedObservable.add((trackedBody) => {
* console.log("Body tracking started");
* });
* bodyTracking.onBodyTrackingFrameUpdateObservable.add((trackedBody) => {
* // The tracked body's joint transforms are already up-to-date.
* });
* ```
*
* ## How It Works
*
* 1. The feature requests the `"body-tracking"` native WebXR feature at session start.
* 2. Each frame, if `XRFrame.body` is available, joint poses are filled into a
* flat Float32Array via the batch `fillPoses()` API (with a per-joint fallback).
* 3. The 4 × 4 matrices are converted from WebXR right-handed coordinates to
* Babylon.js left-handed coordinates in-place (unless the scene is RHS).
* 4. Each matrix is decomposed and written to a TransformNode; skeleton bones
* linked to those nodes animate the rigged mesh automatically.
*
* ## Coordinate System
*
* WebXR data arrives in a **right-handed** coordinate system. Babylon.js
* defaults to **left-handed**. The conversion is handled automatically:
* - Joint matrices are flipped in-place (Z-negation of specific matrix elements).
* - For meshes authored in a right-handed tool (glTF, Blender, etc.), the bone
* data is un-flipped so the skeleton interprets poses correctly.
* - If you use `scene.useRightHandedSystem = true`, no conversion is applied.
*
* @see https://immersive-web.github.io/body-tracking/
*/
export declare class WebXRBodyTracking extends WebXRAbstractFeature {
/** Configuration options for the body tracking feature. */
readonly options: IWebXRBodyTrackingOptions;
/**
* The module's name, used when enabling the feature on the features manager.
* Value: `"xr-body-tracking"`.
*/
static readonly Name: "xr-body-tracking";
/**
* The (Babylon) version of this module.
* This is an integer representing the implementation version.
* This number does not correspond to the WebXR specs version.
*/
static readonly Version = 1;
/**
* Observable fired when body tracking starts (i.e. the first frame where
* `XRFrame.body` returns valid data).
*/
readonly onBodyTrackingStartedObservable: Observable<WebXRTrackedBody>;
/**
* Observable fired when body tracking is lost (i.e. `XRFrame.body` becomes
* `null` or returns no valid poses after previously tracking).
*/
readonly onBodyTrackingEndedObservable: Observable<void>;
/**
* Observable fired every frame that has valid body tracking data.
* At the point of notification, all joint transforms are up-to-date.
*/
readonly onBodyTrackingFrameUpdateObservable: Observable<WebXRTrackedBody>;
/**
* Observable fired when the body mesh has been set via {@link setBodyMesh}
* or during initial configuration.
*/
readonly onBodyMeshSetObservable: Observable<WebXRTrackedBody>;
/** The current tracked body, or null when not tracking. */
private _trackedBody;
/** True while we have an active body tracking session. */
private _isTracking;
/** Observer for world scale changes, so the body mesh can be rescaled. */
private _worldScaleObserver;
/**
* Debug info from the feature-level frame loop.
* Shows why `_onXRFrame` did or did not call `updateFromXRFrame`.
* @internal
*/
_lastFrameDebugInfo: string;
/**
* Get the currently tracked body, if any.
*/
get trackedBody(): Nullable<WebXRTrackedBody>;
/**
* Returns `true` while body tracking data is actively being received.
*/
get isTracking(): boolean;
/**
* Construct a new WebXRBodyTracking feature.
* @param _xrSessionManager The XR session manager.
* @param options Configuration options.
*/
constructor(_xrSessionManager: WebXRSessionManager,
/** Configuration options for the body tracking feature. */
options?: IWebXRBodyTrackingOptions);
/**
* Attach a rigged body mesh (or replace the current one) at any time.
*
* This is a convenience method that forwards to the underlying
* {@link WebXRTrackedBody.setBodyMesh}. The body does not need to be
* already tracking for this to work — the mesh will be applied once
* tracking begins.
*
* @param bodyMesh The rigged mesh to drive.
* @param rigMapping Optional mapping from {@link WebXRBodyJoint} names to bone names.
*/
setBodyMesh(bodyMesh: AbstractMesh, rigMapping?: XRBodyMeshRigMapping): void;
/**
* Attach the feature.
* Called by the features manager when the XR session initialises.
*
* Body tracking is a draft WebXR spec. Some UAs (e.g. Meta Quest) provide
* body data on `XRFrame.body` but do not list `"body-tracking"` in
* `session.enabledFeatures`. To handle this, we temporarily clear
* {@link xrNativeFeatureName} before calling the base `attach()` so the
* enabled-features check is skipped, then restore it afterwards.
* @returns `true` if attachment succeeded.
*/
attach(): boolean;
/**
* Detach the feature.
* Called by the features manager when the XR session ends.
* @returns `true` if detachment succeeded.
*/
detach(): boolean;
/**
* Dispose this feature and all resources.
*/
dispose(): void;
/**
* Called every XR frame by the base class.
* Reads body joint data from the XR runtime and updates transforms.
* @param xrFrame The current XRFrame.
*/
protected _onXRFrame(xrFrame: XRFrame): void;
/**
* Returns the complete ordered list of body joint names tracked by this feature.
* Useful for iterating over all joints or building UI.
*/
static get AllBodyJoints(): readonly WebXRBodyJoint[];
/**
* Capture a single-frame snapshot of all 83 joints and copy it to the
* clipboard. Call this from a playground button or the console while
* wearing the headset:
*
* ```typescript
* bodyTracking.snapshotFrameToClipboard();
* ```
*
* The JSON can later be loaded offline to replay the bone-local
* computation without a headset.
* @returns A promise that resolves when the copy completes, or rejects
* if no body is currently tracked.
*/
snapshotFrameToClipboardAsync(): Promise<void>;
}
/**
* Register side effects for webXRBodyTracking.
* Safe to call multiple times; only the first call has an effect.
*/
export declare function RegisterWebXRBodyTracking(): void;
export {};