@aura3d/animation 1.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/README.md +19 -0
  2. package/dist/AnimationAction.d.ts +40 -0
  3. package/dist/AnimationAction.d.ts.map +1 -0
  4. package/dist/AnimationAction.js +143 -0
  5. package/dist/AnimationAction.js.map +1 -0
  6. package/dist/AnimationClip.d.ts +24 -0
  7. package/dist/AnimationClip.d.ts.map +1 -0
  8. package/dist/AnimationClip.js +45 -0
  9. package/dist/AnimationClip.js.map +1 -0
  10. package/dist/AnimationClipEvents.d.ts +57 -0
  11. package/dist/AnimationClipEvents.d.ts.map +1 -0
  12. package/dist/AnimationClipEvents.js +171 -0
  13. package/dist/AnimationClipEvents.js.map +1 -0
  14. package/dist/AnimationClipRegistry.d.ts +76 -0
  15. package/dist/AnimationClipRegistry.d.ts.map +1 -0
  16. package/dist/AnimationClipRegistry.js +130 -0
  17. package/dist/AnimationClipRegistry.js.map +1 -0
  18. package/dist/AnimationController.d.ts +168 -0
  19. package/dist/AnimationController.d.ts.map +1 -0
  20. package/dist/AnimationController.js +619 -0
  21. package/dist/AnimationController.js.map +1 -0
  22. package/dist/AnimationEvents.d.ts +10 -0
  23. package/dist/AnimationEvents.d.ts.map +1 -0
  24. package/dist/AnimationEvents.js +26 -0
  25. package/dist/AnimationEvents.js.map +1 -0
  26. package/dist/AnimationLayer.d.ts +27 -0
  27. package/dist/AnimationLayer.d.ts.map +1 -0
  28. package/dist/AnimationLayer.js +53 -0
  29. package/dist/AnimationLayer.js.map +1 -0
  30. package/dist/AnimationMixer.d.ts +54 -0
  31. package/dist/AnimationMixer.d.ts.map +1 -0
  32. package/dist/AnimationMixer.js +319 -0
  33. package/dist/AnimationMixer.js.map +1 -0
  34. package/dist/AnimationStateGraph.d.ts +2 -0
  35. package/dist/AnimationStateGraph.d.ts.map +1 -0
  36. package/dist/AnimationStateGraph.js +2 -0
  37. package/dist/AnimationStateGraph.js.map +1 -0
  38. package/dist/AnimationStateMachine.d.ts +59 -0
  39. package/dist/AnimationStateMachine.d.ts.map +1 -0
  40. package/dist/AnimationStateMachine.js +154 -0
  41. package/dist/AnimationStateMachine.js.map +1 -0
  42. package/dist/AnimationTrack.d.ts +23 -0
  43. package/dist/AnimationTrack.d.ts.map +1 -0
  44. package/dist/AnimationTrack.js +124 -0
  45. package/dist/AnimationTrack.js.map +1 -0
  46. package/dist/BlendTree.d.ts +23 -0
  47. package/dist/BlendTree.d.ts.map +1 -0
  48. package/dist/BlendTree.js +68 -0
  49. package/dist/BlendTree.js.map +1 -0
  50. package/dist/Bone.d.ts +20 -0
  51. package/dist/Bone.d.ts.map +1 -0
  52. package/dist/Bone.js +24 -0
  53. package/dist/Bone.js.map +1 -0
  54. package/dist/CrowdAnimation.d.ts +13 -0
  55. package/dist/CrowdAnimation.d.ts.map +1 -0
  56. package/dist/CrowdAnimation.js +14 -0
  57. package/dist/CrowdAnimation.js.map +1 -0
  58. package/dist/ECSAnimationBridge.d.ts +9 -0
  59. package/dist/ECSAnimationBridge.d.ts.map +1 -0
  60. package/dist/ECSAnimationBridge.js +24 -0
  61. package/dist/ECSAnimationBridge.js.map +1 -0
  62. package/dist/HumanoidRetargeting.d.ts +76 -0
  63. package/dist/HumanoidRetargeting.d.ts.map +1 -0
  64. package/dist/HumanoidRetargeting.js +331 -0
  65. package/dist/HumanoidRetargeting.js.map +1 -0
  66. package/dist/IK.d.ts +24 -0
  67. package/dist/IK.d.ts.map +1 -0
  68. package/dist/IK.js +76 -0
  69. package/dist/IK.js.map +1 -0
  70. package/dist/Keyframe.d.ts +50 -0
  71. package/dist/Keyframe.d.ts.map +1 -0
  72. package/dist/Keyframe.js +118 -0
  73. package/dist/Keyframe.js.map +1 -0
  74. package/dist/LocomotionController.d.ts +47 -0
  75. package/dist/LocomotionController.d.ts.map +1 -0
  76. package/dist/LocomotionController.js +106 -0
  77. package/dist/LocomotionController.js.map +1 -0
  78. package/dist/MotionMatchingFixtures.d.ts +58 -0
  79. package/dist/MotionMatchingFixtures.d.ts.map +1 -0
  80. package/dist/MotionMatchingFixtures.js +175 -0
  81. package/dist/MotionMatchingFixtures.js.map +1 -0
  82. package/dist/MotionQuality.d.ts +36 -0
  83. package/dist/MotionQuality.d.ts.map +1 -0
  84. package/dist/MotionQuality.js +117 -0
  85. package/dist/MotionQuality.js.map +1 -0
  86. package/dist/Retargeting.d.ts +12 -0
  87. package/dist/Retargeting.d.ts.map +1 -0
  88. package/dist/Retargeting.js +19 -0
  89. package/dist/Retargeting.js.map +1 -0
  90. package/dist/RootMotion.d.ts +21 -0
  91. package/dist/RootMotion.d.ts.map +1 -0
  92. package/dist/RootMotion.js +84 -0
  93. package/dist/RootMotion.js.map +1 -0
  94. package/dist/SceneAnimationBridge.d.ts +18 -0
  95. package/dist/SceneAnimationBridge.d.ts.map +1 -0
  96. package/dist/SceneAnimationBridge.js +69 -0
  97. package/dist/SceneAnimationBridge.js.map +1 -0
  98. package/dist/SecondaryAnimationFixtures.d.ts +55 -0
  99. package/dist/SecondaryAnimationFixtures.d.ts.map +1 -0
  100. package/dist/SecondaryAnimationFixtures.js +147 -0
  101. package/dist/SecondaryAnimationFixtures.js.map +1 -0
  102. package/dist/Skeleton.d.ts +9 -0
  103. package/dist/Skeleton.d.ts.map +1 -0
  104. package/dist/Skeleton.js +31 -0
  105. package/dist/Skeleton.js.map +1 -0
  106. package/dist/Skinning.d.ts +7 -0
  107. package/dist/Skinning.d.ts.map +1 -0
  108. package/dist/Skinning.js +12 -0
  109. package/dist/Skinning.js.map +1 -0
  110. package/dist/browser-index.d.ts +39 -0
  111. package/dist/browser-index.d.ts.map +1 -0
  112. package/dist/browser-index.js +34 -0
  113. package/dist/browser-index.js.map +1 -0
  114. package/dist/index.d.ts +40 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +35 -0
  117. package/dist/index.js.map +1 -0
  118. package/dist/threejs-compatibility/AnimationAction.d.ts +16 -0
  119. package/dist/threejs-compatibility/AnimationAction.d.ts.map +1 -0
  120. package/dist/threejs-compatibility/AnimationAction.js +25 -0
  121. package/dist/threejs-compatibility/AnimationAction.js.map +1 -0
  122. package/dist/threejs-compatibility/AnimationClip.d.ts +14 -0
  123. package/dist/threejs-compatibility/AnimationClip.d.ts.map +1 -0
  124. package/dist/threejs-compatibility/AnimationClip.js +11 -0
  125. package/dist/threejs-compatibility/AnimationClip.js.map +1 -0
  126. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts +44 -0
  127. package/dist/threejs-compatibility/AnimationDiagnostics.d.ts.map +1 -0
  128. package/dist/threejs-compatibility/AnimationDiagnostics.js +29 -0
  129. package/dist/threejs-compatibility/AnimationDiagnostics.js.map +1 -0
  130. package/dist/threejs-compatibility/AnimationMixer.d.ts +9 -0
  131. package/dist/threejs-compatibility/AnimationMixer.d.ts.map +1 -0
  132. package/dist/threejs-compatibility/AnimationMixer.js +24 -0
  133. package/dist/threejs-compatibility/AnimationMixer.js.map +1 -0
  134. package/dist/threejs-compatibility/MorphTargetMixer.d.ts +10 -0
  135. package/dist/threejs-compatibility/MorphTargetMixer.d.ts.map +1 -0
  136. package/dist/threejs-compatibility/MorphTargetMixer.js +10 -0
  137. package/dist/threejs-compatibility/MorphTargetMixer.js.map +1 -0
  138. package/dist/threejs-compatibility/Skeleton.d.ts +10 -0
  139. package/dist/threejs-compatibility/Skeleton.d.ts.map +1 -0
  140. package/dist/threejs-compatibility/Skeleton.js +8 -0
  141. package/dist/threejs-compatibility/Skeleton.js.map +1 -0
  142. package/dist/threejs-compatibility/SkinnedMesh.d.ts +11 -0
  143. package/dist/threejs-compatibility/SkinnedMesh.d.ts.map +1 -0
  144. package/dist/threejs-compatibility/SkinnedMesh.js +16 -0
  145. package/dist/threejs-compatibility/SkinnedMesh.js.map +1 -0
  146. package/dist/threejs-compatibility/index.d.ts +12 -0
  147. package/dist/threejs-compatibility/index.d.ts.map +1 -0
  148. package/dist/threejs-compatibility/index.js +8 -0
  149. package/dist/threejs-compatibility/index.js.map +1 -0
  150. package/package.json +17 -0
  151. package/src/AnimationAction.ts +166 -0
  152. package/src/AnimationClip.ts +63 -0
  153. package/src/AnimationClipEvents.ts +320 -0
  154. package/src/AnimationClipRegistry.ts +280 -0
  155. package/src/AnimationController.ts +967 -0
  156. package/src/AnimationEvents.ts +34 -0
  157. package/src/AnimationLayer.ts +73 -0
  158. package/src/AnimationMixer.ts +366 -0
  159. package/src/AnimationStateGraph.ts +8 -0
  160. package/src/AnimationStateMachine.ts +217 -0
  161. package/src/AnimationTrack.ts +176 -0
  162. package/src/BlendTree.ts +89 -0
  163. package/src/Bone.ts +35 -0
  164. package/src/CrowdAnimation.ts +25 -0
  165. package/src/ECSAnimationBridge.ts +30 -0
  166. package/src/HumanoidRetargeting.ts +476 -0
  167. package/src/IK.ts +107 -0
  168. package/src/Keyframe.ts +159 -0
  169. package/src/LocomotionController.ts +149 -0
  170. package/src/MotionMatchingFixtures.ts +254 -0
  171. package/src/MotionQuality.ts +156 -0
  172. package/src/Retargeting.ts +29 -0
  173. package/src/RootMotion.ts +124 -0
  174. package/src/SceneAnimationBridge.ts +80 -0
  175. package/src/SecondaryAnimationFixtures.ts +212 -0
  176. package/src/Skeleton.ts +34 -0
  177. package/src/Skinning.ts +18 -0
  178. package/src/browser-index.ts +55 -0
  179. package/src/index.ts +56 -0
  180. package/src/threejs-compatibility/AnimationAction.ts +27 -0
  181. package/src/threejs-compatibility/AnimationClip.ts +16 -0
  182. package/src/threejs-compatibility/AnimationDiagnostics.ts +42 -0
  183. package/src/threejs-compatibility/AnimationMixer.ts +24 -0
  184. package/src/threejs-compatibility/MorphTargetMixer.ts +16 -0
  185. package/src/threejs-compatibility/Skeleton.ts +9 -0
  186. package/src/threejs-compatibility/SkinnedMesh.ts +12 -0
  187. package/src/threejs-compatibility/index.ts +11 -0
