@babylonjs/core 9.3.1 → 9.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Engines/AbstractEngine/abstractEngine.textureSelector.d.ts +45 -0
- package/Engines/AbstractEngine/abstractEngine.textureSelector.js +69 -0
- package/Engines/AbstractEngine/abstractEngine.textureSelector.js.map +1 -0
- package/Engines/AbstractEngine/index.d.ts +2 -0
- package/Engines/AbstractEngine/index.js +4 -0
- package/Engines/AbstractEngine/index.js.map +1 -1
- package/Engines/Extensions/engine.textureSelector.d.ts +2 -45
- package/Engines/Extensions/engine.textureSelector.js +8 -68
- package/Engines/Extensions/engine.textureSelector.js.map +1 -1
- package/Engines/abstractEngine.js +2 -2
- package/Engines/abstractEngine.js.map +1 -1
- package/FlowGraph/flowGraph.d.ts +22 -0
- package/FlowGraph/flowGraph.js +11 -0
- package/FlowGraph/flowGraph.js.map +1 -1
- package/FlowGraph/flowGraphCoordinator.d.ts +2 -1
- package/FlowGraph/flowGraphCoordinator.js +4 -2
- package/FlowGraph/flowGraphCoordinator.js.map +1 -1
- package/FlowGraph/flowGraphParser.js +7 -0
- package/FlowGraph/flowGraphParser.js.map +1 -1
- package/FlowGraph/typeDefinitions.d.ts +8 -0
- package/FlowGraph/typeDefinitions.js.map +1 -1
- package/Lights/Clustered/clusteredLightContainer.d.ts +1 -0
- package/Lights/Clustered/clusteredLightContainer.js +19 -0
- package/Lights/Clustered/clusteredLightContainer.js.map +1 -1
- package/Lights/light.d.ts +6 -0
- package/Lights/light.js +8 -0
- package/Lights/light.js.map +1 -1
- package/Lights/spotLight.d.ts +2 -0
- package/Lights/spotLight.js +10 -0
- package/Lights/spotLight.js.map +1 -1
- package/Materials/Background/backgroundMaterial.js +4 -1
- package/Materials/Background/backgroundMaterial.js.map +1 -1
- package/Materials/GaussianSplatting/gaussianSplattingMaterial.js +6 -2
- package/Materials/GaussianSplatting/gaussianSplattingMaterial.js.map +1 -1
- package/Materials/Node/Blocks/Dual/lightBlock.d.ts +8 -0
- package/Materials/Node/Blocks/Dual/lightBlock.js +16 -0
- package/Materials/Node/Blocks/Dual/lightBlock.js.map +1 -1
- package/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.js +3 -0
- package/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.js.map +1 -1
- package/Materials/Node/nodeMaterial.js +4 -1
- package/Materials/Node/nodeMaterial.js.map +1 -1
- package/Materials/PBR/openpbrMaterial.js +4 -1
- package/Materials/PBR/openpbrMaterial.js.map +1 -1
- package/Materials/PBR/pbrBaseMaterial.js +4 -1
- package/Materials/PBR/pbrBaseMaterial.js.map +1 -1
- package/Materials/materialHelper.functions.d.ts +12 -0
- package/Materials/materialHelper.functions.js +24 -0
- package/Materials/materialHelper.functions.js.map +1 -1
- package/Materials/standardMaterial.js +4 -1
- package/Materials/standardMaterial.js.map +1 -1
- package/Meshes/GaussianSplatting/gaussianSplattingMesh.d.ts +13 -0
- package/Meshes/GaussianSplatting/gaussianSplattingMesh.js +26 -0
- package/Meshes/GaussianSplatting/gaussianSplattingMesh.js.map +1 -1
- package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.d.ts +3 -0
- package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js +113 -10
- package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js.map +1 -1
- package/Misc/tools.js +1 -1
- package/Misc/tools.js.map +1 -1
- package/Particles/gpuParticleSystem.d.ts +35 -2
- package/Particles/gpuParticleSystem.js +272 -6
- package/Particles/gpuParticleSystem.js.map +1 -1
- package/Particles/thinParticleSystem.js +5 -0
- package/Particles/thinParticleSystem.js.map +1 -1
- package/Rendering/depthRenderer.d.ts +8 -0
- package/Rendering/depthRenderer.js +48 -13
- package/Rendering/depthRenderer.js.map +1 -1
- package/Rendering/depthRendererSceneComponent.d.ts +1 -0
- package/Rendering/depthRendererSceneComponent.js +26 -0
- package/Rendering/depthRendererSceneComponent.js.map +1 -1
- package/Shaders/gpuRenderParticles.vertex.js +7 -0
- package/Shaders/gpuRenderParticles.vertex.js.map +1 -1
- package/XR/features/WebXRBodyTracking.d.ts +952 -0
- package/XR/features/WebXRBodyTracking.js +2221 -0
- package/XR/features/WebXRBodyTracking.js.map +1 -0
- package/XR/features/index.d.ts +1 -0
- package/XR/features/index.js +1 -0
- package/XR/features/index.js.map +1 -1
- package/XR/webXRFeaturesManager.d.ts +7 -0
- package/XR/webXRFeaturesManager.js +4 -0
- package/XR/webXRFeaturesManager.js.map +1 -1
- package/package.json +1 -1
- package/sceneComponent.d.ts +1 -0
- package/sceneComponent.js +1 -0
- package/sceneComponent.js.map +1 -1
|
@@ -0,0 +1,2221 @@
|
|
|
1
|
+
import { WebXRAbstractFeature } from "./WebXRAbstractFeature.js";
|
|
2
|
+
import { WebXRFeatureName, WebXRFeaturesManager } from "../webXRFeaturesManager.js";
|
|
3
|
+
import { Matrix, Quaternion, Vector3 } from "../../Maths/math.vector.js";
|
|
4
|
+
import { Observable } from "../../Misc/observable.js";
|
|
5
|
+
import { TransformNode } from "../../Meshes/transformNode.js";
|
|
6
|
+
import { Logger } from "../../Misc/logger.js";
|
|
7
|
+
/**
|
|
8
|
+
* All 83 body joint names as defined by the WebXR Body Tracking specification.
|
|
9
|
+
* @see https://immersive-web.github.io/body-tracking/#xrbody-interface
|
|
10
|
+
*/
|
|
11
|
+
export var WebXRBodyJoint;
|
|
12
|
+
(function (WebXRBodyJoint) {
|
|
13
|
+
/** The center of the hips / pelvis */
|
|
14
|
+
WebXRBodyJoint["HIPS"] = "hips";
|
|
15
|
+
/** Lower spine (lumbar) */
|
|
16
|
+
WebXRBodyJoint["SPINE_LOWER"] = "spine-lower";
|
|
17
|
+
/** Middle spine (thoracic) */
|
|
18
|
+
WebXRBodyJoint["SPINE_MIDDLE"] = "spine-middle";
|
|
19
|
+
/** Upper spine */
|
|
20
|
+
WebXRBodyJoint["SPINE_UPPER"] = "spine-upper";
|
|
21
|
+
/** Chest */
|
|
22
|
+
WebXRBodyJoint["CHEST"] = "chest";
|
|
23
|
+
/** Neck */
|
|
24
|
+
WebXRBodyJoint["NECK"] = "neck";
|
|
25
|
+
/** Head */
|
|
26
|
+
WebXRBodyJoint["HEAD"] = "head";
|
|
27
|
+
// ── Left Arm ──────────────────────────────────────────────
|
|
28
|
+
/** Left shoulder */
|
|
29
|
+
WebXRBodyJoint["LEFT_SHOULDER"] = "left-shoulder";
|
|
30
|
+
/** Left scapula */
|
|
31
|
+
WebXRBodyJoint["LEFT_SCAPULA"] = "left-scapula";
|
|
32
|
+
/** Left upper arm */
|
|
33
|
+
WebXRBodyJoint["LEFT_ARM_UPPER"] = "left-arm-upper";
|
|
34
|
+
/** Left forearm (lower arm) */
|
|
35
|
+
WebXRBodyJoint["LEFT_ARM_LOWER"] = "left-arm-lower";
|
|
36
|
+
/** Left hand wrist twist (forearm twist) */
|
|
37
|
+
WebXRBodyJoint["LEFT_HAND_WRIST_TWIST"] = "left-hand-wrist-twist";
|
|
38
|
+
// ── Right Arm ─────────────────────────────────────────────
|
|
39
|
+
/** Right shoulder */
|
|
40
|
+
WebXRBodyJoint["RIGHT_SHOULDER"] = "right-shoulder";
|
|
41
|
+
/** Right scapula */
|
|
42
|
+
WebXRBodyJoint["RIGHT_SCAPULA"] = "right-scapula";
|
|
43
|
+
/** Right upper arm */
|
|
44
|
+
WebXRBodyJoint["RIGHT_ARM_UPPER"] = "right-arm-upper";
|
|
45
|
+
/** Right forearm (lower arm) */
|
|
46
|
+
WebXRBodyJoint["RIGHT_ARM_LOWER"] = "right-arm-lower";
|
|
47
|
+
/** Right hand wrist twist (forearm twist) */
|
|
48
|
+
WebXRBodyJoint["RIGHT_HAND_WRIST_TWIST"] = "right-hand-wrist-twist";
|
|
49
|
+
// ── Left Hand ─────────────────────────────────────────────
|
|
50
|
+
/** Left palm center */
|
|
51
|
+
WebXRBodyJoint["LEFT_HAND_PALM"] = "left-hand-palm";
|
|
52
|
+
/** Left wrist */
|
|
53
|
+
WebXRBodyJoint["LEFT_HAND_WRIST"] = "left-hand-wrist";
|
|
54
|
+
/** Left thumb metacarpal */
|
|
55
|
+
WebXRBodyJoint["LEFT_HAND_THUMB_METACARPAL"] = "left-hand-thumb-metacarpal";
|
|
56
|
+
/** Left thumb proximal phalanx */
|
|
57
|
+
WebXRBodyJoint["LEFT_HAND_THUMB_PHALANX_PROXIMAL"] = "left-hand-thumb-phalanx-proximal";
|
|
58
|
+
/** Left thumb distal phalanx */
|
|
59
|
+
WebXRBodyJoint["LEFT_HAND_THUMB_PHALANX_DISTAL"] = "left-hand-thumb-phalanx-distal";
|
|
60
|
+
/** Left thumb tip */
|
|
61
|
+
WebXRBodyJoint["LEFT_HAND_THUMB_TIP"] = "left-hand-thumb-tip";
|
|
62
|
+
/** Left index finger metacarpal */
|
|
63
|
+
WebXRBodyJoint["LEFT_HAND_INDEX_METACARPAL"] = "left-hand-index-metacarpal";
|
|
64
|
+
/** Left index finger proximal phalanx */
|
|
65
|
+
WebXRBodyJoint["LEFT_HAND_INDEX_PHALANX_PROXIMAL"] = "left-hand-index-phalanx-proximal";
|
|
66
|
+
/** Left index finger intermediate phalanx */
|
|
67
|
+
WebXRBodyJoint["LEFT_HAND_INDEX_PHALANX_INTERMEDIATE"] = "left-hand-index-phalanx-intermediate";
|
|
68
|
+
/** Left index finger distal phalanx */
|
|
69
|
+
WebXRBodyJoint["LEFT_HAND_INDEX_PHALANX_DISTAL"] = "left-hand-index-phalanx-distal";
|
|
70
|
+
/** Left index finger tip */
|
|
71
|
+
WebXRBodyJoint["LEFT_HAND_INDEX_TIP"] = "left-hand-index-tip";
|
|
72
|
+
/** Left middle finger metacarpal */
|
|
73
|
+
WebXRBodyJoint["LEFT_HAND_MIDDLE_METACARPAL"] = "left-hand-middle-metacarpal";
|
|
74
|
+
/** Left middle finger proximal phalanx */
|
|
75
|
+
WebXRBodyJoint["LEFT_HAND_MIDDLE_PHALANX_PROXIMAL"] = "left-hand-middle-phalanx-proximal";
|
|
76
|
+
/** Left middle finger intermediate phalanx */
|
|
77
|
+
WebXRBodyJoint["LEFT_HAND_MIDDLE_PHALANX_INTERMEDIATE"] = "left-hand-middle-phalanx-intermediate";
|
|
78
|
+
/** Left middle finger distal phalanx */
|
|
79
|
+
WebXRBodyJoint["LEFT_HAND_MIDDLE_PHALANX_DISTAL"] = "left-hand-middle-phalanx-distal";
|
|
80
|
+
/** Left middle finger tip */
|
|
81
|
+
WebXRBodyJoint["LEFT_HAND_MIDDLE_TIP"] = "left-hand-middle-tip";
|
|
82
|
+
/** Left ring finger metacarpal */
|
|
83
|
+
WebXRBodyJoint["LEFT_HAND_RING_METACARPAL"] = "left-hand-ring-metacarpal";
|
|
84
|
+
/** Left ring finger proximal phalanx */
|
|
85
|
+
WebXRBodyJoint["LEFT_HAND_RING_PHALANX_PROXIMAL"] = "left-hand-ring-phalanx-proximal";
|
|
86
|
+
/** Left ring finger intermediate phalanx */
|
|
87
|
+
WebXRBodyJoint["LEFT_HAND_RING_PHALANX_INTERMEDIATE"] = "left-hand-ring-phalanx-intermediate";
|
|
88
|
+
/** Left ring finger distal phalanx */
|
|
89
|
+
WebXRBodyJoint["LEFT_HAND_RING_PHALANX_DISTAL"] = "left-hand-ring-phalanx-distal";
|
|
90
|
+
/** Left ring finger tip */
|
|
91
|
+
WebXRBodyJoint["LEFT_HAND_RING_TIP"] = "left-hand-ring-tip";
|
|
92
|
+
/** Left little finger metacarpal */
|
|
93
|
+
WebXRBodyJoint["LEFT_HAND_LITTLE_METACARPAL"] = "left-hand-little-metacarpal";
|
|
94
|
+
/** Left little finger proximal phalanx */
|
|
95
|
+
WebXRBodyJoint["LEFT_HAND_LITTLE_PHALANX_PROXIMAL"] = "left-hand-little-phalanx-proximal";
|
|
96
|
+
/** Left little finger intermediate phalanx */
|
|
97
|
+
WebXRBodyJoint["LEFT_HAND_LITTLE_PHALANX_INTERMEDIATE"] = "left-hand-little-phalanx-intermediate";
|
|
98
|
+
/** Left little finger distal phalanx */
|
|
99
|
+
WebXRBodyJoint["LEFT_HAND_LITTLE_PHALANX_DISTAL"] = "left-hand-little-phalanx-distal";
|
|
100
|
+
/** Left little finger tip */
|
|
101
|
+
WebXRBodyJoint["LEFT_HAND_LITTLE_TIP"] = "left-hand-little-tip";
|
|
102
|
+
// ── Right Hand ────────────────────────────────────────────
|
|
103
|
+
/** Right palm center */
|
|
104
|
+
WebXRBodyJoint["RIGHT_HAND_PALM"] = "right-hand-palm";
|
|
105
|
+
/** Right wrist */
|
|
106
|
+
WebXRBodyJoint["RIGHT_HAND_WRIST"] = "right-hand-wrist";
|
|
107
|
+
/** Right thumb metacarpal */
|
|
108
|
+
WebXRBodyJoint["RIGHT_HAND_THUMB_METACARPAL"] = "right-hand-thumb-metacarpal";
|
|
109
|
+
/** Right thumb proximal phalanx */
|
|
110
|
+
WebXRBodyJoint["RIGHT_HAND_THUMB_PHALANX_PROXIMAL"] = "right-hand-thumb-phalanx-proximal";
|
|
111
|
+
/** Right thumb distal phalanx */
|
|
112
|
+
WebXRBodyJoint["RIGHT_HAND_THUMB_PHALANX_DISTAL"] = "right-hand-thumb-phalanx-distal";
|
|
113
|
+
/** Right thumb tip */
|
|
114
|
+
WebXRBodyJoint["RIGHT_HAND_THUMB_TIP"] = "right-hand-thumb-tip";
|
|
115
|
+
/** Right index finger metacarpal */
|
|
116
|
+
WebXRBodyJoint["RIGHT_HAND_INDEX_METACARPAL"] = "right-hand-index-metacarpal";
|
|
117
|
+
/** Right index finger proximal phalanx */
|
|
118
|
+
WebXRBodyJoint["RIGHT_HAND_INDEX_PHALANX_PROXIMAL"] = "right-hand-index-phalanx-proximal";
|
|
119
|
+
/** Right index finger intermediate phalanx */
|
|
120
|
+
WebXRBodyJoint["RIGHT_HAND_INDEX_PHALANX_INTERMEDIATE"] = "right-hand-index-phalanx-intermediate";
|
|
121
|
+
/** Right index finger distal phalanx */
|
|
122
|
+
WebXRBodyJoint["RIGHT_HAND_INDEX_PHALANX_DISTAL"] = "right-hand-index-phalanx-distal";
|
|
123
|
+
/** Right index finger tip */
|
|
124
|
+
WebXRBodyJoint["RIGHT_HAND_INDEX_TIP"] = "right-hand-index-tip";
|
|
125
|
+
/** Right middle finger metacarpal */
|
|
126
|
+
WebXRBodyJoint["RIGHT_HAND_MIDDLE_METACARPAL"] = "right-hand-middle-metacarpal";
|
|
127
|
+
/** Right middle finger proximal phalanx */
|
|
128
|
+
WebXRBodyJoint["RIGHT_HAND_MIDDLE_PHALANX_PROXIMAL"] = "right-hand-middle-phalanx-proximal";
|
|
129
|
+
/** Right middle finger intermediate phalanx */
|
|
130
|
+
WebXRBodyJoint["RIGHT_HAND_MIDDLE_PHALANX_INTERMEDIATE"] = "right-hand-middle-phalanx-intermediate";
|
|
131
|
+
/** Right middle finger distal phalanx */
|
|
132
|
+
WebXRBodyJoint["RIGHT_HAND_MIDDLE_PHALANX_DISTAL"] = "right-hand-middle-phalanx-distal";
|
|
133
|
+
/** Right middle finger tip */
|
|
134
|
+
WebXRBodyJoint["RIGHT_HAND_MIDDLE_TIP"] = "right-hand-middle-tip";
|
|
135
|
+
/** Right ring finger metacarpal */
|
|
136
|
+
WebXRBodyJoint["RIGHT_HAND_RING_METACARPAL"] = "right-hand-ring-metacarpal";
|
|
137
|
+
/** Right ring finger proximal phalanx */
|
|
138
|
+
WebXRBodyJoint["RIGHT_HAND_RING_PHALANX_PROXIMAL"] = "right-hand-ring-phalanx-proximal";
|
|
139
|
+
/** Right ring finger intermediate phalanx */
|
|
140
|
+
WebXRBodyJoint["RIGHT_HAND_RING_PHALANX_INTERMEDIATE"] = "right-hand-ring-phalanx-intermediate";
|
|
141
|
+
/** Right ring finger distal phalanx */
|
|
142
|
+
WebXRBodyJoint["RIGHT_HAND_RING_PHALANX_DISTAL"] = "right-hand-ring-phalanx-distal";
|
|
143
|
+
/** Right ring finger tip */
|
|
144
|
+
WebXRBodyJoint["RIGHT_HAND_RING_TIP"] = "right-hand-ring-tip";
|
|
145
|
+
/** Right little finger metacarpal */
|
|
146
|
+
WebXRBodyJoint["RIGHT_HAND_LITTLE_METACARPAL"] = "right-hand-little-metacarpal";
|
|
147
|
+
/** Right little finger proximal phalanx */
|
|
148
|
+
WebXRBodyJoint["RIGHT_HAND_LITTLE_PHALANX_PROXIMAL"] = "right-hand-little-phalanx-proximal";
|
|
149
|
+
/** Right little finger intermediate phalanx */
|
|
150
|
+
WebXRBodyJoint["RIGHT_HAND_LITTLE_PHALANX_INTERMEDIATE"] = "right-hand-little-phalanx-intermediate";
|
|
151
|
+
/** Right little finger distal phalanx */
|
|
152
|
+
WebXRBodyJoint["RIGHT_HAND_LITTLE_PHALANX_DISTAL"] = "right-hand-little-phalanx-distal";
|
|
153
|
+
/** Right little finger tip */
|
|
154
|
+
WebXRBodyJoint["RIGHT_HAND_LITTLE_TIP"] = "right-hand-little-tip";
|
|
155
|
+
// ── Left Leg / Foot ───────────────────────────────────────
|
|
156
|
+
/** Left upper leg (thigh) */
|
|
157
|
+
WebXRBodyJoint["LEFT_UPPER_LEG"] = "left-upper-leg";
|
|
158
|
+
/** Left lower leg (shin) */
|
|
159
|
+
WebXRBodyJoint["LEFT_LOWER_LEG"] = "left-lower-leg";
|
|
160
|
+
/** Left foot ankle twist */
|
|
161
|
+
WebXRBodyJoint["LEFT_FOOT_ANKLE_TWIST"] = "left-foot-ankle-twist";
|
|
162
|
+
/** Left foot ankle */
|
|
163
|
+
WebXRBodyJoint["LEFT_FOOT_ANKLE"] = "left-foot-ankle";
|
|
164
|
+
/** Left foot subtalar */
|
|
165
|
+
WebXRBodyJoint["LEFT_FOOT_SUBTALAR"] = "left-foot-subtalar";
|
|
166
|
+
/** Left foot transverse */
|
|
167
|
+
WebXRBodyJoint["LEFT_FOOT_TRANSVERSE"] = "left-foot-transverse";
|
|
168
|
+
/** Left foot ball */
|
|
169
|
+
WebXRBodyJoint["LEFT_FOOT_BALL"] = "left-foot-ball";
|
|
170
|
+
// ── Right Leg / Foot ──────────────────────────────────────
|
|
171
|
+
/** Right upper leg (thigh) */
|
|
172
|
+
WebXRBodyJoint["RIGHT_UPPER_LEG"] = "right-upper-leg";
|
|
173
|
+
/** Right lower leg (shin) */
|
|
174
|
+
WebXRBodyJoint["RIGHT_LOWER_LEG"] = "right-lower-leg";
|
|
175
|
+
/** Right foot ankle twist */
|
|
176
|
+
WebXRBodyJoint["RIGHT_FOOT_ANKLE_TWIST"] = "right-foot-ankle-twist";
|
|
177
|
+
/** Right foot ankle */
|
|
178
|
+
WebXRBodyJoint["RIGHT_FOOT_ANKLE"] = "right-foot-ankle";
|
|
179
|
+
/** Right foot subtalar */
|
|
180
|
+
WebXRBodyJoint["RIGHT_FOOT_SUBTALAR"] = "right-foot-subtalar";
|
|
181
|
+
/** Right foot transverse */
|
|
182
|
+
WebXRBodyJoint["RIGHT_FOOT_TRANSVERSE"] = "right-foot-transverse";
|
|
183
|
+
/** Right foot ball */
|
|
184
|
+
WebXRBodyJoint["RIGHT_FOOT_BALL"] = "right-foot-ball";
|
|
185
|
+
})(WebXRBodyJoint || (WebXRBodyJoint = {}));
|
|
186
|
+
/**
|
|
187
|
+
* The ordered array of all 83 body joints, matching the iteration order defined
|
|
188
|
+
* by the WebXR Body Tracking specification.
|
|
189
|
+
* @see https://immersive-web.github.io/body-tracking/#xrbody-interface
|
|
190
|
+
*/
|
|
191
|
+
const BodyJointReferenceArray = [
|
|
192
|
+
"hips" /* WebXRBodyJoint.HIPS */,
|
|
193
|
+
"spine-lower" /* WebXRBodyJoint.SPINE_LOWER */,
|
|
194
|
+
"spine-middle" /* WebXRBodyJoint.SPINE_MIDDLE */,
|
|
195
|
+
"spine-upper" /* WebXRBodyJoint.SPINE_UPPER */,
|
|
196
|
+
"chest" /* WebXRBodyJoint.CHEST */,
|
|
197
|
+
"neck" /* WebXRBodyJoint.NECK */,
|
|
198
|
+
"head" /* WebXRBodyJoint.HEAD */,
|
|
199
|
+
"left-shoulder" /* WebXRBodyJoint.LEFT_SHOULDER */,
|
|
200
|
+
"left-scapula" /* WebXRBodyJoint.LEFT_SCAPULA */,
|
|
201
|
+
"left-arm-upper" /* WebXRBodyJoint.LEFT_ARM_UPPER */,
|
|
202
|
+
"left-arm-lower" /* WebXRBodyJoint.LEFT_ARM_LOWER */,
|
|
203
|
+
"left-hand-wrist-twist" /* WebXRBodyJoint.LEFT_HAND_WRIST_TWIST */,
|
|
204
|
+
"right-shoulder" /* WebXRBodyJoint.RIGHT_SHOULDER */,
|
|
205
|
+
"right-scapula" /* WebXRBodyJoint.RIGHT_SCAPULA */,
|
|
206
|
+
"right-arm-upper" /* WebXRBodyJoint.RIGHT_ARM_UPPER */,
|
|
207
|
+
"right-arm-lower" /* WebXRBodyJoint.RIGHT_ARM_LOWER */,
|
|
208
|
+
"right-hand-wrist-twist" /* WebXRBodyJoint.RIGHT_HAND_WRIST_TWIST */,
|
|
209
|
+
"left-hand-palm" /* WebXRBodyJoint.LEFT_HAND_PALM */,
|
|
210
|
+
"left-hand-wrist" /* WebXRBodyJoint.LEFT_HAND_WRIST */,
|
|
211
|
+
"left-hand-thumb-metacarpal" /* WebXRBodyJoint.LEFT_HAND_THUMB_METACARPAL */,
|
|
212
|
+
"left-hand-thumb-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_THUMB_PHALANX_PROXIMAL */,
|
|
213
|
+
"left-hand-thumb-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_THUMB_PHALANX_DISTAL */,
|
|
214
|
+
"left-hand-thumb-tip" /* WebXRBodyJoint.LEFT_HAND_THUMB_TIP */,
|
|
215
|
+
"left-hand-index-metacarpal" /* WebXRBodyJoint.LEFT_HAND_INDEX_METACARPAL */,
|
|
216
|
+
"left-hand-index-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_INDEX_PHALANX_PROXIMAL */,
|
|
217
|
+
"left-hand-index-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_INDEX_PHALANX_INTERMEDIATE */,
|
|
218
|
+
"left-hand-index-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_INDEX_PHALANX_DISTAL */,
|
|
219
|
+
"left-hand-index-tip" /* WebXRBodyJoint.LEFT_HAND_INDEX_TIP */,
|
|
220
|
+
"left-hand-middle-metacarpal" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_METACARPAL */,
|
|
221
|
+
"left-hand-middle-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_PHALANX_PROXIMAL */,
|
|
222
|
+
"left-hand-middle-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_PHALANX_INTERMEDIATE */,
|
|
223
|
+
"left-hand-middle-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_PHALANX_DISTAL */,
|
|
224
|
+
"left-hand-middle-tip" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_TIP */,
|
|
225
|
+
"left-hand-ring-metacarpal" /* WebXRBodyJoint.LEFT_HAND_RING_METACARPAL */,
|
|
226
|
+
"left-hand-ring-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_RING_PHALANX_PROXIMAL */,
|
|
227
|
+
"left-hand-ring-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_RING_PHALANX_INTERMEDIATE */,
|
|
228
|
+
"left-hand-ring-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_RING_PHALANX_DISTAL */,
|
|
229
|
+
"left-hand-ring-tip" /* WebXRBodyJoint.LEFT_HAND_RING_TIP */,
|
|
230
|
+
"left-hand-little-metacarpal" /* WebXRBodyJoint.LEFT_HAND_LITTLE_METACARPAL */,
|
|
231
|
+
"left-hand-little-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_LITTLE_PHALANX_PROXIMAL */,
|
|
232
|
+
"left-hand-little-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_LITTLE_PHALANX_INTERMEDIATE */,
|
|
233
|
+
"left-hand-little-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_LITTLE_PHALANX_DISTAL */,
|
|
234
|
+
"left-hand-little-tip" /* WebXRBodyJoint.LEFT_HAND_LITTLE_TIP */,
|
|
235
|
+
"right-hand-palm" /* WebXRBodyJoint.RIGHT_HAND_PALM */,
|
|
236
|
+
"right-hand-wrist" /* WebXRBodyJoint.RIGHT_HAND_WRIST */,
|
|
237
|
+
"right-hand-thumb-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_THUMB_METACARPAL */,
|
|
238
|
+
"right-hand-thumb-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_THUMB_PHALANX_PROXIMAL */,
|
|
239
|
+
"right-hand-thumb-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_THUMB_PHALANX_DISTAL */,
|
|
240
|
+
"right-hand-thumb-tip" /* WebXRBodyJoint.RIGHT_HAND_THUMB_TIP */,
|
|
241
|
+
"right-hand-index-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_INDEX_METACARPAL */,
|
|
242
|
+
"right-hand-index-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_INDEX_PHALANX_PROXIMAL */,
|
|
243
|
+
"right-hand-index-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_INDEX_PHALANX_INTERMEDIATE */,
|
|
244
|
+
"right-hand-index-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_INDEX_PHALANX_DISTAL */,
|
|
245
|
+
"right-hand-index-tip" /* WebXRBodyJoint.RIGHT_HAND_INDEX_TIP */,
|
|
246
|
+
"right-hand-middle-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_METACARPAL */,
|
|
247
|
+
"right-hand-middle-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_PHALANX_PROXIMAL */,
|
|
248
|
+
"right-hand-middle-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_PHALANX_INTERMEDIATE */,
|
|
249
|
+
"right-hand-middle-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_PHALANX_DISTAL */,
|
|
250
|
+
"right-hand-middle-tip" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_TIP */,
|
|
251
|
+
"right-hand-ring-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_RING_METACARPAL */,
|
|
252
|
+
"right-hand-ring-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_RING_PHALANX_PROXIMAL */,
|
|
253
|
+
"right-hand-ring-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_RING_PHALANX_INTERMEDIATE */,
|
|
254
|
+
"right-hand-ring-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_RING_PHALANX_DISTAL */,
|
|
255
|
+
"right-hand-ring-tip" /* WebXRBodyJoint.RIGHT_HAND_RING_TIP */,
|
|
256
|
+
"right-hand-little-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_METACARPAL */,
|
|
257
|
+
"right-hand-little-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_PHALANX_PROXIMAL */,
|
|
258
|
+
"right-hand-little-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_PHALANX_INTERMEDIATE */,
|
|
259
|
+
"right-hand-little-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_PHALANX_DISTAL */,
|
|
260
|
+
"right-hand-little-tip" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_TIP */,
|
|
261
|
+
"left-upper-leg" /* WebXRBodyJoint.LEFT_UPPER_LEG */,
|
|
262
|
+
"left-lower-leg" /* WebXRBodyJoint.LEFT_LOWER_LEG */,
|
|
263
|
+
"left-foot-ankle-twist" /* WebXRBodyJoint.LEFT_FOOT_ANKLE_TWIST */,
|
|
264
|
+
"left-foot-ankle" /* WebXRBodyJoint.LEFT_FOOT_ANKLE */,
|
|
265
|
+
"left-foot-subtalar" /* WebXRBodyJoint.LEFT_FOOT_SUBTALAR */,
|
|
266
|
+
"left-foot-transverse" /* WebXRBodyJoint.LEFT_FOOT_TRANSVERSE */,
|
|
267
|
+
"left-foot-ball" /* WebXRBodyJoint.LEFT_FOOT_BALL */,
|
|
268
|
+
"right-upper-leg" /* WebXRBodyJoint.RIGHT_UPPER_LEG */,
|
|
269
|
+
"right-lower-leg" /* WebXRBodyJoint.RIGHT_LOWER_LEG */,
|
|
270
|
+
"right-foot-ankle-twist" /* WebXRBodyJoint.RIGHT_FOOT_ANKLE_TWIST */,
|
|
271
|
+
"right-foot-ankle" /* WebXRBodyJoint.RIGHT_FOOT_ANKLE */,
|
|
272
|
+
"right-foot-subtalar" /* WebXRBodyJoint.RIGHT_FOOT_SUBTALAR */,
|
|
273
|
+
"right-foot-transverse" /* WebXRBodyJoint.RIGHT_FOOT_TRANSVERSE */,
|
|
274
|
+
"right-foot-ball" /* WebXRBodyJoint.RIGHT_FOOT_BALL */,
|
|
275
|
+
];
|
|
276
|
+
/**
|
|
277
|
+
* Reverse lookup: {@link WebXRBodyJoint} → index in {@link BodyJointReferenceArray}.
|
|
278
|
+
* Used for O(1) name-based access where previously `indexOf` was called in hot paths.
|
|
279
|
+
*/
|
|
280
|
+
const BodyJointNameToIndex = new Map(BodyJointReferenceArray.map((j, i) => [j, i]));
|
|
281
|
+
/**
|
|
282
|
+
* The total number of joints in the body tracking spec.
|
|
283
|
+
* The XRBody size attribute MUST return this value.
|
|
284
|
+
*/
|
|
285
|
+
const BODY_JOINT_COUNT = 83;
|
|
286
|
+
/**
|
|
287
|
+
* Parent index for each joint in {@link BodyJointReferenceArray} order.
|
|
288
|
+
* -1 means "root" (no parent). Used to convert world-space XR poses to
|
|
289
|
+
* local-space transforms suitable for skeleton bones.
|
|
290
|
+
*
|
|
291
|
+
* Hierarchy follows the WebXR Body Tracking specification and standard
|
|
292
|
+
* humanoid anatomy.
|
|
293
|
+
*/
|
|
294
|
+
// prettier-ignore
|
|
295
|
+
export const BodyJointParentIndex = [
|
|
296
|
+
// 0: hips (root)
|
|
297
|
+
-1,
|
|
298
|
+
// 1: spine-lower → hips
|
|
299
|
+
0,
|
|
300
|
+
// 2: spine-middle → spine-lower
|
|
301
|
+
1,
|
|
302
|
+
// 3: spine-upper → spine-middle
|
|
303
|
+
2,
|
|
304
|
+
// 4: chest → spine-upper
|
|
305
|
+
3,
|
|
306
|
+
// 5: neck → chest
|
|
307
|
+
4,
|
|
308
|
+
// 6: head → neck
|
|
309
|
+
5,
|
|
310
|
+
// Left arm (7-11)
|
|
311
|
+
4, // 7: left-shoulder → chest
|
|
312
|
+
7, // 8: left-scapula → left-shoulder
|
|
313
|
+
8, // 9: left-arm-upper → left-scapula
|
|
314
|
+
9, // 10: left-arm-lower → left-arm-upper
|
|
315
|
+
10, // 11: left-hand-wrist-twist → left-arm-lower
|
|
316
|
+
// Right arm (12-16)
|
|
317
|
+
4, // 12: right-shoulder → chest
|
|
318
|
+
12, // 13: right-scapula → right-shoulder
|
|
319
|
+
13, // 14: right-arm-upper → right-scapula
|
|
320
|
+
14, // 15: right-arm-lower → right-arm-upper
|
|
321
|
+
15, // 16: right-hand-wrist-twist → right-arm-lower
|
|
322
|
+
// Left hand (17-42)
|
|
323
|
+
11, // 17: left-hand-palm → left-hand-wrist-twist
|
|
324
|
+
11, // 18: left-hand-wrist → left-hand-wrist-twist
|
|
325
|
+
18, // 19: left-hand-thumb-metacarpal → left-hand-wrist
|
|
326
|
+
19, // 20: left-hand-thumb-phalanx-proximal
|
|
327
|
+
20, // 21: left-hand-thumb-phalanx-distal
|
|
328
|
+
21, // 22: left-hand-thumb-tip
|
|
329
|
+
18, // 23: left-hand-index-metacarpal → left-hand-wrist
|
|
330
|
+
23, // 24: left-hand-index-phalanx-proximal
|
|
331
|
+
24, // 25: left-hand-index-phalanx-intermediate
|
|
332
|
+
25, // 26: left-hand-index-phalanx-distal
|
|
333
|
+
26, // 27: left-hand-index-tip
|
|
334
|
+
18, // 28: left-hand-middle-metacarpal → left-hand-wrist
|
|
335
|
+
28, // 29: left-hand-middle-phalanx-proximal
|
|
336
|
+
29, // 30: left-hand-middle-phalanx-intermediate
|
|
337
|
+
30, // 31: left-hand-middle-phalanx-distal
|
|
338
|
+
31, // 32: left-hand-middle-tip
|
|
339
|
+
18, // 33: left-hand-ring-metacarpal → left-hand-wrist
|
|
340
|
+
33, // 34: left-hand-ring-phalanx-proximal
|
|
341
|
+
34, // 35: left-hand-ring-phalanx-intermediate
|
|
342
|
+
35, // 36: left-hand-ring-phalanx-distal
|
|
343
|
+
36, // 37: left-hand-ring-tip
|
|
344
|
+
18, // 38: left-hand-little-metacarpal → left-hand-wrist
|
|
345
|
+
38, // 39: left-hand-little-phalanx-proximal
|
|
346
|
+
39, // 40: left-hand-little-phalanx-intermediate
|
|
347
|
+
40, // 41: left-hand-little-phalanx-distal
|
|
348
|
+
41, // 42: left-hand-little-tip
|
|
349
|
+
// Right hand (43-68)
|
|
350
|
+
16, // 43: right-hand-palm → right-hand-wrist-twist
|
|
351
|
+
16, // 44: right-hand-wrist → right-hand-wrist-twist
|
|
352
|
+
44, // 45: right-hand-thumb-metacarpal → right-hand-wrist
|
|
353
|
+
45, // 46: right-hand-thumb-phalanx-proximal
|
|
354
|
+
46, // 47: right-hand-thumb-phalanx-distal
|
|
355
|
+
47, // 48: right-hand-thumb-tip
|
|
356
|
+
44, // 49: right-hand-index-metacarpal → right-hand-wrist
|
|
357
|
+
49, // 50: right-hand-index-phalanx-proximal
|
|
358
|
+
50, // 51: right-hand-index-phalanx-intermediate
|
|
359
|
+
51, // 52: right-hand-index-phalanx-distal
|
|
360
|
+
52, // 53: right-hand-index-tip
|
|
361
|
+
44, // 54: right-hand-middle-metacarpal → right-hand-wrist
|
|
362
|
+
54, // 55: right-hand-middle-phalanx-proximal
|
|
363
|
+
55, // 56: right-hand-middle-phalanx-intermediate
|
|
364
|
+
56, // 57: right-hand-middle-phalanx-distal
|
|
365
|
+
57, // 58: right-hand-middle-tip
|
|
366
|
+
44, // 59: right-hand-ring-metacarpal → right-hand-wrist
|
|
367
|
+
59, // 60: right-hand-ring-phalanx-proximal
|
|
368
|
+
60, // 61: right-hand-ring-phalanx-intermediate
|
|
369
|
+
61, // 62: right-hand-ring-phalanx-distal
|
|
370
|
+
62, // 63: right-hand-ring-tip
|
|
371
|
+
44, // 64: right-hand-little-metacarpal → right-hand-wrist
|
|
372
|
+
64, // 65: right-hand-little-phalanx-proximal
|
|
373
|
+
65, // 66: right-hand-little-phalanx-intermediate
|
|
374
|
+
66, // 67: right-hand-little-phalanx-distal
|
|
375
|
+
67, // 68: right-hand-little-tip
|
|
376
|
+
// Left leg / foot (69-75)
|
|
377
|
+
0, // 69: left-upper-leg → hips
|
|
378
|
+
69, // 70: left-lower-leg → left-upper-leg
|
|
379
|
+
70, // 71: left-foot-ankle-twist → left-lower-leg
|
|
380
|
+
71, // 72: left-foot-ankle → left-foot-ankle-twist
|
|
381
|
+
72, // 73: left-foot-subtalar → left-foot-ankle
|
|
382
|
+
73, // 74: left-foot-transverse → left-foot-subtalar
|
|
383
|
+
74, // 75: left-foot-ball → left-foot-transverse
|
|
384
|
+
// Right leg / foot (76-82)
|
|
385
|
+
0, // 76: right-upper-leg → hips
|
|
386
|
+
76, // 77: right-lower-leg → right-upper-leg
|
|
387
|
+
77, // 78: right-foot-ankle-twist → right-lower-leg
|
|
388
|
+
78, // 79: right-foot-ankle → right-foot-ankle-twist
|
|
389
|
+
79, // 80: right-foot-subtalar → right-foot-ankle
|
|
390
|
+
80, // 81: right-foot-transverse → right-foot-subtalar
|
|
391
|
+
81, // 82: right-foot-ball → right-foot-transverse
|
|
392
|
+
];
|
|
393
|
+
/**
|
|
394
|
+
* Logical body parts for convenient grouping of joints.
|
|
395
|
+
*/
|
|
396
|
+
export var BodyPart;
|
|
397
|
+
(function (BodyPart) {
|
|
398
|
+
/** Torso / spine (hips through head) */
|
|
399
|
+
BodyPart["TORSO"] = "torso";
|
|
400
|
+
/** Left arm (shoulder through wrist twist) */
|
|
401
|
+
BodyPart["LEFT_ARM"] = "left-arm";
|
|
402
|
+
/** Right arm (shoulder through wrist twist) */
|
|
403
|
+
BodyPart["RIGHT_ARM"] = "right-arm";
|
|
404
|
+
/** Left hand (palm through finger tips) */
|
|
405
|
+
BodyPart["LEFT_HAND"] = "left-hand";
|
|
406
|
+
/** Right hand (palm through finger tips) */
|
|
407
|
+
BodyPart["RIGHT_HAND"] = "right-hand";
|
|
408
|
+
/** Left leg (upper leg through foot ball) */
|
|
409
|
+
BodyPart["LEFT_LEG"] = "left-leg";
|
|
410
|
+
/** Right leg (upper leg through foot ball) */
|
|
411
|
+
BodyPart["RIGHT_LEG"] = "right-leg";
|
|
412
|
+
})(BodyPart || (BodyPart = {}));
|
|
413
|
+
/**
|
|
414
|
+
* Which body joints belong to each body part.
|
|
415
|
+
*/
|
|
416
|
+
const BodyPartsDefinition = {
|
|
417
|
+
["torso" /* BodyPart.TORSO */]: [
|
|
418
|
+
"hips" /* WebXRBodyJoint.HIPS */,
|
|
419
|
+
"spine-lower" /* WebXRBodyJoint.SPINE_LOWER */,
|
|
420
|
+
"spine-middle" /* WebXRBodyJoint.SPINE_MIDDLE */,
|
|
421
|
+
"spine-upper" /* WebXRBodyJoint.SPINE_UPPER */,
|
|
422
|
+
"chest" /* WebXRBodyJoint.CHEST */,
|
|
423
|
+
"neck" /* WebXRBodyJoint.NECK */,
|
|
424
|
+
"head" /* WebXRBodyJoint.HEAD */,
|
|
425
|
+
],
|
|
426
|
+
["left-arm" /* BodyPart.LEFT_ARM */]: [
|
|
427
|
+
"left-shoulder" /* WebXRBodyJoint.LEFT_SHOULDER */,
|
|
428
|
+
"left-scapula" /* WebXRBodyJoint.LEFT_SCAPULA */,
|
|
429
|
+
"left-arm-upper" /* WebXRBodyJoint.LEFT_ARM_UPPER */,
|
|
430
|
+
"left-arm-lower" /* WebXRBodyJoint.LEFT_ARM_LOWER */,
|
|
431
|
+
"left-hand-wrist-twist" /* WebXRBodyJoint.LEFT_HAND_WRIST_TWIST */,
|
|
432
|
+
],
|
|
433
|
+
["right-arm" /* BodyPart.RIGHT_ARM */]: [
|
|
434
|
+
"right-shoulder" /* WebXRBodyJoint.RIGHT_SHOULDER */,
|
|
435
|
+
"right-scapula" /* WebXRBodyJoint.RIGHT_SCAPULA */,
|
|
436
|
+
"right-arm-upper" /* WebXRBodyJoint.RIGHT_ARM_UPPER */,
|
|
437
|
+
"right-arm-lower" /* WebXRBodyJoint.RIGHT_ARM_LOWER */,
|
|
438
|
+
"right-hand-wrist-twist" /* WebXRBodyJoint.RIGHT_HAND_WRIST_TWIST */,
|
|
439
|
+
],
|
|
440
|
+
["left-hand" /* BodyPart.LEFT_HAND */]: [
|
|
441
|
+
"left-hand-palm" /* WebXRBodyJoint.LEFT_HAND_PALM */,
|
|
442
|
+
"left-hand-wrist" /* WebXRBodyJoint.LEFT_HAND_WRIST */,
|
|
443
|
+
"left-hand-thumb-metacarpal" /* WebXRBodyJoint.LEFT_HAND_THUMB_METACARPAL */,
|
|
444
|
+
"left-hand-thumb-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_THUMB_PHALANX_PROXIMAL */,
|
|
445
|
+
"left-hand-thumb-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_THUMB_PHALANX_DISTAL */,
|
|
446
|
+
"left-hand-thumb-tip" /* WebXRBodyJoint.LEFT_HAND_THUMB_TIP */,
|
|
447
|
+
"left-hand-index-metacarpal" /* WebXRBodyJoint.LEFT_HAND_INDEX_METACARPAL */,
|
|
448
|
+
"left-hand-index-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_INDEX_PHALANX_PROXIMAL */,
|
|
449
|
+
"left-hand-index-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_INDEX_PHALANX_INTERMEDIATE */,
|
|
450
|
+
"left-hand-index-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_INDEX_PHALANX_DISTAL */,
|
|
451
|
+
"left-hand-index-tip" /* WebXRBodyJoint.LEFT_HAND_INDEX_TIP */,
|
|
452
|
+
"left-hand-middle-metacarpal" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_METACARPAL */,
|
|
453
|
+
"left-hand-middle-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_PHALANX_PROXIMAL */,
|
|
454
|
+
"left-hand-middle-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_PHALANX_INTERMEDIATE */,
|
|
455
|
+
"left-hand-middle-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_PHALANX_DISTAL */,
|
|
456
|
+
"left-hand-middle-tip" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_TIP */,
|
|
457
|
+
"left-hand-ring-metacarpal" /* WebXRBodyJoint.LEFT_HAND_RING_METACARPAL */,
|
|
458
|
+
"left-hand-ring-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_RING_PHALANX_PROXIMAL */,
|
|
459
|
+
"left-hand-ring-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_RING_PHALANX_INTERMEDIATE */,
|
|
460
|
+
"left-hand-ring-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_RING_PHALANX_DISTAL */,
|
|
461
|
+
"left-hand-ring-tip" /* WebXRBodyJoint.LEFT_HAND_RING_TIP */,
|
|
462
|
+
"left-hand-little-metacarpal" /* WebXRBodyJoint.LEFT_HAND_LITTLE_METACARPAL */,
|
|
463
|
+
"left-hand-little-phalanx-proximal" /* WebXRBodyJoint.LEFT_HAND_LITTLE_PHALANX_PROXIMAL */,
|
|
464
|
+
"left-hand-little-phalanx-intermediate" /* WebXRBodyJoint.LEFT_HAND_LITTLE_PHALANX_INTERMEDIATE */,
|
|
465
|
+
"left-hand-little-phalanx-distal" /* WebXRBodyJoint.LEFT_HAND_LITTLE_PHALANX_DISTAL */,
|
|
466
|
+
"left-hand-little-tip" /* WebXRBodyJoint.LEFT_HAND_LITTLE_TIP */,
|
|
467
|
+
],
|
|
468
|
+
["right-hand" /* BodyPart.RIGHT_HAND */]: [
|
|
469
|
+
"right-hand-palm" /* WebXRBodyJoint.RIGHT_HAND_PALM */,
|
|
470
|
+
"right-hand-wrist" /* WebXRBodyJoint.RIGHT_HAND_WRIST */,
|
|
471
|
+
"right-hand-thumb-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_THUMB_METACARPAL */,
|
|
472
|
+
"right-hand-thumb-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_THUMB_PHALANX_PROXIMAL */,
|
|
473
|
+
"right-hand-thumb-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_THUMB_PHALANX_DISTAL */,
|
|
474
|
+
"right-hand-thumb-tip" /* WebXRBodyJoint.RIGHT_HAND_THUMB_TIP */,
|
|
475
|
+
"right-hand-index-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_INDEX_METACARPAL */,
|
|
476
|
+
"right-hand-index-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_INDEX_PHALANX_PROXIMAL */,
|
|
477
|
+
"right-hand-index-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_INDEX_PHALANX_INTERMEDIATE */,
|
|
478
|
+
"right-hand-index-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_INDEX_PHALANX_DISTAL */,
|
|
479
|
+
"right-hand-index-tip" /* WebXRBodyJoint.RIGHT_HAND_INDEX_TIP */,
|
|
480
|
+
"right-hand-middle-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_METACARPAL */,
|
|
481
|
+
"right-hand-middle-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_PHALANX_PROXIMAL */,
|
|
482
|
+
"right-hand-middle-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_PHALANX_INTERMEDIATE */,
|
|
483
|
+
"right-hand-middle-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_PHALANX_DISTAL */,
|
|
484
|
+
"right-hand-middle-tip" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_TIP */,
|
|
485
|
+
"right-hand-ring-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_RING_METACARPAL */,
|
|
486
|
+
"right-hand-ring-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_RING_PHALANX_PROXIMAL */,
|
|
487
|
+
"right-hand-ring-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_RING_PHALANX_INTERMEDIATE */,
|
|
488
|
+
"right-hand-ring-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_RING_PHALANX_DISTAL */,
|
|
489
|
+
"right-hand-ring-tip" /* WebXRBodyJoint.RIGHT_HAND_RING_TIP */,
|
|
490
|
+
"right-hand-little-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_METACARPAL */,
|
|
491
|
+
"right-hand-little-phalanx-proximal" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_PHALANX_PROXIMAL */,
|
|
492
|
+
"right-hand-little-phalanx-intermediate" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_PHALANX_INTERMEDIATE */,
|
|
493
|
+
"right-hand-little-phalanx-distal" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_PHALANX_DISTAL */,
|
|
494
|
+
"right-hand-little-tip" /* WebXRBodyJoint.RIGHT_HAND_LITTLE_TIP */,
|
|
495
|
+
],
|
|
496
|
+
["left-leg" /* BodyPart.LEFT_LEG */]: [
|
|
497
|
+
"left-upper-leg" /* WebXRBodyJoint.LEFT_UPPER_LEG */,
|
|
498
|
+
"left-lower-leg" /* WebXRBodyJoint.LEFT_LOWER_LEG */,
|
|
499
|
+
"left-foot-ankle-twist" /* WebXRBodyJoint.LEFT_FOOT_ANKLE_TWIST */,
|
|
500
|
+
"left-foot-ankle" /* WebXRBodyJoint.LEFT_FOOT_ANKLE */,
|
|
501
|
+
"left-foot-subtalar" /* WebXRBodyJoint.LEFT_FOOT_SUBTALAR */,
|
|
502
|
+
"left-foot-transverse" /* WebXRBodyJoint.LEFT_FOOT_TRANSVERSE */,
|
|
503
|
+
"left-foot-ball" /* WebXRBodyJoint.LEFT_FOOT_BALL */,
|
|
504
|
+
],
|
|
505
|
+
["right-leg" /* BodyPart.RIGHT_LEG */]: [
|
|
506
|
+
"right-upper-leg" /* WebXRBodyJoint.RIGHT_UPPER_LEG */,
|
|
507
|
+
"right-lower-leg" /* WebXRBodyJoint.RIGHT_LOWER_LEG */,
|
|
508
|
+
"right-foot-ankle-twist" /* WebXRBodyJoint.RIGHT_FOOT_ANKLE_TWIST */,
|
|
509
|
+
"right-foot-ankle" /* WebXRBodyJoint.RIGHT_FOOT_ANKLE */,
|
|
510
|
+
"right-foot-subtalar" /* WebXRBodyJoint.RIGHT_FOOT_SUBTALAR */,
|
|
511
|
+
"right-foot-transverse" /* WebXRBodyJoint.RIGHT_FOOT_TRANSVERSE */,
|
|
512
|
+
"right-foot-ball" /* WebXRBodyJoint.RIGHT_FOOT_BALL */,
|
|
513
|
+
],
|
|
514
|
+
};
|
|
515
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
516
|
+
// Built-in rig mappings
|
|
517
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
518
|
+
/**
|
|
519
|
+
* Default rig mapping for Mixamo-rigged humanoid characters.
|
|
520
|
+
*
|
|
521
|
+
* Maps each supported {@link WebXRBodyJoint} to the corresponding Mixamo bone
|
|
522
|
+
* name, **without** the `mixamorig:` prefix. When the feature applies this
|
|
523
|
+
* mapping, it auto-detects whether the skeleton uses the `mixamorig:` prefix
|
|
524
|
+
* and prepends it as needed, so the same table works for both prefixed and
|
|
525
|
+
* unprefixed exports.
|
|
526
|
+
*
|
|
527
|
+
* @example
|
|
528
|
+
* ```ts
|
|
529
|
+
* xr.featuresManager.enableFeature(WebXRFeatureName.BODY_TRACKING, "latest", {
|
|
530
|
+
* bodyMesh: myMixamoMesh,
|
|
531
|
+
* isMixamoModel: true,
|
|
532
|
+
* });
|
|
533
|
+
* ```
|
|
534
|
+
*
|
|
535
|
+
* Or, if you want to extend or customize it:
|
|
536
|
+
* ```ts
|
|
537
|
+
* import { MixamoRigMapping } from "@babylonjs/core";
|
|
538
|
+
* const rigMapping: XRBodyMeshRigMapping = { ...MixamoRigMapping, [WebXRBodyJoint.NECK]: "MyNeckBone" };
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
export const MixamoRigMapping = {
|
|
542
|
+
["hips" /* WebXRBodyJoint.HIPS */]: "Hips",
|
|
543
|
+
["spine-lower" /* WebXRBodyJoint.SPINE_LOWER */]: "Spine",
|
|
544
|
+
["spine-middle" /* WebXRBodyJoint.SPINE_MIDDLE */]: "Spine1",
|
|
545
|
+
["spine-upper" /* WebXRBodyJoint.SPINE_UPPER */]: "Spine2",
|
|
546
|
+
["neck" /* WebXRBodyJoint.NECK */]: "Neck",
|
|
547
|
+
["head" /* WebXRBodyJoint.HEAD */]: "Head",
|
|
548
|
+
["left-shoulder" /* WebXRBodyJoint.LEFT_SHOULDER */]: "LeftShoulder",
|
|
549
|
+
["left-arm-upper" /* WebXRBodyJoint.LEFT_ARM_UPPER */]: "LeftArm",
|
|
550
|
+
["left-arm-lower" /* WebXRBodyJoint.LEFT_ARM_LOWER */]: "LeftForeArm",
|
|
551
|
+
["left-hand-wrist" /* WebXRBodyJoint.LEFT_HAND_WRIST */]: "LeftHand",
|
|
552
|
+
["right-shoulder" /* WebXRBodyJoint.RIGHT_SHOULDER */]: "RightShoulder",
|
|
553
|
+
["right-arm-upper" /* WebXRBodyJoint.RIGHT_ARM_UPPER */]: "RightArm",
|
|
554
|
+
["right-arm-lower" /* WebXRBodyJoint.RIGHT_ARM_LOWER */]: "RightForeArm",
|
|
555
|
+
["right-hand-wrist" /* WebXRBodyJoint.RIGHT_HAND_WRIST */]: "RightHand",
|
|
556
|
+
["left-upper-leg" /* WebXRBodyJoint.LEFT_UPPER_LEG */]: "LeftUpLeg",
|
|
557
|
+
["left-lower-leg" /* WebXRBodyJoint.LEFT_LOWER_LEG */]: "LeftLeg",
|
|
558
|
+
["left-foot-ankle" /* WebXRBodyJoint.LEFT_FOOT_ANKLE */]: "LeftFoot",
|
|
559
|
+
["left-foot-ball" /* WebXRBodyJoint.LEFT_FOOT_BALL */]: "LeftToeBase",
|
|
560
|
+
["right-upper-leg" /* WebXRBodyJoint.RIGHT_UPPER_LEG */]: "RightUpLeg",
|
|
561
|
+
["right-lower-leg" /* WebXRBodyJoint.RIGHT_LOWER_LEG */]: "RightLeg",
|
|
562
|
+
["right-foot-ankle" /* WebXRBodyJoint.RIGHT_FOOT_ANKLE */]: "RightFoot",
|
|
563
|
+
["right-foot-ball" /* WebXRBodyJoint.RIGHT_FOOT_BALL */]: "RightToeBase",
|
|
564
|
+
};
|
|
565
|
+
/**
|
|
566
|
+
* Default aim-child overrides for Mixamo-rigged humanoids.
|
|
567
|
+
*
|
|
568
|
+
* Redirects the short / noisy XR spine segments to longer, stable ones so that
|
|
569
|
+
* {@link IWebXRBodyTrackingOptions.useBoneOrientationOffsets} produces clean
|
|
570
|
+
* torso rotations. In WebXR data, `hips`→`spine-lower` is typically only ~1 cm
|
|
571
|
+
* apart — too short to give a stable aim direction — so we reroute Mixamo's
|
|
572
|
+
* Hips/Spine/Spine1 bones to aim at `spine-upper` / `neck` instead.
|
|
573
|
+
*/
|
|
574
|
+
export const MixamoAimChildOverrides = {
|
|
575
|
+
["hips" /* WebXRBodyJoint.HIPS */]: "spine-upper" /* WebXRBodyJoint.SPINE_UPPER */,
|
|
576
|
+
["spine-lower" /* WebXRBodyJoint.SPINE_LOWER */]: "neck" /* WebXRBodyJoint.NECK */,
|
|
577
|
+
["spine-middle" /* WebXRBodyJoint.SPINE_MIDDLE */]: "neck" /* WebXRBodyJoint.NECK */,
|
|
578
|
+
// Hands have no mapped finger descendants on a typical Mixamo rig. Aim
|
|
579
|
+
// them at the tracked Middle-finger metacarpal joint to give the wrist
|
|
580
|
+
// a real orientation reference instead of relying on Meta's raw wrist
|
|
581
|
+
// rotation (which tends to flop around when the cameras can't resolve
|
|
582
|
+
// the hand pose).
|
|
583
|
+
["left-hand-wrist" /* WebXRBodyJoint.LEFT_HAND_WRIST */]: "left-hand-middle-metacarpal" /* WebXRBodyJoint.LEFT_HAND_MIDDLE_METACARPAL */,
|
|
584
|
+
["right-hand-wrist" /* WebXRBodyJoint.RIGHT_HAND_WRIST */]: "right-hand-middle-metacarpal" /* WebXRBodyJoint.RIGHT_HAND_MIDDLE_METACARPAL */,
|
|
585
|
+
};
|
|
586
|
+
/**
|
|
587
|
+
* Resolve the Mixamo rig mapping for a given body mesh, auto-detecting the
|
|
588
|
+
* `mixamorig:` bone-name prefix. Falls back to the unprefixed names.
|
|
589
|
+
* @param bodyMesh The rigged Mixamo body mesh.
|
|
590
|
+
* @returns An {@link XRBodyMeshRigMapping} whose bone names include the
|
|
591
|
+
* detected prefix (if any).
|
|
592
|
+
* @internal
|
|
593
|
+
*/
|
|
594
|
+
export function _ResolveMixamoRigMapping(bodyMesh) {
|
|
595
|
+
let skeleton = bodyMesh.skeleton;
|
|
596
|
+
if (!skeleton) {
|
|
597
|
+
for (const child of bodyMesh.getChildMeshes()) {
|
|
598
|
+
if (child.skeleton) {
|
|
599
|
+
skeleton = child.skeleton;
|
|
600
|
+
break;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
let prefix = "";
|
|
605
|
+
if (skeleton) {
|
|
606
|
+
// Look for the Hips bone to determine whether bones use the mixamorig: prefix.
|
|
607
|
+
if (skeleton.getBoneIndexByName("mixamorig:Hips") !== -1) {
|
|
608
|
+
prefix = "mixamorig:";
|
|
609
|
+
}
|
|
610
|
+
else if (skeleton.getBoneIndexByName("Hips") === -1) {
|
|
611
|
+
// Neither prefixed nor unprefixed Hips was found — try to auto-detect
|
|
612
|
+
// the prefix by scanning bone names.
|
|
613
|
+
for (const b of skeleton.bones) {
|
|
614
|
+
const match = /^(.*?)Hips$/.exec(b.name);
|
|
615
|
+
if (match) {
|
|
616
|
+
prefix = match[1];
|
|
617
|
+
break;
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (!prefix) {
|
|
623
|
+
return MixamoRigMapping;
|
|
624
|
+
}
|
|
625
|
+
const resolved = {};
|
|
626
|
+
for (const key of Object.keys(MixamoRigMapping)) {
|
|
627
|
+
resolved[key] = prefix + MixamoRigMapping[key];
|
|
628
|
+
}
|
|
629
|
+
return resolved;
|
|
630
|
+
}
|
|
631
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
632
|
+
// WebXRTrackedBody — runtime representation of a tracked body
|
|
633
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
634
|
+
/**
|
|
635
|
+
* Represents a tracked body during a WebXR session.
|
|
636
|
+
*
|
|
637
|
+
* This class manages the bridge between the WebXR body pose data and the
|
|
638
|
+
* Babylon.js scene graph. It creates a set of {@link TransformNode}s — one per
|
|
639
|
+
* body joint — whose transforms are updated every frame from the XR runtime.
|
|
640
|
+
* When a rigged body mesh is attached, its skeleton bones are linked to these
|
|
641
|
+
* transform nodes, causing the mesh to follow the user's body automatically.
|
|
642
|
+
*
|
|
643
|
+
* Coordinate-system handling:
|
|
644
|
+
* - WebXR delivers poses in a right-handed system.
|
|
645
|
+
* - By default, Babylon.js uses a left-handed system.
|
|
646
|
+
* - The class converts the data in-place (negating the Z components of every
|
|
647
|
+
* 4 × 4 joint matrix) before decomposing into Babylon transforms.
|
|
648
|
+
* - If the mesh was authored in a right-handed tool (the common case for glTF),
|
|
649
|
+
* the bone transforms are un-flipped so the skeleton interprets them correctly.
|
|
650
|
+
*/
|
|
651
|
+
export class WebXRTrackedBody {
|
|
652
|
+
/**
|
|
653
|
+
* Get the current body mesh (if any).
|
|
654
|
+
*/
|
|
655
|
+
get bodyMesh() {
|
|
656
|
+
return this._bodyMesh;
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Get or set the scale factor for local joint offsets.
|
|
660
|
+
* @see {@link IWebXRBodyTrackingOptions.jointScaleFactor}
|
|
661
|
+
*/
|
|
662
|
+
get jointScaleFactor() {
|
|
663
|
+
return this._jointScaleFactor;
|
|
664
|
+
}
|
|
665
|
+
set jointScaleFactor(value) {
|
|
666
|
+
this._jointScaleFactor = value;
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Returns the array of transform nodes representing each body joint.
|
|
670
|
+
* The order matches {@link WebXRBodyTracking.AllBodyJoints}; use
|
|
671
|
+
* {@link getJointTransform} or {@link getBodyPartTransforms} for
|
|
672
|
+
* name-based lookup.
|
|
673
|
+
*
|
|
674
|
+
* Note: when a body mesh is attached, these transform nodes are also
|
|
675
|
+
* used as the skeleton's link targets for mapped joints. In that case
|
|
676
|
+
* the values held by mapped-joint nodes are skeleton-local (parent bone's
|
|
677
|
+
* frame), not XR world-space. Unmapped-joint nodes always hold world-space
|
|
678
|
+
* pose. If you need world-space poses for every joint regardless of
|
|
679
|
+
* mapping, sample the bone matrices directly via the attached skeleton.
|
|
680
|
+
*/
|
|
681
|
+
get jointTransforms() {
|
|
682
|
+
return this._jointTransforms;
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Get the transform node for a specific body joint.
|
|
686
|
+
* @param jointName The name of the body joint (from {@link WebXRBodyJoint}).
|
|
687
|
+
* @returns The transform node corresponding to that joint, or `undefined` if not found.
|
|
688
|
+
*/
|
|
689
|
+
getJointTransform(jointName) {
|
|
690
|
+
const idx = BodyJointNameToIndex.get(jointName);
|
|
691
|
+
return idx !== undefined ? this._jointTransforms[idx] : undefined;
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Get all joint transform nodes that belong to a given body part.
|
|
695
|
+
* @param part The body part to query.
|
|
696
|
+
* @param result Optional pre-allocated array to fill (avoids per-call allocation).
|
|
697
|
+
* The array is cleared and populated with the results.
|
|
698
|
+
* @returns An array of TransformNodes for that body part.
|
|
699
|
+
*/
|
|
700
|
+
getBodyPartTransforms(part, result) {
|
|
701
|
+
const joints = BodyPartsDefinition[part];
|
|
702
|
+
if (!result) {
|
|
703
|
+
result = new Array(joints.length);
|
|
704
|
+
}
|
|
705
|
+
else {
|
|
706
|
+
result.length = joints.length;
|
|
707
|
+
}
|
|
708
|
+
for (let i = 0; i < joints.length; i++) {
|
|
709
|
+
result[i] = this._jointTransforms[BodyJointNameToIndex.get(joints[i])];
|
|
710
|
+
}
|
|
711
|
+
return result;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Construct a new tracked body instance.
|
|
715
|
+
* @param scene The Babylon.js scene.
|
|
716
|
+
* @param bodyMesh Optional rigged body mesh to attach immediately.
|
|
717
|
+
* @param rigMapping Optional mapping from WebXR joint names to skeleton bone names.
|
|
718
|
+
* @param jointScaleFactor Scale factor for local joint offsets (default 1.0).
|
|
719
|
+
* @param preserveBindPoseBonePositions Whether mapped bones should keep bind-pose local translations.
|
|
720
|
+
* @param useBoneOrientationOffsets Whether mapped bones should correct XR joint rotations using bind-space offsets.
|
|
721
|
+
* @param aimChildOverrides Per–XR-joint override for the aim child used with `useBoneOrientationOffsets`.
|
|
722
|
+
* @param jointLocalRotationOffset Optional rotation re-basing each XR joint's local frame (e.g. Z-along-bone → Y-along-bone).
|
|
723
|
+
*/
|
|
724
|
+
constructor(scene, bodyMesh, rigMapping, jointScaleFactor = 1.0, preserveBindPoseBonePositions = false, useBoneOrientationOffsets = false, aimChildOverrides, jointLocalRotationOffset) {
|
|
725
|
+
/**
|
|
726
|
+
* Fired when the body mesh is changed via {@link setBodyMesh}.
|
|
727
|
+
*/
|
|
728
|
+
this.onBodyMeshSetObservable = new Observable();
|
|
729
|
+
/**
|
|
730
|
+
* One {@link TransformNode} per joint. These receive the WebXR matrix data
|
|
731
|
+
* every frame and serve as link targets for skeleton bones.
|
|
732
|
+
*/
|
|
733
|
+
this._jointTransforms = new Array(BODY_JOINT_COUNT);
|
|
734
|
+
/**
|
|
735
|
+
* Flat Float32Array that receives transform matrices directly from the
|
|
736
|
+
* WebXR API (via `fillPoses`). 16 floats per joint × 83 joints = 1 328 floats.
|
|
737
|
+
*/
|
|
738
|
+
this._jointTransformMatrices = new Float32Array(BODY_JOINT_COUNT * 16);
|
|
739
|
+
/**
|
|
740
|
+
* Copy of the raw RHS XR matrices (before LHS conversion).
|
|
741
|
+
* Used to compute bone-local transforms for glTF skeletons that
|
|
742
|
+
* operate in RHS space.
|
|
743
|
+
*/
|
|
744
|
+
this._jointTransformMatricesRHS = new Float32Array(BODY_JOINT_COUNT * 16);
|
|
745
|
+
/**
|
|
746
|
+
* Cached array of XRBodySpace objects extracted from the XRBody, kept in the
|
|
747
|
+
* same order as {@link BodyJointReferenceArray}.
|
|
748
|
+
*/
|
|
749
|
+
this._jointSpaces = new Array(BODY_JOINT_COUNT);
|
|
750
|
+
/** Temporary matrix: this joint's XR world-space matrix. */
|
|
751
|
+
this._tempJointMatrix = new Matrix();
|
|
752
|
+
/** Temporary matrix: parent joint's XR world-space matrix. */
|
|
753
|
+
this._tempParentMatrix = new Matrix();
|
|
754
|
+
/** Temporary matrix: computed bone-local matrix. */
|
|
755
|
+
this._tempLocalMatrix = new Matrix();
|
|
756
|
+
/** Temporary vector for scale extracted from decompose. */
|
|
757
|
+
this._tempScaleVector = new Vector3();
|
|
758
|
+
/** Temporary quaternion for decompose. */
|
|
759
|
+
this._tempRotQuat = new Quaternion();
|
|
760
|
+
/** Temporary position vector for decompose. */
|
|
761
|
+
this._tempPosVec = new Vector3();
|
|
762
|
+
/** Temporary quaternion for alternate rotation calculations. */
|
|
763
|
+
this._tempRotQuat2 = new Quaternion();
|
|
764
|
+
/** Temporary vector for desired child direction. */
|
|
765
|
+
this._tempDirection = new Vector3();
|
|
766
|
+
/** Temporary vector for joint-local child direction. */
|
|
767
|
+
this._tempLocalDirection = new Vector3();
|
|
768
|
+
/** Cached desired final positions for mapped joints. */
|
|
769
|
+
this._desiredFinalPositions = Array.from({ length: BODY_JOINT_COUNT }, () => new Vector3());
|
|
770
|
+
/**
|
|
771
|
+
* For each joint index, the joint index of the nearest mapped SKELETON
|
|
772
|
+
* ancestor bone. -1 when the bone has no mapped ancestor (root level).
|
|
773
|
+
* Precomputed in {@link setBodyMesh} by walking the skeleton hierarchy.
|
|
774
|
+
*/
|
|
775
|
+
this._jointParentJointIdx = new Array(BODY_JOINT_COUNT).fill(-1);
|
|
776
|
+
/** Tracks which joint indices have a linked bone (for step 4b). */
|
|
777
|
+
this._jointHasBone = new Array(BODY_JOINT_COUNT).fill(false);
|
|
778
|
+
/** Bone → XR joint index lookup, built in {@link setBodyMesh}. */
|
|
779
|
+
this._boneToJointIdx = new Map();
|
|
780
|
+
/** Original bind-pose local matrices for mapped bones. */
|
|
781
|
+
this._mappedBoneBindLocals = new Map();
|
|
782
|
+
/** Nearest mapped child bone for each mapped bone. */
|
|
783
|
+
this._mappedChildBones = new Map();
|
|
784
|
+
/** Bind-space local child direction for each mapped bone. */
|
|
785
|
+
this._bindLocalAimDirections = new Map();
|
|
786
|
+
/**
|
|
787
|
+
* XR joint index to aim each mapped bone at. This can be a mapped joint
|
|
788
|
+
* (same as `_boneToJointIdx.get(aimChildBone)`) or an **unmapped** XR
|
|
789
|
+
* joint whose tracked position is nonetheless useful for aim correction —
|
|
790
|
+
* e.g. `LEFT_HAND_MIDDLE_METACARPAL` for `mixamorig:LeftHand`, which has
|
|
791
|
+
* no mapped finger descendant but whose tracked position still defines
|
|
792
|
+
* "where the hand is pointing".
|
|
793
|
+
*/
|
|
794
|
+
this._boneAimTargetJointIdx = new Map();
|
|
795
|
+
/**
|
|
796
|
+
* Per-mapped-bone bind-pose world rotation in mesh-local space
|
|
797
|
+
* (decomposed from `bone.getFinalMatrix()` at bind time). Used by the
|
|
798
|
+
* delta-from-bind retarget path (axis-convention-invariant).
|
|
799
|
+
*/
|
|
800
|
+
this._bindBoneWorldRotMeshLocal = new Map();
|
|
801
|
+
/**
|
|
802
|
+
* Bind-pose tracked joint rotation in mesh-local space per mapped joint.
|
|
803
|
+
* Captured on the first tracked frame (or on demand via
|
|
804
|
+
* {@link captureTrackedBind}). `null` until captured.
|
|
805
|
+
*/
|
|
806
|
+
this._trackedBindDesiredFinalRot = null;
|
|
807
|
+
/** Bind-pose tracked joint position (mesh-local), captured alongside rotation. */
|
|
808
|
+
this._trackedBindDesiredFinalPos = null;
|
|
809
|
+
/** True once a tracked-bind snapshot has been taken. */
|
|
810
|
+
this._hasTrackedBind = false;
|
|
811
|
+
/**
|
|
812
|
+
* When `true` (default), the first tracked frame after the feature
|
|
813
|
+
* attaches is used as the "rest" pose for delta-from-bind retargeting.
|
|
814
|
+
* Set to `false` to require an explicit {@link captureTrackedBind} call.
|
|
815
|
+
*/
|
|
816
|
+
this.autoCaptureBindOnFirstFrame = true;
|
|
817
|
+
/** Scratch quaternion for retarget delta composition. */
|
|
818
|
+
this._tempDeltaQuat = new Quaternion();
|
|
819
|
+
this._tempBoneWorldRot = new Quaternion();
|
|
820
|
+
this._tempParentNewWorldRotInv = new Quaternion();
|
|
821
|
+
this._tempTrackedCurRot = new Quaternion();
|
|
822
|
+
this._tempTrackedCurPos = new Vector3();
|
|
823
|
+
this._tempBindLocalScale = new Vector3();
|
|
824
|
+
this._tempBindLocalPos = new Vector3();
|
|
825
|
+
/**
|
|
826
|
+
* Per-bone cache of the current frame's computed world rotation.
|
|
827
|
+
* Entries are pooled across frames (values are reused via `copyFrom`)
|
|
828
|
+
* to avoid allocating a fresh Quaternion per mapped bone per frame.
|
|
829
|
+
* A bone is considered "populated this frame" iff it has been visited
|
|
830
|
+
* by the current retarget pass (tracked via `_computedBoneNewWorldRotFrameId`).
|
|
831
|
+
*/
|
|
832
|
+
this._computedBoneNewWorldRot = new Map();
|
|
833
|
+
/** Per-bone marker: frame id at which the pooled rotation above was last set. */
|
|
834
|
+
this._computedBoneNewWorldRotFrameId = new Map();
|
|
835
|
+
this._currentRetargetFrameId = 0;
|
|
836
|
+
/** Scratch quaternion reused for the parent-world accumulation loop. */
|
|
837
|
+
this._tempParentAccumRot = new Quaternion();
|
|
838
|
+
/** Scratch quaternion reused for the parent-world intermediate product. */
|
|
839
|
+
this._tempParentAccumTmp = new Quaternion();
|
|
840
|
+
/** The skeleton reference for iterating bones in parent-first order. */
|
|
841
|
+
this._skeleton = null;
|
|
842
|
+
/** Cached inverse of the skeleton mesh's world matrix. */
|
|
843
|
+
this._meshWorldMatrixInverse = new Matrix();
|
|
844
|
+
/** Cached inverse of the skeleton mesh pose matrix when initial skinning is used. */
|
|
845
|
+
this._initialSkinMatrixInverse = new Matrix();
|
|
846
|
+
/**
|
|
847
|
+
* Pre-allocated desiredFinal matrices (one per joint slot).
|
|
848
|
+
* `desiredFinal[i] = strip(xrWorld[i] × inv(meshWorld))` — the bone's
|
|
849
|
+
* target skeleton-space final matrix with parasitic scale removed.
|
|
850
|
+
*/
|
|
851
|
+
this._desiredFinals = [];
|
|
852
|
+
/**
|
|
853
|
+
* Standalone TransformNodes created for unmapped skinned bones.
|
|
854
|
+
* These TNs are initialized to the bone's bind-pose local and linked
|
|
855
|
+
* so that `prepare()` reads deterministic values rather than the
|
|
856
|
+
* original glTF scene-graph TNs (which we don't control).
|
|
857
|
+
*/
|
|
858
|
+
this._unmappedBoneNodes = [];
|
|
859
|
+
/** The mesh that owns the skeleton (used for world-matrix inverse). */
|
|
860
|
+
this._skeletonMesh = null;
|
|
861
|
+
/** The body mesh root (topmost parent), used to parent the mesh to the camera. */
|
|
862
|
+
this._bodyMeshRoot = null;
|
|
863
|
+
/** The rigged body mesh, if any. */
|
|
864
|
+
this._bodyMesh = null;
|
|
865
|
+
/**
|
|
866
|
+
* Runtime-mutable rotation applied in each tracked joint's local frame to
|
|
867
|
+
* re-base XR joint axes (e.g., "+Z-along-bone" → "+Y-along-bone").
|
|
868
|
+
* `null` = identity / disabled.
|
|
869
|
+
*/
|
|
870
|
+
this.jointLocalRotationOffset = null;
|
|
871
|
+
/** Cached 4×4 matrix form of {@link jointLocalRotationOffset} for the fast path. */
|
|
872
|
+
this._jointLocalRotationOffsetMatrix = new Matrix();
|
|
873
|
+
/** Temporary matrix used when applying {@link jointLocalRotationOffset}. */
|
|
874
|
+
this._tempOffsetAppliedMatrix = new Matrix();
|
|
875
|
+
/**
|
|
876
|
+
* When true, bypass skeleton.prepare() and write skin matrices directly.
|
|
877
|
+
* This is a diagnostic flag to help isolate rendering issues. When the
|
|
878
|
+
* standard pipeline (TN → bone → prepare → skin matrices) produces
|
|
879
|
+
* unexpected results, enabling this writes `absInvBind × final` directly
|
|
880
|
+
* into the skeleton's transform matrix buffer.
|
|
881
|
+
* @internal
|
|
882
|
+
*/
|
|
883
|
+
this._directSkinWrite = false;
|
|
884
|
+
/**
|
|
885
|
+
* Debug info string from the last `updateFromXRFrame` call.
|
|
886
|
+
* Useful for diagnosing tracking failures on-device.
|
|
887
|
+
* @internal
|
|
888
|
+
*/
|
|
889
|
+
this._lastDebugInfo = "TB:not called";
|
|
890
|
+
this._scene = scene;
|
|
891
|
+
this._jointScaleFactor = jointScaleFactor;
|
|
892
|
+
this._preserveBindPoseBonePositions = preserveBindPoseBonePositions;
|
|
893
|
+
this._useBoneOrientationOffsets = useBoneOrientationOffsets;
|
|
894
|
+
this._aimChildOverrides = aimChildOverrides;
|
|
895
|
+
if (jointLocalRotationOffset) {
|
|
896
|
+
this.jointLocalRotationOffset = jointLocalRotationOffset.clone();
|
|
897
|
+
}
|
|
898
|
+
// Initialize a TransformNode for every body joint.
|
|
899
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
900
|
+
this._jointTransforms[i] = new TransformNode(BodyJointReferenceArray[i], this._scene);
|
|
901
|
+
this._jointTransforms[i].rotationQuaternion = new Quaternion();
|
|
902
|
+
}
|
|
903
|
+
if (bodyMesh) {
|
|
904
|
+
this.setBodyMesh(bodyMesh, rigMapping);
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
/**
|
|
908
|
+
* Attach (or replace) a rigged body mesh.
|
|
909
|
+
*
|
|
910
|
+
* The mesh's skeleton bones are linked to the internal transform nodes
|
|
911
|
+
* that receive WebXR tracking data each frame. If the mesh has a skeleton,
|
|
912
|
+
* the `rigMapping` (or a direct name match) is used to bind each bone.
|
|
913
|
+
*
|
|
914
|
+
* @param bodyMesh The rigged mesh to drive.
|
|
915
|
+
* @param rigMapping An optional mapping from {@link WebXRBodyJoint} to bone name.
|
|
916
|
+
* If omitted, bones are expected to be named after the WebXR joint names.
|
|
917
|
+
*/
|
|
918
|
+
setBodyMesh(bodyMesh, rigMapping) {
|
|
919
|
+
this._bodyMesh = bodyMesh;
|
|
920
|
+
// Walk up to find the mesh root for parenting.
|
|
921
|
+
this._bodyMeshRoot = this._bodyMesh;
|
|
922
|
+
while (this._bodyMeshRoot.parent) {
|
|
923
|
+
this._bodyMeshRoot = this._bodyMeshRoot.parent;
|
|
924
|
+
}
|
|
925
|
+
// Disable frustum culling on the body mesh and its children so
|
|
926
|
+
// the mesh is never culled while the user is in XR.
|
|
927
|
+
bodyMesh.alwaysSelectAsActiveMesh = true;
|
|
928
|
+
for (const child of bodyMesh.getChildMeshes()) {
|
|
929
|
+
child.alwaysSelectAsActiveMesh = true;
|
|
930
|
+
}
|
|
931
|
+
// Bind skeleton bones via linkTransformNode so that skeleton.prepare()
|
|
932
|
+
// copies TransformNode data → bone local at the correct time (after
|
|
933
|
+
// animations, before final matrix computation).
|
|
934
|
+
// We also precompute which joints have bones and their nearest mapped
|
|
935
|
+
// skeleton ancestor so we can write bone-local data onto the nodes.
|
|
936
|
+
this._jointHasBone.fill(false);
|
|
937
|
+
this._jointParentJointIdx.fill(-1);
|
|
938
|
+
this._boneToJointIdx.clear();
|
|
939
|
+
this._mappedBoneBindLocals.clear();
|
|
940
|
+
this._mappedChildBones.clear();
|
|
941
|
+
this._bindLocalAimDirections.clear();
|
|
942
|
+
this._boneAimTargetJointIdx.clear();
|
|
943
|
+
this._bindBoneWorldRotMeshLocal.clear();
|
|
944
|
+
this._computedBoneNewWorldRot.clear();
|
|
945
|
+
this._computedBoneNewWorldRotFrameId.clear();
|
|
946
|
+
this._trackedBindDesiredFinalRot = null;
|
|
947
|
+
this._trackedBindDesiredFinalPos = null;
|
|
948
|
+
this._hasTrackedBind = false;
|
|
949
|
+
this._skeleton = null;
|
|
950
|
+
for (const { standaloneNode } of this._unmappedBoneNodes) {
|
|
951
|
+
standaloneNode.dispose();
|
|
952
|
+
}
|
|
953
|
+
this._unmappedBoneNodes = [];
|
|
954
|
+
this._skeletonMesh = null;
|
|
955
|
+
let skeleton = this._bodyMesh.skeleton;
|
|
956
|
+
let skeletonMesh = this._bodyMesh.skeleton ? this._bodyMesh : null;
|
|
957
|
+
if (!skeleton) {
|
|
958
|
+
for (const child of this._bodyMesh.getChildMeshes()) {
|
|
959
|
+
if (child.skeleton) {
|
|
960
|
+
skeleton = child.skeleton;
|
|
961
|
+
skeletonMesh = child;
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
if (skeleton) {
|
|
967
|
+
this._skeletonMesh = skeletonMesh;
|
|
968
|
+
this._skeleton = skeleton;
|
|
969
|
+
skeleton.prepare(true);
|
|
970
|
+
const bindPoseFinals = new Map();
|
|
971
|
+
for (const bone of skeleton.bones) {
|
|
972
|
+
bindPoseFinals.set(bone, bone.getFinalMatrix().clone());
|
|
973
|
+
}
|
|
974
|
+
// Step A: link transform nodes to bones
|
|
975
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
976
|
+
const jointName = BodyJointReferenceArray[i];
|
|
977
|
+
const boneName = rigMapping ? rigMapping[jointName] : jointName;
|
|
978
|
+
if (!boneName) {
|
|
979
|
+
continue;
|
|
980
|
+
}
|
|
981
|
+
const boneIdx = skeleton.getBoneIndexByName(boneName);
|
|
982
|
+
if (boneIdx !== -1) {
|
|
983
|
+
const bone = skeleton.bones[boneIdx];
|
|
984
|
+
// Initialize the joint's TransformNode to the bone's current
|
|
985
|
+
// (bind-pose) local transform BEFORE linking, so the mesh
|
|
986
|
+
// stays in its bind pose until the first XR frame arrives.
|
|
987
|
+
// Without this the TN defaults (identity rotation, zero
|
|
988
|
+
// position, unit scale) would be copied into the bone's
|
|
989
|
+
// local on the next skeleton.prepare() and collapse the mesh.
|
|
990
|
+
const jointTransform = this._jointTransforms[i];
|
|
991
|
+
bone.getLocalMatrix().decompose(jointTransform.scaling, jointTransform.rotationQuaternion, jointTransform.position);
|
|
992
|
+
bone.linkTransformNode(jointTransform);
|
|
993
|
+
this._jointHasBone[i] = true;
|
|
994
|
+
this._boneToJointIdx.set(bone, i);
|
|
995
|
+
this._mappedBoneBindLocals.set(bone, bone.getLocalMatrix().clone());
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const findMappedChildBone = (bone) => {
|
|
999
|
+
for (const child of bone.children) {
|
|
1000
|
+
if (this._boneToJointIdx.has(child)) {
|
|
1001
|
+
return child;
|
|
1002
|
+
}
|
|
1003
|
+
const descendant = findMappedChildBone(child);
|
|
1004
|
+
if (descendant) {
|
|
1005
|
+
return descendant;
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
return null;
|
|
1009
|
+
};
|
|
1010
|
+
const bindWorldPositions = new Map();
|
|
1011
|
+
const bindWorldRotations = new Map();
|
|
1012
|
+
for (const bone of skeleton.bones) {
|
|
1013
|
+
const bindFinal = bindPoseFinals.get(bone);
|
|
1014
|
+
if (!bindFinal) {
|
|
1015
|
+
continue;
|
|
1016
|
+
}
|
|
1017
|
+
bindFinal.decompose(this._tempScaleVector, this._tempRotQuat, this._tempPosVec);
|
|
1018
|
+
bindWorldPositions.set(bone, this._tempPosVec.clone());
|
|
1019
|
+
bindWorldRotations.set(bone, this._tempRotQuat.clone());
|
|
1020
|
+
// Persist bone bind-pose world rotation for delta-from-bind retarget.
|
|
1021
|
+
if (this._boneToJointIdx.has(bone)) {
|
|
1022
|
+
this._bindBoneWorldRotMeshLocal.set(bone, this._tempRotQuat.clone());
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
for (const bone of Array.from(this._boneToJointIdx.keys())) {
|
|
1026
|
+
// Prefer an explicit override (XR-joint → XR-joint) when provided.
|
|
1027
|
+
const selfJointIdx = this._boneToJointIdx.get(bone);
|
|
1028
|
+
const selfJointName = BodyJointReferenceArray[selfJointIdx];
|
|
1029
|
+
const overrideTargetJointName = this._aimChildOverrides?.[selfJointName];
|
|
1030
|
+
let childBone = null;
|
|
1031
|
+
let overrideTargetJointIdx = -1;
|
|
1032
|
+
if (overrideTargetJointName) {
|
|
1033
|
+
overrideTargetJointIdx = BodyJointNameToIndex.get(overrideTargetJointName) ?? -1;
|
|
1034
|
+
const overrideBoneName = rigMapping ? rigMapping[overrideTargetJointName] : overrideTargetJointName;
|
|
1035
|
+
if (overrideBoneName) {
|
|
1036
|
+
const overrideBoneIdx = skeleton.getBoneIndexByName(overrideBoneName);
|
|
1037
|
+
if (overrideBoneIdx !== -1) {
|
|
1038
|
+
const candidate = skeleton.bones[overrideBoneIdx];
|
|
1039
|
+
if (this._boneToJointIdx.has(candidate)) {
|
|
1040
|
+
childBone = candidate;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
// Case A: override target is a MAPPED bone → use bind positions
|
|
1046
|
+
// of both bones (as before).
|
|
1047
|
+
if (childBone) {
|
|
1048
|
+
const boneBindPos = bindWorldPositions.get(bone);
|
|
1049
|
+
const childBindPos = bindWorldPositions.get(childBone);
|
|
1050
|
+
const bindWorldRotation = bindWorldRotations.get(bone);
|
|
1051
|
+
if (boneBindPos && childBindPos && bindWorldRotation) {
|
|
1052
|
+
childBindPos.subtractToRef(boneBindPos, this._tempDirection);
|
|
1053
|
+
if (this._tempDirection.lengthSquared() > 1e-8) {
|
|
1054
|
+
this._tempDirection.normalize();
|
|
1055
|
+
Quaternion.InverseToRef(bindWorldRotation, this._tempRotQuat2);
|
|
1056
|
+
this._tempDirection.rotateByQuaternionToRef(this._tempRotQuat2, this._tempLocalDirection);
|
|
1057
|
+
if (this._tempLocalDirection.lengthSquared() > 1e-8) {
|
|
1058
|
+
this._tempLocalDirection.normalize();
|
|
1059
|
+
this._mappedChildBones.set(bone, childBone);
|
|
1060
|
+
this._bindLocalAimDirections.set(bone, this._tempLocalDirection.clone());
|
|
1061
|
+
this._boneAimTargetJointIdx.set(bone, this._boneToJointIdx.get(childBone));
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
// Case B: override target is an UNMAPPED XR joint (e.g.
|
|
1068
|
+
// hand → middle metacarpal). Try to resolve it to a real
|
|
1069
|
+
// unmapped descendant bone by name match so we can compute
|
|
1070
|
+
// bind-local aim from the rig's own bind pose (the tracked
|
|
1071
|
+
// bind pose of the user doesn't necessarily match the rig's
|
|
1072
|
+
// bind orientation — using it would introduce a constant
|
|
1073
|
+
// offset).
|
|
1074
|
+
if (overrideTargetJointIdx !== -1 && overrideTargetJointName) {
|
|
1075
|
+
this._boneAimTargetJointIdx.set(bone, overrideTargetJointIdx);
|
|
1076
|
+
// Extract significant tokens from target joint name.
|
|
1077
|
+
// "LEFT_HAND_MIDDLE_METACARPAL" → ["middle", "metacarpal"]
|
|
1078
|
+
const tokens = overrideTargetJointName
|
|
1079
|
+
.toLowerCase()
|
|
1080
|
+
.split(/[_\-\s]+/)
|
|
1081
|
+
.filter((t) => t.length >= 4 && t !== "left" && t !== "right" && t !== "hand" && t !== "foot" && t !== "joint" && t !== "body");
|
|
1082
|
+
// Find best-matching descendant bone: most tokens matched,
|
|
1083
|
+
// prefer shorter names (closer to the target).
|
|
1084
|
+
let bestDescendant = null;
|
|
1085
|
+
let bestScore = 0;
|
|
1086
|
+
const walk = (b) => {
|
|
1087
|
+
for (const child of b.children) {
|
|
1088
|
+
if (!this._boneToJointIdx.has(child)) {
|
|
1089
|
+
const lname = child.name.toLowerCase();
|
|
1090
|
+
let score = 0;
|
|
1091
|
+
for (const t of tokens) {
|
|
1092
|
+
if (lname.indexOf(t) !== -1) {
|
|
1093
|
+
score++;
|
|
1094
|
+
}
|
|
1095
|
+
}
|
|
1096
|
+
if (score > bestScore) {
|
|
1097
|
+
bestScore = score;
|
|
1098
|
+
bestDescendant = child;
|
|
1099
|
+
}
|
|
1100
|
+
walk(child);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
walk(bone);
|
|
1105
|
+
if (bestDescendant && bestScore > 0) {
|
|
1106
|
+
const descendant = bestDescendant;
|
|
1107
|
+
const boneBindPos = bindWorldPositions.get(bone);
|
|
1108
|
+
// Walk up: descendant's bind world position isn't in
|
|
1109
|
+
// bindWorldPositions (only mapped bones were added).
|
|
1110
|
+
// Compute it from the final matrix captured at bind.
|
|
1111
|
+
const descBindFinal = bindPoseFinals.get(descendant);
|
|
1112
|
+
const bindWorldRotation = bindWorldRotations.get(bone);
|
|
1113
|
+
if (boneBindPos && descBindFinal && bindWorldRotation) {
|
|
1114
|
+
descBindFinal.decompose(undefined, undefined, this._tempPosVec);
|
|
1115
|
+
this._tempPosVec.subtractToRef(boneBindPos, this._tempDirection);
|
|
1116
|
+
if (this._tempDirection.lengthSquared() > 1e-8) {
|
|
1117
|
+
this._tempDirection.normalize();
|
|
1118
|
+
Quaternion.InverseToRef(bindWorldRotation, this._tempRotQuat2);
|
|
1119
|
+
this._tempDirection.rotateByQuaternionToRef(this._tempRotQuat2, this._tempLocalDirection);
|
|
1120
|
+
if (this._tempLocalDirection.lengthSquared() > 1e-8) {
|
|
1121
|
+
this._tempLocalDirection.normalize();
|
|
1122
|
+
this._bindLocalAimDirections.set(bone, this._tempLocalDirection.clone());
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
// If we couldn't find a descendant, bind aim will be
|
|
1128
|
+
// computed from tracked-bind positions later (fallback).
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
// Case C: no override → auto-detect a mapped descendant.
|
|
1132
|
+
const autoChild = findMappedChildBone(bone);
|
|
1133
|
+
if (!autoChild) {
|
|
1134
|
+
continue;
|
|
1135
|
+
}
|
|
1136
|
+
const boneBindPos = bindWorldPositions.get(bone);
|
|
1137
|
+
const childBindPos = bindWorldPositions.get(autoChild);
|
|
1138
|
+
const bindWorldRotation = bindWorldRotations.get(bone);
|
|
1139
|
+
if (!boneBindPos || !childBindPos || !bindWorldRotation) {
|
|
1140
|
+
continue;
|
|
1141
|
+
}
|
|
1142
|
+
childBindPos.subtractToRef(boneBindPos, this._tempDirection);
|
|
1143
|
+
if (this._tempDirection.lengthSquared() < 1e-8) {
|
|
1144
|
+
continue;
|
|
1145
|
+
}
|
|
1146
|
+
this._tempDirection.normalize();
|
|
1147
|
+
Quaternion.InverseToRef(bindWorldRotation, this._tempRotQuat2);
|
|
1148
|
+
this._tempDirection.rotateByQuaternionToRef(this._tempRotQuat2, this._tempLocalDirection);
|
|
1149
|
+
if (this._tempLocalDirection.lengthSquared() < 1e-8) {
|
|
1150
|
+
continue;
|
|
1151
|
+
}
|
|
1152
|
+
this._tempLocalDirection.normalize();
|
|
1153
|
+
this._mappedChildBones.set(bone, autoChild);
|
|
1154
|
+
this._bindLocalAimDirections.set(bone, this._tempLocalDirection.clone());
|
|
1155
|
+
this._boneAimTargetJointIdx.set(bone, this._boneToJointIdx.get(autoChild));
|
|
1156
|
+
}
|
|
1157
|
+
// Step B: for each mapped bone, walk UP the skeleton hierarchy
|
|
1158
|
+
// to find the nearest ancestor bone that is also mapped to an
|
|
1159
|
+
// XR joint. This lets us compute correct bone-local transforms
|
|
1160
|
+
// even when unmapped bones exist between two mapped ones.
|
|
1161
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1162
|
+
if (!this._jointHasBone[i]) {
|
|
1163
|
+
continue;
|
|
1164
|
+
}
|
|
1165
|
+
const bone = skeleton.bones[skeleton.getBoneIndexByName((rigMapping ? rigMapping[BodyJointReferenceArray[i]] : BodyJointReferenceArray[i]))];
|
|
1166
|
+
let ancestor = bone.getParent();
|
|
1167
|
+
while (ancestor) {
|
|
1168
|
+
const pIdx = this._boneToJointIdx.get(ancestor);
|
|
1169
|
+
if (pIdx !== undefined) {
|
|
1170
|
+
this._jointParentJointIdx[i] = pIdx;
|
|
1171
|
+
break;
|
|
1172
|
+
}
|
|
1173
|
+
ancestor = ancestor.getParent();
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
// Step C: pre-allocate desiredFinal matrices and compute
|
|
1177
|
+
// bind-pose finals (via skeleton.prepare) for unmapped bone setup.
|
|
1178
|
+
if (this._desiredFinals.length === 0) {
|
|
1179
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1180
|
+
this._desiredFinals.push(new Matrix());
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
skeleton.prepare(true);
|
|
1184
|
+
// Step D: create standalone TNs for unmapped skinned bones.
|
|
1185
|
+
// Bones that influence vertices (_index >= 0) but aren't mapped to
|
|
1186
|
+
// any XR joint need standalone TNs so prepare() reads bind-pose
|
|
1187
|
+
// locals rather than the original glTF scene-graph TNs (which we
|
|
1188
|
+
// don't control). During tracking, these bones keep their
|
|
1189
|
+
// bind-pose local and chain naturally with tracked parent finals —
|
|
1190
|
+
// their vertices follow the parent bone's motion.
|
|
1191
|
+
this._unmappedBoneNodes = [];
|
|
1192
|
+
for (const bone of skeleton.bones) {
|
|
1193
|
+
if (this._boneToJointIdx.has(bone)) {
|
|
1194
|
+
continue; // mapped — handled by body tracking
|
|
1195
|
+
}
|
|
1196
|
+
const boneIndex = bone._index;
|
|
1197
|
+
if (boneIndex === -1 || boneIndex === null) {
|
|
1198
|
+
continue; // unskinned (e.g. Armature)
|
|
1199
|
+
}
|
|
1200
|
+
// This bone influences vertices but isn't tracked by an XR joint.
|
|
1201
|
+
// Create a standalone TN with the bind-pose local so the bone
|
|
1202
|
+
// keeps its offset from its parent and follows parent motion.
|
|
1203
|
+
const tn = new TransformNode(`_xrUnmapped_${bone.name}`, skeleton.getScene());
|
|
1204
|
+
tn.rotationQuaternion = new Quaternion();
|
|
1205
|
+
// Initialize TN with the current (bind-pose) local
|
|
1206
|
+
bone.getLocalMatrix().decompose(tn.scaling, tn.rotationQuaternion, tn.position);
|
|
1207
|
+
bone.linkTransformNode(tn);
|
|
1208
|
+
this._unmappedBoneNodes.push({ bone, standaloneNode: tn });
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
this.onBodyMeshSetObservable.notifyObservers(this);
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Update joint transforms from the current XR frame.
|
|
1215
|
+
*
|
|
1216
|
+
* This method is called once per frame by the feature class. Internally it:
|
|
1217
|
+
* 1. Extracts all XRBodySpaces from the XRBody.
|
|
1218
|
+
* 2. Fills the transform-matrix buffer via `fillPoses()` (or per-joint fallback).
|
|
1219
|
+
* 3. Converts from WebXR right-handed to Babylon left-handed coordinates.
|
|
1220
|
+
* 4. Decomposes each matrix into the corresponding TransformNode.
|
|
1221
|
+
* 5. Parents the body mesh root to the XR camera so it tracks correctly.
|
|
1222
|
+
*
|
|
1223
|
+
* @param xrFrame The current XRFrame.
|
|
1224
|
+
* @param referenceSpace The XRReferenceSpace to resolve poses against.
|
|
1225
|
+
* @param xrCameraParent The parent node of the XR camera (used for parenting).
|
|
1226
|
+
* @returns `true` if valid tracking data was processed, `false` otherwise.
|
|
1227
|
+
*/
|
|
1228
|
+
updateFromXRFrame(xrFrame, referenceSpace, xrCameraParent) {
|
|
1229
|
+
const body = xrFrame.body;
|
|
1230
|
+
if (!body) {
|
|
1231
|
+
this._lastDebugInfo = "no body";
|
|
1232
|
+
return false;
|
|
1233
|
+
}
|
|
1234
|
+
// ── Step 1: collect the body spaces in order ──────────────────────
|
|
1235
|
+
// Some UAs expose joints as direct properties on the body object in
|
|
1236
|
+
// addition to (or instead of) the Map-like .get() accessor — mirror
|
|
1237
|
+
// the same double-lookup hand tracking uses.
|
|
1238
|
+
const anyBody = body;
|
|
1239
|
+
let validSpaceCount = 0;
|
|
1240
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1241
|
+
const jointName = BodyJointReferenceArray[i];
|
|
1242
|
+
const space = anyBody[jointName] || body.get(jointName);
|
|
1243
|
+
this._jointSpaces[i] = space;
|
|
1244
|
+
if (space) {
|
|
1245
|
+
validSpaceCount++;
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (validSpaceCount === 0) {
|
|
1249
|
+
this._lastDebugInfo = "0 spaces";
|
|
1250
|
+
if (this._bodyMesh) {
|
|
1251
|
+
this._bodyMesh.isVisible = false;
|
|
1252
|
+
}
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
// ── Step 2: fill matrices (prefer batch API, fall back to per-joint) ──
|
|
1256
|
+
let trackingSuccessful = false;
|
|
1257
|
+
let fillPosesError = "";
|
|
1258
|
+
// Only use fillPoses when ALL joint spaces resolved — the batch API
|
|
1259
|
+
// does not tolerate undefined entries. Wrap in try/catch because
|
|
1260
|
+
// fillPoses may not accept body-tracking spaces on every UA.
|
|
1261
|
+
if (validSpaceCount === BODY_JOINT_COUNT && xrFrame.fillPoses) {
|
|
1262
|
+
try {
|
|
1263
|
+
trackingSuccessful = xrFrame.fillPoses(this._jointSpaces, referenceSpace, this._jointTransformMatrices);
|
|
1264
|
+
if (!trackingSuccessful) {
|
|
1265
|
+
fillPosesError = "returned false";
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
catch (e) {
|
|
1269
|
+
fillPosesError = "" + e;
|
|
1270
|
+
trackingSuccessful = false;
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
// Fallback: query each joint individually via getPose().
|
|
1274
|
+
// Lenient — skips missing joints and succeeds as long as at least
|
|
1275
|
+
// one joint returned valid data.
|
|
1276
|
+
let getPoseCount = 0;
|
|
1277
|
+
let getPoseNullCount = 0;
|
|
1278
|
+
if (!trackingSuccessful) {
|
|
1279
|
+
let anyJointTracked = false;
|
|
1280
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1281
|
+
const space = this._jointSpaces[i];
|
|
1282
|
+
if (!space) {
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
const pose = xrFrame.getPose(space, referenceSpace);
|
|
1286
|
+
if (pose) {
|
|
1287
|
+
this._jointTransformMatrices.set(pose.transform.matrix, i * 16);
|
|
1288
|
+
anyJointTracked = true;
|
|
1289
|
+
getPoseCount++;
|
|
1290
|
+
}
|
|
1291
|
+
else {
|
|
1292
|
+
getPoseNullCount++;
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
trackingSuccessful = anyJointTracked;
|
|
1296
|
+
}
|
|
1297
|
+
this._lastDebugInfo = "spaces:" + validSpaceCount + " fillPoses:" + fillPosesError + " getPose:" + getPoseCount + "/" + getPoseNullCount + " ok:" + trackingSuccessful;
|
|
1298
|
+
if (!trackingSuccessful) {
|
|
1299
|
+
if (this._bodyMesh) {
|
|
1300
|
+
this._bodyMesh.isVisible = false;
|
|
1301
|
+
}
|
|
1302
|
+
return false;
|
|
1303
|
+
}
|
|
1304
|
+
this._processTrackedJointMatrices(xrCameraParent);
|
|
1305
|
+
return true;
|
|
1306
|
+
}
|
|
1307
|
+
/**
|
|
1308
|
+
* Replay a pre-captured joint matrix set through the retargeting
|
|
1309
|
+
* pipeline as if it had just been delivered by an XR frame.
|
|
1310
|
+
*
|
|
1311
|
+
* Useful for headset-less testing: call with a snapshot captured via
|
|
1312
|
+
* {@link snapshotFrame} (the `jointMatricesRHS` array, or `jointMatricesLHS`
|
|
1313
|
+
* already flipped). The matrices are assumed to be RHS unless
|
|
1314
|
+
* `isAlreadyLhs=true`, in which case the RHS→LHS flip step is skipped.
|
|
1315
|
+
*
|
|
1316
|
+
* @param rawMatrices Float32Array of BODY_JOINT_COUNT × 16 (= 1328) floats.
|
|
1317
|
+
* @param isAlreadyLhs Set to `true` if matrices are already LHS-converted.
|
|
1318
|
+
*/
|
|
1319
|
+
replayRawJointMatrices(rawMatrices, isAlreadyLhs = false) {
|
|
1320
|
+
const src = rawMatrices instanceof Float32Array ? rawMatrices : new Float32Array(rawMatrices);
|
|
1321
|
+
const expectedLen = BODY_JOINT_COUNT * 16;
|
|
1322
|
+
if (src.length !== expectedLen) {
|
|
1323
|
+
Logger.Warn("WebXR Body Tracking: replayRawJointMatrices expected " + expectedLen + " floats, got " + src.length);
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
this._jointTransformMatrices.set(src);
|
|
1327
|
+
this._processTrackedJointMatrices(null, /*skipRhsToLhs*/ isAlreadyLhs);
|
|
1328
|
+
}
|
|
1329
|
+
/**
|
|
1330
|
+
* Run steps 2.5 → 5 of the retargeting pipeline using whatever is
|
|
1331
|
+
* currently in `_jointTransformMatrices` (as if the WebXR API just
|
|
1332
|
+
* filled it for the current frame).
|
|
1333
|
+
* @param xrCameraParent Parent node of the XR camera (used to parent the body mesh root during live XR).
|
|
1334
|
+
* @param skipRhsToLhs If true, skip the RHS→LHS flip (matrices are already in the scene's handedness).
|
|
1335
|
+
*/
|
|
1336
|
+
_processTrackedJointMatrices(xrCameraParent, skipRhsToLhs = false) {
|
|
1337
|
+
// ── Step 2.5: re-base joint local axes (optional) ────────────────
|
|
1338
|
+
// If jointLocalRotationOffset is set, right-multiply every joint's
|
|
1339
|
+
// world matrix by its 4×4 form. Under Babylon's row-vector convention
|
|
1340
|
+
// (`v_world = v_local × M`), `M_new = R × M` re-bases the joint-local
|
|
1341
|
+
// frame so that what used to be along `R⁻¹·x` is now along `x`.
|
|
1342
|
+
// Typical use: convert "+Z-along-bone" XR data to "+Y-along-bone" via
|
|
1343
|
+
// `Quaternion.RotationAxis(Vector3.Right(), -Math.PI / 2)`.
|
|
1344
|
+
if (this.jointLocalRotationOffset) {
|
|
1345
|
+
Matrix.FromQuaternionToRef(this.jointLocalRotationOffset, this._jointLocalRotationOffsetMatrix);
|
|
1346
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1347
|
+
const o = i * 16;
|
|
1348
|
+
Matrix.FromArrayToRef(this._jointTransformMatrices, o, this._tempJointMatrix);
|
|
1349
|
+
this._jointLocalRotationOffsetMatrix.multiplyToRef(this._tempJointMatrix, this._tempOffsetAppliedMatrix);
|
|
1350
|
+
this._tempOffsetAppliedMatrix.copyToArray(this._jointTransformMatrices, o);
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
// ── Step 3: RHS → LHS coordinate conversion ──────────────────────
|
|
1354
|
+
// WebXR delivers right-handed matrices. When Babylon is running in
|
|
1355
|
+
// its default left-handed mode we negate the Z-related components of
|
|
1356
|
+
// every column-major 4 × 4 matrix in-place.
|
|
1357
|
+
//
|
|
1358
|
+
// Column-major layout (indices 0-15):
|
|
1359
|
+
// [ m00 m10 m20 m30 ] indices 0 1 2 3
|
|
1360
|
+
// [ m01 m11 m21 m31 ] indices 4 5 6 7
|
|
1361
|
+
// [ m02 m12 m22 m32 ] indices 8 9 10 11
|
|
1362
|
+
// [ m03 m13 m23 m33 ] indices 12 13 14 15
|
|
1363
|
+
//
|
|
1364
|
+
// Negated indices: 2, 6, 8, 9, 14 (third row + translation Z)
|
|
1365
|
+
// Always keep a copy of the pre-flip (raw RHS) matrices so that
|
|
1366
|
+
// `snapshotFrame()` always has them available, regardless of the
|
|
1367
|
+
// scene handedness or the `skipRhsToLhs` flag.
|
|
1368
|
+
this._jointTransformMatricesRHS.set(this._jointTransformMatrices);
|
|
1369
|
+
if (!skipRhsToLhs && !this._scene.useRightHandedSystem) {
|
|
1370
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1371
|
+
const o = i * 16;
|
|
1372
|
+
this._jointTransformMatrices[o + 2] *= -1;
|
|
1373
|
+
this._jointTransformMatrices[o + 6] *= -1;
|
|
1374
|
+
this._jointTransformMatrices[o + 8] *= -1;
|
|
1375
|
+
this._jointTransformMatrices[o + 9] *= -1;
|
|
1376
|
+
this._jointTransformMatrices[o + 14] *= -1;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
// ── Step 4: decompose world-space matrices into TransformNodes ─────
|
|
1380
|
+
// For unmapped joints, TransformNodes hold world-space pose for
|
|
1381
|
+
// consumers (e.g., debug boxes). For mapped joints (linked to
|
|
1382
|
+
// skeleton bones), we overwrite with bone-local data in step 4b.
|
|
1383
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1384
|
+
if (!this._jointHasBone[i]) {
|
|
1385
|
+
const jointTransform = this._jointTransforms[i];
|
|
1386
|
+
Matrix.FromArrayToRef(this._jointTransformMatrices, i * 16, this._tempJointMatrix);
|
|
1387
|
+
this._tempJointMatrix.decompose(undefined, jointTransform.rotationQuaternion, jointTransform.position);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
// ── Step 4b: compute bone-local transforms via desiredFinals ──────
|
|
1391
|
+
// For every mapped joint we compute the target skeleton-space final:
|
|
1392
|
+
// desiredFinal = strip(xrWorld × inv(meshWorld))
|
|
1393
|
+
// All desiredFinals are in the same space (skeleton space), so
|
|
1394
|
+
// locals derived from them have consistent units and scale.
|
|
1395
|
+
// We then iterate ALL skeleton bones in array order (parents first)
|
|
1396
|
+
// and compute each bone's local as:
|
|
1397
|
+
// mapped bone: local = desiredFinal × inv(parentBone.final)
|
|
1398
|
+
// unmapped bone: local unchanged (bind-pose), chain with parent final
|
|
1399
|
+
if (this._skeletonMesh && this._skeleton) {
|
|
1400
|
+
this._skeletonMesh.getWorldMatrix().invertToRef(this._meshWorldMatrixInverse);
|
|
1401
|
+
const scaleFactor = this._jointScaleFactor;
|
|
1402
|
+
const useInitialSkinMatrix = this._skeleton.needInitialSkinMatrix;
|
|
1403
|
+
const useBoneOrientationOffsets = this._useBoneOrientationOffsets;
|
|
1404
|
+
const computedWorldRotations = useBoneOrientationOffsets ? new Map() : null;
|
|
1405
|
+
if (useInitialSkinMatrix) {
|
|
1406
|
+
this._skeletonMesh.getPoseMatrix().invertToRef(this._initialSkinMatrixInverse);
|
|
1407
|
+
}
|
|
1408
|
+
// Pass 1: compute desiredFinals for all mapped joints.
|
|
1409
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1410
|
+
if (!this._jointHasBone[i]) {
|
|
1411
|
+
continue;
|
|
1412
|
+
}
|
|
1413
|
+
Matrix.FromArrayToRef(this._jointTransformMatrices, i * 16, this._tempJointMatrix);
|
|
1414
|
+
this._tempJointMatrix.multiplyToRef(this._meshWorldMatrixInverse, this._desiredFinals[i]);
|
|
1415
|
+
// Strip parasitic scale from inv(meshWorld) to ±1.
|
|
1416
|
+
this._desiredFinals[i].decompose(this._tempScaleVector, this._tempRotQuat, this._tempPosVec);
|
|
1417
|
+
this._tempScaleVector.set(Math.sign(this._tempScaleVector.x) || 1, Math.sign(this._tempScaleVector.y) || 1, Math.sign(this._tempScaleVector.z) || 1);
|
|
1418
|
+
Matrix.ComposeToRef(this._tempScaleVector, this._tempRotQuat, this._tempPosVec, this._desiredFinals[i]);
|
|
1419
|
+
this._desiredFinalPositions[i].copyFrom(this._tempPosVec);
|
|
1420
|
+
}
|
|
1421
|
+
// Pass 1b: compute mesh-local positions for any UNMAPPED XR joints
|
|
1422
|
+
// that are referenced as aim targets (e.g. hand → middle metacarpal).
|
|
1423
|
+
// Rotations are not needed for these joints — only positions.
|
|
1424
|
+
// Use Map.forEach to avoid allocating an array and to stay within the ES5
|
|
1425
|
+
// target's iteration rules.
|
|
1426
|
+
this._boneAimTargetJointIdx.forEach((targetIdx) => {
|
|
1427
|
+
if (this._jointHasBone[targetIdx]) {
|
|
1428
|
+
return; // position already filled above.
|
|
1429
|
+
}
|
|
1430
|
+
Matrix.FromArrayToRef(this._jointTransformMatrices, targetIdx * 16, this._tempJointMatrix);
|
|
1431
|
+
this._tempJointMatrix.multiplyToRef(this._meshWorldMatrixInverse, this._tempLocalMatrix);
|
|
1432
|
+
this._tempLocalMatrix.decompose(undefined, undefined, this._desiredFinalPositions[targetIdx]);
|
|
1433
|
+
});
|
|
1434
|
+
// Auto-capture bind on first frame when enabled.
|
|
1435
|
+
if (!this._hasTrackedBind && this.autoCaptureBindOnFirstFrame) {
|
|
1436
|
+
this._captureTrackedBindFromDesiredFinals();
|
|
1437
|
+
}
|
|
1438
|
+
if (this._hasTrackedBind && this._trackedBindDesiredFinalRot && this._trackedBindDesiredFinalPos) {
|
|
1439
|
+
// ── Pass 2 (primary): delta-from-bind retargeting ─────────
|
|
1440
|
+
// Axis-convention-invariant. For every mapped bone:
|
|
1441
|
+
// deltaMeshLocalRot[j] = trackedBindDesiredRot[j]⁻¹ × trackedCurrentDesiredRot[j]
|
|
1442
|
+
// newBoneWorldRot[b] = bindBoneWorldRot[b] × deltaMeshLocalRot[j(b)]
|
|
1443
|
+
// newBoneLocalRot[b] = newBoneWorldRot[b] × newBoneParentWorldRot[b]⁻¹
|
|
1444
|
+
this._retargetDeltaFromBind(scaleFactor);
|
|
1445
|
+
// Unmapped bones: chain bind-pose local × parent final.
|
|
1446
|
+
for (const bone of this._skeleton.bones) {
|
|
1447
|
+
if (this._boneToJointIdx.has(bone)) {
|
|
1448
|
+
continue;
|
|
1449
|
+
}
|
|
1450
|
+
const parentBone = bone.getParent();
|
|
1451
|
+
if (parentBone) {
|
|
1452
|
+
bone.getLocalMatrix().multiplyToRef(parentBone.getFinalMatrix(), bone.getFinalMatrix());
|
|
1453
|
+
}
|
|
1454
|
+
else {
|
|
1455
|
+
bone.getFinalMatrix().copyFrom(bone.getLocalMatrix());
|
|
1456
|
+
}
|
|
1457
|
+
}
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
// ── Pass 2 (fallback): direct retarget ─────────────────────
|
|
1461
|
+
// Used before a tracked bind has been captured (e.g. during
|
|
1462
|
+
// the first frame with autoCaptureBindOnFirstFrame=false).
|
|
1463
|
+
this._retargetDirect(useInitialSkinMatrix, useBoneOrientationOffsets, scaleFactor, computedWorldRotations);
|
|
1464
|
+
}
|
|
1465
|
+
// ── Step 4c (diagnostic): direct skin matrix write ───────────
|
|
1466
|
+
// When _directSkinWrite is enabled, write absInvBind × final
|
|
1467
|
+
// directly into the skeleton's transform matrix buffer, bypassing
|
|
1468
|
+
// the normal TN → bone → prepare() → skin matrix pipeline.
|
|
1469
|
+
if (this._directSkinWrite) {
|
|
1470
|
+
const skinMatrices = this._skeleton.getTransformMatrices(this._skeletonMesh);
|
|
1471
|
+
if (skinMatrices) {
|
|
1472
|
+
for (const bone of this._skeleton.bones) {
|
|
1473
|
+
const bIdx = bone._index;
|
|
1474
|
+
if (bIdx === -1 || bIdx === null) {
|
|
1475
|
+
continue;
|
|
1476
|
+
}
|
|
1477
|
+
const mappedIndex = bIdx;
|
|
1478
|
+
bone.getAbsoluteInverseBindMatrix().multiplyToArray(bone.getFinalMatrix(), skinMatrices, mappedIndex * 16);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
// ── Step 5: parent the mesh to the XR camera ─────────────────────
|
|
1484
|
+
if (this._bodyMesh) {
|
|
1485
|
+
this._bodyMesh.isVisible = true;
|
|
1486
|
+
if (this._bodyMeshRoot && xrCameraParent) {
|
|
1487
|
+
this._bodyMeshRoot.parent = xrCameraParent;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
/**
|
|
1492
|
+
* Capture the current tracked-joint desired-final rotations and positions
|
|
1493
|
+
* as the "rest pose" for delta-from-bind retargeting.
|
|
1494
|
+
*
|
|
1495
|
+
* Delta-from-bind is the production retarget path: every subsequent
|
|
1496
|
+
* frame is interpreted as a rotation delta from this snapshot, which
|
|
1497
|
+
* makes retargeting invariant to the XR-joint axis convention and to
|
|
1498
|
+
* any skeletal-proportion differences between the tracked user and the
|
|
1499
|
+
* avatar.
|
|
1500
|
+
*
|
|
1501
|
+
* Call this after the user assumes a known rest pose (e.g. T-pose,
|
|
1502
|
+
* A-pose, arms-at-sides). By default the feature auto-captures on the
|
|
1503
|
+
* first tracked frame — disable via {@link autoCaptureBindOnFirstFrame}.
|
|
1504
|
+
*/
|
|
1505
|
+
captureTrackedBind() {
|
|
1506
|
+
this._captureTrackedBindFromDesiredFinals();
|
|
1507
|
+
}
|
|
1508
|
+
/**
|
|
1509
|
+
* Clear any captured bind, reverting subsequent frames to the fallback
|
|
1510
|
+
* direct-retarget path (or re-triggering auto-capture on the next frame).
|
|
1511
|
+
*/
|
|
1512
|
+
clearTrackedBind() {
|
|
1513
|
+
this._hasTrackedBind = false;
|
|
1514
|
+
}
|
|
1515
|
+
/** Internal: copy current desiredFinals into the tracked-bind slots. */
|
|
1516
|
+
_captureTrackedBindFromDesiredFinals() {
|
|
1517
|
+
if (!this._trackedBindDesiredFinalRot) {
|
|
1518
|
+
this._trackedBindDesiredFinalRot = Array.from({ length: BODY_JOINT_COUNT }, () => new Quaternion());
|
|
1519
|
+
this._trackedBindDesiredFinalPos = Array.from({ length: BODY_JOINT_COUNT }, () => new Vector3());
|
|
1520
|
+
}
|
|
1521
|
+
for (let i = 0; i < BODY_JOINT_COUNT; i++) {
|
|
1522
|
+
if (!this._jointHasBone[i]) {
|
|
1523
|
+
continue;
|
|
1524
|
+
}
|
|
1525
|
+
this._desiredFinals[i].decompose(undefined, this._tempRotQuat, this._tempPosVec);
|
|
1526
|
+
this._trackedBindDesiredFinalRot[i].copyFrom(this._tempRotQuat);
|
|
1527
|
+
this._trackedBindDesiredFinalPos[i].copyFrom(this._tempPosVec);
|
|
1528
|
+
}
|
|
1529
|
+
// Also capture positions for aim-target joints that aren't mapped
|
|
1530
|
+
// (e.g. hand's middle metacarpal). These are only needed for the
|
|
1531
|
+
// positional aim-correction reference.
|
|
1532
|
+
for (const targetIdx of Array.from(this._boneAimTargetJointIdx.values())) {
|
|
1533
|
+
if (this._jointHasBone[targetIdx]) {
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1536
|
+
this._trackedBindDesiredFinalPos[targetIdx].copyFrom(this._desiredFinalPositions[targetIdx]);
|
|
1537
|
+
}
|
|
1538
|
+
// For any bone whose aim target is UNMAPPED and therefore wasn't
|
|
1539
|
+
// resolved at setBodyMesh time, compute its bind-local aim direction
|
|
1540
|
+
// now — using tracked bind positions for both endpoints and the
|
|
1541
|
+
// bone's bind-world rotation as the frame.
|
|
1542
|
+
for (const [bone, targetIdx] of Array.from(this._boneAimTargetJointIdx)) {
|
|
1543
|
+
if (this._bindLocalAimDirections.has(bone)) {
|
|
1544
|
+
continue;
|
|
1545
|
+
}
|
|
1546
|
+
const selfJointIdx = this._boneToJointIdx.get(bone);
|
|
1547
|
+
const bindWorldRot = this._bindBoneWorldRotMeshLocal.get(bone);
|
|
1548
|
+
if (selfJointIdx === undefined || !bindWorldRot) {
|
|
1549
|
+
continue;
|
|
1550
|
+
}
|
|
1551
|
+
this._trackedBindDesiredFinalPos[targetIdx].subtractToRef(this._trackedBindDesiredFinalPos[selfJointIdx], this._tempDirection);
|
|
1552
|
+
if (this._tempDirection.lengthSquared() < 1e-8) {
|
|
1553
|
+
continue;
|
|
1554
|
+
}
|
|
1555
|
+
this._tempDirection.normalize();
|
|
1556
|
+
Quaternion.InverseToRef(bindWorldRot, this._tempRotQuat2);
|
|
1557
|
+
this._tempDirection.rotateByQuaternionToRef(this._tempRotQuat2, this._tempLocalDirection);
|
|
1558
|
+
if (this._tempLocalDirection.lengthSquared() < 1e-8) {
|
|
1559
|
+
continue;
|
|
1560
|
+
}
|
|
1561
|
+
this._tempLocalDirection.normalize();
|
|
1562
|
+
this._bindLocalAimDirections.set(bone, this._tempLocalDirection.clone());
|
|
1563
|
+
}
|
|
1564
|
+
this._hasTrackedBind = true;
|
|
1565
|
+
}
|
|
1566
|
+
/**
|
|
1567
|
+
* Delta-from-bind retarget: axis-convention-invariant.
|
|
1568
|
+
*
|
|
1569
|
+
* For each mapped bone in skeleton order (parents first):
|
|
1570
|
+
* let j = joint mapped to this bone
|
|
1571
|
+
* deltaMeshLocalRot = bindTracked[j]⁻¹ × currentTracked[j] (right-side in row-vector)
|
|
1572
|
+
* newBoneWorldRot = bindBoneWorldRot × deltaMeshLocalRot
|
|
1573
|
+
* newBoneLocalRot = newBoneWorldRot × parentNewBoneWorldRot⁻¹
|
|
1574
|
+
*
|
|
1575
|
+
* Positions: root bone receives tracked world delta (so the avatar
|
|
1576
|
+
* translates with the user). All other mapped bones keep their rig's
|
|
1577
|
+
* bind-pose local translation to preserve segment lengths.
|
|
1578
|
+
* @param scaleFactor Additional scale applied to joint local positions.
|
|
1579
|
+
*/
|
|
1580
|
+
_retargetDeltaFromBind(scaleFactor) {
|
|
1581
|
+
if (!this._skeleton || !this._trackedBindDesiredFinalRot || !this._trackedBindDesiredFinalPos) {
|
|
1582
|
+
return;
|
|
1583
|
+
}
|
|
1584
|
+
// Bump the frame id so pooled per-bone rotations are treated as stale.
|
|
1585
|
+
// The Quaternion instances themselves are reused across frames (no allocations).
|
|
1586
|
+
this._currentRetargetFrameId++;
|
|
1587
|
+
for (const bone of this._skeleton.bones) {
|
|
1588
|
+
const parentBone = bone.getParent();
|
|
1589
|
+
const jointIdx = this._boneToJointIdx.get(bone);
|
|
1590
|
+
if (jointIdx === undefined) {
|
|
1591
|
+
// Unmapped bones are handled by the caller.
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const jointTransform = this._jointTransforms[jointIdx];
|
|
1595
|
+
const bindBoneWorldRot = this._bindBoneWorldRotMeshLocal.get(bone);
|
|
1596
|
+
if (!bindBoneWorldRot) {
|
|
1597
|
+
continue;
|
|
1598
|
+
}
|
|
1599
|
+
// Current tracked rotation/position in mesh-local (already in _desiredFinals).
|
|
1600
|
+
this._desiredFinals[jointIdx].decompose(undefined, this._tempTrackedCurRot, this._tempTrackedCurPos);
|
|
1601
|
+
// Babylon Quaternion multiplication is column-vector Hamilton:
|
|
1602
|
+
// worldQ = parentWorldQ.multiply(localQ) (== parent * local)
|
|
1603
|
+
//
|
|
1604
|
+
// World-frame delta joint rotation:
|
|
1605
|
+
// deltaWorld = curJointWorld * bindJointWorld⁻¹
|
|
1606
|
+
Quaternion.InverseToRef(this._trackedBindDesiredFinalRot[jointIdx], this._tempRotQuat2);
|
|
1607
|
+
this._tempTrackedCurRot.multiplyToRef(this._tempRotQuat2, this._tempDeltaQuat);
|
|
1608
|
+
// Apply world delta to bone's bind world rotation:
|
|
1609
|
+
// newBoneWorld = deltaWorld * boneBindWorld
|
|
1610
|
+
this._tempDeltaQuat.multiplyToRef(bindBoneWorldRot, this._tempBoneWorldRot);
|
|
1611
|
+
this._tempBoneWorldRot.normalize();
|
|
1612
|
+
// ── Aim correction (positional) ─────────────────────────────
|
|
1613
|
+
// Meta's emitted rotation for some joints (notably the
|
|
1614
|
+
// forearm, LEFT/RIGHT_ARM_LOWER) is unreliable: the cameras
|
|
1615
|
+
// frequently report a rotation that makes the forearm bone
|
|
1616
|
+
// align with the upper arm, so the elbow "raises" but the
|
|
1617
|
+
// forearm does not bend when the user lifts/rotates a hand.
|
|
1618
|
+
//
|
|
1619
|
+
// The tracked *positions*, however, are accurate. We re-aim
|
|
1620
|
+
// the bone so that the bind-time local aim axis (bone →
|
|
1621
|
+
// child bone) ends up pointing along the live (childJoint −
|
|
1622
|
+
// selfJoint) direction in mesh-local space. This is a single
|
|
1623
|
+
// shortest-arc rotation applied to the world rotation before
|
|
1624
|
+
// it is chained into children.
|
|
1625
|
+
if (this._useBoneOrientationOffsets) {
|
|
1626
|
+
const targetIdx = this._boneAimTargetJointIdx.get(bone);
|
|
1627
|
+
const bindLocalAim = this._bindLocalAimDirections.get(bone);
|
|
1628
|
+
if (bindLocalAim && targetIdx !== undefined) {
|
|
1629
|
+
// Desired aim direction in mesh-local (tracked positions).
|
|
1630
|
+
this._desiredFinalPositions[targetIdx].subtractToRef(this._desiredFinalPositions[jointIdx], this._tempDirection);
|
|
1631
|
+
if (this._tempDirection.lengthSquared() > 1e-8) {
|
|
1632
|
+
this._tempDirection.normalize();
|
|
1633
|
+
// Current aim direction in mesh-local: rotate bind-local aim by newBoneWorld.
|
|
1634
|
+
bindLocalAim.rotateByQuaternionToRef(this._tempBoneWorldRot, this._tempLocalDirection);
|
|
1635
|
+
if (this._tempLocalDirection.lengthSquared() > 1e-8) {
|
|
1636
|
+
this._tempLocalDirection.normalize();
|
|
1637
|
+
// Correction: shortest-arc rotation taking current aim → desired aim.
|
|
1638
|
+
Quaternion.FromUnitVectorsToRef(this._tempLocalDirection, this._tempDirection, this._tempRotQuat2);
|
|
1639
|
+
// Apply correction in world space (pre-multiply column-vector).
|
|
1640
|
+
this._tempRotQuat2.multiplyToRef(this._tempBoneWorldRot, this._tempDeltaQuat);
|
|
1641
|
+
this._tempBoneWorldRot.copyFrom(this._tempDeltaQuat);
|
|
1642
|
+
this._tempBoneWorldRot.normalize();
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
// Store for children's parent lookup (reuse pooled quaternion).
|
|
1648
|
+
let pooled = this._computedBoneNewWorldRot.get(bone);
|
|
1649
|
+
if (!pooled) {
|
|
1650
|
+
pooled = new Quaternion();
|
|
1651
|
+
this._computedBoneNewWorldRot.set(bone, pooled);
|
|
1652
|
+
}
|
|
1653
|
+
pooled.copyFrom(this._tempBoneWorldRot);
|
|
1654
|
+
this._computedBoneNewWorldRotFrameId.set(bone, this._currentRetargetFrameId);
|
|
1655
|
+
// Parent's new world rotation: either the directly-computed
|
|
1656
|
+
// mapped parent's rotation, or the closest mapped ancestor's
|
|
1657
|
+
// rotation chained through any unmapped intermediates' bind
|
|
1658
|
+
// local rotations.
|
|
1659
|
+
let parentNewWorldRot = null;
|
|
1660
|
+
if (parentBone) {
|
|
1661
|
+
const directMapped = this._computedBoneNewWorldRotFrameId.get(parentBone) === this._currentRetargetFrameId ? this._computedBoneNewWorldRot.get(parentBone) : undefined;
|
|
1662
|
+
if (directMapped) {
|
|
1663
|
+
parentNewWorldRot = directMapped;
|
|
1664
|
+
}
|
|
1665
|
+
else {
|
|
1666
|
+
// Walk up to nearest mapped ancestor. Chain any
|
|
1667
|
+
// unmapped intermediates' bind local rotations:
|
|
1668
|
+
// effectiveParentWorld = mappedAncestorNewWorld * child1Local * child2Local * ... * parentBoneLocal
|
|
1669
|
+
// (column-vector composition).
|
|
1670
|
+
const accum = this._tempParentAccumRot;
|
|
1671
|
+
accum.set(0, 0, 0, 1);
|
|
1672
|
+
let cursor = parentBone;
|
|
1673
|
+
while (cursor) {
|
|
1674
|
+
const mapped = this._computedBoneNewWorldRotFrameId.get(cursor) === this._currentRetargetFrameId ? this._computedBoneNewWorldRot.get(cursor) : undefined;
|
|
1675
|
+
if (mapped) {
|
|
1676
|
+
// mapped * accum, where accum is the local chain below.
|
|
1677
|
+
mapped.multiplyToRef(accum, this._tempParentAccumTmp);
|
|
1678
|
+
accum.copyFrom(this._tempParentAccumTmp);
|
|
1679
|
+
parentNewWorldRot = accum;
|
|
1680
|
+
break;
|
|
1681
|
+
}
|
|
1682
|
+
cursor.getLocalMatrix().decompose(undefined, this._tempRotQuat2, undefined);
|
|
1683
|
+
// accum = localCursor * accum (column-vector: cursor's local applied to child chain)
|
|
1684
|
+
this._tempRotQuat2.multiplyToRef(accum, this._tempParentAccumTmp);
|
|
1685
|
+
accum.copyFrom(this._tempParentAccumTmp);
|
|
1686
|
+
cursor = cursor.getParent();
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
}
|
|
1690
|
+
// newBoneLocal = parentNewWorldInv * newBoneWorld
|
|
1691
|
+
if (parentNewWorldRot) {
|
|
1692
|
+
Quaternion.InverseToRef(parentNewWorldRot, this._tempParentNewWorldRotInv);
|
|
1693
|
+
this._tempParentNewWorldRotInv.multiplyToRef(this._tempBoneWorldRot, jointTransform.rotationQuaternion);
|
|
1694
|
+
}
|
|
1695
|
+
else {
|
|
1696
|
+
jointTransform.rotationQuaternion.copyFrom(this._tempBoneWorldRot);
|
|
1697
|
+
}
|
|
1698
|
+
jointTransform.rotationQuaternion.normalize();
|
|
1699
|
+
// Position/scale: preserve rig's bind-local values. Root bone
|
|
1700
|
+
// additionally receives the tracked-position delta so the avatar
|
|
1701
|
+
// translates with the user.
|
|
1702
|
+
const bindLocal = this._mappedBoneBindLocals.get(bone);
|
|
1703
|
+
if (bindLocal) {
|
|
1704
|
+
bindLocal.decompose(this._tempBindLocalScale, undefined, this._tempBindLocalPos);
|
|
1705
|
+
jointTransform.scaling.copyFrom(this._tempBindLocalScale);
|
|
1706
|
+
jointTransform.position.copyFrom(this._tempBindLocalPos);
|
|
1707
|
+
}
|
|
1708
|
+
if (!parentNewWorldRot) {
|
|
1709
|
+
// Root: add tracked mesh-local position delta to bind local.
|
|
1710
|
+
const bindTrackedPos = this._trackedBindDesiredFinalPos[jointIdx];
|
|
1711
|
+
jointTransform.position.addInPlace(this._tempTrackedCurPos).subtractInPlace(bindTrackedPos);
|
|
1712
|
+
}
|
|
1713
|
+
if (scaleFactor !== 1.0) {
|
|
1714
|
+
jointTransform.position.scaleInPlace(scaleFactor);
|
|
1715
|
+
}
|
|
1716
|
+
// Compose and chain final.
|
|
1717
|
+
Matrix.ComposeToRef(jointTransform.scaling, jointTransform.rotationQuaternion, jointTransform.position, this._tempLocalMatrix);
|
|
1718
|
+
if (parentBone) {
|
|
1719
|
+
this._tempLocalMatrix.multiplyToRef(parentBone.getFinalMatrix(), bone.getFinalMatrix());
|
|
1720
|
+
}
|
|
1721
|
+
else {
|
|
1722
|
+
bone.getFinalMatrix().copyFrom(this._tempLocalMatrix);
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
/**
|
|
1727
|
+
* Legacy direct-retarget path (pre-bind-capture). Kept as fallback.
|
|
1728
|
+
* @param useInitialSkinMatrix Skeleton needs initial-skin-matrix chaining for the root.
|
|
1729
|
+
* @param useBoneOrientationOffsets Apply aim-direction correction per mapped bone.
|
|
1730
|
+
* @param scaleFactor Additional scale applied to joint local positions.
|
|
1731
|
+
* @param computedWorldRotations Optional map used by the aim-correction path for parent lookups.
|
|
1732
|
+
*/
|
|
1733
|
+
_retargetDirect(useInitialSkinMatrix, useBoneOrientationOffsets, scaleFactor, computedWorldRotations) {
|
|
1734
|
+
if (!this._skeleton || !this._skeletonMesh) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
for (const bone of this._skeleton.bones) {
|
|
1738
|
+
const parentBone = bone.getParent();
|
|
1739
|
+
const jointIdx = this._boneToJointIdx.get(bone);
|
|
1740
|
+
if (jointIdx !== undefined) {
|
|
1741
|
+
const jointTransform = this._jointTransforms[jointIdx];
|
|
1742
|
+
if (parentBone) {
|
|
1743
|
+
parentBone.getFinalMatrix().invertToRef(this._tempParentMatrix);
|
|
1744
|
+
this._desiredFinals[jointIdx].multiplyToRef(this._tempParentMatrix, this._tempLocalMatrix);
|
|
1745
|
+
}
|
|
1746
|
+
else {
|
|
1747
|
+
if (useInitialSkinMatrix) {
|
|
1748
|
+
this._desiredFinals[jointIdx].multiplyToRef(this._initialSkinMatrixInverse, this._tempLocalMatrix);
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
this._tempLocalMatrix.copyFrom(this._desiredFinals[jointIdx]);
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
this._tempLocalMatrix.decompose(jointTransform.scaling, jointTransform.rotationQuaternion, jointTransform.position);
|
|
1755
|
+
if (useBoneOrientationOffsets) {
|
|
1756
|
+
const childBone = this._mappedChildBones.get(bone);
|
|
1757
|
+
const bindLocalAimDirection = this._bindLocalAimDirections.get(bone);
|
|
1758
|
+
const childJointIdx = childBone ? this._boneToJointIdx.get(childBone) : undefined;
|
|
1759
|
+
if (bindLocalAimDirection && childJointIdx !== undefined) {
|
|
1760
|
+
this._desiredFinalPositions[childJointIdx].subtractToRef(this._desiredFinalPositions[jointIdx], this._tempDirection);
|
|
1761
|
+
if (this._tempDirection.lengthSquared() > 1e-8) {
|
|
1762
|
+
this._tempDirection.normalize();
|
|
1763
|
+
this._desiredFinals[jointIdx].decompose(this._tempScaleVector, this._tempRotQuat, this._tempPosVec);
|
|
1764
|
+
Quaternion.InverseToRef(this._tempRotQuat, this._tempRotQuat2);
|
|
1765
|
+
this._tempDirection.rotateByQuaternionToRef(this._tempRotQuat2, this._tempLocalDirection);
|
|
1766
|
+
if (this._tempLocalDirection.lengthSquared() > 1e-8) {
|
|
1767
|
+
this._tempLocalDirection.normalize();
|
|
1768
|
+
Quaternion.FromUnitVectorsToRef(bindLocalAimDirection, this._tempLocalDirection, this._tempRotQuat2);
|
|
1769
|
+
this._tempRotQuat.multiplyToRef(this._tempRotQuat2, this._tempRotQuat);
|
|
1770
|
+
if (parentBone) {
|
|
1771
|
+
const parentWorldRotation = computedWorldRotations?.get(parentBone);
|
|
1772
|
+
if (parentWorldRotation) {
|
|
1773
|
+
Quaternion.InverseToRef(parentWorldRotation, this._tempRotQuat2);
|
|
1774
|
+
this._tempRotQuat.multiplyToRef(this._tempRotQuat2, jointTransform.rotationQuaternion);
|
|
1775
|
+
}
|
|
1776
|
+
else {
|
|
1777
|
+
jointTransform.rotationQuaternion.copyFrom(this._tempRotQuat);
|
|
1778
|
+
}
|
|
1779
|
+
}
|
|
1780
|
+
else if (useInitialSkinMatrix) {
|
|
1781
|
+
this._skeletonMesh.getPoseMatrix().decompose(this._tempScaleVector, this._tempRotQuat2, this._tempPosVec);
|
|
1782
|
+
Quaternion.InverseToRef(this._tempRotQuat2, this._tempRotQuat2);
|
|
1783
|
+
this._tempRotQuat.multiplyToRef(this._tempRotQuat2, jointTransform.rotationQuaternion);
|
|
1784
|
+
}
|
|
1785
|
+
else {
|
|
1786
|
+
jointTransform.rotationQuaternion.copyFrom(this._tempRotQuat);
|
|
1787
|
+
}
|
|
1788
|
+
jointTransform.rotationQuaternion.normalize();
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1793
|
+
if (this._preserveBindPoseBonePositions) {
|
|
1794
|
+
const bindLocal = this._mappedBoneBindLocals.get(bone);
|
|
1795
|
+
if (bindLocal) {
|
|
1796
|
+
bindLocal.decompose(this._tempScaleVector, this._tempRotQuat, this._tempPosVec);
|
|
1797
|
+
jointTransform.scaling.copyFrom(this._tempScaleVector);
|
|
1798
|
+
if (parentBone) {
|
|
1799
|
+
jointTransform.position.copyFrom(this._tempPosVec);
|
|
1800
|
+
}
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
if (scaleFactor !== 1.0) {
|
|
1804
|
+
jointTransform.position.scaleInPlace(scaleFactor);
|
|
1805
|
+
}
|
|
1806
|
+
Matrix.ComposeToRef(jointTransform.scaling, jointTransform.rotationQuaternion, jointTransform.position, this._tempLocalMatrix);
|
|
1807
|
+
if (parentBone) {
|
|
1808
|
+
this._tempLocalMatrix.multiplyToRef(parentBone.getFinalMatrix(), bone.getFinalMatrix());
|
|
1809
|
+
}
|
|
1810
|
+
else {
|
|
1811
|
+
bone.getFinalMatrix().copyFrom(this._tempLocalMatrix);
|
|
1812
|
+
}
|
|
1813
|
+
if (computedWorldRotations) {
|
|
1814
|
+
bone.getFinalMatrix().decompose(this._tempScaleVector, this._tempRotQuat, this._tempPosVec);
|
|
1815
|
+
computedWorldRotations.set(bone, this._tempRotQuat.clone());
|
|
1816
|
+
}
|
|
1817
|
+
}
|
|
1818
|
+
else {
|
|
1819
|
+
// Unmapped bone: chain bind-pose local × parent final.
|
|
1820
|
+
if (parentBone) {
|
|
1821
|
+
bone.getLocalMatrix().multiplyToRef(parentBone.getFinalMatrix(), bone.getFinalMatrix());
|
|
1822
|
+
}
|
|
1823
|
+
else {
|
|
1824
|
+
bone.getFinalMatrix().copyFrom(bone.getLocalMatrix());
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
/**
|
|
1830
|
+
* Capture a snapshot of the current frame's raw XR joint matrices and
|
|
1831
|
+
* skeleton metadata. Returns a JSON string that can be used offline to
|
|
1832
|
+
* replay / debug bone-local computation without a headset.
|
|
1833
|
+
*
|
|
1834
|
+
* The snapshot includes:
|
|
1835
|
+
* - `jointMatricesRHS` – 83 × 16 raw RHS matrices (before LHS conversion)
|
|
1836
|
+
* - `jointMatricesLHS` – 83 × 16 LHS-converted matrices (after step 3)
|
|
1837
|
+
* - `meshWorldMatrix` – 16 floats, the skeleton mesh's world matrix (if any)
|
|
1838
|
+
* - `jointHasBone` – boolean[83], which joints are mapped to bones
|
|
1839
|
+
* - `jointParentJointIdx` – number[83], mapped ancestor for each joint
|
|
1840
|
+
* - `useRightHandedSystem` – scene handedness setting
|
|
1841
|
+
* - `jointNames` – the 83 joint names in order
|
|
1842
|
+
*
|
|
1843
|
+
* @returns A JSON string with the snapshot data.
|
|
1844
|
+
*/
|
|
1845
|
+
snapshotFrame() {
|
|
1846
|
+
const data = {
|
|
1847
|
+
jointMatricesRHS: Array.from(this._jointTransformMatricesRHS).map((x) => +x.toFixed(3)), // round for readability
|
|
1848
|
+
jointMatricesLHS: Array.from(this._jointTransformMatrices).map((x) => +x.toFixed(3)),
|
|
1849
|
+
meshWorldMatrix: this._skeletonMesh ? Array.from(this._skeletonMesh.getWorldMatrix().toArray()).map((x) => +x.toFixed(3)) : null,
|
|
1850
|
+
jointHasBone: Array.from(this._jointHasBone),
|
|
1851
|
+
jointParentJointIdx: Array.from(this._jointParentJointIdx),
|
|
1852
|
+
useRightHandedSystem: this._scene.useRightHandedSystem,
|
|
1853
|
+
jointNames: BodyJointReferenceArray.slice(),
|
|
1854
|
+
};
|
|
1855
|
+
return JSON.stringify(data);
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Capture a snapshot and copy it to the system clipboard.
|
|
1859
|
+
* Logs to the console on success or failure.
|
|
1860
|
+
* @returns A promise that resolves when the copy completes.
|
|
1861
|
+
*/
|
|
1862
|
+
async snapshotFrameToClipboardAsync() {
|
|
1863
|
+
const json = this.snapshotFrame();
|
|
1864
|
+
// compress the string by removing whitespace (makes it less human-readable but more compact for clipboard)
|
|
1865
|
+
// also remove 0. padding from numbers (e.g. "0.123" → ".123") to further reduce size, since the leading zero is not needed for parsing.
|
|
1866
|
+
const compressed = json.replace(/\s+/g, "").replace(/:0\.(\d+)/g, ":.$1");
|
|
1867
|
+
try {
|
|
1868
|
+
await navigator.clipboard.writeText(compressed);
|
|
1869
|
+
Logger.Log("WebXR Body Tracking: snapshot copied to clipboard (" + compressed.length + " chars)");
|
|
1870
|
+
}
|
|
1871
|
+
catch (e) {
|
|
1872
|
+
Logger.Warn("WebXR Body Tracking: clipboard write failed: " + e + " — logging snapshot to console instead");
|
|
1873
|
+
// eslint-disable-next-line no-console
|
|
1874
|
+
console.log(compressed);
|
|
1875
|
+
}
|
|
1876
|
+
}
|
|
1877
|
+
/**
|
|
1878
|
+
* Dispose of this tracked body and its resources.
|
|
1879
|
+
* @param disposeMesh If `true`, the body mesh and its skeleton are disposed as well.
|
|
1880
|
+
*/
|
|
1881
|
+
dispose(disposeMesh = false) {
|
|
1882
|
+
if (this._bodyMesh) {
|
|
1883
|
+
if (disposeMesh) {
|
|
1884
|
+
this._bodyMesh.skeleton?.dispose();
|
|
1885
|
+
this._bodyMesh.dispose(false, true);
|
|
1886
|
+
}
|
|
1887
|
+
else {
|
|
1888
|
+
this._bodyMesh.isVisible = false;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
for (const transform of this._jointTransforms) {
|
|
1892
|
+
transform.dispose();
|
|
1893
|
+
}
|
|
1894
|
+
this._jointTransforms.length = 0;
|
|
1895
|
+
for (const { standaloneNode } of this._unmappedBoneNodes) {
|
|
1896
|
+
standaloneNode.dispose();
|
|
1897
|
+
}
|
|
1898
|
+
this._unmappedBoneNodes = [];
|
|
1899
|
+
this._boneToJointIdx.clear();
|
|
1900
|
+
this._skeleton = null;
|
|
1901
|
+
this._jointHasBone.fill(false);
|
|
1902
|
+
this._jointParentJointIdx.fill(-1);
|
|
1903
|
+
this._skeletonMesh = null;
|
|
1904
|
+
this.onBodyMeshSetObservable.clear();
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1908
|
+
// WebXRBodyTracking — the feature class
|
|
1909
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
1910
|
+
/**
|
|
1911
|
+
* WebXR Body Tracking feature.
|
|
1912
|
+
*
|
|
1913
|
+
* This feature tracks the user's full-body pose using the
|
|
1914
|
+
* [WebXR Body Tracking Module](https://immersive-web.github.io/body-tracking/),
|
|
1915
|
+
* which exposes 83 articulated joints covering the torso, arms, hands, legs and feet.
|
|
1916
|
+
*
|
|
1917
|
+
* ## Quick Start
|
|
1918
|
+
*
|
|
1919
|
+
* ```typescript
|
|
1920
|
+
* // Enable body tracking when creating the default XR experience:
|
|
1921
|
+
* const xr = await scene.createDefaultXRExperienceAsync();
|
|
1922
|
+
* const bodyTracking = xr.baseExperience.featuresManager.enableFeature(
|
|
1923
|
+
* WebXRFeatureName.BODY_TRACKING,
|
|
1924
|
+
* "latest",
|
|
1925
|
+
* {
|
|
1926
|
+
* bodyMesh: myRiggedBodyMesh,
|
|
1927
|
+
* rigMapping: {
|
|
1928
|
+
* "hips": "Bip01_Pelvis",
|
|
1929
|
+
* "spine-lower": "Bip01_Spine",
|
|
1930
|
+
* // … one entry per joint you want to drive …
|
|
1931
|
+
* },
|
|
1932
|
+
* } as IWebXRBodyTrackingOptions,
|
|
1933
|
+
* );
|
|
1934
|
+
*
|
|
1935
|
+
* // React to tracking changes:
|
|
1936
|
+
* bodyTracking.onBodyTrackingStartedObservable.add((trackedBody) => {
|
|
1937
|
+
* console.log("Body tracking started");
|
|
1938
|
+
* });
|
|
1939
|
+
* bodyTracking.onBodyTrackingFrameUpdateObservable.add((trackedBody) => {
|
|
1940
|
+
* // The tracked body's joint transforms are already up-to-date.
|
|
1941
|
+
* });
|
|
1942
|
+
* ```
|
|
1943
|
+
*
|
|
1944
|
+
* ## How It Works
|
|
1945
|
+
*
|
|
1946
|
+
* 1. The feature requests the `"body-tracking"` native WebXR feature at session start.
|
|
1947
|
+
* 2. Each frame, if `XRFrame.body` is available, joint poses are filled into a
|
|
1948
|
+
* flat Float32Array via the batch `fillPoses()` API (with a per-joint fallback).
|
|
1949
|
+
* 3. The 4 × 4 matrices are converted from WebXR right-handed coordinates to
|
|
1950
|
+
* Babylon.js left-handed coordinates in-place (unless the scene is RHS).
|
|
1951
|
+
* 4. Each matrix is decomposed and written to a TransformNode; skeleton bones
|
|
1952
|
+
* linked to those nodes animate the rigged mesh automatically.
|
|
1953
|
+
*
|
|
1954
|
+
* ## Coordinate System
|
|
1955
|
+
*
|
|
1956
|
+
* WebXR data arrives in a **right-handed** coordinate system. Babylon.js
|
|
1957
|
+
* defaults to **left-handed**. The conversion is handled automatically:
|
|
1958
|
+
* - Joint matrices are flipped in-place (Z-negation of specific matrix elements).
|
|
1959
|
+
* - For meshes authored in a right-handed tool (glTF, Blender, etc.), the bone
|
|
1960
|
+
* data is un-flipped so the skeleton interprets poses correctly.
|
|
1961
|
+
* - If you use `scene.useRightHandedSystem = true`, no conversion is applied.
|
|
1962
|
+
*
|
|
1963
|
+
* @see https://immersive-web.github.io/body-tracking/
|
|
1964
|
+
*/
|
|
1965
|
+
export class WebXRBodyTracking extends WebXRAbstractFeature {
|
|
1966
|
+
/**
|
|
1967
|
+
* Get the currently tracked body, if any.
|
|
1968
|
+
*/
|
|
1969
|
+
get trackedBody() {
|
|
1970
|
+
return this._trackedBody;
|
|
1971
|
+
}
|
|
1972
|
+
/**
|
|
1973
|
+
* Returns `true` while body tracking data is actively being received.
|
|
1974
|
+
*/
|
|
1975
|
+
get isTracking() {
|
|
1976
|
+
return this._isTracking;
|
|
1977
|
+
}
|
|
1978
|
+
/**
|
|
1979
|
+
* Construct a new WebXRBodyTracking feature.
|
|
1980
|
+
* @param _xrSessionManager The XR session manager.
|
|
1981
|
+
* @param options Configuration options.
|
|
1982
|
+
*/
|
|
1983
|
+
constructor(_xrSessionManager,
|
|
1984
|
+
/** Configuration options for the body tracking feature. */
|
|
1985
|
+
options = {}) {
|
|
1986
|
+
super(_xrSessionManager);
|
|
1987
|
+
this.options = options;
|
|
1988
|
+
/**
|
|
1989
|
+
* Observable fired when body tracking starts (i.e. the first frame where
|
|
1990
|
+
* `XRFrame.body` returns valid data).
|
|
1991
|
+
*/
|
|
1992
|
+
this.onBodyTrackingStartedObservable = new Observable();
|
|
1993
|
+
/**
|
|
1994
|
+
* Observable fired when body tracking is lost (i.e. `XRFrame.body` becomes
|
|
1995
|
+
* `null` or returns no valid poses after previously tracking).
|
|
1996
|
+
*/
|
|
1997
|
+
this.onBodyTrackingEndedObservable = new Observable();
|
|
1998
|
+
/**
|
|
1999
|
+
* Observable fired every frame that has valid body tracking data.
|
|
2000
|
+
* At the point of notification, all joint transforms are up-to-date.
|
|
2001
|
+
*/
|
|
2002
|
+
this.onBodyTrackingFrameUpdateObservable = new Observable();
|
|
2003
|
+
/**
|
|
2004
|
+
* Observable fired when the body mesh has been set via {@link setBodyMesh}
|
|
2005
|
+
* or during initial configuration.
|
|
2006
|
+
*/
|
|
2007
|
+
this.onBodyMeshSetObservable = new Observable();
|
|
2008
|
+
/** The current tracked body, or null when not tracking. */
|
|
2009
|
+
this._trackedBody = null;
|
|
2010
|
+
/** True while we have an active body tracking session. */
|
|
2011
|
+
this._isTracking = false;
|
|
2012
|
+
/** Observer for world scale changes, so the body mesh can be rescaled. */
|
|
2013
|
+
this._worldScaleObserver = null;
|
|
2014
|
+
/**
|
|
2015
|
+
* Debug info from the feature-level frame loop.
|
|
2016
|
+
* Shows why `_onXRFrame` did or did not call `updateFromXRFrame`.
|
|
2017
|
+
* @internal
|
|
2018
|
+
*/
|
|
2019
|
+
this._lastFrameDebugInfo = "not called";
|
|
2020
|
+
this.xrNativeFeatureName = "body-tracking";
|
|
2021
|
+
}
|
|
2022
|
+
/**
|
|
2023
|
+
* Attach a rigged body mesh (or replace the current one) at any time.
|
|
2024
|
+
*
|
|
2025
|
+
* This is a convenience method that forwards to the underlying
|
|
2026
|
+
* {@link WebXRTrackedBody.setBodyMesh}. The body does not need to be
|
|
2027
|
+
* already tracking for this to work — the mesh will be applied once
|
|
2028
|
+
* tracking begins.
|
|
2029
|
+
*
|
|
2030
|
+
* @param bodyMesh The rigged mesh to drive.
|
|
2031
|
+
* @param rigMapping Optional mapping from {@link WebXRBodyJoint} names to bone names.
|
|
2032
|
+
*/
|
|
2033
|
+
setBodyMesh(bodyMesh, rigMapping) {
|
|
2034
|
+
// Store on options so it is picked up on (re-)attach.
|
|
2035
|
+
this.options.bodyMesh = bodyMesh;
|
|
2036
|
+
this.options.rigMapping = rigMapping;
|
|
2037
|
+
if (this._trackedBody) {
|
|
2038
|
+
this._trackedBody.setBodyMesh(bodyMesh, rigMapping);
|
|
2039
|
+
this.onBodyMeshSetObservable.notifyObservers(this._trackedBody);
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
// ── Attach / Detach ──────────────────────────────────────────────────
|
|
2043
|
+
/**
|
|
2044
|
+
* Attach the feature.
|
|
2045
|
+
* Called by the features manager when the XR session initialises.
|
|
2046
|
+
*
|
|
2047
|
+
* Body tracking is a draft WebXR spec. Some UAs (e.g. Meta Quest) provide
|
|
2048
|
+
* body data on `XRFrame.body` but do not list `"body-tracking"` in
|
|
2049
|
+
* `session.enabledFeatures`. To handle this, we temporarily clear
|
|
2050
|
+
* {@link xrNativeFeatureName} before calling the base `attach()` so the
|
|
2051
|
+
* enabled-features check is skipped, then restore it afterwards.
|
|
2052
|
+
* @returns `true` if attachment succeeded.
|
|
2053
|
+
*/
|
|
2054
|
+
attach() {
|
|
2055
|
+
// Temporarily clear the native feature name so super.attach() does
|
|
2056
|
+
// not reject when "body-tracking" is absent from enabledFeatures.
|
|
2057
|
+
// The actual data availability is checked every frame in _onXRFrame.
|
|
2058
|
+
const nativeName = this.xrNativeFeatureName;
|
|
2059
|
+
this.xrNativeFeatureName = "";
|
|
2060
|
+
const attached = super.attach();
|
|
2061
|
+
this.xrNativeFeatureName = nativeName;
|
|
2062
|
+
if (!attached) {
|
|
2063
|
+
this._lastFrameDebugInfo = "super.attach() returned false";
|
|
2064
|
+
return false;
|
|
2065
|
+
}
|
|
2066
|
+
// Create the tracked body container (transform nodes etc.).
|
|
2067
|
+
try {
|
|
2068
|
+
let rigMapping = this.options.rigMapping;
|
|
2069
|
+
let aimChildOverrides = this.options.aimChildOverrides;
|
|
2070
|
+
let useBoneOrientationOffsets = this.options.useBoneOrientationOffsets;
|
|
2071
|
+
if (this.options.isMixamoModel && this.options.bodyMesh) {
|
|
2072
|
+
rigMapping = rigMapping ?? _ResolveMixamoRigMapping(this.options.bodyMesh);
|
|
2073
|
+
aimChildOverrides = aimChildOverrides ?? MixamoAimChildOverrides;
|
|
2074
|
+
useBoneOrientationOffsets = useBoneOrientationOffsets ?? true;
|
|
2075
|
+
}
|
|
2076
|
+
this._trackedBody = new WebXRTrackedBody(this._xrSessionManager.scene, this.options.bodyMesh, rigMapping, this.options.jointScaleFactor ?? 1.0, this.options.preserveBindPoseBonePositions ?? false, useBoneOrientationOffsets ?? false, aimChildOverrides, this.options.jointLocalRotationOffset);
|
|
2077
|
+
}
|
|
2078
|
+
catch (e) {
|
|
2079
|
+
this._lastFrameDebugInfo = "ATTACH ERROR: " + e;
|
|
2080
|
+
Logger.Warn("WebXR Body Tracking: failed to create tracked body: " + e);
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
// Observe world-scale changes to rescale the body mesh.
|
|
2084
|
+
if (this.options.bodyMesh) {
|
|
2085
|
+
this.options.bodyMesh.scaling.setAll(this._xrSessionManager.worldScalingFactor);
|
|
2086
|
+
this._worldScaleObserver = this._xrSessionManager.onWorldScaleFactorChangedObservable.add((factors) => {
|
|
2087
|
+
if (this.options.bodyMesh) {
|
|
2088
|
+
this.options.bodyMesh.scaling.scaleInPlace(factors.newScaleFactor / factors.previousScaleFactor);
|
|
2089
|
+
}
|
|
2090
|
+
});
|
|
2091
|
+
}
|
|
2092
|
+
this._lastFrameDebugInfo = "attached OK, waiting for frames";
|
|
2093
|
+
return true;
|
|
2094
|
+
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Detach the feature.
|
|
2097
|
+
* Called by the features manager when the XR session ends.
|
|
2098
|
+
* @returns `true` if detachment succeeded.
|
|
2099
|
+
*/
|
|
2100
|
+
detach() {
|
|
2101
|
+
if (!super.detach()) {
|
|
2102
|
+
return false;
|
|
2103
|
+
}
|
|
2104
|
+
if (this._isTracking) {
|
|
2105
|
+
this._isTracking = false;
|
|
2106
|
+
this.onBodyTrackingEndedObservable.notifyObservers();
|
|
2107
|
+
}
|
|
2108
|
+
if (this._trackedBody) {
|
|
2109
|
+
this._trackedBody.dispose();
|
|
2110
|
+
this._trackedBody = null;
|
|
2111
|
+
}
|
|
2112
|
+
if (this._worldScaleObserver) {
|
|
2113
|
+
this._xrSessionManager.onWorldScaleFactorChangedObservable.remove(this._worldScaleObserver);
|
|
2114
|
+
this._worldScaleObserver = null;
|
|
2115
|
+
}
|
|
2116
|
+
return true;
|
|
2117
|
+
}
|
|
2118
|
+
/**
|
|
2119
|
+
* Dispose this feature and all resources.
|
|
2120
|
+
*/
|
|
2121
|
+
dispose() {
|
|
2122
|
+
super.dispose();
|
|
2123
|
+
this.onBodyTrackingStartedObservable.clear();
|
|
2124
|
+
this.onBodyTrackingEndedObservable.clear();
|
|
2125
|
+
this.onBodyTrackingFrameUpdateObservable.clear();
|
|
2126
|
+
this.onBodyMeshSetObservable.clear();
|
|
2127
|
+
}
|
|
2128
|
+
// ── Frame loop ───────────────────────────────────────────────────────
|
|
2129
|
+
/**
|
|
2130
|
+
* Called every XR frame by the base class.
|
|
2131
|
+
* Reads body joint data from the XR runtime and updates transforms.
|
|
2132
|
+
* @param xrFrame The current XRFrame.
|
|
2133
|
+
*/
|
|
2134
|
+
_onXRFrame(xrFrame) {
|
|
2135
|
+
try {
|
|
2136
|
+
if (!this._trackedBody) {
|
|
2137
|
+
this._lastFrameDebugInfo = "no trackedBody";
|
|
2138
|
+
return;
|
|
2139
|
+
}
|
|
2140
|
+
const body = xrFrame.body;
|
|
2141
|
+
if (!body) {
|
|
2142
|
+
this._lastFrameDebugInfo = "no xrFrame.body (hasBody prop:" + ("body" in xrFrame) + ")";
|
|
2143
|
+
// Tracking lost this frame.
|
|
2144
|
+
if (this._isTracking) {
|
|
2145
|
+
this._isTracking = false;
|
|
2146
|
+
this.onBodyTrackingEndedObservable.notifyObservers();
|
|
2147
|
+
}
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
this._lastFrameDebugInfo = "body.size:" + body.size + " calling update";
|
|
2151
|
+
const success = this._trackedBody.updateFromXRFrame(xrFrame, this._xrSessionManager.referenceSpace, this._xrSessionManager.scene.activeCamera?.parent ?? null);
|
|
2152
|
+
this._lastFrameDebugInfo = "body.size:" + body.size + " update:" + success + " | " + this._trackedBody._lastDebugInfo;
|
|
2153
|
+
if (!success) {
|
|
2154
|
+
if (this._isTracking) {
|
|
2155
|
+
this._isTracking = false;
|
|
2156
|
+
this.onBodyTrackingEndedObservable.notifyObservers();
|
|
2157
|
+
}
|
|
2158
|
+
return;
|
|
2159
|
+
}
|
|
2160
|
+
// Detect tracking start / continuation.
|
|
2161
|
+
if (!this._isTracking) {
|
|
2162
|
+
this._isTracking = true;
|
|
2163
|
+
this.onBodyTrackingStartedObservable.notifyObservers(this._trackedBody);
|
|
2164
|
+
}
|
|
2165
|
+
this.onBodyTrackingFrameUpdateObservable.notifyObservers(this._trackedBody);
|
|
2166
|
+
}
|
|
2167
|
+
catch (e) {
|
|
2168
|
+
// Catch absolutely everything so we never break the XR render loop.
|
|
2169
|
+
this._lastFrameDebugInfo = "EXCEPTION: " + e;
|
|
2170
|
+
Logger.Warn("WebXR Body Tracking: error in _onXRFrame: " + e);
|
|
2171
|
+
}
|
|
2172
|
+
}
|
|
2173
|
+
/**
|
|
2174
|
+
* Returns the complete ordered list of body joint names tracked by this feature.
|
|
2175
|
+
* Useful for iterating over all joints or building UI.
|
|
2176
|
+
*/
|
|
2177
|
+
static get AllBodyJoints() {
|
|
2178
|
+
return BodyJointReferenceArray;
|
|
2179
|
+
}
|
|
2180
|
+
/**
|
|
2181
|
+
* Capture a single-frame snapshot of all 83 joints and copy it to the
|
|
2182
|
+
* clipboard. Call this from a playground button or the console while
|
|
2183
|
+
* wearing the headset:
|
|
2184
|
+
*
|
|
2185
|
+
* ```typescript
|
|
2186
|
+
* bodyTracking.snapshotFrameToClipboard();
|
|
2187
|
+
* ```
|
|
2188
|
+
*
|
|
2189
|
+
* The JSON can later be loaded offline to replay the bone-local
|
|
2190
|
+
* computation without a headset.
|
|
2191
|
+
* @returns A promise that resolves when the copy completes, or rejects
|
|
2192
|
+
* if no body is currently tracked.
|
|
2193
|
+
*/
|
|
2194
|
+
async snapshotFrameToClipboardAsync() {
|
|
2195
|
+
if (!this._trackedBody) {
|
|
2196
|
+
Logger.Warn("WebXR Body Tracking: no tracked body to snapshot");
|
|
2197
|
+
return;
|
|
2198
|
+
}
|
|
2199
|
+
return await this._trackedBody.snapshotFrameToClipboardAsync();
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
/**
|
|
2203
|
+
* The module's name, used when enabling the feature on the features manager.
|
|
2204
|
+
* Value: `"xr-body-tracking"`.
|
|
2205
|
+
*/
|
|
2206
|
+
WebXRBodyTracking.Name = WebXRFeatureName.BODY_TRACKING;
|
|
2207
|
+
/**
|
|
2208
|
+
* The (Babylon) version of this module.
|
|
2209
|
+
* This is an integer representing the implementation version.
|
|
2210
|
+
* This number does not correspond to the WebXR specs version.
|
|
2211
|
+
*/
|
|
2212
|
+
WebXRBodyTracking.Version = 1;
|
|
2213
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2214
|
+
// Feature registration
|
|
2215
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
2216
|
+
// Register the feature so it can be enabled via the features manager:
|
|
2217
|
+
// featuresManager.enableFeature(WebXRFeatureName.BODY_TRACKING, "latest", options);
|
|
2218
|
+
WebXRFeaturesManager.AddWebXRFeature(WebXRBodyTracking.Name, (xrSessionManager, options) => {
|
|
2219
|
+
return () => new WebXRBodyTracking(xrSessionManager, options);
|
|
2220
|
+
}, WebXRBodyTracking.Version, false);
|
|
2221
|
+
//# sourceMappingURL=WebXRBodyTracking.js.map
|