@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.
- package/README.md +19 -0
- package/dist/AnimationAction.d.ts +40 -0
- package/dist/AnimationAction.d.ts.map +1 -0
- package/dist/AnimationAction.js +143 -0
- package/dist/AnimationAction.js.map +1 -0
- package/dist/AnimationClip.d.ts +24 -0
- package/dist/AnimationClip.d.ts.map +1 -0
- package/dist/AnimationClip.js +45 -0
- package/dist/AnimationClip.js.map +1 -0
- package/dist/AnimationClipEvents.d.ts +57 -0
- package/dist/AnimationClipEvents.d.ts.map +1 -0
- package/dist/AnimationClipEvents.js +171 -0
- package/dist/AnimationClipEvents.js.map +1 -0
- package/dist/AnimationClipRegistry.d.ts +76 -0
- package/dist/AnimationClipRegistry.d.ts.map +1 -0
- package/dist/AnimationClipRegistry.js +130 -0
- package/dist/AnimationClipRegistry.js.map +1 -0
- package/dist/AnimationController.d.ts +168 -0
- package/dist/AnimationController.d.ts.map +1 -0
- package/dist/AnimationController.js +619 -0
- package/dist/AnimationController.js.map +1 -0
- package/dist/AnimationEvents.d.ts +10 -0
- package/dist/AnimationEvents.d.ts.map +1 -0
- package/dist/AnimationEvents.js +26 -0
- package/dist/AnimationEvents.js.map +1 -0
- package/dist/AnimationLayer.d.ts +27 -0
- package/dist/AnimationLayer.d.ts.map +1 -0
- package/dist/AnimationLayer.js +53 -0
- package/dist/AnimationLayer.js.map +1 -0
- package/dist/AnimationMixer.d.ts +54 -0
- package/dist/AnimationMixer.d.ts.map +1 -0
- package/dist/AnimationMixer.js +319 -0
- package/dist/AnimationMixer.js.map +1 -0
- package/dist/AnimationStateGraph.d.ts +2 -0
- package/dist/AnimationStateGraph.d.ts.map +1 -0
- package/dist/AnimationStateGraph.js +2 -0
- package/dist/AnimationStateGraph.js.map +1 -0
- package/dist/AnimationStateMachine.d.ts +59 -0
- package/dist/AnimationStateMachine.d.ts.map +1 -0
- package/dist/AnimationStateMachine.js +154 -0
- package/dist/AnimationStateMachine.js.map +1 -0
- package/dist/AnimationTrack.d.ts +23 -0
- package/dist/AnimationTrack.d.ts.map +1 -0
- package/dist/AnimationTrack.js +124 -0
- package/dist/AnimationTrack.js.map +1 -0
- package/dist/BlendTree.d.ts +23 -0
- package/dist/BlendTree.d.ts.map +1 -0
- package/dist/BlendTree.js +68 -0
- package/dist/BlendTree.js.map +1 -0
- package/dist/Bone.d.ts +20 -0
- package/dist/Bone.d.ts.map +1 -0
- package/dist/Bone.js +24 -0
- package/dist/Bone.js.map +1 -0
- package/dist/CrowdAnimation.d.ts +13 -0
- package/dist/CrowdAnimation.d.ts.map +1 -0
- package/dist/CrowdAnimation.js +14 -0
- package/dist/CrowdAnimation.js.map +1 -0
- package/dist/ECSAnimationBridge.d.ts +9 -0
- package/dist/ECSAnimationBridge.d.ts.map +1 -0
- package/dist/ECSAnimationBridge.js +24 -0
- package/dist/ECSAnimationBridge.js.map +1 -0
- package/dist/HumanoidRetargeting.d.ts +76 -0
- package/dist/HumanoidRetargeting.d.ts.map +1 -0
- package/dist/HumanoidRetargeting.js +331 -0
- package/dist/HumanoidRetargeting.js.map +1 -0
- package/dist/IK.d.ts +24 -0
- package/dist/IK.d.ts.map +1 -0
- package/dist/IK.js +76 -0
- package/dist/IK.js.map +1 -0
- package/dist/Keyframe.d.ts +50 -0
- package/dist/Keyframe.d.ts.map +1 -0
- package/dist/Keyframe.js +118 -0
- package/dist/Keyframe.js.map +1 -0
- package/dist/LocomotionController.d.ts +47 -0
- package/dist/LocomotionController.d.ts.map +1 -0
- package/dist/LocomotionController.js +106 -0
- package/dist/LocomotionController.js.map +1 -0
- package/dist/MotionMatchingFixtures.d.ts +58 -0
- package/dist/MotionMatchingFixtures.d.ts.map +1 -0
- package/dist/MotionMatchingFixtures.js +175 -0
- package/dist/MotionMatchingFixtures.js.map +1 -0
- package/dist/MotionQuality.d.ts +36 -0
- package/dist/MotionQuality.d.ts.map +1 -0
- package/dist/MotionQuality.js +117 -0
- package/dist/MotionQuality.js.map +1 -0
- package/dist/Retargeting.d.ts +12 -0
- package/dist/Retargeting.d.ts.map +1 -0
- package/dist/Retargeting.js +19 -0
- package/dist/Retargeting.js.map +1 -0
- package/dist/RootMotion.d.ts +21 -0
- package/dist/RootMotion.d.ts.map +1 -0
- package/dist/RootMotion.js +84 -0
- package/dist/RootMotion.js.map +1 -0
- package/dist/SceneAnimationBridge.d.ts +18 -0
- package/dist/SceneAnimationBridge.d.ts.map +1 -0
- package/dist/SceneAnimationBridge.js +69 -0
- package/dist/SceneAnimationBridge.js.map +1 -0
- package/dist/SecondaryAnimationFixtures.d.ts +55 -0
- package/dist/SecondaryAnimationFixtures.d.ts.map +1 -0
- package/dist/SecondaryAnimationFixtures.js +147 -0
- package/dist/SecondaryAnimationFixtures.js.map +1 -0
- package/dist/Skeleton.d.ts +9 -0
- package/dist/Skeleton.d.ts.map +1 -0
- package/dist/Skeleton.js +31 -0
- package/dist/Skeleton.js.map +1 -0
- package/dist/Skinning.d.ts +7 -0
- package/dist/Skinning.d.ts.map +1 -0
- package/dist/Skinning.js +12 -0
- package/dist/Skinning.js.map +1 -0
- package/dist/browser-index.d.ts +39 -0
- package/dist/browser-index.d.ts.map +1 -0
- package/dist/browser-index.js +34 -0
- package/dist/browser-index.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/threejs-compatibility/AnimationAction.d.ts +16 -0
- package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationAction.js +25 -0
- package/dist/threejs-compatibility/AnimationAction.js.map +1 -0
- package/dist/threejs-compatibility/AnimationClip.d.ts +14 -0
- package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationClip.js +11 -0
- package/dist/threejs-compatibility/AnimationClip.js.map +1 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.d.ts +44 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.js +29 -0
- package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -0
- package/dist/threejs-compatibility/AnimationMixer.d.ts +9 -0
- package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -0
- package/dist/threejs-compatibility/AnimationMixer.js +24 -0
- package/dist/threejs-compatibility/AnimationMixer.js.map +1 -0
- package/dist/threejs-compatibility/MorphTargetMixer.d.ts +10 -0
- package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -0
- package/dist/threejs-compatibility/MorphTargetMixer.js +10 -0
- package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -0
- package/dist/threejs-compatibility/Skeleton.d.ts +10 -0
- package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -0
- package/dist/threejs-compatibility/Skeleton.js +8 -0
- package/dist/threejs-compatibility/Skeleton.js.map +1 -0
- package/dist/threejs-compatibility/SkinnedMesh.d.ts +11 -0
- package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -0
- package/dist/threejs-compatibility/SkinnedMesh.js +16 -0
- package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -0
- package/dist/threejs-compatibility/index.d.ts +12 -0
- package/dist/threejs-compatibility/index.d.ts.map +1 -0
- package/dist/threejs-compatibility/index.js +8 -0
- package/dist/threejs-compatibility/index.js.map +1 -0
- package/package.json +17 -0
- package/src/AnimationAction.ts +166 -0
- package/src/AnimationClip.ts +63 -0
- package/src/AnimationClipEvents.ts +320 -0
- package/src/AnimationClipRegistry.ts +280 -0
- package/src/AnimationController.ts +967 -0
- package/src/AnimationEvents.ts +34 -0
- package/src/AnimationLayer.ts +73 -0
- package/src/AnimationMixer.ts +366 -0
- package/src/AnimationStateGraph.ts +8 -0
- package/src/AnimationStateMachine.ts +217 -0
- package/src/AnimationTrack.ts +176 -0
- package/src/BlendTree.ts +89 -0
- package/src/Bone.ts +35 -0
- package/src/CrowdAnimation.ts +25 -0
- package/src/ECSAnimationBridge.ts +30 -0
- package/src/HumanoidRetargeting.ts +476 -0
- package/src/IK.ts +107 -0
- package/src/Keyframe.ts +159 -0
- package/src/LocomotionController.ts +149 -0
- package/src/MotionMatchingFixtures.ts +254 -0
- package/src/MotionQuality.ts +156 -0
- package/src/Retargeting.ts +29 -0
- package/src/RootMotion.ts +124 -0
- package/src/SceneAnimationBridge.ts +80 -0
- package/src/SecondaryAnimationFixtures.ts +212 -0
- package/src/Skeleton.ts +34 -0
- package/src/Skinning.ts +18 -0
- package/src/browser-index.ts +55 -0
- package/src/index.ts +56 -0
- package/src/threejs-compatibility/AnimationAction.ts +27 -0
- package/src/threejs-compatibility/AnimationClip.ts +16 -0
- package/src/threejs-compatibility/AnimationDiagnostics.ts +42 -0
- package/src/threejs-compatibility/AnimationMixer.ts +24 -0
- package/src/threejs-compatibility/MorphTargetMixer.ts +16 -0
- package/src/threejs-compatibility/Skeleton.ts +9 -0
- package/src/threejs-compatibility/SkinnedMesh.ts +12 -0
- 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
|
+
}
|