@@ -0,0 +1,156 @@
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 AnimationMotionQualityOptions {
21
+ readonly maxSamples?: number;
22
+ readonly minimumSamples?: number;
23
+ readonly minimumTimeRangeSeconds?: number;
24
+ readonly minimumPoseDiversityScore?: number;
25
+ }
26
+
27
+ export class AnimationMotionQualityTracker {
28
+ private readonly maxSamples: number;
29
+ private readonly minimumSamples: number;
30
+ private readonly minimumTimeRangeSeconds: number;
31
+ private readonly minimumPoseDiversityScore: number;
32
+ private readonly samples: AnimationMotionSample[] = [];
33
+
34
+ constructor(options: AnimationMotionQualityOptions = {}) {
35
+ this.maxSamples = positiveInteger(options.maxSamples ?? 180, "maxSamples");
36
+ this.minimumSamples = positiveInteger(options.minimumSamples ?? 8, "minimumSamples");
37
+ this.minimumTimeRangeSeconds = nonNegativeFinite(options.minimumTimeRangeSeconds ?? 0.18, "minimumTimeRangeSeconds");
38
+ this.minimumPoseDiversityScore = nonNegativeFinite(options.minimumPoseDiversityScore ?? 0.08, "minimumPoseDiversityScore");
39
+ }
40
+
41
+ record(sample: AnimationMotionSample): AnimationMotionQualityReport {
42
+ validateSample(sample);
43
+ this.samples.push(sample);
44
+ if (this.samples.length > this.maxSamples) {
45
+ this.samples.shift();
46
+ }
47
+ return this.report();
48
+ }
49
+
50
+ report(): AnimationMotionQualityReport {
51
+ return summarizeAnimationMotion(this.samples, {
52
+ minimumSamples: this.minimumSamples,
53
+ minimumTimeRangeSeconds: this.minimumTimeRangeSeconds,
54
+ minimumPoseDiversityScore: this.minimumPoseDiversityScore
55
+ });
56
+ }
57
+
58
+ reset(): void {
59
+ this.samples.length = 0;
60
+ }
61
+ }
62
+
63
+ export function summarizeAnimationMotion(
64
+ samples: readonly AnimationMotionSample[],
65
+ options: Pick<AnimationMotionQualityOptions, "minimumSamples" | "minimumTimeRangeSeconds" | "minimumPoseDiversityScore"> = {}
66
+ ): AnimationMotionQualityReport {
67
+ const minimumSamples = positiveInteger(options.minimumSamples ?? 8, "minimumSamples");
68
+ const minimumTimeRangeSeconds = nonNegativeFinite(options.minimumTimeRangeSeconds ?? 0.18, "minimumTimeRangeSeconds");
69
+ const minimumPoseDiversityScore = nonNegativeFinite(options.minimumPoseDiversityScore ?? 0.08, "minimumPoseDiversityScore");
70
+ const ordered = samples.slice().sort((left, right) => left.timeSeconds - right.timeSeconds);
71
+ const sampleCount = ordered.length;
72
+ if (sampleCount === 0) {
73
+ return emptyReport();
74
+ }
75
+
76
+ const firstTime = ordered[0]!.timeSeconds;
77
+ const lastTime = ordered[sampleCount - 1]!.timeSeconds;
78
+ const timeRangeSeconds = Math.max(0, lastTime - firstTime);
79
+ const activeTrackFrames = ordered.filter((sample) => (sample.tracksApplied ?? 0) > 0).length;
80
+ const activeSkinningFrames = ordered.filter((sample) => (sample.skinningPalettesUpdated ?? 0) > 0).length;
81
+ const animatedSubjectFrames = ordered.filter((sample) => (sample.animatedSubjects ?? 0) > 0).length;
82
+ const strides = ordered.map((sample) => Math.abs(sample.stride ?? 0));
83
+ const averageStride = strides.reduce((sum, value) => sum + value, 0) / sampleCount;
84
+ const poseDiversityScore = computePoseDiversity(ordered);
85
+ return {
86
+ sampleCount,
87
+ timeRangeSeconds: round(timeRangeSeconds),
88
+ activeTrackFrames,
89
+ activeSkinningFrames,
90
+ animatedSubjectFrames,
91
+ averageStride: round(averageStride),
92
+ poseDiversityScore: round(poseDiversityScore),
93
+ healthy: sampleCount >= minimumSamples
94
+ && timeRangeSeconds >= minimumTimeRangeSeconds
95
+ && poseDiversityScore >= minimumPoseDiversityScore
96
+ && (activeTrackFrames > 0 || activeSkinningFrames > 0 || animatedSubjectFrames > 0)
97
+ };
98
+ }
99
+
100
+ function computePoseDiversity(samples: readonly AnimationMotionSample[]): number {
101
+ if (samples.length < 2) return 0;
102
+ let diversity = 0;
103
+ for (let index = 1; index < samples.length; index += 1) {
104
+ const previous = samples[index - 1]!;
105
+ const current = samples[index]!;
106
+ diversity += Math.abs(current.timeSeconds - previous.timeSeconds);
107
+ diversity += Math.abs((current.tracksApplied ?? 0) - (previous.tracksApplied ?? 0)) * 0.01;
108
+ diversity += Math.abs((current.skinningPalettesUpdated ?? 0) - (previous.skinningPalettesUpdated ?? 0)) * 0.01;
109
+ diversity += Math.abs((current.stride ?? 0) - (previous.stride ?? 0));
110
+ diversity += Math.abs((current.animatedSubjects ?? 0) - (previous.animatedSubjects ?? 0)) * 0.02;
111
+ }
112
+ return diversity;
113
+ }
114
+
115
+ function validateSample(sample: AnimationMotionSample): void {
116
+ nonNegativeFinite(sample.timeSeconds, "timeSeconds");
117
+ optionalNonNegativeFinite(sample.tracksApplied, "tracksApplied");
118
+ optionalNonNegativeFinite(sample.skinningPalettesUpdated, "skinningPalettesUpdated");
119
+ optionalNonNegativeFinite(sample.stride, "stride");
120
+ optionalNonNegativeFinite(sample.animatedSubjects, "animatedSubjects");
121
+ }
122
+
123
+ function optionalNonNegativeFinite(value: number | undefined, label: string): void {
124
+ if (value !== undefined) nonNegativeFinite(value, label);
125
+ }
126
+
127
+ function nonNegativeFinite(value: number, label: string): number {
128
+ if (!Number.isFinite(value) || value < 0) {
129
+ throw new RangeError(`Animation motion ${label} must be finite and non-negative.`);
130
+ }
131
+ return value;
132
+ }
133
+
134
+ function positiveInteger(value: number, label: string): number {
135
+ if (!Number.isInteger(value) || value <= 0) {
136
+ throw new RangeError(`Animation motion ${label} must be a positive integer.`);
137
+ }
138
+ return value;
139
+ }
140
+
141
+ function emptyReport(): AnimationMotionQualityReport {
142
+ return {
143
+ sampleCount: 0,
144
+ timeRangeSeconds: 0,
145
+ activeTrackFrames: 0,
146
+ activeSkinningFrames: 0,
147
+ animatedSubjectFrames: 0,
148
+ averageStride: 0,
149
+ poseDiversityScore: 0,
150
+ healthy: false
151
+ };
152
+ }
153
+
154
+ function round(value: number): number {
155
+ return Number(value.toFixed(4));
156
+ }
@@ -0,0 +1,29 @@
1
+ import { Matrix4 } from "@aura3d/math";
2
+
3
+ export interface RetargetBoneMap {
4
+ readonly source: string;
5
+ readonly target: string;
6
+ readonly scale?: number;
7
+ }
8
+
9
+ export interface RetargetPoseInput {
10
+ readonly sourcePose: ReadonlyMap<string, Matrix4>;
11
+ readonly boneMap: readonly RetargetBoneMap[];
12
+ }
13
+
14
+ export function retargetPose(input: RetargetPoseInput): Map<string, Matrix4> {
15
+ const output = new Map<string, Matrix4>();
16
+ for (const mapping of input.boneMap) {
17
+ const source = input.sourcePose.get(mapping.source);
18
+ if (!source) continue;
19
+ const elements = [...source.elements];
20
+ const scale = mapping.scale ?? 1;
21
+ if (scale !== 1) {
22
+ elements[12] *= scale;
23
+ elements[13] *= scale;
24
+ elements[14] *= scale;
25
+ }
26
+ output.set(mapping.target, new Matrix4(elements as unknown as Matrix4["elements"]));
27
+ }
28
+ return output;
29
+ }
@@ -0,0 +1,124 @@
1
+ import type { AnimationClip } from "./AnimationClip.js";
2
+ import type { AnimationTrack } from "./AnimationTrack.js";
3
+ import type { Vec3 } from "./Keyframe.js";
4
+
5
+ export type RootMotionExtractionDescriptor = {
6
+ readonly target?: string;
7
+ readonly fromTime: number;
8
+ readonly toTime: number;
9
+ readonly loop?: boolean;
10
+ };
11
+
12
+ export type RootMotionSample = {
13
+ readonly target: string;
14
+ readonly fromTime: number;
15
+ readonly toTime: number;
16
+ readonly looped: boolean;
17
+ readonly delta: Vec3;
18
+ };
19
+
20
+ export type RootMotionTarget = {
21
+ position: [number, number, number];
22
+ };
23
+
24
+ const defaultRootMotionTarget = "root.position";
25
+
26
+ export function extractRootMotion(clip: AnimationClip, descriptor: RootMotionExtractionDescriptor): RootMotionSample {
27
+ const target = descriptor.target ?? defaultRootMotionTarget;
28
+ if (!Number.isFinite(descriptor.fromTime) || !Number.isFinite(descriptor.toTime)) {
29
+ throw new Error("Root motion times must be finite.");
30
+ }
31
+ const track = clip.tracks.find((candidate) => candidate.target === target);
32
+ if (!track) {
33
+ throw new Error(`Root motion track "${target}" was not found.`);
34
+ }
35
+ if (track.valueType !== "vector3") {
36
+ throw new Error(`Root motion track "${target}" must be a vector3 track.`);
37
+ }
38
+
39
+ const loop = descriptor.loop ?? false;
40
+ const delta = loop
41
+ ? extractLoopingDelta(track, clip.duration, descriptor.fromTime, descriptor.toTime)
42
+ : subtractVec3(
43
+ sampleVec3(track, clampTime(descriptor.toTime, clip.duration)),
44
+ sampleVec3(track, clampTime(descriptor.fromTime, clip.duration))
45
+ );
46
+
47
+ return {
48
+ target,
49
+ fromTime: descriptor.fromTime,
50
+ toTime: descriptor.toTime,
51
+ looped: loop,
52
+ delta
53
+ };
54
+ }
55
+
56
+ export function applyRootMotion(target: RootMotionTarget, sample: RootMotionSample, scale = 1): RootMotionTarget {
57
+ if (!Number.isFinite(scale)) {
58
+ throw new Error("Root motion scale must be finite.");
59
+ }
60
+ target.position = [
61
+ target.position[0] + sample.delta[0] * scale,
62
+ target.position[1] + sample.delta[1] * scale,
63
+ target.position[2] + sample.delta[2] * scale
64
+ ];
65
+ return target;
66
+ }
67
+
68
+ function extractLoopingDelta(track: AnimationTrack, duration: number, fromTime: number, toTime: number): Vec3 {
69
+ if (!Number.isFinite(duration) || duration <= 0) {
70
+ throw new Error("Looping root motion requires a positive clip duration.");
71
+ }
72
+
73
+ let effectiveTo = toTime;
74
+ if (effectiveTo < fromTime) {
75
+ effectiveTo += Math.ceil((fromTime - effectiveTo) / duration) * duration;
76
+ }
77
+
78
+ const fromCycle = Math.floor(fromTime / duration);
79
+ const toCycle = Math.floor(effectiveTo / duration);
80
+ const fromWrapped = wrapTime(fromTime, duration);
81
+ const toWrapped = wrapTime(effectiveTo, duration);
82
+
83
+ if (fromCycle === toCycle) {
84
+ return subtractVec3(sampleVec3(track, toWrapped), sampleVec3(track, fromWrapped));
85
+ }
86
+
87
+ const cycleDelta = subtractVec3(sampleVec3(track, duration), sampleVec3(track, 0));
88
+ const firstPartial = subtractVec3(sampleVec3(track, duration), sampleVec3(track, fromWrapped));
89
+ const wholeCycles = Math.max(0, toCycle - fromCycle - 1);
90
+ const finalPartial = subtractVec3(sampleVec3(track, toWrapped), sampleVec3(track, 0));
91
+ return addVec3(addVec3(firstPartial, scaleVec3(cycleDelta, wholeCycles)), finalPartial);
92
+ }
93
+
94
+ function clampTime(time: number, duration: number): number {
95
+ if (!Number.isFinite(duration) || duration < 0) {
96
+ throw new Error("Root motion clip duration must be finite and non-negative.");
97
+ }
98
+ return Math.max(0, Math.min(duration, time));
99
+ }
100
+
101
+ function wrapTime(time: number, duration: number): number {
102
+ const wrapped = time % duration;
103
+ return wrapped < 0 ? wrapped + duration : wrapped;
104
+ }
105
+
106
+ function sampleVec3(track: AnimationTrack, time: number): Vec3 {
107
+ const value = track.sample(time);
108
+ if (!Array.isArray(value) || value.length !== 3 || value.some((component) => typeof component !== "number" || !Number.isFinite(component))) {
109
+ throw new Error(`Root motion track "${track.target}" sampled an invalid vector3 value.`);
110
+ }
111
+ return [value[0], value[1], value[2]];
112
+ }
113
+
114
+ function addVec3(a: Vec3, b: Vec3): Vec3 {
115
+ return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
116
+ }
117
+
118
+ function subtractVec3(a: Vec3, b: Vec3): Vec3 {
119
+ return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
120
+ }
121
+
122
+ function scaleVec3(a: Vec3, scale: number): Vec3 {
123
+ return [a[0] * scale, a[1] * scale, a[2] * scale];
124
+ }
@@ -0,0 +1,80 @@
1
+ import type { AnimationValue } from "./Keyframe.js";
2
+
3
+ export type SceneAnimationNode = {
4
+ name?: string;
5
+ position?: [number, number, number];
6
+ rotation?: [number, number, number, number];
7
+ scale?: [number, number, number];
8
+ weights?: number[];
9
+ setPosition?: (value: readonly [number, number, number]) => void;
10
+ setRotation?: (value: readonly [number, number, number, number]) => void;
11
+ setScale?: (value: readonly [number, number, number]) => void;
12
+ setWeights?: (value: readonly number[]) => void;
13
+ };
14
+
15
+ export class SceneAnimationBridge {
16
+ private readonly nodes = new Map<string, SceneAnimationNode>();
17
+
18
+ register(name: string, node: SceneAnimationNode): void {
19
+ if (name.trim().length === 0) {
20
+ throw new Error("Scene animation target name cannot be empty.");
21
+ }
22
+ this.nodes.set(name, node);
23
+ }
24
+
25
+ setAnimationValue(target: string, value: AnimationValue): void {
26
+ const [nodeName, property] = target.split(".");
27
+ if (!nodeName || !property) {
28
+ throw new Error(`Invalid scene animation target ${target}.`);
29
+ }
30
+ const node = this.nodes.get(nodeName);
31
+ if (!node) {
32
+ throw new Error(`Missing scene animation target ${nodeName}.`);
33
+ }
34
+ if (property === "position" || property === "translation") {
35
+ writeVec3(node, "position", "setPosition", value);
36
+ } else if (property === "rotation") {
37
+ writeQuat(node, value);
38
+ } else if (property === "scale") {
39
+ writeVec3(node, "scale", "setScale", value);
40
+ } else if (property === "weights") {
41
+ writeWeights(node, value);
42
+ } else {
43
+ throw new Error(`Unsupported scene animation property ${property}.`);
44
+ }
45
+ }
46
+ }
47
+
48
+ function writeWeights(node: SceneAnimationNode, value: AnimationValue): void {
49
+ if (!Array.isArray(value) || value.some((weight) => typeof weight !== "number" || !Number.isFinite(weight))) {
50
+ throw new Error("weights animation value must be a finite number array.");
51
+ }
52
+ const weights = value.map((weight) => Math.max(0, Math.min(1, weight)));
53
+ if (node.setWeights) {
54
+ node.setWeights(weights);
55
+ } else {
56
+ node.weights = weights;
57
+ }
58
+ }
59
+
60
+ function writeVec3(node: SceneAnimationNode, property: "position" | "scale", setter: "setPosition" | "setScale", value: AnimationValue): void {
61
+ if (!Array.isArray(value) || value.length !== 3) {
62
+ throw new Error(`${property} animation value must be a vec3.`);
63
+ }
64
+ if (node[setter]) {
65
+ node[setter](value as [number, number, number]);
66
+ } else {
67
+ node[property] = [value[0], value[1], value[2]];
68
+ }
69
+ }
70
+
71
+ function writeQuat(node: SceneAnimationNode, value: AnimationValue): void {
72
+ if (!Array.isArray(value) || value.length !== 4) {
73
+ throw new Error("rotation animation value must be a quaternion.");
74
+ }
75
+ if (node.setRotation) {
76
+ node.setRotation(value as [number, number, number, number]);
77
+ } else {
78
+ node.rotation = [value[0], value[1], value[2], value[3]];
79
+ }
80
+ }
@@ -0,0 +1,212 @@
1
+ import type { Vec3 } from "./Keyframe";
2
+
3
+ export interface SecondaryAnimationFixtureOptions {
4
+ readonly stridePhase?: number;
5
+ readonly rootHeight?: number;
6
+ readonly velocity?: Vec3;
7
+ readonly terrainSlope?: number;
8
+ readonly deltaSeconds?: number;
9
+ readonly seed?: number;
10
+ }
11
+
12
+ export interface FootIkSample {
13
+ readonly side: "left" | "right";
14
+ readonly sourceFoot: Vec3;
15
+ readonly plantedFoot: Vec3;
16
+ readonly groundNormal: Vec3;
17
+ readonly grounded: boolean;
18
+ readonly verticalCorrection: number;
19
+ readonly targetError: number;
20
+ }
21
+
22
+ export interface SpringBoneSample {
23
+ readonly chainName: "ponytail";
24
+ readonly boneCount: number;
25
+ readonly stiffness: number;
26
+ readonly damping: number;
27
+ readonly collisionRadius: number;
28
+ readonly substeps: number;
29
+ readonly gravity: Vec3;
30
+ readonly wind: Vec3;
31
+ readonly rootPosition: Vec3;
32
+ readonly tipPosition: Vec3;
33
+ readonly maxDisplacement: number;
34
+ readonly collisionContacts: number;
35
+ }
36
+
37
+ export interface SecondaryAnimationFixture {
38
+ readonly source: "origin-master-foot-ik-spring-bone-adapted";
39
+ readonly footIk: {
40
+ readonly feet: readonly FootIkSample[];
41
+ readonly hipOffset: number;
42
+ readonly averageTargetError: number;
43
+ readonly groundedFeet: number;
44
+ readonly terrainSlope: number;
45
+ };
46
+ readonly springBone: SpringBoneSample;
47
+ readonly productionReadiness: {
48
+ readonly footPlacementTelemetry: boolean;
49
+ readonly hipAdjustmentTelemetry: boolean;
50
+ readonly springChainTelemetry: boolean;
51
+ readonly collisionTelemetry: boolean;
52
+ readonly deterministicReplay: boolean;
53
+ };
54
+ readonly hash: string;
55
+ readonly claimBoundary: string;
56
+ readonly blockedClaims: readonly string[];
57
+ }
58
+
59
+ export function sampleSecondaryAnimationFixture(options: SecondaryAnimationFixtureOptions = {}): SecondaryAnimationFixture {
60
+ const stridePhase = wrap01(options.stridePhase ?? 0.35);
61
+ const rootHeight = clamp(options.rootHeight ?? 1.08, 0.4, 2.4);
62
+ const velocity = options.velocity ?? [0.9, 0, 0.22];
63
+ const terrainSlope = clamp(options.terrainSlope ?? 0.18, -0.7, 0.7);
64
+ const deltaSeconds = clamp(options.deltaSeconds ?? 1 / 60, 1 / 240, 1 / 10);
65
+ const seed = integerSeed(options.seed ?? 0x3d2025);
66
+ const groundNormal = normalize([-terrainSlope, 1, 0.08]);
67
+ const left = footSample("left", stridePhase, rootHeight, terrainSlope, groundNormal, velocity, seed);
68
+ const right = footSample("right", wrap01(stridePhase + 0.5), rootHeight, terrainSlope, groundNormal, velocity, seed + 17);
69
+ const feet = [left, right] as const;
70
+ const averageTargetError = round(feet.reduce((sum, foot) => sum + foot.targetError, 0) / feet.length);
71
+ const hipOffset = round(-Math.max(...feet.map((foot) => Math.max(0, foot.verticalCorrection))) * 0.72);
72
+ const springBone = springSample(rootHeight, velocity, deltaSeconds, seed);
73
+ const fixture = {
74
+ source: "origin-master-foot-ik-spring-bone-adapted" as const,
75
+ footIk: {
76
+ feet,
77
+ hipOffset,
78
+ averageTargetError,
79
+ groundedFeet: feet.filter((foot) => foot.grounded).length,
80
+ terrainSlope: round(terrainSlope)
81
+ },
82
+ springBone,
83
+ productionReadiness: {
84
+ footPlacementTelemetry: feet.every((foot) => foot.grounded && foot.targetError <= 0.015),
85
+ hipAdjustmentTelemetry: hipOffset < 0,
86
+ springChainTelemetry: springBone.boneCount >= 4 && springBone.maxDisplacement > 0,
87
+ collisionTelemetry: springBone.collisionContacts > 0,
88
+ deterministicReplay: true
89
+ },
90
+ hash: "",
91
+ claimBoundary: "Secondary animation fixture adapts old foot-IK ground placement and spring-bone secondary motion into deterministic telemetry; it does not claim production foot locking, retargeting, full-body IK, cloth/hair simulation, Unity Animation Rigging parity, or Unreal Control Rig parity.",
92
+ blockedClaims: [
93
+ "production foot locking parity",
94
+ "full-body IK solver parity",
95
+ "runtime retargeting parity",
96
+ "production spring-bone collision parity",
97
+ "Unity Animation Rigging parity",
98
+ "Unreal Control Rig parity"
99
+ ] as const
100
+ };
101
+ return {
102
+ ...fixture,
103
+ hash: stableHash([
104
+ fixture.footIk.groundedFeet,
105
+ fixture.footIk.hipOffset,
106
+ fixture.footIk.averageTargetError,
107
+ fixture.springBone.maxDisplacement,
108
+ fixture.springBone.collisionContacts,
109
+ fixture.springBone.tipPosition.join(",")
110
+ ].join("|"))
111
+ };
112
+ }
113
+
114
+ function footSample(side: "left" | "right", phase: number, rootHeight: number, terrainSlope: number, normal: Vec3, velocity: Vec3, seed: number): FootIkSample {
115
+ const lateral = side === "left" ? -0.18 : 0.18;
116
+ const stride = Math.sin(phase * Math.PI * 2) * 0.22 + velocity[0] * 0.06;
117
+ const lift = Math.max(0, Math.sin(phase * Math.PI * 2)) * 0.08;
118
+ const jitter = (noise(seed) - 0.5) * 0.01;
119
+ const x = round(stride + jitter);
120
+ const z = round(lateral + velocity[2] * 0.04);
121
+ const sourceY = round(rootHeight - 1.05 + lift);
122
+ const groundY = round(x * terrainSlope - Math.abs(lateral) * 0.04);
123
+ const planted: Vec3 = [x, groundY + 0.035, z];
124
+ const source: Vec3 = [x, sourceY, z];
125
+ const verticalCorrection = round(sourceY - planted[1]);
126
+ return {
127
+ side,
128
+ sourceFoot: source,
129
+ plantedFoot: planted,
130
+ groundNormal: normal,
131
+ grounded: true,
132
+ verticalCorrection,
133
+ targetError: round(Math.abs(planted[1] - (groundY + 0.035)))
134
+ };
135
+ }
136
+
137
+ function springSample(rootHeight: number, velocity: Vec3, deltaSeconds: number, seed: number): SpringBoneSample {
138
+ const stiffness = 0.62;
139
+ const damping = 0.38;
140
+ const collisionRadius = 0.08;
141
+ const substeps = Math.max(1, Math.min(4, Math.ceil(deltaSeconds / (1 / 120))));
142
+ const rootPosition: Vec3 = [0, round(rootHeight + 0.32), -0.08];
143
+ const wind: Vec3 = [round(velocity[0] * 0.18 + noise(seed + 9) * 0.05), 0, round(0.1 + velocity[2] * 0.08)];
144
+ const gravity: Vec3 = [0, -9.81, 0];
145
+ const displacement = round(Math.hypot(wind[0], wind[2]) * (1 - damping) + 0.055);
146
+ const rawTip: Vec3 = [
147
+ round(rootPosition[0] + wind[0] * 0.38),
148
+ round(rootPosition[1] - 0.42 - displacement * 0.35),
149
+ round(rootPosition[2] + wind[2] * 0.42)
150
+ ];
151
+ const collisionFloor = rootHeight - 0.22;
152
+ const collisionContacts = rawTip[1] < collisionFloor + collisionRadius ? 1 : 0;
153
+ const tipPosition: Vec3 = collisionContacts > 0 ? [rawTip[0], round(collisionFloor + collisionRadius), rawTip[2]] : rawTip;
154
+ return {
155
+ chainName: "ponytail",
156
+ boneCount: 5,
157
+ stiffness,
158
+ damping,
159
+ collisionRadius,
160
+ substeps,
161
+ gravity,
162
+ wind,
163
+ rootPosition,
164
+ tipPosition,
165
+ maxDisplacement: round(distance(rootPosition, tipPosition)),
166
+ collisionContacts
167
+ };
168
+ }
169
+
170
+ function wrap01(value: number): number {
171
+ const wrapped = value - Math.floor(value);
172
+ return Number.isFinite(wrapped) ? wrapped : 0;
173
+ }
174
+
175
+ function integerSeed(value: number): number {
176
+ if (!Number.isInteger(value)) throw new Error("Secondary animation fixture seed must be an integer.");
177
+ return value;
178
+ }
179
+
180
+ function normalize(value: Vec3): Vec3 {
181
+ const length = Math.hypot(value[0], value[1], value[2]);
182
+ if (length <= 1e-6) return [0, 1, 0];
183
+ return [round(value[0] / length), round(value[1] / length), round(value[2] / length)];
184
+ }
185
+
186
+ function distance(left: Vec3, right: Vec3): number {
187
+ return Math.hypot(left[0] - right[0], left[1] - right[1], left[2] - right[2]);
188
+ }
189
+
190
+ function noise(seed: number): number {
191
+ let value = Math.imul(seed ^ 0x9e3779b9, 0x85ebca6b);
192
+ value ^= value >>> 13;
193
+ value = Math.imul(value, 0xc2b2ae35);
194
+ return ((value ^ (value >>> 16)) >>> 0) / 0xffffffff;
195
+ }
196
+
197
+ function clamp(value: number, min: number, max: number): number {
198
+ return Math.max(min, Math.min(max, Number.isFinite(value) ? value : min));
199
+ }
200
+
201
+ function round(value: number): number {
202
+ return Number(value.toFixed(4));
203
+ }
204
+
205
+ function stableHash(value: string): string {
206
+ let hash = 0x811c9dc5;
207
+ for (let index = 0; index < value.length; index += 1) {
208
+ hash ^= value.charCodeAt(index);
209
+ hash = Math.imul(hash, 0x01000193);
210
+ }
211
+ return (hash >>> 0).toString(16).padStart(8, "0");
212
+ }
@@ -0,0 +1,34 @@
1
+ import { Bone, type BoneDescriptor } from "./Bone.js";
2
+ import { multiplyMat4, type Mat4 } from "./Keyframe.js";
3
+
4
+ export class Skeleton {
5
+ readonly bones: readonly Bone[];
6
+
7
+ constructor(bones: readonly (Bone | BoneDescriptor)[]) {
8
+ if (bones.length === 0) {
9
+ throw new Error("Skeleton requires at least one bone.");
10
+ }
11
+ this.bones = bones.map((bone) => bone instanceof Bone ? bone : new Bone(bone));
12
+ for (let index = 0; index < this.bones.length; index += 1) {
13
+ const parent = this.bones[index]!.parentIndex;
14
+ if (parent >= index || parent < -1) {
15
+ throw new Error(`Bone ${this.bones[index]!.name} has invalid parent index ${parent}.`);
16
+ }
17
+ }
18
+ }
19
+
20
+ worldMatrices(): readonly Mat4[] {
21
+ const matrices: Mat4[] = [];
22
+ for (let index = 0; index < this.bones.length; index += 1) {
23
+ const bone = this.bones[index]!;
24
+ const local = bone.localMatrix();
25
+ matrices[index] = bone.parentIndex >= 0 ? multiplyMat4(matrices[bone.parentIndex]!, local) : local;
26
+ }
27
+ return matrices;
28
+ }
29
+
30
+ matrixPalette(): readonly Mat4[] {
31
+ const worlds = this.worldMatrices();
32
+ return this.bones.map((bone, index) => multiplyMat4(worlds[index]!, bone.inverseBindMatrix));
33
+ }
34
+ }
@@ -0,0 +1,18 @@
1
+ import type { Skeleton } from "./Skeleton.js";
2
+
3
+ export type SkinningPalette = {
4
+ readonly jointCount: number;
5
+ readonly matrices: Float32Array;
6
+ };
7
+
8
+ export function buildSkinningPalette(skeleton: Skeleton, maxJoints = 256): SkinningPalette {
9
+ const palette = skeleton.matrixPalette();
10
+ if (palette.length > maxJoints) {
11
+ throw new Error(`Skeleton has ${palette.length} joints, exceeding max ${maxJoints}.`);
12
+ }
13
+ const matrices = new Float32Array(palette.length * 16);
14
+ for (let index = 0; index < palette.length; index += 1) {
15
+ matrices.set(palette[index]!, index * 16);
16
+ }
17
+ return { jointCount: palette.length, matrices };
18
+ }