@aura3d/animation 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +36 -3
- package/src/AnimationAction.ts +0 -166
- package/src/AnimationClip.ts +0 -63
- package/src/AnimationClipEvents.ts +0 -501
- package/src/AnimationClipRegistry.ts +0 -404
- package/src/AnimationController.ts +0 -1017
- package/src/AnimationEvents.ts +0 -34
- package/src/AnimationLayer.ts +0 -73
- package/src/AnimationMixer.ts +0 -402
- package/src/AnimationStateGraph.ts +0 -105
- package/src/AnimationStateMachine.ts +0 -265
- package/src/AnimationTrack.ts +0 -176
- package/src/BlendTree.ts +0 -89
- package/src/Bone.ts +0 -40
- package/src/CrowdAnimation.ts +0 -25
- package/src/ECSAnimationBridge.ts +0 -30
- package/src/FighterAnimationAdapter.ts +0 -124
- package/src/FootIk.ts +0 -251
- package/src/HumanoidBoneInference.ts +0 -323
- package/src/HumanoidRetargeting.ts +0 -700
- package/src/IK.ts +0 -117
- package/src/Inertialization.ts +0 -269
- package/src/Keyframe.ts +0 -159
- package/src/LocomotionController.ts +0 -155
- package/src/LocomotionKit.ts +0 -89
- package/src/MotionMatchingFixtures.ts +0 -254
- package/src/MotionQuality.ts +0 -182
- package/src/RigQuality.ts +0 -101
- package/src/RootMotion.ts +0 -124
- package/src/SceneAnimationBridge.ts +0 -80
- package/src/SecondaryAnimationFixtures.ts +0 -210
- package/src/Skeleton.ts +0 -78
- package/src/Skinning.ts +0 -18
- package/src/SpringBones.ts +0 -243
- package/src/browser-index.ts +0 -66
- package/src/index.ts +0 -66
- package/src/library/performanceStateGraph.ts +0 -173
- package/src/library/standardHumanoidClips.ts +0 -659
- package/src/threejs-compatibility/AnimationAction.ts +0 -27
- package/src/threejs-compatibility/AnimationClip.ts +0 -16
- package/src/threejs-compatibility/AnimationDiagnostics.ts +0 -42
- package/src/threejs-compatibility/AnimationMixer.ts +0 -24
- package/src/threejs-compatibility/MorphTargetMixer.ts +0 -16
- package/src/threejs-compatibility/Skeleton.ts +0 -9
- package/src/threejs-compatibility/SkinnedMesh.ts +0 -12
- package/src/threejs-compatibility/index.ts +0 -11
|
@@ -1,700 +0,0 @@
|
|
|
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
|
-
* Source rig rest-pose local rotation for this bone (identity when unknown). Captured at
|
|
113
|
-
* map-build time from `source.restPose[bone].rotation` or the binding's own `rotation` so the
|
|
114
|
-
* pose retargeter can reconcile differing bind orientations without re-reading the rigs.
|
|
115
|
-
*/
|
|
116
|
-
readonly sourceRest?: AnimationQuaternion;
|
|
117
|
-
/** Target rig rest-pose local rotation for this bone (identity when unknown). */
|
|
118
|
-
readonly targetRest?: AnimationQuaternion;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export interface HumanoidRetargetingMap {
|
|
122
|
-
readonly sourceRigId: string;
|
|
123
|
-
readonly targetRigId: string;
|
|
124
|
-
readonly ok: boolean;
|
|
125
|
-
readonly coverage: number;
|
|
126
|
-
readonly requiredCoverage: number;
|
|
127
|
-
readonly bindings: Partial<Record<HumanoidBoneName, HumanoidBoneRetargetBinding>>;
|
|
128
|
-
readonly diagnostics: readonly HumanoidRetargetingDiagnostic[];
|
|
129
|
-
/** Source rig facing axis (defaults to `"z"`) used for hip/root facing reconciliation. */
|
|
130
|
-
readonly sourceFacingAxis?: HumanoidAxis;
|
|
131
|
-
/** Target rig facing axis (defaults to `"z"`) used for hip/root facing reconciliation. */
|
|
132
|
-
readonly targetFacingAxis?: HumanoidAxis;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export interface RetargetHumanoidPoseOptions {
|
|
136
|
-
readonly includeUnmappedSourceBones?: boolean;
|
|
137
|
-
readonly includeSemanticBoneNames?: boolean;
|
|
138
|
-
readonly scaleRootMotion?: boolean;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export interface AnimationHumanoidRetargetingOptions extends HumanoidRetargetingOptions {
|
|
142
|
-
readonly requiredClips?: readonly string[];
|
|
143
|
-
readonly availableClips?: readonly string[];
|
|
144
|
-
readonly mouthBlendshapeNames?: readonly string[];
|
|
145
|
-
readonly retargetMapProvided?: boolean;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export interface AnimationHumanoidRetargetingDiagnostics extends HumanoidRigDiagnostics {
|
|
149
|
-
readonly kind: "animation-humanoid-retargeting-diagnostics";
|
|
150
|
-
readonly mouthReady: boolean;
|
|
151
|
-
readonly clipReady: boolean;
|
|
152
|
-
readonly retargetMapProvided: boolean;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function analyzeHumanoidRig(
|
|
156
|
-
rig: HumanoidRigDefinition,
|
|
157
|
-
options: HumanoidRetargetingOptions = {}
|
|
158
|
-
): HumanoidRigDiagnostics {
|
|
159
|
-
const diagnostics: HumanoidRetargetingDiagnostic[] = [];
|
|
160
|
-
const requiredBones: readonly HumanoidBoneName[] = options.requireToes
|
|
161
|
-
? [...REQUIRED_HUMANOID_BONES, "leftToes", "rightToes"]
|
|
162
|
-
: REQUIRED_HUMANOID_BONES;
|
|
163
|
-
let presentRequiredBones = 0;
|
|
164
|
-
|
|
165
|
-
if (!rig.id) {
|
|
166
|
-
diagnostics.push({
|
|
167
|
-
severity: "error",
|
|
168
|
-
code: "HUMANOID_RIG_ID_MISSING",
|
|
169
|
-
message: "Humanoid rigs must have a stable id."
|
|
170
|
-
});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
for (const bone of requiredBones) {
|
|
174
|
-
if (rig.bones[bone]) {
|
|
175
|
-
presentRequiredBones += 1;
|
|
176
|
-
continue;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
diagnostics.push({
|
|
180
|
-
severity: "error",
|
|
181
|
-
code: "HUMANOID_REQUIRED_BONE_MISSING",
|
|
182
|
-
message: `Required humanoid bone "${bone}" is missing.`,
|
|
183
|
-
rigId: rig.id,
|
|
184
|
-
bone
|
|
185
|
-
});
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
for (const pair of HUMANOID_SIDE_PAIRS) {
|
|
189
|
-
const left = rig.bones[pair.left];
|
|
190
|
-
const right = rig.bones[pair.right];
|
|
191
|
-
if ((left && !right) || (!left && right)) {
|
|
192
|
-
diagnostics.push({
|
|
193
|
-
severity: "warning",
|
|
194
|
-
code: "HUMANOID_SIDE_ASYMMETRY",
|
|
195
|
-
message: `Humanoid side pair "${pair.left}" and "${pair.right}" is incomplete.`,
|
|
196
|
-
rigId: rig.id,
|
|
197
|
-
bone: left ? pair.right : pair.left
|
|
198
|
-
});
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
const duplicateNames = findDuplicateBoneNames(rig);
|
|
203
|
-
for (const name of duplicateNames) {
|
|
204
|
-
diagnostics.push({
|
|
205
|
-
severity: "warning",
|
|
206
|
-
code: "HUMANOID_DUPLICATE_BONE_NAME",
|
|
207
|
-
message: `Bone node "${name}" is assigned to multiple humanoid slots.`,
|
|
208
|
-
rigId: rig.id
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
for (const bone of HUMANOID_BONES) {
|
|
213
|
-
const binding = rig.bones[bone];
|
|
214
|
-
if (!binding) continue;
|
|
215
|
-
|
|
216
|
-
if (!binding.name) {
|
|
217
|
-
diagnostics.push({
|
|
218
|
-
severity: "error",
|
|
219
|
-
code: "HUMANOID_BONE_NODE_NAME_MISSING",
|
|
220
|
-
message: `Humanoid bone "${bone}" must reference a node name.`,
|
|
221
|
-
rigId: rig.id,
|
|
222
|
-
bone
|
|
223
|
-
});
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (binding.length !== undefined && (!Number.isFinite(binding.length) || binding.length <= 0)) {
|
|
227
|
-
diagnostics.push({
|
|
228
|
-
severity: "warning",
|
|
229
|
-
code: "HUMANOID_BONE_LENGTH_INVALID",
|
|
230
|
-
message: `Humanoid bone "${bone}" has an invalid length.`,
|
|
231
|
-
rigId: rig.id,
|
|
232
|
-
bone
|
|
233
|
-
});
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
if (options.requireRestPose && !rig.restPose?.[bone]) {
|
|
237
|
-
diagnostics.push({
|
|
238
|
-
severity: "warning",
|
|
239
|
-
code: "HUMANOID_REST_POSE_BONE_MISSING",
|
|
240
|
-
message: `Rest pose is missing humanoid bone "${bone}".`,
|
|
241
|
-
rigId: rig.id,
|
|
242
|
-
bone
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (rig.scale !== undefined && (!Number.isFinite(rig.scale) || rig.scale <= 0)) {
|
|
248
|
-
diagnostics.push({
|
|
249
|
-
severity: "error",
|
|
250
|
-
code: "HUMANOID_SCALE_INVALID",
|
|
251
|
-
message: "Humanoid rig scale must be a positive finite number.",
|
|
252
|
-
rigId: rig.id
|
|
253
|
-
});
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
if (options.expectedFacingAxis && rig.facingAxis && rig.facingAxis !== options.expectedFacingAxis) {
|
|
257
|
-
diagnostics.push({
|
|
258
|
-
severity: "warning",
|
|
259
|
-
code: "HUMANOID_FACING_AXIS_MISMATCH",
|
|
260
|
-
message: `Humanoid rig faces "${rig.facingAxis}", expected "${options.expectedFacingAxis}".`,
|
|
261
|
-
rigId: rig.id
|
|
262
|
-
});
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
if (options.expectedUnits && rig.units && rig.units !== options.expectedUnits) {
|
|
266
|
-
diagnostics.push({
|
|
267
|
-
severity: "info",
|
|
268
|
-
code: "HUMANOID_UNITS_MISMATCH",
|
|
269
|
-
message: `Humanoid rig uses "${rig.units}", expected "${options.expectedUnits}". Retargeting may need scale compensation.`,
|
|
270
|
-
rigId: rig.id
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
const requiredCoverage = requiredBones.length > 0 ? presentRequiredBones / requiredBones.length : 1;
|
|
275
|
-
const coverage = HUMANOID_BONES.filter((bone) => rig.bones[bone]).length / HUMANOID_BONES.length;
|
|
276
|
-
const minCoverage = options.minRequiredCoverage ?? 0.75;
|
|
277
|
-
|
|
278
|
-
if (requiredCoverage < minCoverage) {
|
|
279
|
-
diagnostics.push({
|
|
280
|
-
severity: "error",
|
|
281
|
-
code: "HUMANOID_REQUIRED_COVERAGE_LOW",
|
|
282
|
-
message: `Humanoid rig required-bone coverage is ${round(requiredCoverage)}; expected at least ${round(minCoverage)}.`,
|
|
283
|
-
rigId: rig.id
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
rigId: rig.id,
|
|
289
|
-
ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
|
|
290
|
-
coverage,
|
|
291
|
-
requiredCoverage,
|
|
292
|
-
presentRequiredBones,
|
|
293
|
-
requiredBones: requiredBones.length,
|
|
294
|
-
diagnostics
|
|
295
|
-
};
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
export function createHumanoidRetargetingMap(
|
|
299
|
-
source: HumanoidRigDefinition,
|
|
300
|
-
target: HumanoidRigDefinition,
|
|
301
|
-
options: HumanoidRetargetingOptions = {}
|
|
302
|
-
): HumanoidRetargetingMap {
|
|
303
|
-
const diagnostics: HumanoidRetargetingDiagnostic[] = [
|
|
304
|
-
...analyzeHumanoidRig(source, options).diagnostics,
|
|
305
|
-
...analyzeHumanoidRig(target, options).diagnostics
|
|
306
|
-
];
|
|
307
|
-
const bindings: Partial<Record<HumanoidBoneName, HumanoidBoneRetargetBinding>> = {};
|
|
308
|
-
const requiredBones = options.requireToes
|
|
309
|
-
? [...REQUIRED_HUMANOID_BONES, "leftToes", "rightToes"]
|
|
310
|
-
: REQUIRED_HUMANOID_BONES;
|
|
311
|
-
let mappedRequiredBones = 0;
|
|
312
|
-
let mappedBones = 0;
|
|
313
|
-
|
|
314
|
-
for (const bone of HUMANOID_BONES) {
|
|
315
|
-
const sourceBinding = source.bones[bone];
|
|
316
|
-
const targetBinding = target.bones[bone];
|
|
317
|
-
|
|
318
|
-
if (!sourceBinding || !targetBinding) {
|
|
319
|
-
if (requiredBones.includes(bone)) {
|
|
320
|
-
diagnostics.push({
|
|
321
|
-
severity: "error",
|
|
322
|
-
code: "HUMANOID_RETARGET_REQUIRED_MAPPING_MISSING",
|
|
323
|
-
message: `Required retarget mapping for "${bone}" is missing.`,
|
|
324
|
-
rigId: target.id,
|
|
325
|
-
bone,
|
|
326
|
-
sourceBone: sourceBinding?.name,
|
|
327
|
-
targetBone: targetBinding?.name
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
mappedBones += 1;
|
|
334
|
-
if (requiredBones.includes(bone)) {
|
|
335
|
-
mappedRequiredBones += 1;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
bindings[bone] = {
|
|
339
|
-
bone,
|
|
340
|
-
source: sourceBinding,
|
|
341
|
-
target: targetBinding,
|
|
342
|
-
scale: estimateBoneScale(sourceBinding, targetBinding, source.scale, target.scale),
|
|
343
|
-
sourceRest: restRotationFor(source, bone, sourceBinding),
|
|
344
|
-
targetRest: restRotationFor(target, bone, targetBinding)
|
|
345
|
-
};
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
const coverage = mappedBones / HUMANOID_BONES.length;
|
|
349
|
-
const requiredCoverage = requiredBones.length > 0 ? mappedRequiredBones / requiredBones.length : 1;
|
|
350
|
-
const minCoverage = options.minRequiredCoverage ?? 0.75;
|
|
351
|
-
|
|
352
|
-
if (requiredCoverage < minCoverage) {
|
|
353
|
-
diagnostics.push({
|
|
354
|
-
severity: "error",
|
|
355
|
-
code: "HUMANOID_RETARGET_COVERAGE_LOW",
|
|
356
|
-
message: `Humanoid retarget map required-bone coverage is ${round(requiredCoverage)}; expected at least ${round(minCoverage)}.`,
|
|
357
|
-
rigId: target.id
|
|
358
|
-
});
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
return {
|
|
362
|
-
sourceRigId: source.id,
|
|
363
|
-
targetRigId: target.id,
|
|
364
|
-
ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
|
|
365
|
-
coverage,
|
|
366
|
-
requiredCoverage,
|
|
367
|
-
bindings,
|
|
368
|
-
diagnostics,
|
|
369
|
-
sourceFacingAxis: source.facingAxis ?? "z",
|
|
370
|
-
targetFacingAxis: target.facingAxis ?? "z"
|
|
371
|
-
};
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
/**
|
|
375
|
-
* Retarget a source-rig {@link AnimationPose} onto the target rig described by `map`.
|
|
376
|
-
*
|
|
377
|
-
* This is NOT a 1:1 rotation copy. For each bone we reconcile differing rest/bind orientations and
|
|
378
|
-
* a differing facing axis, and scale translations by the per-bone length ratio:
|
|
379
|
-
*
|
|
380
|
-
* 1. Rest-pose reconciliation (per bone, local space). Let `Rs0` be the source rest local
|
|
381
|
-
* rotation and `Rt0` the target rest local rotation (both from the rigs' `restPose`/binding
|
|
382
|
-
* `rotation`, identity when unknown). An animation expresses a source local rotation `Ra`.
|
|
383
|
-
* The motion *away from rest* in the source's local frame is the delta `D = Rs0⁻¹ · Ra`. We
|
|
384
|
-
* re-anchor that delta onto the target rest: `Rt = Rt0 · D = Rt0 · Rs0⁻¹ · Ra`.
|
|
385
|
-
* When both rests are identity (or equal) this collapses to a straight copy, and an identity
|
|
386
|
-
* map (source rig === target rig) is therefore a no-op.
|
|
387
|
-
*
|
|
388
|
-
* 2. Facing-axis / coordinate-frame conversion. If the source faces a different cardinal axis
|
|
389
|
-
* than the target, the hips (root) rotation is pre-multiplied by the frame-alignment rotation
|
|
390
|
-
* `F` that carries the source facing axis onto the target facing axis: `Rt_hips = F · Rt_hips`.
|
|
391
|
-
* This only touches the root so child bones inherit the reframe through the hierarchy.
|
|
392
|
-
*
|
|
393
|
-
* 3. Position / hip-height scaling (unchanged from before): translations are scaled by the
|
|
394
|
-
* bone's length ratio (`binding.scale`), and optional root motion by the average bone scale.
|
|
395
|
-
*
|
|
396
|
-
* The signature and return type are unchanged.
|
|
397
|
-
*
|
|
398
|
-
* Honesty note: step 1 is the well-known "rest-delta" reconciliation. It is exact when the source
|
|
399
|
-
* and target rest orientations are the bones' true local bind rotations and the bones are mapped
|
|
400
|
-
* one-to-one (the common case for humanoid GLBs). It does NOT model differing bone *roll* beyond
|
|
401
|
-
* what the rest rotations capture, nor parent-chain reparametrization when intermediate bones
|
|
402
|
-
* (e.g. chest/upperChest) are present on one rig and absent on the other — those remain
|
|
403
|
-
* approximate. Step 2 corrects whole-body facing only via the root; per-limb axis remaps that
|
|
404
|
-
* aren't expressible as a single root rotation are not handled.
|
|
405
|
-
*/
|
|
406
|
-
export function retargetHumanoidPose(
|
|
407
|
-
sourcePose: AnimationPose,
|
|
408
|
-
map: HumanoidRetargetingMap,
|
|
409
|
-
options: RetargetHumanoidPoseOptions = {}
|
|
410
|
-
): AnimationPose {
|
|
411
|
-
const bones: Record<string, AnimationPoseTransform> = {};
|
|
412
|
-
|
|
413
|
-
if (options.includeUnmappedSourceBones) {
|
|
414
|
-
for (const [boneName, transform] of Object.entries(sourcePose.bones)) {
|
|
415
|
-
bones[boneName] = cloneTransform(transform);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const facing = facingAlignmentQuat(map.sourceFacingAxis ?? "z", map.targetFacingAxis ?? "z");
|
|
420
|
-
|
|
421
|
-
for (const bone of HUMANOID_BONES) {
|
|
422
|
-
const binding = map.bindings[bone];
|
|
423
|
-
if (!binding) continue;
|
|
424
|
-
|
|
425
|
-
const sourceTransform = sourcePose.bones[binding.source.name] ?? sourcePose.bones[bone];
|
|
426
|
-
if (!sourceTransform) continue;
|
|
427
|
-
|
|
428
|
-
const retargeted = retargetBoneTransform(sourceTransform, binding, bone === "hips" ? facing : undefined);
|
|
429
|
-
bones[binding.target.name] = retargeted;
|
|
430
|
-
|
|
431
|
-
if (options.includeSemanticBoneNames) {
|
|
432
|
-
bones[bone] = retargeted;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return {
|
|
437
|
-
bones,
|
|
438
|
-
morphTargets: sourcePose.morphTargets ? { ...sourcePose.morphTargets } : undefined,
|
|
439
|
-
rootMotion: sourcePose.rootMotion && options.scaleRootMotion
|
|
440
|
-
? {
|
|
441
|
-
translation: sourcePose.rootMotion.translation
|
|
442
|
-
? scaleVector(sourcePose.rootMotion.translation, averageBindingScale(map))
|
|
443
|
-
: undefined,
|
|
444
|
-
rotation: sourcePose.rootMotion.rotation
|
|
445
|
-
}
|
|
446
|
-
: sourcePose.rootMotion,
|
|
447
|
-
metadata: {
|
|
448
|
-
...sourcePose.metadata,
|
|
449
|
-
retargetedFrom: map.sourceRigId,
|
|
450
|
-
retargetedTo: map.targetRigId,
|
|
451
|
-
retargetingCoverage: map.requiredCoverage
|
|
452
|
-
}
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
export function humanoidRetargetingDiagnostics(
|
|
457
|
-
source: HumanoidRigDefinition,
|
|
458
|
-
target: HumanoidRigDefinition,
|
|
459
|
-
options: HumanoidRetargetingOptions = {}
|
|
460
|
-
): readonly HumanoidRetargetingDiagnostic[] {
|
|
461
|
-
return createHumanoidRetargetingMap(source, target, options).diagnostics;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
export function analyzeAnimationHumanoidRetargeting(
|
|
465
|
-
rig: HumanoidRigDefinition,
|
|
466
|
-
options: AnimationHumanoidRetargetingOptions = {}
|
|
467
|
-
): AnimationHumanoidRetargetingDiagnostics {
|
|
468
|
-
const base = analyzeHumanoidRig(rig, { minRequiredCoverage: 0.9, requireRestPose: true, ...options });
|
|
469
|
-
const diagnostics: HumanoidRetargetingDiagnostic[] = [...base.diagnostics];
|
|
470
|
-
const requiredClips = options.requiredClips ?? ["Idle", "Talk", "Gesture", "Walk"];
|
|
471
|
-
const availableClips = new Set(options.availableClips ?? []);
|
|
472
|
-
const missingClips = requiredClips.filter((clip) => !availableClips.has(clip));
|
|
473
|
-
const mouthReady = (options.mouthBlendshapeNames?.length ?? 0) > 0
|
|
474
|
-
|| Array.isArray(rig.metadata?.mouthBlendshapeNames) && (rig.metadata.mouthBlendshapeNames as unknown[]).length > 0;
|
|
475
|
-
const retargetMapProvided = options.retargetMapProvided ?? Boolean(rig.metadata?.retargetMapProvided);
|
|
476
|
-
|
|
477
|
-
if (!mouthReady) {
|
|
478
|
-
diagnostics.push({
|
|
479
|
-
severity: "error",
|
|
480
|
-
code: "ANIMATION_MOUTH_METADATA_MISSING",
|
|
481
|
-
message: "Animation retargeting requires mouth blendshape or mouth-card metadata.",
|
|
482
|
-
rigId: rig.id
|
|
483
|
-
});
|
|
484
|
-
}
|
|
485
|
-
for (const clip of missingClips) {
|
|
486
|
-
diagnostics.push({
|
|
487
|
-
severity: "error",
|
|
488
|
-
code: "ANIMATION_REQUIRED_CLIP_MISSING",
|
|
489
|
-
message: `Animation retargeting requires clip "${clip}".`,
|
|
490
|
-
rigId: rig.id
|
|
491
|
-
});
|
|
492
|
-
}
|
|
493
|
-
if (!retargetMapProvided) {
|
|
494
|
-
diagnostics.push({
|
|
495
|
-
severity: "error",
|
|
496
|
-
code: "ANIMATION_RETARGET_MAP_MISSING",
|
|
497
|
-
message: "Animation retargeting requires external bone-map/retarget metadata.",
|
|
498
|
-
rigId: rig.id
|
|
499
|
-
});
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
return {
|
|
503
|
-
kind: "animation-humanoid-retargeting-diagnostics",
|
|
504
|
-
...base,
|
|
505
|
-
ok: !diagnostics.some((diagnostic) => diagnostic.severity === "error"),
|
|
506
|
-
diagnostics,
|
|
507
|
-
mouthReady,
|
|
508
|
-
clipReady: missingClips.length === 0,
|
|
509
|
-
retargetMapProvided
|
|
510
|
-
};
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
const HUMANOID_SIDE_PAIRS: readonly { readonly left: HumanoidBoneName; readonly right: HumanoidBoneName }[] = [
|
|
514
|
-
{ left: "leftShoulder", right: "rightShoulder" },
|
|
515
|
-
{ left: "leftUpperArm", right: "rightUpperArm" },
|
|
516
|
-
{ left: "leftLowerArm", right: "rightLowerArm" },
|
|
517
|
-
{ left: "leftHand", right: "rightHand" },
|
|
518
|
-
{ left: "leftUpperLeg", right: "rightUpperLeg" },
|
|
519
|
-
{ left: "leftLowerLeg", right: "rightLowerLeg" },
|
|
520
|
-
{ left: "leftFoot", right: "rightFoot" },
|
|
521
|
-
{ left: "leftToes", right: "rightToes" }
|
|
522
|
-
];
|
|
523
|
-
|
|
524
|
-
function findDuplicateBoneNames(rig: HumanoidRigDefinition): readonly string[] {
|
|
525
|
-
const counts = new Map<string, number>();
|
|
526
|
-
|
|
527
|
-
for (const binding of Object.values(rig.bones)) {
|
|
528
|
-
if (!binding?.name) continue;
|
|
529
|
-
counts.set(binding.name, (counts.get(binding.name) ?? 0) + 1);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
return [...counts.entries()]
|
|
533
|
-
.filter(([, count]) => count > 1)
|
|
534
|
-
.map(([name]) => name);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
function estimateBoneScale(
|
|
538
|
-
source: HumanoidBoneBinding,
|
|
539
|
-
target: HumanoidBoneBinding,
|
|
540
|
-
sourceRigScale = 1,
|
|
541
|
-
targetRigScale = 1
|
|
542
|
-
): number {
|
|
543
|
-
const sourceScale = Number.isFinite(sourceRigScale) && sourceRigScale > 0 ? sourceRigScale : 1;
|
|
544
|
-
const targetScale = Number.isFinite(targetRigScale) && targetRigScale > 0 ? targetRigScale : 1;
|
|
545
|
-
const rigScale = targetScale / sourceScale;
|
|
546
|
-
|
|
547
|
-
if (source.length && target.length && source.length > 0 && target.length > 0) {
|
|
548
|
-
return (target.length / source.length) * rigScale;
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
return rigScale;
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function averageBindingScale(map: HumanoidRetargetingMap): number {
|
|
555
|
-
const scales = Object.values(map.bindings)
|
|
556
|
-
.map((binding) => binding?.scale)
|
|
557
|
-
.filter((scale): scale is number => typeof scale === "number" && Number.isFinite(scale) && scale > 0);
|
|
558
|
-
|
|
559
|
-
if (scales.length === 0) return 1;
|
|
560
|
-
return scales.reduce((sum, scale) => sum + scale, 0) / scales.length;
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
const IDENTITY_QUAT: AnimationQuaternion = { x: 0, y: 0, z: 0, w: 1 };
|
|
564
|
-
|
|
565
|
-
/**
|
|
566
|
-
* Extract the rest local rotation for a bone, preferring the rig's explicit `restPose` entry and
|
|
567
|
-
* falling back to the binding's own `rotation`. Returns identity when neither is present.
|
|
568
|
-
*/
|
|
569
|
-
function restRotationFor(
|
|
570
|
-
rig: HumanoidRigDefinition,
|
|
571
|
-
bone: HumanoidBoneName,
|
|
572
|
-
binding: HumanoidBoneBinding
|
|
573
|
-
): AnimationQuaternion {
|
|
574
|
-
const fromRestPose = rig.restPose?.[bone]?.rotation;
|
|
575
|
-
if (fromRestPose) return normalizeQuatObj(fromRestPose);
|
|
576
|
-
if (binding.rotation) return normalizeQuatObj(binding.rotation);
|
|
577
|
-
return IDENTITY_QUAT;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
/**
|
|
581
|
-
* Apply rest-pose reconciliation + (root-only) facing alignment + length scaling to one bone.
|
|
582
|
-
* See {@link retargetHumanoidPose} for the math derivation.
|
|
583
|
-
*/
|
|
584
|
-
function retargetBoneTransform(
|
|
585
|
-
transform: AnimationPoseTransform,
|
|
586
|
-
binding: HumanoidBoneRetargetBinding,
|
|
587
|
-
facing?: AnimationQuaternion
|
|
588
|
-
): AnimationPoseTransform {
|
|
589
|
-
const sourceRest = binding.sourceRest ?? IDENTITY_QUAT;
|
|
590
|
-
const targetRest = binding.targetRest ?? IDENTITY_QUAT;
|
|
591
|
-
|
|
592
|
-
let rotation = transform.rotation;
|
|
593
|
-
if (rotation) {
|
|
594
|
-
// Rt = Rt0 · Rs0⁻¹ · Ra
|
|
595
|
-
const delta = multiplyQuat(invertQuat(sourceRest), normalizeQuatObj(rotation));
|
|
596
|
-
rotation = multiplyQuat(targetRest, delta);
|
|
597
|
-
if (facing) {
|
|
598
|
-
// Whole-body facing correction, applied on the root before its local rotation.
|
|
599
|
-
rotation = multiplyQuat(facing, rotation);
|
|
600
|
-
}
|
|
601
|
-
rotation = normalizeQuatObj(rotation);
|
|
602
|
-
} else if (facing) {
|
|
603
|
-
// No animated rotation but a facing delta still needs to reframe the root.
|
|
604
|
-
rotation = normalizeQuatObj(multiplyQuat(facing, targetRest));
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
return {
|
|
608
|
-
position: transform.position ? scaleVector(transform.position, binding.scale) : undefined,
|
|
609
|
-
rotation,
|
|
610
|
-
scale: transform.scale
|
|
611
|
-
};
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
/**
|
|
615
|
-
* Rotation that carries `from` cardinal axis onto `to` cardinal axis (shortest arc). Used to
|
|
616
|
-
* reconcile rigs that face different world axes (e.g. +Z forward vs -Z forward, or +X vs +Z).
|
|
617
|
-
*/
|
|
618
|
-
function facingAlignmentQuat(from: HumanoidAxis, to: HumanoidAxis): AnimationQuaternion | undefined {
|
|
619
|
-
if (from === to) return undefined;
|
|
620
|
-
const a = axisVector(from);
|
|
621
|
-
const b = axisVector(to);
|
|
622
|
-
return shortestArcQuat(a, b);
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
function axisVector(axis: HumanoidAxis): AnimationVector3 {
|
|
626
|
-
switch (axis) {
|
|
627
|
-
case "x": return { x: 1, y: 0, z: 0 };
|
|
628
|
-
case "-x": return { x: -1, y: 0, z: 0 };
|
|
629
|
-
case "y": return { x: 0, y: 1, z: 0 };
|
|
630
|
-
case "-y": return { x: 0, y: -1, z: 0 };
|
|
631
|
-
case "z": return { x: 0, y: 0, z: 1 };
|
|
632
|
-
case "-z": return { x: 0, y: 0, z: -1 };
|
|
633
|
-
default: return { x: 0, y: 0, z: 1 };
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
/** Quaternion of the shortest rotation from unit vector `a` to unit vector `b`. */
|
|
638
|
-
function shortestArcQuat(a: AnimationVector3, b: AnimationVector3): AnimationQuaternion {
|
|
639
|
-
const dot = a.x * b.x + a.y * b.y + a.z * b.z;
|
|
640
|
-
if (dot >= 1 - 1e-8) return IDENTITY_QUAT;
|
|
641
|
-
if (dot <= -1 + 1e-8) {
|
|
642
|
-
// Opposite vectors: rotate 180° about any axis orthogonal to a.
|
|
643
|
-
let ortho: AnimationVector3 = Math.abs(a.x) < 0.9 ? { x: 1, y: 0, z: 0 } : { x: 0, y: 1, z: 0 };
|
|
644
|
-
ortho = crossVec(a, ortho);
|
|
645
|
-
const len = Math.hypot(ortho.x, ortho.y, ortho.z) || 1;
|
|
646
|
-
return normalizeQuatObj({ x: ortho.x / len, y: ortho.y / len, z: ortho.z / len, w: 0 });
|
|
647
|
-
}
|
|
648
|
-
const c = crossVec(a, b);
|
|
649
|
-
const w = 1 + dot;
|
|
650
|
-
return normalizeQuatObj({ x: c.x, y: c.y, z: c.z, w });
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
function crossVec(a: AnimationVector3, b: AnimationVector3): AnimationVector3 {
|
|
654
|
-
return {
|
|
655
|
-
x: a.y * b.z - a.z * b.y,
|
|
656
|
-
y: a.z * b.x - a.x * b.z,
|
|
657
|
-
z: a.x * b.y - a.y * b.x
|
|
658
|
-
};
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
function multiplyQuat(a: AnimationQuaternion, b: AnimationQuaternion): AnimationQuaternion {
|
|
662
|
-
return {
|
|
663
|
-
x: a.w * b.x + a.x * b.w + a.y * b.z - a.z * b.y,
|
|
664
|
-
y: a.w * b.y - a.x * b.z + a.y * b.w + a.z * b.x,
|
|
665
|
-
z: a.w * b.z + a.x * b.y - a.y * b.x + a.z * b.w,
|
|
666
|
-
w: a.w * b.w - a.x * b.x - a.y * b.y - a.z * b.z
|
|
667
|
-
};
|
|
668
|
-
}
|
|
669
|
-
|
|
670
|
-
function invertQuat(q: AnimationQuaternion): AnimationQuaternion {
|
|
671
|
-
// Unit quaternion inverse is its conjugate; normalize defensively first.
|
|
672
|
-
const n = normalizeQuatObj(q);
|
|
673
|
-
return { x: -n.x, y: -n.y, z: -n.z, w: n.w };
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function normalizeQuatObj(q: AnimationQuaternion): AnimationQuaternion {
|
|
677
|
-
const len = Math.hypot(q.x, q.y, q.z, q.w);
|
|
678
|
-
if (len <= 1e-9) return IDENTITY_QUAT;
|
|
679
|
-
return { x: q.x / len, y: q.y / len, z: q.z / len, w: q.w / len };
|
|
680
|
-
}
|
|
681
|
-
|
|
682
|
-
function cloneTransform(transform: AnimationPoseTransform): AnimationPoseTransform {
|
|
683
|
-
return {
|
|
684
|
-
position: transform.position ? { ...transform.position } : undefined,
|
|
685
|
-
rotation: transform.rotation ? { ...transform.rotation } : undefined,
|
|
686
|
-
scale: transform.scale ? { ...transform.scale } : undefined
|
|
687
|
-
};
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
function scaleVector(vector: AnimationVector3, scale: number): AnimationVector3 {
|
|
691
|
-
return {
|
|
692
|
-
x: vector.x * scale,
|
|
693
|
-
y: vector.y * scale,
|
|
694
|
-
z: vector.z * scale
|
|
695
|
-
};
|
|
696
|
-
}
|
|
697
|
-
|
|
698
|
-
function round(value: number): number {
|
|
699
|
-
return Math.round(value * 1000) / 1000;
|
|
700
|
-
}
|