@aura3d/animation 1.0.6

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 (187) hide show
  1. package/README.md +19 -0
  2. package/dist/AnimationAction.d.ts +40 -0
  3. package/dist/AnimationAction.d.ts.map +1 -0
  4. package/dist/AnimationAction.js +143 -0
  5. package/dist/AnimationAction.js.map +1 -0
  6. package/dist/AnimationClip.d.ts +24 -0
  7. package/dist/AnimationClip.d.ts.map +1 -0
  8. package/dist/AnimationClip.js +45 -0
  9. package/dist/AnimationClip.js.map +1 -0
  10. package/dist/AnimationClipEvents.d.ts +57 -0
  11. package/dist/AnimationClipEvents.d.ts.map +1 -0
  12. package/dist/AnimationClipEvents.js +171 -0
  13. package/dist/AnimationClipEvents.js.map +1 -0
  14. package/dist/AnimationClipRegistry.d.ts +76 -0
  15. package/dist/AnimationClipRegistry.d.ts.map +1 -0
  16. package/dist/AnimationClipRegistry.js +130 -0
  17. package/dist/AnimationClipRegistry.js.map +1 -0
  18. package/dist/AnimationController.d.ts +168 -0
  19. package/dist/AnimationController.d.ts.map +1 -0
  20. package/dist/AnimationController.js +619 -0
  21. package/dist/AnimationController.js.map +1 -0
  22. package/dist/AnimationEvents.d.ts +10 -0
  23. package/dist/AnimationEvents.d.ts.map +1 -0
  24. package/dist/AnimationEvents.js +26 -0
  25. package/dist/AnimationEvents.js.map +1 -0
  26. package/dist/AnimationLayer.d.ts +27 -0
  27. package/dist/AnimationLayer.d.ts.map +1 -0
  28. package/dist/AnimationLayer.js +53 -0
  29. package/dist/AnimationLayer.js.map +1 -0
  30. package/dist/AnimationMixer.d.ts +54 -0
  31. package/dist/AnimationMixer.d.ts.map +1 -0
  32. package/dist/AnimationMixer.js +319 -0
  33. package/dist/AnimationMixer.js.map +1 -0
  34. package/dist/AnimationStateGraph.d.ts +2 -0
  35. package/dist/AnimationStateGraph.d.ts.map +1 -0
  36. package/dist/AnimationStateGraph.js +2 -0
  37. package/dist/AnimationStateGraph.js.map +1 -0
  38. package/dist/AnimationStateMachine.d.ts +59 -0
  39. package/dist/AnimationStateMachine.d.ts.map +1 -0
  40. package/dist/AnimationStateMachine.js +154 -0
  41. package/dist/AnimationStateMachine.js.map +1 -0
  42. package/dist/AnimationTrack.d.ts +23 -0
  43. package/dist/AnimationTrack.d.ts.map +1 -0
  44. package/dist/AnimationTrack.js +124 -0
  45. package/dist/AnimationTrack.js.map +1 -0
  46. package/dist/BlendTree.d.ts +23 -0
  47. package/dist/BlendTree.d.ts.map +1 -0
  48. package/dist/BlendTree.js +68 -0
  49. package/dist/BlendTree.js.map +1 -0
  50. package/dist/Bone.d.ts +20 -0
  51. package/dist/Bone.d.ts.map +1 -0
  52. package/dist/Bone.js +24 -0
  53. package/dist/Bone.js.map +1 -0
  54. package/dist/CrowdAnimation.d.ts +13 -0
  55. package/dist/CrowdAnimation.d.ts.map +1 -0
  56. package/dist/CrowdAnimation.js +14 -0
  57. package/dist/CrowdAnimation.js.map +1 -0
  58. package/dist/ECSAnimationBridge.d.ts +9 -0
  59. package/dist/ECSAnimationBridge.d.ts.map +1 -0
  60. package/dist/ECSAnimationBridge.js +24 -0
  61. package/dist/ECSAnimationBridge.js.map +1 -0
  62. package/dist/HumanoidRetargeting.d.ts +76 -0
  63. package/dist/HumanoidRetargeting.d.ts.map +1 -0
  64. package/dist/HumanoidRetargeting.js +331 -0
  65. package/dist/HumanoidRetargeting.js.map +1 -0
  66. package/dist/IK.d.ts +24 -0
  67. package/dist/IK.d.ts.map +1 -0
  68. package/dist/IK.js +76 -0
  69. package/dist/IK.js.map +1 -0
  70. package/dist/Keyframe.d.ts +50 -0
  71. package/dist/Keyframe.d.ts.map +1 -0
  72. package/dist/Keyframe.js +118 -0
  73. package/dist/Keyframe.js.map +1 -0
  74. package/dist/LocomotionController.d.ts +47 -0
  75. package/dist/LocomotionController.d.ts.map +1 -0
  76. package/dist/LocomotionController.js +106 -0
  77. package/dist/LocomotionController.js.map +1 -0
  78. package/dist/MotionMatchingFixtures.d.ts +58 -0
  79. package/dist/MotionMatchingFixtures.d.ts.map +1 -0
  80. package/dist/MotionMatchingFixtures.js +175 -0
  81. package/dist/MotionMatchingFixtures.js.map +1 -0
  82. package/dist/MotionQuality.d.ts +36 -0
  83. package/dist/MotionQuality.d.ts.map +1 -0
  84. package/dist/MotionQuality.js +117 -0
  85. package/dist/MotionQuality.js.map +1 -0
  86. package/dist/Retargeting.d.ts +12 -0
  87. package/dist/Retargeting.d.ts.map +1 -0
  88. package/dist/Retargeting.js +19 -0
  89. package/dist/Retargeting.js.map +1 -0
  90. package/dist/RootMotion.d.ts +21 -0
  91. package/dist/RootMotion.d.ts.map +1 -0
  92. package/dist/RootMotion.js +84 -0
  93. package/dist/RootMotion.js.map +1 -0
  94. package/dist/SceneAnimationBridge.d.ts +18 -0
  95. package/dist/SceneAnimationBridge.d.ts.map +1 -0
  96. package/dist/SceneAnimationBridge.js +69 -0
  97. package/dist/SceneAnimationBridge.js.map +1 -0
  98. package/dist/SecondaryAnimationFixtures.d.ts +55 -0
  99. package/dist/SecondaryAnimationFixtures.d.ts.map +1 -0
  100. package/dist/SecondaryAnimationFixtures.js +147 -0
  101. package/dist/SecondaryAnimationFixtures.js.map +1 -0
  102. package/dist/Skeleton.d.ts +9 -0
  103. package/dist/Skeleton.d.ts.map +1 -0
  104. package/dist/Skeleton.js +31 -0
  105. package/dist/Skeleton.js.map +1 -0
  106. package/dist/Skinning.d.ts +7 -0
  107. package/dist/Skinning.d.ts.map +1 -0
  108. package/dist/Skinning.js +12 -0
  109. package/dist/Skinning.js.map +1 -0
  110. package/dist/browser-index.d.ts +39 -0
  111. package/dist/browser-index.d.ts.map +1 -0
  112. package/dist/browser-index.js +34 -0
  113. package/dist/browser-index.js.map +1 -0
  114. package/dist/index.d.ts +40 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +35 -0
  117. package/dist/index.js.map +1 -0
  118. package/dist/threejs-compatibility/AnimationAction.d.ts +16 -0
  119. package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -0
  120. package/dist/threejs-compatibility/AnimationAction.js +25 -0
  121. package/dist/threejs-compatibility/AnimationAction.js.map +1 -0
  122. package/dist/threejs-compatibility/AnimationClip.d.ts +14 -0
  123. package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -0
  124. package/dist/threejs-compatibility/AnimationClip.js +11 -0
  125. package/dist/threejs-compatibility/AnimationClip.js.map +1 -0
  126. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts +44 -0
  127. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -0
  128. package/dist/threejs-compatibility/AnimationDiagnostics.js +29 -0
  129. package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -0
  130. package/dist/threejs-compatibility/AnimationMixer.d.ts +9 -0
  131. package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -0
  132. package/dist/threejs-compatibility/AnimationMixer.js +24 -0
  133. package/dist/threejs-compatibility/AnimationMixer.js.map +1 -0
  134. package/dist/threejs-compatibility/MorphTargetMixer.d.ts +10 -0
  135. package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -0
  136. package/dist/threejs-compatibility/MorphTargetMixer.js +10 -0
  137. package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -0
  138. package/dist/threejs-compatibility/Skeleton.d.ts +10 -0
  139. package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -0
  140. package/dist/threejs-compatibility/Skeleton.js +8 -0
  141. package/dist/threejs-compatibility/Skeleton.js.map +1 -0
  142. package/dist/threejs-compatibility/SkinnedMesh.d.ts +11 -0
  143. package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -0
  144. package/dist/threejs-compatibility/SkinnedMesh.js +16 -0
  145. package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -0
  146. package/dist/threejs-compatibility/index.d.ts +12 -0
  147. package/dist/threejs-compatibility/index.d.ts.map +1 -0
  148. package/dist/threejs-compatibility/index.js +8 -0
  149. package/dist/threejs-compatibility/index.js.map +1 -0
  150. package/package.json +17 -0
  151. package/src/AnimationAction.ts +166 -0
  152. package/src/AnimationClip.ts +63 -0
  153. package/src/AnimationClipEvents.ts +320 -0
  154. package/src/AnimationClipRegistry.ts +280 -0
  155. package/src/AnimationController.ts +967 -0
  156. package/src/AnimationEvents.ts +34 -0
  157. package/src/AnimationLayer.ts +73 -0
  158. package/src/AnimationMixer.ts +366 -0
  159. package/src/AnimationStateGraph.ts +8 -0
  160. package/src/AnimationStateMachine.ts +217 -0
  161. package/src/AnimationTrack.ts +176 -0
  162. package/src/BlendTree.ts +89 -0
  163. package/src/Bone.ts +35 -0
  164. package/src/CrowdAnimation.ts +25 -0
  165. package/src/ECSAnimationBridge.ts +30 -0
  166. package/src/HumanoidRetargeting.ts +476 -0
  167. package/src/IK.ts +107 -0
  168. package/src/Keyframe.ts +159 -0
  169. package/src/LocomotionController.ts +149 -0
  170. package/src/MotionMatchingFixtures.ts +254 -0
  171. package/src/MotionQuality.ts +156 -0
  172. package/src/Retargeting.ts +29 -0
  173. package/src/RootMotion.ts +124 -0
  174. package/src/SceneAnimationBridge.ts +80 -0
  175. package/src/SecondaryAnimationFixtures.ts +212 -0
  176. package/src/Skeleton.ts +34 -0
  177. package/src/Skinning.ts +18 -0
  178. package/src/browser-index.ts +55 -0
  179. package/src/index.ts +56 -0
  180. package/src/threejs-compatibility/AnimationAction.ts +27 -0
  181. package/src/threejs-compatibility/AnimationClip.ts +16 -0
  182. package/src/threejs-compatibility/AnimationDiagnostics.ts +42 -0
  183. package/src/threejs-compatibility/AnimationMixer.ts +24 -0
  184. package/src/threejs-compatibility/MorphTargetMixer.ts +16 -0
  185. package/src/threejs-compatibility/Skeleton.ts +9 -0
  186. package/src/threejs-compatibility/SkinnedMesh.ts +12 -0
  187. package/src/threejs-compatibility/index.ts +11 -0
