@aura3d/animation 1.4.0 → 1.4.2

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 (46) hide show
  1. package/package.json +36 -3
  2. package/src/AnimationAction.ts +0 -166
  3. package/src/AnimationClip.ts +0 -63
  4. package/src/AnimationClipEvents.ts +0 -501
  5. package/src/AnimationClipRegistry.ts +0 -404
  6. package/src/AnimationController.ts +0 -1017
  7. package/src/AnimationEvents.ts +0 -34
  8. package/src/AnimationLayer.ts +0 -73
  9. package/src/AnimationMixer.ts +0 -402
  10. package/src/AnimationStateGraph.ts +0 -105
  11. package/src/AnimationStateMachine.ts +0 -265
  12. package/src/AnimationTrack.ts +0 -176
  13. package/src/BlendTree.ts +0 -89
  14. package/src/Bone.ts +0 -40
  15. package/src/CrowdAnimation.ts +0 -25
  16. package/src/ECSAnimationBridge.ts +0 -30
  17. package/src/FighterAnimationAdapter.ts +0 -124
  18. package/src/FootIk.ts +0 -251
  19. package/src/HumanoidBoneInference.ts +0 -323
  20. package/src/HumanoidRetargeting.ts +0 -700
  21. package/src/IK.ts +0 -117
  22. package/src/Inertialization.ts +0 -269
  23. package/src/Keyframe.ts +0 -159
  24. package/src/LocomotionController.ts +0 -155
  25. package/src/LocomotionKit.ts +0 -89
  26. package/src/MotionMatchingFixtures.ts +0 -254
  27. package/src/MotionQuality.ts +0 -182
  28. package/src/RigQuality.ts +0 -101
  29. package/src/RootMotion.ts +0 -124
  30. package/src/SceneAnimationBridge.ts +0 -80
  31. package/src/SecondaryAnimationFixtures.ts +0 -210
  32. package/src/Skeleton.ts +0 -78
  33. package/src/Skinning.ts +0 -18
  34. package/src/SpringBones.ts +0 -243
  35. package/src/browser-index.ts +0 -66
  36. package/src/index.ts +0 -66
  37. package/src/library/performanceStateGraph.ts +0 -173
  38. package/src/library/standardHumanoidClips.ts +0 -659
  39. package/src/threejs-compatibility/AnimationAction.ts +0 -27
  40. package/src/threejs-compatibility/AnimationClip.ts +0 -16
  41. package/src/threejs-compatibility/AnimationDiagnostics.ts +0 -42
  42. package/src/threejs-compatibility/AnimationMixer.ts +0 -24
  43. package/src/threejs-compatibility/MorphTargetMixer.ts +0 -16
  44. package/src/threejs-compatibility/Skeleton.ts +0 -9
  45. package/src/threejs-compatibility/SkinnedMesh.ts +0 -12
  46. package/src/threejs-compatibility/index.ts +0 -11
