@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.
Files changed (84) hide show
  1. package/Engines/AbstractEngine/abstractEngine.textureSelector.d.ts +45 -0
  2. package/Engines/AbstractEngine/abstractEngine.textureSelector.js +69 -0
  3. package/Engines/AbstractEngine/abstractEngine.textureSelector.js.map +1 -0
  4. package/Engines/AbstractEngine/index.d.ts +2 -0
  5. package/Engines/AbstractEngine/index.js +4 -0
  6. package/Engines/AbstractEngine/index.js.map +1 -1
  7. package/Engines/Extensions/engine.textureSelector.d.ts +2 -45
  8. package/Engines/Extensions/engine.textureSelector.js +8 -68
  9. package/Engines/Extensions/engine.textureSelector.js.map +1 -1
  10. package/Engines/abstractEngine.js +2 -2
  11. package/Engines/abstractEngine.js.map +1 -1
  12. package/FlowGraph/flowGraph.d.ts +22 -0
  13. package/FlowGraph/flowGraph.js +11 -0
  14. package/FlowGraph/flowGraph.js.map +1 -1
  15. package/FlowGraph/flowGraphCoordinator.d.ts +2 -1
  16. package/FlowGraph/flowGraphCoordinator.js +4 -2
  17. package/FlowGraph/flowGraphCoordinator.js.map +1 -1
  18. package/FlowGraph/flowGraphParser.js +7 -0
  19. package/FlowGraph/flowGraphParser.js.map +1 -1
  20. package/FlowGraph/typeDefinitions.d.ts +8 -0
  21. package/FlowGraph/typeDefinitions.js.map +1 -1
  22. package/Lights/Clustered/clusteredLightContainer.d.ts +1 -0
  23. package/Lights/Clustered/clusteredLightContainer.js +19 -0
  24. package/Lights/Clustered/clusteredLightContainer.js.map +1 -1
  25. package/Lights/light.d.ts +6 -0
  26. package/Lights/light.js +8 -0
  27. package/Lights/light.js.map +1 -1
  28. package/Lights/spotLight.d.ts +2 -0
  29. package/Lights/spotLight.js +10 -0
  30. package/Lights/spotLight.js.map +1 -1
  31. package/Materials/Background/backgroundMaterial.js +4 -1
  32. package/Materials/Background/backgroundMaterial.js.map +1 -1
  33. package/Materials/GaussianSplatting/gaussianSplattingMaterial.js +6 -2
  34. package/Materials/GaussianSplatting/gaussianSplattingMaterial.js.map +1 -1
  35. package/Materials/Node/Blocks/Dual/lightBlock.d.ts +8 -0
  36. package/Materials/Node/Blocks/Dual/lightBlock.js +16 -0
  37. package/Materials/Node/Blocks/Dual/lightBlock.js.map +1 -1
  38. package/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.js +3 -0
  39. package/Materials/Node/Blocks/PBR/pbrMetallicRoughnessBlock.js.map +1 -1
  40. package/Materials/Node/nodeMaterial.js +4 -1
  41. package/Materials/Node/nodeMaterial.js.map +1 -1
  42. package/Materials/PBR/openpbrMaterial.js +4 -1
  43. package/Materials/PBR/openpbrMaterial.js.map +1 -1
  44. package/Materials/PBR/pbrBaseMaterial.js +4 -1
  45. package/Materials/PBR/pbrBaseMaterial.js.map +1 -1
  46. package/Materials/materialHelper.functions.d.ts +12 -0
  47. package/Materials/materialHelper.functions.js +24 -0
  48. package/Materials/materialHelper.functions.js.map +1 -1
  49. package/Materials/standardMaterial.js +4 -1
  50. package/Materials/standardMaterial.js.map +1 -1
  51. package/Meshes/GaussianSplatting/gaussianSplattingMesh.d.ts +13 -0
  52. package/Meshes/GaussianSplatting/gaussianSplattingMesh.js +26 -0
  53. package/Meshes/GaussianSplatting/gaussianSplattingMesh.js.map +1 -1
  54. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.d.ts +3 -0
  55. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js +113 -10
  56. package/Meshes/GaussianSplatting/gaussianSplattingMeshBase.js.map +1 -1
  57. package/Misc/tools.js +1 -1
  58. package/Misc/tools.js.map +1 -1
  59. package/Particles/gpuParticleSystem.d.ts +35 -2
  60. package/Particles/gpuParticleSystem.js +272 -6
  61. package/Particles/gpuParticleSystem.js.map +1 -1
  62. package/Particles/thinParticleSystem.js +5 -0
  63. package/Particles/thinParticleSystem.js.map +1 -1
  64. package/Rendering/depthRenderer.d.ts +8 -0
  65. package/Rendering/depthRenderer.js +48 -13
  66. package/Rendering/depthRenderer.js.map +1 -1
  67. package/Rendering/depthRendererSceneComponent.d.ts +1 -0
  68. package/Rendering/depthRendererSceneComponent.js +26 -0
  69. package/Rendering/depthRendererSceneComponent.js.map +1 -1
  70. package/Shaders/gpuRenderParticles.vertex.js +7 -0
  71. package/Shaders/gpuRenderParticles.vertex.js.map +1 -1
  72. package/XR/features/WebXRBodyTracking.d.ts +952 -0
  73. package/XR/features/WebXRBodyTracking.js +2221 -0
  74. package/XR/features/WebXRBodyTracking.js.map +1 -0
  75. package/XR/features/index.d.ts +1 -0
  76. package/XR/features/index.js +1 -0
  77. package/XR/features/index.js.map +1 -1
  78. package/XR/webXRFeaturesManager.d.ts +7 -0
  79. package/XR/webXRFeaturesManager.js +4 -0
  80. package/XR/webXRFeaturesManager.js.map +1 -1
  81. package/package.json +1 -1
  82. package/sceneComponent.d.ts +1 -0
  83. package/sceneComponent.js +1 -0
  84. 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