@@ -0,0 +1,476 @@
1
+ import type {
2
+ AnimationPose,
3
+ AnimationPoseTransform,
4
+ AnimationQuaternion,
5
+ AnimationVector3
6
+ } from "./AnimationController.js";
7
+
8
+ export const HUMANOID_BONES = [
9
+ "hips",
10
+ "spine",
11
+ "chest",
12
+ "upperChest",
13
+ "neck",
14
+ "head",
15
+ "leftShoulder",
16
+ "leftUpperArm",
17
+ "leftLowerArm",
18
+ "leftHand",
19
+ "rightShoulder",
20
+ "rightUpperArm",
21
+ "rightLowerArm",
22
+ "rightHand",
23
+ "leftUpperLeg",
24
+ "leftLowerLeg",
25
+ "leftFoot",
26
+ "leftToes",
27
+ "rightUpperLeg",
28
+ "rightLowerLeg",
29
+ "rightFoot",
30
+ "rightToes"
31
+ ] as const;
32
+
33
+ export type HumanoidBoneName = typeof HUMANOID_BONES[number];
34
+ export type HumanoidAxis = "x" | "y" | "z" | "-x" | "-y" | "-z";
35
+ export type HumanoidRigUnits = "meters" | "centimeters" | "unknown" | (string & {});
36
+
37
+ export const REQUIRED_HUMANOID_BONES: readonly HumanoidBoneName[] = [
38
+ "hips",
39
+ "spine",
40
+ "head",
41
+ "leftUpperArm",
42
+ "leftLowerArm",
43
+ "leftHand",
44
+ "rightUpperArm",
45
+ "rightLowerArm",
46
+ "rightHand",
47
+ "leftUpperLeg",
48
+ "leftLowerLeg",
49
+ "leftFoot",
50
+ "rightUpperLeg",
51
+ "rightLowerLeg",
52
+ "rightFoot"
53
+ ];
54
+
55
+ export interface HumanoidBoneBinding {
56
+ readonly name: string;
57
+ readonly path?: string;
58
+ readonly parent?: HumanoidBoneName | string;
59
+ readonly length?: number;
60
+ readonly position?: AnimationVector3;
61
+ readonly rotation?: AnimationQuaternion;
62
+ readonly aliases?: readonly string[];
63
+ }
64
+
65
+ export interface HumanoidRigDefinition {
66
+ readonly id: string;
67
+ readonly name?: string;
68
+ readonly bones: Partial<Record<HumanoidBoneName, HumanoidBoneBinding>>;
69
+ readonly scale?: number;
70
+ readonly units?: HumanoidRigUnits;
71
+ readonly facingAxis?: HumanoidAxis;
72
+ readonly restPose?: Partial<Record<HumanoidBoneName, AnimationPoseTransform>>;
73
+ readonly metadata?: Record<string, unknown>;
74
+ }
75
+
76
+ export type HumanoidRetargetingDiagnosticSeverity = "info" | "warning" | "error";
77
+
78
+ export interface HumanoidRetargetingDiagnostic {
79
+ readonly severity: HumanoidRetargetingDiagnosticSeverity;
80
+ readonly code: string;
81
+ readonly message: string;
82
+ readonly rigId?: string;
83
+ readonly bone?: HumanoidBoneName;
84
+ readonly sourceBone?: string;
85
+ readonly targetBone?: string;
86
+ }
87
+
88
+ export interface HumanoidRigDiagnostics {
89
+ readonly rigId: string;
90
+ readonly ok: boolean;
91
+ readonly coverage: number;
92
+ readonly requiredCoverage: number;
93
+ readonly presentRequiredBones: number;
94
+ readonly requiredBones: number;
95
+ readonly diagnostics: readonly HumanoidRetargetingDiagnostic[];
96
+ }
97
+
98
+ export interface HumanoidRetargetingOptions {
99
+ readonly minRequiredCoverage?: number;
100
+ readonly expectedFacingAxis?: HumanoidAxis;
101
+ readonly expectedUnits?: HumanoidRigUnits;
102
+ readonly requireRestPose?: boolean;
103
+ readonly requireToes?: boolean;
104
+ }
105
+
106
+ export interface HumanoidBoneRetargetBinding {
107
+ readonly bone: HumanoidBoneName;
108
+ readonly source: HumanoidBoneBinding;
109
+ readonly target: HumanoidBoneBinding;
110
+ readonly scale: number;
111
+ }
112
+
113
+ export interface HumanoidRetargetingMap {
114
+ readonly sourceRigId: string;
115
+ readonly targetRigId: string;
116
+ readonly ok: boolean;
117
+ readonly coverage: number;
118
+ readonly requiredCoverage: number;
119
+ readonly bindings: Partial<Record<HumanoidBoneName, HumanoidBoneRetargetBinding>>;
120
+ readonly diagnostics: readonly HumanoidRetargetingDiagnostic[];
121
+ }
122
+
123
+ export interface RetargetHumanoidPoseOptions {
124
+ readonly includeUnmappedSourceBones?: boolean;
125
+ readonly includeSemanticBoneNames?: boolean;
126
+ readonly scaleRootMotion?: boolean;
127
+ }
128
+
129
+ export function analyzeHumanoidRig(
130
+ rig: HumanoidRigDefinition,
131
+ options: HumanoidRetargetingOptions = {}
132
+ ): HumanoidRigDiagnostics {
133
+ const diagnostics: HumanoidRetargetingDiagnostic[] = [];
134
+ const requiredBones: readonly HumanoidBoneName[] = options.requireToes
135
+ ? [...REQUIRED_HUMANOID_BONES, "leftToes", "rightToes"]
136
+ : REQUIRED_HUMANOID_BONES;
137
+ let presentRequiredBones = 0;
138
+
139
+ if (!rig.id) {
140
+ diagnostics.push({
141
+ severity: "error",
142
+ code: "HUMANOID_RIG_ID_MISSING",
143
+ message: "Humanoid rigs must have a stable id."
144
+ });
145
+ }
146
+
147
+ for (const bone of requiredBones) {
148
+ if (rig.bones[bone]) {
149
+ presentRequiredBones += 1;
150
+ continue;
151
+ }
152
+
153
+ diagnostics.push({
154
+ severity: "error",
155
+ code: "HUMANOID_REQUIRED_BONE_MISSING",
156
+ message: `Required humanoid bone "${bone}" is missing.`,
157
+ rigId: rig.id,
158
+ bone
159
+ });
160
+ }
161
+
162
+ for (const pair of HUMANOID_SIDE_PAIRS) {
163
+ const left = rig.bones[pair.left];
164
+ const right = rig.bones[pair.right];
165
+ if ((left && !right) || (!left && right)) {
166
+ diagnostics.push({
167
+ severity: "warning",
168
+ code: "HUMANOID_SIDE_ASYMMETRY",
169
+ message: `Humanoid side pair "${pair.left}" and "${pair.right}" is incomplete.`,
170
+ rigId: rig.id,
171
+ bone: left ? pair.right : pair.left
172
+ });
173
+ }
174
+ }
175
+
176
+ const duplicateNames = findDuplicateBoneNames(rig);
177
+ for (const name of duplicateNames) {
178
+ diagnostics.push({
179
+ severity: "warning",
180
+ code: "HUMANOID_DUPLICATE_BONE_NAME",
181
+ message: `Bone node "${name}" is assigned to multiple humanoid slots.`,
182
+ rigId: rig.id
183
+ });
184
+ }
185
+
186
+ for (const bone of HUMANOID_BONES) {
187
+ const binding = rig.bones[bone];
188
+ if (!binding) continue;
189
+
190
+ if (!binding.name) {
191
+ diagnostics.push({
192
+ severity: "error",
193
+ code: "HUMANOID_BONE_NODE_NAME_MISSING",
194
+ message: `Humanoid bone "${bone}" must reference a node name.`,
195
+ rigId: rig.id,
196
+ bone
197
+ });
198
+ }
199
+
200
+ if (binding.length !== undefined && (!Number.isFinite(binding.length) || binding.length <= 0)) {
201
+ diagnostics.push({
202
+ severity: "warning",
203
+ code: "HUMANOID_BONE_LENGTH_INVALID",
204
+ message: `Humanoid bone "${bone}" has an invalid length.`,
205
+ rigId: rig.id,
206
+ bone
207
+ });
208
+ }
209
+
210
+ if (options.requireRestPose && !rig.restPose?.[bone]) {
211
+ diagnostics.push({
212
+ severity: "warning",
213
+ code: "HUMANOID_REST_POSE_BONE_MISSING",
214
+ message: `Rest pose is missing humanoid bone "${bone}".`,
215
+ rigId: rig.id,
216
+ bone
217
+ });
218
+ }
219
+ }
220
+
221
+ if (rig.scale !== undefined && (!Number.isFinite(rig.scale) || rig.scale <= 0)) {
222
+ diagnostics.push({
223
+ severity: "error",
224
+ code: "HUMANOID_SCALE_INVALID",
225
+ message: "Humanoid rig scale must be a positive finite number.",
226
+ rigId: rig.id
227
+ });
228
+ }
229
+
230
+ if (options.expectedFacingAxis && rig.facingAxis && rig.facingAxis !== options.expectedFacingAxis) {
231
+ diagnostics.push({
232
+ severity: "warning",
233
+ code: "HUMANOID_FACING_AXIS_MISMATCH",
234
+ message: `Humanoid rig faces "${rig.facingAxis}", expected "${options.expectedFacingAxis}".`,
235
+ rigId: rig.id
236
+ });
237
+ }
238
+
239
+ if (options.expectedUnits && rig.units && rig.units !== options.expectedUnits) {
240
+ diagnostics.push({
241
+ severity: "info",
242
+ code: "HUMANOID_UNITS_MISMATCH",
243
+ message: `Humanoid rig uses "${rig.units}", expected "${options.expectedUnits}". Retargeting may need scale compensation.`,
244
+ rigId: rig.id
245
+ });
246
+ }
247
+
248
+ const requiredCoverage = requiredBones.length > 0 ? presentRequiredBones / requiredBones.length : 1;
249
+ const coverage = HUMANOID_BONES.filter((bone) => rig.bones[bone]).length / HUMANOID_BONES.length;
250
+ const minCoverage = options.minRequiredCoverage ?? 0.75;
251
+
252
+ if (requiredCoverage < minCoverage) {
253
+ diagnostics.push({
254
+ severity: "error",
255
+ code: "HUMANOID_REQUIRED_COVERAGE_LOW",
256
+ message: `Humanoid rig required-bone coverage is ${round(requiredCoverage)}; expected at least ${round(minCoverage)}.`,
257
+ rigId: rig.id
258
+ });
259
+ }
260
+
261
+ return {
262
+ rigId: rig.id,
263
+ ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
264
+ coverage,
265
+ requiredCoverage,
266
+ presentRequiredBones,
267
+ requiredBones: requiredBones.length,
268
+ diagnostics
269
+ };
270
+ }
271
+
272
+ export function createHumanoidRetargetingMap(
273
+ source: HumanoidRigDefinition,
274
+ target: HumanoidRigDefinition,
275
+ options: HumanoidRetargetingOptions = {}
276
+ ): HumanoidRetargetingMap {
277
+ const diagnostics: HumanoidRetargetingDiagnostic[] = [
278
+ ...analyzeHumanoidRig(source, options).diagnostics,
279
+ ...analyzeHumanoidRig(target, options).diagnostics
280
+ ];
281
+ const bindings: Partial<Record<HumanoidBoneName, HumanoidBoneRetargetBinding>> = {};
282
+ const requiredBones = options.requireToes
283
+ ? [...REQUIRED_HUMANOID_BONES, "leftToes", "rightToes"]
284
+ : REQUIRED_HUMANOID_BONES;
285
+ let mappedRequiredBones = 0;
286
+ let mappedBones = 0;
287
+
288
+ for (const bone of HUMANOID_BONES) {
289
+ const sourceBinding = source.bones[bone];
290
+ const targetBinding = target.bones[bone];
291
+
292
+ if (!sourceBinding || !targetBinding) {
293
+ if (requiredBones.includes(bone)) {
294
+ diagnostics.push({
295
+ severity: "error",
296
+ code: "HUMANOID_RETARGET_REQUIRED_MAPPING_MISSING",
297
+ message: `Required retarget mapping for "${bone}" is missing.`,
298
+ rigId: target.id,
299
+ bone,
300
+ sourceBone: sourceBinding?.name,
301
+ targetBone: targetBinding?.name
302
+ });
303
+ }
304
+ continue;
305
+ }
306
+
307
+ mappedBones += 1;
308
+ if (requiredBones.includes(bone)) {
309
+ mappedRequiredBones += 1;
310
+ }
311
+
312
+ bindings[bone] = {
313
+ bone,
314
+ source: sourceBinding,
315
+ target: targetBinding,
316
+ scale: estimateBoneScale(sourceBinding, targetBinding, source.scale, target.scale)
317
+ };
318
+ }
319
+
320
+ const coverage = mappedBones / HUMANOID_BONES.length;
321
+ const requiredCoverage = requiredBones.length > 0 ? mappedRequiredBones / requiredBones.length : 1;
322
+ const minCoverage = options.minRequiredCoverage ?? 0.75;
323
+
324
+ if (requiredCoverage < minCoverage) {
325
+ diagnostics.push({
326
+ severity: "error",
327
+ code: "HUMANOID_RETARGET_COVERAGE_LOW",
328
+ message: `Humanoid retarget map required-bone coverage is ${round(requiredCoverage)}; expected at least ${round(minCoverage)}.`,
329
+ rigId: target.id
330
+ });
331
+ }
332
+
333
+ return {
334
+ sourceRigId: source.id,
335
+ targetRigId: target.id,
336
+ ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
337
+ coverage,
338
+ requiredCoverage,
339
+ bindings,
340
+ diagnostics
341
+ };
342
+ }
343
+
344
+ export function retargetHumanoidPose(
345
+ sourcePose: AnimationPose,
346
+ map: HumanoidRetargetingMap,
347
+ options: RetargetHumanoidPoseOptions = {}
348
+ ): AnimationPose {
349
+ const bones: Record<string, AnimationPoseTransform> = {};
350
+
351
+ if (options.includeUnmappedSourceBones) {
352
+ for (const [boneName, transform] of Object.entries(sourcePose.bones)) {
353
+ bones[boneName] = cloneTransform(transform);
354
+ }
355
+ }
356
+
357
+ for (const bone of HUMANOID_BONES) {
358
+ const binding = map.bindings[bone];
359
+ if (!binding) continue;
360
+
361
+ const sourceTransform = sourcePose.bones[binding.source.name] ?? sourcePose.bones[bone];
362
+ if (!sourceTransform) continue;
363
+
364
+ const retargeted = scaleTransform(sourceTransform, binding.scale);
365
+ bones[binding.target.name] = retargeted;
366
+
367
+ if (options.includeSemanticBoneNames) {
368
+ bones[bone] = retargeted;
369
+ }
370
+ }
371
+
372
+ return {
373
+ bones,
374
+ morphTargets: sourcePose.morphTargets ? { ...sourcePose.morphTargets } : undefined,
375
+ rootMotion: sourcePose.rootMotion && options.scaleRootMotion
376
+ ? {
377
+ translation: sourcePose.rootMotion.translation
378
+ ? scaleVector(sourcePose.rootMotion.translation, averageBindingScale(map))
379
+ : undefined,
380
+ rotation: sourcePose.rootMotion.rotation
381
+ }
382
+ : sourcePose.rootMotion,
383
+ metadata: {
384
+ ...sourcePose.metadata,
385
+ retargetedFrom: map.sourceRigId,
386
+ retargetedTo: map.targetRigId,
387
+ retargetingCoverage: map.requiredCoverage
388
+ }
389
+ };
390
+ }
391
+
392
+ export function humanoidRetargetingDiagnostics(
393
+ source: HumanoidRigDefinition,
394
+ target: HumanoidRigDefinition,
395
+ options: HumanoidRetargetingOptions = {}
396
+ ): readonly HumanoidRetargetingDiagnostic[] {
397
+ return createHumanoidRetargetingMap(source, target, options).diagnostics;
398
+ }
399
+
400
+ const HUMANOID_SIDE_PAIRS: readonly { readonly left: HumanoidBoneName; readonly right: HumanoidBoneName }[] = [
401
+ { left: "leftShoulder", right: "rightShoulder" },
402
+ { left: "leftUpperArm", right: "rightUpperArm" },
403
+ { left: "leftLowerArm", right: "rightLowerArm" },
404
+ { left: "leftHand", right: "rightHand" },
405
+ { left: "leftUpperLeg", right: "rightUpperLeg" },
406
+ { left: "leftLowerLeg", right: "rightLowerLeg" },
407
+ { left: "leftFoot", right: "rightFoot" },
408
+ { left: "leftToes", right: "rightToes" }
409
+ ];
410
+
411
+ function findDuplicateBoneNames(rig: HumanoidRigDefinition): readonly string[] {
412
+ const counts = new Map<string, number>();
413
+
414
+ for (const binding of Object.values(rig.bones)) {
415
+ if (!binding?.name) continue;
416
+ counts.set(binding.name, (counts.get(binding.name) ?? 0) + 1);
417
+ }
418
+
419
+ return [...counts.entries()]
420
+ .filter(([, count]) => count > 1)
421
+ .map(([name]) => name);
422
+ }
423
+
424
+ function estimateBoneScale(
425
+ source: HumanoidBoneBinding,
426
+ target: HumanoidBoneBinding,
427
+ sourceRigScale = 1,
428
+ targetRigScale = 1
429
+ ): number {
430
+ const sourceScale = Number.isFinite(sourceRigScale) && sourceRigScale > 0 ? sourceRigScale : 1;
431
+ const targetScale = Number.isFinite(targetRigScale) && targetRigScale > 0 ? targetRigScale : 1;
432
+ const rigScale = targetScale / sourceScale;
433
+
434
+ if (source.length && target.length && source.length > 0 && target.length > 0) {
435
+ return (target.length / source.length) * rigScale;
436
+ }
437
+
438
+ return rigScale;
439
+ }
440
+
441
+ function averageBindingScale(map: HumanoidRetargetingMap): number {
442
+ const scales = Object.values(map.bindings)
443
+ .map((binding) => binding?.scale)
444
+ .filter((scale): scale is number => typeof scale === "number" && Number.isFinite(scale) && scale > 0);
445
+
446
+ if (scales.length === 0) return 1;
447
+ return scales.reduce((sum, scale) => sum + scale, 0) / scales.length;
448
+ }
449
+
450
+ function scaleTransform(transform: AnimationPoseTransform, scale: number): AnimationPoseTransform {
451
+ return {
452
+ position: transform.position ? scaleVector(transform.position, scale) : undefined,
453
+ rotation: transform.rotation,
454
+ scale: transform.scale
455
+ };
456
+ }
457
+
458
+ function cloneTransform(transform: AnimationPoseTransform): AnimationPoseTransform {
459
+ return {
460
+ position: transform.position ? { ...transform.position } : undefined,
461
+ rotation: transform.rotation ? { ...transform.rotation } : undefined,
462
+ scale: transform.scale ? { ...transform.scale } : undefined
463
+ };
464
+ }
465
+
466
+ function scaleVector(vector: AnimationVector3, scale: number): AnimationVector3 {
467
+ return {
468
+ x: vector.x * scale,
469
+ y: vector.y * scale,
470
+ z: vector.z * scale
471
+ };
472
+ }
473
+
474
+ function round(value: number): number {
475
+ return Math.round(value * 1000) / 1000;
476
+ }
package/src/IK.ts ADDED
@@ -0,0 +1,107 @@
1
+ import { lerpVec3, type Vec3 } from "./Keyframe.js";
2
+
3
+ export interface TwoBoneIkInput {
4
+ readonly root: Vec3;
5
+ readonly mid: Vec3;
6
+ readonly end: Vec3;
7
+ readonly target: Vec3;
8
+ readonly pole?: Vec3;
9
+ readonly weight?: number;
10
+ readonly allowStretch?: boolean;
11
+ }
12
+
13
+ export interface TwoBoneIkResult {
14
+ readonly root: Vec3;
15
+ readonly mid: Vec3;
16
+ readonly end: Vec3;
17
+ readonly reached: boolean;
18
+ readonly stretched: boolean;
19
+ readonly upperLength: number;
20
+ readonly lowerLength: number;
21
+ readonly targetDistance: number;
22
+ readonly endDistanceToTarget: number;
23
+ readonly poleInfluence: number;
24
+ }
25
+
26
+ export function solveTwoBoneIk(input: TwoBoneIkInput): TwoBoneIkResult {
27
+ const weight = clamp(input.weight ?? 1, 0, 1);
28
+ const upperLength = distance(input.root, input.mid);
29
+ const lowerLength = distance(input.mid, input.end);
30
+ if (upperLength <= 1e-6 || lowerLength <= 1e-6) {
31
+ throw new Error("Two-bone IK requires non-zero upper and lower segment lengths.");
32
+ }
33
+ const targetDelta = subtract(input.target, input.root);
34
+ const targetDistance = length(targetDelta);
35
+ if (targetDistance <= 1e-6) {
36
+ throw new Error("Two-bone IK target must be distinct from the root.");
37
+ }
38
+
39
+ const maxReach = upperLength + lowerLength;
40
+ const minReach = Math.abs(upperLength - lowerLength) + 1e-5;
41
+ const solveDistance = input.allowStretch ? targetDistance : clamp(targetDistance, minReach, maxReach - 1e-5);
42
+ const axis = normalize(targetDelta);
43
+ const poleDirection = projectedPoleDirection(input.root, axis, input.pole ?? input.mid);
44
+ const adjacent = clamp((upperLength * upperLength + solveDistance * solveDistance - lowerLength * lowerLength) / (2 * solveDistance), 0, upperLength);
45
+ const height = Math.sqrt(Math.max(0, upperLength * upperLength - adjacent * adjacent));
46
+ const solvedEnd = add(input.root, scale(axis, solveDistance));
47
+ const solvedMid = add(add(input.root, scale(axis, adjacent)), scale(poleDirection, height));
48
+ const mid = lerpVec3(input.mid, solvedMid, weight);
49
+ const end = lerpVec3(input.end, solvedEnd, weight);
50
+ const endDistanceToTarget = distance(end, input.target);
51
+ return {
52
+ root: input.root,
53
+ mid,
54
+ end,
55
+ reached: endDistanceToTarget <= 1e-3,
56
+ stretched: targetDistance > maxReach && input.allowStretch === true,
57
+ upperLength,
58
+ lowerLength,
59
+ targetDistance,
60
+ endDistanceToTarget,
61
+ poleInfluence: Math.abs(dot(normalize(subtract(mid, input.root)), poleDirection))
62
+ };
63
+ }
64
+
65
+ function projectedPoleDirection(root: Vec3, axis: Vec3, pole: Vec3): Vec3 {
66
+ const poleDelta = subtract(pole, root);
67
+ const projected = subtract(poleDelta, scale(axis, dot(poleDelta, axis)));
68
+ const projectedLength = length(projected);
69
+ if (projectedLength > 1e-6) return scale(projected, 1 / projectedLength);
70
+ const fallback = Math.abs(axis[1]) < 0.9 ? [0, 1, 0] as const : [1, 0, 0] as const;
71
+ const fallbackProjected = subtract(fallback, scale(axis, dot(fallback, axis)));
72
+ return normalize(fallbackProjected);
73
+ }
74
+
75
+ function add(left: Vec3, right: Vec3): Vec3 {
76
+ return [left[0] + right[0], left[1] + right[1], left[2] + right[2]];
77
+ }
78
+
79
+ function subtract(left: Vec3, right: Vec3): Vec3 {
80
+ return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
81
+ }
82
+
83
+ function scale(value: Vec3, scalar: number): Vec3 {
84
+ return [value[0] * scalar, value[1] * scalar, value[2] * scalar];
85
+ }
86
+
87
+ function dot(left: Vec3, right: Vec3): number {
88
+ return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
89
+ }
90
+
91
+ function length(value: Vec3): number {
92
+ return Math.hypot(value[0], value[1], value[2]);
93
+ }
94
+
95
+ function distance(left: Vec3, right: Vec3): number {
96
+ return length(subtract(left, right));
97
+ }
98
+
99
+ function normalize(value: Vec3): Vec3 {
100
+ const valueLength = length(value);
101
+ if (valueLength <= 1e-6) return [0, 1, 0];
102
+ return scale(value, 1 / valueLength);
103
+ }
104
+
105
+ function clamp(value: number, min: number, max: number): number {
106
+ return Math.max(min, Math.min(max, value));
107
+ }