@@ -1,89 +0,0 @@
1
- import { BlendTree1D, type BlendTreeWeight } from "./BlendTree.js";
2
- import { createLocomotionAnimationStateGraph } from "./AnimationStateGraph.js";
3
- import type { AnimationStateBlend, AnimationStateMachine } from "./AnimationStateMachine.js";
4
- import type { FootIkRig } from "./FootIk.js";
5
-
6
- /**
7
- * Composable locomotion kit: maps a scalar movement speed into a discrete locomotion state
8
- * (idle/walk/run) plus blended clip weights, using the real `BlendTree1D` and
9
- * `createLocomotionAnimationStateGraph` primitives. This is the deterministic, browser-free
10
- * core a `character-controller` template wires to a physics `CharacterController`, input, and
11
- * an `AnimationController` crossfade.
12
- */
13
- export interface LocomotionKitOptions<TClipId extends string = string> {
14
- readonly idleClip: TClipId;
15
- readonly walkClip: TClipId;
16
- readonly runClip: TClipId;
17
- /** Speed at which the walk clip reaches full weight (default 1). */
18
- readonly walkSpeed?: number;
19
- /** Speed at which the run clip reaches full weight (default 4). */
20
- readonly runSpeed?: number;
21
- /** Speed above which the character is considered moving (default 0.05). */
22
- readonly movingThreshold?: number;
23
- /** Speed at/above which the character is considered running (default = runSpeed, so state stays coherent with the blend). */
24
- readonly runningThreshold?: number;
25
- /** Optional foot-IK rig so locomotion can ground feet on uneven terrain (no foot sliding). */
26
- readonly footIkRig?: FootIkRig;
27
- }
28
-
29
- export interface LocomotionKitSample<TClipId extends string = string> {
30
- readonly speed: number;
31
- readonly state: string;
32
- readonly moving: boolean;
33
- readonly running: boolean;
34
- readonly clipWeights: ReadonlyArray<{ readonly clip: TClipId; readonly weight: number }>;
35
- /**
36
- * Inertialized blend between the previously-active discrete state and the current one. When the
37
- * state graph switches (e.g. walk→run), the previous state's clip fades out on a critically-damped
38
- * curve instead of snapping — momentum-preserving idle↔walk↔run transitions.
39
- */
40
- readonly stateTransition: AnimationStateBlend;
41
- }
42
-
43
- export interface LocomotionKit<TClipId extends string = string> {
44
- readonly graph: AnimationStateMachine;
45
- /** Optional foot-IK rig hook; call `footIk.solveFootPlacement(...)` to ground feet. */
46
- readonly footIk: FootIkRig | undefined;
47
- /** Blend clip weights for a speed (sum ~= 1), independent of the discrete state graph. */
48
- blendWeights(speed: number): ReadonlyArray<BlendTreeWeight<TClipId>>;
49
- /** Advance the state graph and produce the full locomotion sample for a speed. */
50
- sample(speed: number, delta?: number): LocomotionKitSample<TClipId>;
51
- }
52
-
53
- export function createLocomotionKit<TClipId extends string = string>(options: LocomotionKitOptions<TClipId>): LocomotionKit<TClipId> {
54
- const walkSpeed = options.walkSpeed ?? 1;
55
- const runSpeed = options.runSpeed ?? 4;
56
- const movingThreshold = options.movingThreshold ?? 0.05;
57
- const runningThreshold = options.runningThreshold ?? runSpeed;
58
-
59
- const tree = new BlendTree1D<TClipId>([
60
- { value: options.idleClip, threshold: 0 },
61
- { value: options.walkClip, threshold: walkSpeed },
62
- { value: options.runClip, threshold: runSpeed }
63
- ]);
64
- const graph = createLocomotionAnimationStateGraph();
65
-
66
- return {
67
- graph,
68
- footIk: options.footIkRig,
69
- blendWeights(speed: number) {
70
- return tree.weights(Math.max(0, speed));
71
- },
72
- sample(speed: number, delta = 1 / 30): LocomotionKitSample<TClipId> {
73
- const clamped = Math.max(0, speed);
74
- const moving = clamped > movingThreshold;
75
- const running = clamped >= runningThreshold;
76
- graph.setParameter("isMoving", moving);
77
- graph.setParameter("isRunning", running);
78
- const state = graph.update(delta);
79
- return {
80
- speed: clamped,
81
- state,
82
- moving,
83
- running,
84
- clipWeights: tree.weights(clamped).map((weight) => ({ clip: weight.value, weight: weight.weight })),
85
- stateTransition: graph.stateBlend()
86
- };
87
- }
88
- };
89
- }
@@ -1,254 +0,0 @@
1
- import type { Vec3 } from "./Keyframe.js";
2
-
3
- export interface MotionMatchingFixtureOptions {
4
- readonly currentPosition?: Vec3;
5
- readonly moveDirection?: Vec3;
6
- readonly facingDirection?: Vec3;
7
- readonly speed?: number;
8
- readonly elapsedSeconds?: number;
9
- readonly previousPoseId?: string;
10
- readonly seed?: number;
11
- }
12
-
13
- export interface MotionMatchingTrajectorySample {
14
- readonly time: number;
15
- readonly position: Vec3;
16
- readonly direction: Vec3;
17
- }
18
-
19
- export interface MotionMatchingPoseCandidate {
20
- readonly id: string;
21
- readonly clip: "idle" | "walk" | "run" | "strafe" | "turn" | "jump";
22
- readonly time: number;
23
- readonly tags: readonly string[];
24
- readonly rootVelocity: Vec3;
25
- readonly footLeft: Vec3;
26
- readonly footRight: Vec3;
27
- readonly trajectory: readonly MotionMatchingTrajectorySample[];
28
- }
29
-
30
- export interface MotionMatchingCandidateScore {
31
- readonly poseId: string;
32
- readonly clip: MotionMatchingPoseCandidate["clip"];
33
- readonly tags: readonly string[];
34
- readonly velocityCost: number;
35
- readonly facingCost: number;
36
- readonly trajectoryCost: number;
37
- readonly continuityCost: number;
38
- readonly totalCost: number;
39
- }
40
-
41
- export interface MotionMatchingFixtureSample {
42
- readonly id: "external-parity-old-branch-motion-matching-fixture";
43
- readonly source: "origin-master-motion-matching-system-adapted";
44
- readonly databasePoseCount: number;
45
- readonly candidateScores: readonly MotionMatchingCandidateScore[];
46
- readonly selectedPoseId: string;
47
- readonly selectedClip: MotionMatchingPoseCandidate["clip"];
48
- readonly selectedTags: readonly string[];
49
- readonly selectedTime: number;
50
- readonly transitioned: boolean;
51
- readonly blendWeight: number;
52
- readonly transitionDurationSeconds: number;
53
- readonly queryTrajectory: readonly MotionMatchingTrajectorySample[];
54
- readonly querySpeed: number;
55
- readonly queryFacingAlignment: number;
56
- readonly bestCost: number;
57
- readonly secondBestCost: number;
58
- readonly costMargin: number;
59
- readonly hash: string;
60
- readonly claimBoundary: string;
61
- }
62
-
63
- const predictionTimes = [0.2, 0.45, 0.75] as const;
64
-
65
- export function sampleMotionMatchingFixture(options: MotionMatchingFixtureOptions = {}): MotionMatchingFixtureSample {
66
- const seed = options.seed ?? 0x3d2025;
67
- if (!Number.isInteger(seed)) throw new RangeError("Motion matching fixture seed must be an integer.");
68
- const currentPosition = vec3(options.currentPosition ?? [0, 0, 0], "currentPosition");
69
- const moveDirection = normalize(vec3(options.moveDirection ?? [1, 0, 0.18], "moveDirection"));
70
- const facingDirection = normalize(vec3(options.facingDirection ?? [1, 0, 0], "facingDirection"));
71
- const speed = clamp(finite(options.speed ?? 0.74, "speed"), 0, 1);
72
- const elapsedSeconds = Math.max(0, finite(options.elapsedSeconds ?? 0, "elapsedSeconds"));
73
- const maxSpeed = 6.2;
74
- const queryVelocity = scale(moveDirection, speed * maxSpeed);
75
- const queryTrajectory = predictionTimes.map((time) => ({
76
- time,
77
- position: roundVec3(add(currentPosition, scale(queryVelocity, time))),
78
- direction: roundVec3(normalize(lerpVec3(moveDirection, facingDirection, Math.min(1, time * 0.8))))
79
- }));
80
- const database = createMotionDatabase(seed);
81
- const previousPoseId = options.previousPoseId ?? database[0]?.id ?? "idle-0";
82
- const scored = database
83
- .map((candidate) => scoreCandidate(candidate, queryVelocity, facingDirection, queryTrajectory, previousPoseId))
84
- .sort((left, right) => left.totalCost - right.totalCost);
85
- const selected = scored[0];
86
- if (!selected) throw new Error("Motion matching fixture database is empty.");
87
- const selectedPose = database.find((candidate) => candidate.id === selected.poseId);
88
- if (!selectedPose) throw new Error("Motion matching fixture selected an unknown pose.");
89
- const secondBestCost = scored[1]?.totalCost ?? selected.totalCost;
90
- const transitioned = selected.poseId !== previousPoseId;
91
- const transitionDurationSeconds = 0.18;
92
- const blendWeight = transitioned ? clamp(elapsedSeconds / transitionDurationSeconds, 0, 1) : 1;
93
- return {
94
- id: "external-parity-old-branch-motion-matching-fixture",
95
- source: "origin-master-motion-matching-system-adapted",
96
- databasePoseCount: database.length,
97
- candidateScores: scored.slice(0, 6),
98
- selectedPoseId: selected.poseId,
99
- selectedClip: selected.clip,
100
- selectedTags: selected.tags,
101
- selectedTime: selectedPose.time,
102
- transitioned,
103
- blendWeight: Number(blendWeight.toFixed(4)),
104
- transitionDurationSeconds,
105
- queryTrajectory,
106
- querySpeed: Number(length(queryVelocity).toFixed(4)),
107
- queryFacingAlignment: Number(dot(moveDirection, facingDirection).toFixed(4)),
108
- bestCost: selected.totalCost,
109
- secondBestCost,
110
- costMargin: Number((secondBestCost - selected.totalCost).toFixed(5)),
111
- hash: hashMotionFixture(scored, queryTrajectory),
112
- claimBoundary: "Deterministic pose-database scoring, trajectory prediction, tag filtering, continuity cost, and transition telemetry adapted from the old motion-matching system; this is bounded runtime evidence, not a full animation database, inertialization, pose application, foot locking, or Unity/Unreal animation middleware parity."
113
- };
114
- }
115
-
116
- function createMotionDatabase(seed: number): readonly MotionMatchingPoseCandidate[] {
117
- const clips: readonly MotionMatchingPoseCandidate["clip"][] = ["idle", "walk", "run", "strafe", "turn", "jump"];
118
- return clips.flatMap((clip, clipIndex) => {
119
- const speed = clip === "idle" ? 0 : clip === "walk" ? 2.1 : clip === "run" ? 5.8 : clip === "strafe" ? 3.2 : clip === "turn" ? 0.9 : 3.8;
120
- const angle = clip === "strafe" ? Math.PI / 2 : clip === "turn" ? 0.62 : seeded01(seed + clipIndex * 41) * 0.14 - 0.07;
121
- const direction: Vec3 = [Math.cos(angle), 0, Math.sin(angle)];
122
- return [0, 1, 2].map((poseIndex) => {
123
- const time = Number((poseIndex * 0.18 + seeded01(seed + clipIndex * 101 + poseIndex) * 0.04).toFixed(4));
124
- const phase = poseIndex / 3;
125
- return {
126
- id: `${clip}-${poseIndex}`,
127
- clip,
128
- time,
129
- tags: clip === "idle" ? ["idle"] : clip === "jump" ? ["action", "airborne"] : ["locomotion", clip],
130
- rootVelocity: roundVec3(scale(direction, speed * (0.92 + phase * 0.12))),
131
- footLeft: roundVec3([-0.16, Math.sin(phase * Math.PI * 2) * 0.08, 0.08]),
132
- footRight: roundVec3([0.16, Math.sin((phase + 0.5) * Math.PI * 2) * 0.08, -0.08]),
133
- trajectory: predictionTimes.map((predictionTime) => ({
134
- time: predictionTime,
135
- position: roundVec3(scale(direction, speed * predictionTime)),
136
- direction: roundVec3(direction)
137
- }))
138
- };
139
- });
140
- });
141
- }
142
-
143
- function scoreCandidate(
144
- candidate: MotionMatchingPoseCandidate,
145
- queryVelocity: Vec3,
146
- facingDirection: Vec3,
147
- queryTrajectory: readonly MotionMatchingTrajectorySample[],
148
- previousPoseId: string
149
- ): MotionMatchingCandidateScore {
150
- const velocityCost = distance(candidate.rootVelocity, queryVelocity);
151
- const facingCost = 1 - clamp(dot(normalize(candidate.trajectory[0]?.direction ?? [1, 0, 0]), facingDirection), -1, 1);
152
- const trajectoryCost = queryTrajectory.reduce((sum, query, index) => {
153
- const candidateSample = candidate.trajectory[index] ?? candidate.trajectory[candidate.trajectory.length - 1]!;
154
- return sum + distance(candidateSample.position, subtract(query.position, queryTrajectory[0]?.position ?? [0, 0, 0]));
155
- }, 0) / Math.max(1, queryTrajectory.length);
156
- const continuityCost = candidate.id === previousPoseId ? 0 : candidate.clip === previousPoseId.split("-")[0] ? 0.12 : 0.34;
157
- const totalCost = velocityCost * 0.46 + facingCost * 1.2 + trajectoryCost * 0.38 + continuityCost;
158
- return {
159
- poseId: candidate.id,
160
- clip: candidate.clip,
161
- tags: candidate.tags,
162
- velocityCost: Number(velocityCost.toFixed(5)),
163
- facingCost: Number(facingCost.toFixed(5)),
164
- trajectoryCost: Number(trajectoryCost.toFixed(5)),
165
- continuityCost: Number(continuityCost.toFixed(5)),
166
- totalCost: Number(totalCost.toFixed(5))
167
- };
168
- }
169
-
170
- function hashMotionFixture(scores: readonly MotionMatchingCandidateScore[], trajectory: readonly MotionMatchingTrajectorySample[]): string {
171
- let hash = 0x811c9dc5;
172
- for (const score of scores) {
173
- for (const char of `${score.poseId}:${score.totalCost}:${score.velocityCost}`) {
174
- hash ^= char.charCodeAt(0);
175
- hash = Math.imul(hash, 0x01000193) >>> 0;
176
- }
177
- }
178
- for (const sample of trajectory) {
179
- for (const value of [sample.time, ...sample.position, ...sample.direction]) {
180
- const scaled = Math.round(value * 10_000);
181
- hash ^= scaled & 0xff;
182
- hash = Math.imul(hash, 0x01000193) >>> 0;
183
- hash ^= (scaled >>> 8) & 0xff;
184
- hash = Math.imul(hash, 0x01000193) >>> 0;
185
- }
186
- }
187
- return hash.toString(16).padStart(8, "0");
188
- }
189
-
190
- function vec3(value: readonly number[], label: string): Vec3 {
191
- if (value.length !== 3 || value.some((entry) => !Number.isFinite(entry))) {
192
- throw new RangeError(`Motion matching fixture ${label} must contain three finite numbers.`);
193
- }
194
- return [value[0]!, value[1]!, value[2]!];
195
- }
196
-
197
- function roundVec3(value: Vec3): Vec3 {
198
- return [Number(value[0].toFixed(4)), Number(value[1].toFixed(4)), Number(value[2].toFixed(4))];
199
- }
200
-
201
- function add(left: Vec3, right: Vec3): Vec3 {
202
- return [left[0] + right[0], left[1] + right[1], left[2] + right[2]];
203
- }
204
-
205
- function subtract(left: Vec3, right: Vec3): Vec3 {
206
- return [left[0] - right[0], left[1] - right[1], left[2] - right[2]];
207
- }
208
-
209
- function scale(value: Vec3, scalar: number): Vec3 {
210
- return [value[0] * scalar, value[1] * scalar, value[2] * scalar];
211
- }
212
-
213
- function lerpVec3(left: Vec3, right: Vec3, weight: number): Vec3 {
214
- return [
215
- left[0] + (right[0] - left[0]) * weight,
216
- left[1] + (right[1] - left[1]) * weight,
217
- left[2] + (right[2] - left[2]) * weight
218
- ];
219
- }
220
-
221
- function dot(left: Vec3, right: Vec3): number {
222
- return left[0] * right[0] + left[1] * right[1] + left[2] * right[2];
223
- }
224
-
225
- function length(value: Vec3): number {
226
- return Math.hypot(value[0], value[1], value[2]);
227
- }
228
-
229
- function distance(left: Vec3, right: Vec3): number {
230
- return length(subtract(left, right));
231
- }
232
-
233
- function normalize(value: Vec3): Vec3 {
234
- const valueLength = length(value);
235
- if (valueLength <= 1e-6) return [1, 0, 0];
236
- return scale(value, 1 / valueLength);
237
- }
238
-
239
- function finite(value: number, label: string): number {
240
- if (!Number.isFinite(value)) throw new RangeError(`Motion matching fixture ${label} must be finite.`);
241
- return value;
242
- }
243
-
244
- function clamp(value: number, min: number, max: number): number {
245
- return Math.max(min, Math.min(max, value));
246
- }
247
-
248
- function seeded01(seed: number): number {
249
- let value = seed >>> 0;
250
- value ^= value << 13;
251
- value ^= value >>> 17;
252
- value ^= value << 5;
253
- return ((value >>> 0) % 10_000) / 10_000;
254
- }
@@ -1,182 +0,0 @@
1
- export interface AnimationMotionSample {
2
- readonly timeSeconds: number;
3
- readonly tracksApplied?: number;
4
- readonly skinningPalettesUpdated?: number;
5
- readonly stride?: number;
6
- readonly animatedSubjects?: number;
7
- }
8
-
9
- export interface AnimationMotionQualityReport {
10
- readonly sampleCount: number;
11
- readonly timeRangeSeconds: number;
12
- readonly activeTrackFrames: number;
13
- readonly activeSkinningFrames: number;
14
- readonly animatedSubjectFrames: number;
15
- readonly averageStride: number;
16
- readonly poseDiversityScore: number;
17
- readonly healthy: boolean;
18
- }
19
-
20
- export interface AnimationAnimationMotionQualityReport extends AnimationMotionQualityReport {
21
- readonly kind: "animation-animation-motion-quality";
22
- readonly staticPoseRejected: boolean;
23
- readonly issues: readonly string[];
24
- }
25
-
26
- export interface AnimationMotionQualityOptions {
27
- readonly maxSamples?: number;
28
- readonly minimumSamples?: number;
29
- readonly minimumTimeRangeSeconds?: number;
30
- readonly minimumPoseDiversityScore?: number;
31
- }
32
-
33
- export class AnimationMotionQualityTracker {
34
- private readonly maxSamples: number;
35
- private readonly minimumSamples: number;
36
- private readonly minimumTimeRangeSeconds: number;
37
- private readonly minimumPoseDiversityScore: number;
38
- private readonly samples: AnimationMotionSample[] = [];
39
-
40
- constructor(options: AnimationMotionQualityOptions = {}) {
41
- this.maxSamples = positiveInteger(options.maxSamples ?? 180, "maxSamples");
42
- this.minimumSamples = positiveInteger(options.minimumSamples ?? 8, "minimumSamples");
43
- this.minimumTimeRangeSeconds = nonNegativeFinite(options.minimumTimeRangeSeconds ?? 0.18, "minimumTimeRangeSeconds");
44
- this.minimumPoseDiversityScore = nonNegativeFinite(options.minimumPoseDiversityScore ?? 0.08, "minimumPoseDiversityScore");
45
- }
46
-
47
- record(sample: AnimationMotionSample): AnimationMotionQualityReport {
48
- validateSample(sample);
49
- this.samples.push(sample);
50
- if (this.samples.length > this.maxSamples) {
51
- this.samples.shift();
52
- }
53
- return this.report();
54
- }
55
-
56
- report(): AnimationMotionQualityReport {
57
- return summarizeAnimationMotion(this.samples, {
58
- minimumSamples: this.minimumSamples,
59
- minimumTimeRangeSeconds: this.minimumTimeRangeSeconds,
60
- minimumPoseDiversityScore: this.minimumPoseDiversityScore
61
- });
62
- }
63
-
64
- reset(): void {
65
- this.samples.length = 0;
66
- }
67
- }
68
-
69
- export function summarizeAnimationMotion(
70
- samples: readonly AnimationMotionSample[],
71
- options: Pick<AnimationMotionQualityOptions, "minimumSamples" | "minimumTimeRangeSeconds" | "minimumPoseDiversityScore"> = {}
72
- ): AnimationMotionQualityReport {
73
- const minimumSamples = positiveInteger(options.minimumSamples ?? 8, "minimumSamples");
74
- const minimumTimeRangeSeconds = nonNegativeFinite(options.minimumTimeRangeSeconds ?? 0.18, "minimumTimeRangeSeconds");
75
- const minimumPoseDiversityScore = nonNegativeFinite(options.minimumPoseDiversityScore ?? 0.08, "minimumPoseDiversityScore");
76
- const ordered = samples.slice().sort((left, right) => left.timeSeconds - right.timeSeconds);
77
- const sampleCount = ordered.length;
78
- if (sampleCount === 0) {
79
- return emptyReport();
80
- }
81
-
82
- const firstTime = ordered[0]!.timeSeconds;
83
- const lastTime = ordered[sampleCount - 1]!.timeSeconds;
84
- const timeRangeSeconds = Math.max(0, lastTime - firstTime);
85
- const activeTrackFrames = ordered.filter((sample) => (sample.tracksApplied ?? 0) > 0).length;
86
- const activeSkinningFrames = ordered.filter((sample) => (sample.skinningPalettesUpdated ?? 0) > 0).length;
87
- const animatedSubjectFrames = ordered.filter((sample) => (sample.animatedSubjects ?? 0) > 0).length;
88
- const strides = ordered.map((sample) => Math.abs(sample.stride ?? 0));
89
- const averageStride = strides.reduce((sum, value) => sum + value, 0) / sampleCount;
90
- const poseDiversityScore = computePoseDiversity(ordered);
91
- return {
92
- sampleCount,
93
- timeRangeSeconds: round(timeRangeSeconds),
94
- activeTrackFrames,
95
- activeSkinningFrames,
96
- animatedSubjectFrames,
97
- averageStride: round(averageStride),
98
- poseDiversityScore: round(poseDiversityScore),
99
- healthy: sampleCount >= minimumSamples
100
- && timeRangeSeconds >= minimumTimeRangeSeconds
101
- && poseDiversityScore >= minimumPoseDiversityScore
102
- && (activeTrackFrames > 0 || activeSkinningFrames > 0 || animatedSubjectFrames > 0)
103
- };
104
- }
105
-
106
- export function summarizeAnimationAnimationMotion(
107
- samples: readonly AnimationMotionSample[],
108
- options: Pick<AnimationMotionQualityOptions, "minimumSamples" | "minimumTimeRangeSeconds" | "minimumPoseDiversityScore"> = {}
109
- ): AnimationAnimationMotionQualityReport {
110
- const base = summarizeAnimationMotion(samples, options);
111
- const issues = [
112
- base.sampleCount < (options.minimumSamples ?? 8) ? "not enough motion samples" : undefined,
113
- base.timeRangeSeconds < (options.minimumTimeRangeSeconds ?? 0.18) ? "motion time range is too short" : undefined,
114
- base.poseDiversityScore < (options.minimumPoseDiversityScore ?? 0.08) ? "pose diversity is too low" : undefined,
115
- base.activeTrackFrames === 0 && base.activeSkinningFrames === 0 && base.animatedSubjectFrames === 0 ? "no active track, skinning, or subject motion" : undefined
116
- ].filter((issue): issue is string => Boolean(issue));
117
- return {
118
- ...base,
119
- kind: "animation-animation-motion-quality",
120
- staticPoseRejected: issues.length > 0,
121
- issues,
122
- healthy: issues.length === 0
123
- };
124
- }
125
-
126
- function computePoseDiversity(samples: readonly AnimationMotionSample[]): number {
127
- if (samples.length < 2) return 0;
128
- let diversity = 0;
129
- for (let index = 1; index < samples.length; index += 1) {
130
- const previous = samples[index - 1]!;
131
- const current = samples[index]!;
132
- diversity += Math.abs(current.timeSeconds - previous.timeSeconds);
133
- diversity += Math.abs((current.tracksApplied ?? 0) - (previous.tracksApplied ?? 0)) * 0.01;
134
- diversity += Math.abs((current.skinningPalettesUpdated ?? 0) - (previous.skinningPalettesUpdated ?? 0)) * 0.01;
135
- diversity += Math.abs((current.stride ?? 0) - (previous.stride ?? 0));
136
- diversity += Math.abs((current.animatedSubjects ?? 0) - (previous.animatedSubjects ?? 0)) * 0.02;
137
- }
138
- return diversity;
139
- }
140
-
141
- function validateSample(sample: AnimationMotionSample): void {
142
- nonNegativeFinite(sample.timeSeconds, "timeSeconds");
143
- optionalNonNegativeFinite(sample.tracksApplied, "tracksApplied");
144
- optionalNonNegativeFinite(sample.skinningPalettesUpdated, "skinningPalettesUpdated");
145
- optionalNonNegativeFinite(sample.stride, "stride");
146
- optionalNonNegativeFinite(sample.animatedSubjects, "animatedSubjects");
147
- }
148
-
149
- function optionalNonNegativeFinite(value: number | undefined, label: string): void {
150
- if (value !== undefined) nonNegativeFinite(value, label);
151
- }
152
-
153
- function nonNegativeFinite(value: number, label: string): number {
154
- if (!Number.isFinite(value) || value < 0) {
155
- throw new RangeError(`Animation motion ${label} must be finite and non-negative.`);
156
- }
157
- return value;
158
- }
159
-
160
- function positiveInteger(value: number, label: string): number {
161
- if (!Number.isInteger(value) || value <= 0) {
162
- throw new RangeError(`Animation motion ${label} must be a positive integer.`);
163
- }
164
- return value;
165
- }
166
-
167
- function emptyReport(): AnimationMotionQualityReport {
168
- return {
169
- sampleCount: 0,
170
- timeRangeSeconds: 0,
171
- activeTrackFrames: 0,
172
- activeSkinningFrames: 0,
173
- animatedSubjectFrames: 0,
174
- averageStride: 0,
175
- poseDiversityScore: 0,
176
- healthy: false
177
- };
178
- }
179
-
180
- function round(value: number): number {
181
- return Number(value.toFixed(4));
182
- }
package/src/RigQuality.ts DELETED
@@ -1,101 +0,0 @@
1
- import {
2
- HUMANOID_BONES,
3
- type HumanoidBoneName,
4
- type HumanoidRigDefinition
5
- } from "./HumanoidRetargeting.js";
6
-
7
- /**
8
- * Body-acting suitability grade for an (inferred) humanoid rig.
9
- *
10
- * - **A** — full humanoid: spine/neck/head, both arms with forearms + hands, both legs with lower
11
- * legs + feet. Safe for the full performance vocabulary (gesture, point, walk/run, foot work).
12
- * - **B** — partial humanoid: enough of the body to act (spine/head + both arms or both legs) but
13
- * missing limbs/extremities; the director should avoid the motions that need the missing chain.
14
- * - **C** — mascot / sparse (~8 mappable nodes): a torso + head + stub arms; head/torso acting only,
15
- * no real limb motion.
16
- * - **D** — not suitable for body acting: no usable skeleton (root/props only).
17
- */
18
- export type RigGrade = "A" | "B" | "C" | "D";
19
-
20
- export interface RigQualityReport {
21
- readonly grade: RigGrade;
22
- /** Human-readable justifications for the grade (what was present / missing). */
23
- readonly reasons: string[];
24
- readonly hasLegs: boolean;
25
- readonly hasKnees: boolean;
26
- readonly hasAnkles: boolean;
27
- readonly hasFeet: boolean;
28
- /** Count of canonical humanoid bones that resolve to a real node. */
29
- readonly mappedBoneCount: number;
30
- }
31
-
32
- /** A bone counts as mapped when it resolves to a non-empty target node name. */
33
- function isMapped(rig: HumanoidRigDefinition, bone: HumanoidBoneName): boolean {
34
- const binding = rig.bones[bone];
35
- return Boolean(binding && typeof binding.name === "string" && binding.name.length > 0);
36
- }
37
-
38
- /**
39
- * Grade a rig for body-acting suitability. Pure + deterministic — driven only by which canonical
40
- * {@link HUMANOID_BONES} resolve to nodes, so it works on the output of `inferHumanoidRig` for any
41
- * skeleton naming convention (Mixamo / Blender / UE / sparse mascot / rich humanoid).
42
- */
43
- export function gradeRig(rig: HumanoidRigDefinition): RigQualityReport {
44
- const has = (bone: HumanoidBoneName): boolean => isMapped(rig, bone);
45
- const mappedBoneCount = HUMANOID_BONES.reduce((n, bone) => (has(bone) ? n + 1 : n), 0);
46
-
47
- // Limb-chain capability (mirrors FootIk's notion of a usable leg chain).
48
- const hasLegs = (has("leftUpperLeg") || has("leftLowerLeg")) && (has("rightUpperLeg") || has("rightLowerLeg"));
49
- const hasKnees = has("leftLowerLeg") && has("rightLowerLeg");
50
- const hasAnkles = has("leftFoot") && has("rightFoot");
51
- const hasFeet = hasAnkles; // foot node == ankle/foot joint in the canonical skeleton.
52
-
53
- // Spine / head axis.
54
- const hasSpine = has("spine") || has("chest") || has("upperChest");
55
- const hasHead = has("head");
56
- const hasNeck = has("neck");
57
-
58
- // Arm chains.
59
- const hasBothUpperArms = has("leftUpperArm") && has("rightUpperArm");
60
- const hasBothForearms = has("leftLowerArm") && has("rightLowerArm");
61
- const hasBothHands = has("leftHand") && has("rightHand");
62
- const hasBothLowerLegs = has("leftLowerLeg") && has("rightLowerLeg");
63
-
64
- const reasons: string[] = [];
65
- reasons.push(`${mappedBoneCount} of ${HUMANOID_BONES.length} canonical humanoid bones mapped`);
66
-
67
- // --- D: no usable skeleton --------------------------------------------------------------------
68
- // Without a spine/head axis OR both upper arms there is nothing to act with.
69
- if (mappedBoneCount < 5 || (!hasSpine && !hasHead) || (!hasBothUpperArms && !hasLegs)) {
70
- reasons.push("not enough body structure to drive body acting (D)");
71
- if (!hasSpine && !hasHead) reasons.push("missing spine/head axis");
72
- if (!hasBothUpperArms) reasons.push("missing a pair of upper arms");
73
- if (!hasLegs) reasons.push("missing a usable pair of legs");
74
- return { grade: "D", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
75
- }
76
-
77
- // --- A: full humanoid -------------------------------------------------------------------------
78
- const fullUpperBody = hasSpine && hasNeck && hasHead && hasBothUpperArms && hasBothForearms && hasBothHands;
79
- const fullLowerBody = hasLegs && hasBothLowerLegs && hasFeet;
80
- if (mappedBoneCount >= 18 && fullUpperBody && fullLowerBody) {
81
- reasons.push("full humanoid: spine/neck/head + both arms (forearms+hands) + both legs (lower legs+feet) (A)");
82
- return { grade: "A", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
83
- }
84
-
85
- // --- B: partial humanoid ----------------------------------------------------------------------
86
- // Enough body to act: a spine/head axis plus at least a full pair of arms OR a full pair of legs.
87
- const usableArms = hasBothUpperArms && hasBothForearms;
88
- const usableLegs = hasLegs && hasBothLowerLegs;
89
- if ((hasSpine || hasHead) && (usableArms || usableLegs) && mappedBoneCount >= 10) {
90
- reasons.push("partial humanoid: body acting possible but a limb chain or extremities are missing (B)");
91
- if (!fullUpperBody) reasons.push("incomplete upper body (forearms/hands/neck)");
92
- if (!fullLowerBody) reasons.push("incomplete lower body (lower legs/feet)");
93
- return { grade: "B", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
94
- }
95
-
96
- // --- C: mascot / sparse -----------------------------------------------------------------------
97
- reasons.push("mascot/sparse rig: head + torso acting only, no real limb chains (C)");
98
- if (!usableArms) reasons.push("no usable forearm chain on both arms");
99
- if (!usableLegs) reasons.push("no usable lower-leg chain on both legs");
100
- return { grade: "C", reasons, hasLegs, hasKnees, hasAnkles, hasFeet, mappedBoneCount };
101
- }