@aura3d/animation 1.4.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +36 -3
- package/src/AnimationAction.ts +0 -166
- package/src/AnimationClip.ts +0 -63
- package/src/AnimationClipEvents.ts +0 -501
- package/src/AnimationClipRegistry.ts +0 -404
- package/src/AnimationController.ts +0 -1017
- package/src/AnimationEvents.ts +0 -34
- package/src/AnimationLayer.ts +0 -73
- package/src/AnimationMixer.ts +0 -402
- package/src/AnimationStateGraph.ts +0 -105
- package/src/AnimationStateMachine.ts +0 -265
- package/src/AnimationTrack.ts +0 -176
- package/src/BlendTree.ts +0 -89
- package/src/Bone.ts +0 -40
- package/src/CrowdAnimation.ts +0 -25
- package/src/ECSAnimationBridge.ts +0 -30
- package/src/FighterAnimationAdapter.ts +0 -124
- package/src/FootIk.ts +0 -251
- package/src/HumanoidBoneInference.ts +0 -323
- package/src/HumanoidRetargeting.ts +0 -700
- package/src/IK.ts +0 -117
- package/src/Inertialization.ts +0 -269
- package/src/Keyframe.ts +0 -159
- package/src/LocomotionController.ts +0 -155
- package/src/LocomotionKit.ts +0 -89
- package/src/MotionMatchingFixtures.ts +0 -254
- package/src/MotionQuality.ts +0 -182
- package/src/RigQuality.ts +0 -101
- package/src/RootMotion.ts +0 -124
- package/src/SceneAnimationBridge.ts +0 -80
- package/src/SecondaryAnimationFixtures.ts +0 -210
- package/src/Skeleton.ts +0 -78
- package/src/Skinning.ts +0 -18
- package/src/SpringBones.ts +0 -243
- package/src/browser-index.ts +0 -66
- package/src/index.ts +0 -66
- package/src/library/performanceStateGraph.ts +0 -173
- package/src/library/standardHumanoidClips.ts +0 -659
- package/src/threejs-compatibility/AnimationAction.ts +0 -27
- package/src/threejs-compatibility/AnimationClip.ts +0 -16
- package/src/threejs-compatibility/AnimationDiagnostics.ts +0 -42
- package/src/threejs-compatibility/AnimationMixer.ts +0 -24
- package/src/threejs-compatibility/MorphTargetMixer.ts +0 -16
- package/src/threejs-compatibility/Skeleton.ts +0 -9
- package/src/threejs-compatibility/SkinnedMesh.ts +0 -12
- package/src/threejs-compatibility/index.ts +0 -11
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { DEFAULT_INERTIALIZATION_HALF_LIFE, inertializedTransitionWeight } from "./Inertialization.js";
|
|
2
|
-
|
|
3
|
-
export type StateTransition = {
|
|
4
|
-
readonly to: string;
|
|
5
|
-
readonly label?: string;
|
|
6
|
-
readonly priority?: number;
|
|
7
|
-
readonly exitTime?: number;
|
|
8
|
-
readonly consumeParameters?: readonly string[];
|
|
9
|
-
readonly condition: (parameters: Readonly<Record<string, number | boolean | string>>) => boolean;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type AnimationState = {
|
|
13
|
-
readonly name: string;
|
|
14
|
-
readonly duration?: number;
|
|
15
|
-
readonly oneShot?: boolean;
|
|
16
|
-
readonly terminal?: boolean;
|
|
17
|
-
readonly onComplete?: string;
|
|
18
|
-
readonly completedParameter?: string;
|
|
19
|
-
readonly resetParametersOnEnter?: readonly string[];
|
|
20
|
-
readonly transitions?: readonly StateTransition[];
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type AnimationStateMachineGraphState = {
|
|
24
|
-
readonly name: string;
|
|
25
|
-
readonly current: boolean;
|
|
26
|
-
readonly transitionCount: number;
|
|
27
|
-
readonly duration?: number;
|
|
28
|
-
readonly oneShot?: boolean;
|
|
29
|
-
readonly terminal?: boolean;
|
|
30
|
-
readonly completed?: boolean;
|
|
31
|
-
readonly onComplete?: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
export type AnimationStateMachineGraphTransition = {
|
|
35
|
-
readonly from: string;
|
|
36
|
-
readonly to: string;
|
|
37
|
-
readonly index: number;
|
|
38
|
-
readonly priority: number;
|
|
39
|
-
readonly label: string;
|
|
40
|
-
readonly exitTime?: number;
|
|
41
|
-
readonly consumeParameters?: readonly string[];
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
export type AnimationStateMachineGraphSnapshot = {
|
|
45
|
-
readonly currentState: string;
|
|
46
|
-
readonly stateTime: number;
|
|
47
|
-
readonly parameters: Readonly<Record<string, number | boolean | string>>;
|
|
48
|
-
readonly states: readonly AnimationStateMachineGraphState[];
|
|
49
|
-
readonly transitions: readonly AnimationStateMachineGraphTransition[];
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** Inertialized blend between the previously-active state and the current state. */
|
|
53
|
-
export type AnimationStateBlend = {
|
|
54
|
-
/** State exited at the last transition (equals `to` when no transition is in flight). */
|
|
55
|
-
readonly from: string;
|
|
56
|
-
/** Currently-active state. */
|
|
57
|
-
readonly to: string;
|
|
58
|
-
/** [fromWeight, toWeight], summing to 1; fromWeight decays critically-damped after a switch. */
|
|
59
|
-
readonly weights: readonly [number, number];
|
|
60
|
-
/** Seconds since the last state change. */
|
|
61
|
-
readonly elapsed: number;
|
|
62
|
-
/** True once the previous state has effectively faded out. */
|
|
63
|
-
readonly done: boolean;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export class AnimationStateMachine {
|
|
67
|
-
readonly states = new Map<string, AnimationState>();
|
|
68
|
-
readonly parameters: Record<string, number | boolean | string> = {};
|
|
69
|
-
currentState: string;
|
|
70
|
-
stateTime = 0;
|
|
71
|
-
/** State active immediately before the most recent transition (for inertialized blending). */
|
|
72
|
-
previousState: string;
|
|
73
|
-
/** Seconds since the most recent state change. */
|
|
74
|
-
transitionElapsed = Number.POSITIVE_INFINITY;
|
|
75
|
-
/** Half-life (seconds) of the inertialized state-transition blend. */
|
|
76
|
-
transitionHalfLife = DEFAULT_INERTIALIZATION_HALF_LIFE;
|
|
77
|
-
|
|
78
|
-
constructor(states: readonly AnimationState[], initialState: string) {
|
|
79
|
-
for (const state of states) {
|
|
80
|
-
if (state.duration !== undefined && (!Number.isFinite(state.duration) || state.duration < 0)) {
|
|
81
|
-
throw new Error(`State ${state.name} has invalid duration.`);
|
|
82
|
-
}
|
|
83
|
-
for (const transition of state.transitions ?? []) {
|
|
84
|
-
if (transition.exitTime !== undefined && (!Number.isFinite(transition.exitTime) || transition.exitTime < 0)) {
|
|
85
|
-
throw new Error(`Transition from ${state.name} to ${transition.to} has invalid exit time.`);
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
this.states.set(state.name, state);
|
|
89
|
-
}
|
|
90
|
-
if (!this.states.has(initialState)) {
|
|
91
|
-
throw new Error(`Initial state ${initialState} does not exist.`);
|
|
92
|
-
}
|
|
93
|
-
for (const state of this.states.values()) {
|
|
94
|
-
if (state.onComplete !== undefined && !this.states.has(state.onComplete)) {
|
|
95
|
-
throw new Error(`State ${state.name} completion target ${state.onComplete} does not exist.`);
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
this.currentState = initialState;
|
|
99
|
-
this.previousState = initialState;
|
|
100
|
-
this.resetParametersForState(this.requireState(initialState));
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
setParameter(name: string, value: number | boolean | string): void {
|
|
104
|
-
this.parameters[name] = value;
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Inertialized blend between {@link previousState} and {@link currentState}. The previous
|
|
109
|
-
* state's weight decays critically-damped (zero initial slope, momentum-preserving) over
|
|
110
|
-
* {@link transitionHalfLife}; consumers crossfade the two states' clips by these weights instead
|
|
111
|
-
* of snapping or linearly ramping. Deterministic given the elapsed transition time.
|
|
112
|
-
*/
|
|
113
|
-
stateBlend(halfLife = this.transitionHalfLife): AnimationStateBlend {
|
|
114
|
-
const elapsed = this.transitionElapsed;
|
|
115
|
-
if (this.previousState === this.currentState || !Number.isFinite(elapsed)) {
|
|
116
|
-
return { from: this.currentState, to: this.currentState, weights: [0, 1], elapsed: Number.isFinite(elapsed) ? elapsed : 0, done: true };
|
|
117
|
-
}
|
|
118
|
-
const fromWeight = inertializedTransitionWeight(elapsed, halfLife);
|
|
119
|
-
const done = fromWeight <= 1e-3;
|
|
120
|
-
return {
|
|
121
|
-
from: this.previousState,
|
|
122
|
-
to: this.currentState,
|
|
123
|
-
weights: [done ? 0 : fromWeight, done ? 1 : 1 - fromWeight],
|
|
124
|
-
elapsed,
|
|
125
|
-
done
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
update(delta = 0): string {
|
|
130
|
-
if (!Number.isFinite(delta) || delta < 0) {
|
|
131
|
-
throw new Error("AnimationStateMachine delta must be finite and non-negative.");
|
|
132
|
-
}
|
|
133
|
-
this.stateTime += delta;
|
|
134
|
-
this.transitionElapsed += delta;
|
|
135
|
-
const state = this.requireState(this.currentState);
|
|
136
|
-
|
|
137
|
-
if (isStateComplete(state, this.stateTime)) {
|
|
138
|
-
if (state.completedParameter) {
|
|
139
|
-
this.parameters[state.completedParameter] = true;
|
|
140
|
-
}
|
|
141
|
-
if (state.onComplete) {
|
|
142
|
-
this.transitionTo(state.onComplete);
|
|
143
|
-
return this.currentState;
|
|
144
|
-
}
|
|
145
|
-
if (state.terminal || state.oneShot) {
|
|
146
|
-
return this.currentState;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (state.terminal || state.oneShot) {
|
|
151
|
-
return this.currentState;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const transition = [...(state.transitions ?? [])]
|
|
155
|
-
.filter((candidate) => candidate.condition(this.parameters))
|
|
156
|
-
.filter((candidate) => candidate.exitTime === undefined || this.stateTime >= candidate.exitTime)
|
|
157
|
-
.sort((a, b) => (b.priority ?? 0) - (a.priority ?? 0) || a.to.localeCompare(b.to))[0];
|
|
158
|
-
if (transition) {
|
|
159
|
-
if (!this.states.has(transition.to)) {
|
|
160
|
-
throw new Error(`Transition target ${transition.to} does not exist.`);
|
|
161
|
-
}
|
|
162
|
-
consumeParameters(this.parameters, transition.consumeParameters);
|
|
163
|
-
this.transitionTo(transition.to);
|
|
164
|
-
}
|
|
165
|
-
return this.currentState;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
graphSnapshot(): AnimationStateMachineGraphSnapshot {
|
|
169
|
-
const states: AnimationStateMachineGraphState[] = [];
|
|
170
|
-
const transitions: AnimationStateMachineGraphTransition[] = [];
|
|
171
|
-
for (const state of this.states.values()) {
|
|
172
|
-
const stateTransitions = state.transitions ?? [];
|
|
173
|
-
states.push({
|
|
174
|
-
name: state.name,
|
|
175
|
-
current: state.name === this.currentState,
|
|
176
|
-
transitionCount: stateTransitions.length,
|
|
177
|
-
...(state.duration === undefined ? {} : { duration: state.duration }),
|
|
178
|
-
...(state.oneShot ? { oneShot: true } : {}),
|
|
179
|
-
...(state.terminal ? { terminal: true } : {}),
|
|
180
|
-
...(state.name === this.currentState && isStateComplete(state, this.stateTime) ? { completed: true } : {}),
|
|
181
|
-
...(state.onComplete === undefined ? {} : { onComplete: state.onComplete })
|
|
182
|
-
});
|
|
183
|
-
stateTransitions.forEach((transition, index) => {
|
|
184
|
-
transitions.push({
|
|
185
|
-
from: state.name,
|
|
186
|
-
to: transition.to,
|
|
187
|
-
index,
|
|
188
|
-
priority: transition.priority ?? 0,
|
|
189
|
-
label: transition.label ?? `${state.name}->${transition.to}`,
|
|
190
|
-
...(transition.exitTime === undefined ? {} : { exitTime: transition.exitTime }),
|
|
191
|
-
...(transition.consumeParameters === undefined || transition.consumeParameters.length === 0
|
|
192
|
-
? {}
|
|
193
|
-
: { consumeParameters: [...transition.consumeParameters] })
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
}
|
|
197
|
-
return {
|
|
198
|
-
currentState: this.currentState,
|
|
199
|
-
stateTime: this.stateTime,
|
|
200
|
-
parameters: { ...this.parameters },
|
|
201
|
-
states,
|
|
202
|
-
transitions
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
debugGraph(): string {
|
|
207
|
-
const snapshot = this.graphSnapshot();
|
|
208
|
-
const lines = [
|
|
209
|
-
`AnimationStateMachine current=${snapshot.currentState} time=${formatNumber(snapshot.stateTime)}`
|
|
210
|
-
];
|
|
211
|
-
for (const state of snapshot.states) {
|
|
212
|
-
lines.push(`state ${state.name}${state.current ? " *" : ""} transitions=${state.transitionCount}`);
|
|
213
|
-
for (const transition of snapshot.transitions.filter((candidate) => candidate.from === state.name)) {
|
|
214
|
-
const exitTime = transition.exitTime === undefined ? "" : ` exit=${formatNumber(transition.exitTime)}`;
|
|
215
|
-
lines.push(` -> ${transition.to} priority=${transition.priority}${exitTime} label=${transition.label}`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
return lines.join("\n");
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
private requireState(name: string): AnimationState {
|
|
222
|
-
const state = this.states.get(name);
|
|
223
|
-
if (!state) {
|
|
224
|
-
throw new Error(`Current state ${name} does not exist.`);
|
|
225
|
-
}
|
|
226
|
-
return state;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
private transitionTo(name: string): void {
|
|
230
|
-
const state = this.requireState(name);
|
|
231
|
-
this.previousState = this.currentState;
|
|
232
|
-
this.currentState = name;
|
|
233
|
-
this.stateTime = 0;
|
|
234
|
-
this.transitionElapsed = this.previousState === name ? Number.POSITIVE_INFINITY : 0;
|
|
235
|
-
this.resetParametersForState(state);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private resetParametersForState(state: AnimationState): void {
|
|
239
|
-
consumeParameters(this.parameters, state.resetParametersOnEnter);
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function formatNumber(value: number): string {
|
|
244
|
-
return Number(value.toFixed(6)).toString();
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
function isStateComplete(state: AnimationState, stateTime: number): boolean {
|
|
248
|
-
return state.duration !== undefined && stateTime >= state.duration;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function consumeParameters(
|
|
252
|
-
parameters: Record<string, number | boolean | string>,
|
|
253
|
-
names: readonly string[] | undefined
|
|
254
|
-
): void {
|
|
255
|
-
for (const name of names ?? []) {
|
|
256
|
-
const value = parameters[name];
|
|
257
|
-
if (typeof value === "boolean") {
|
|
258
|
-
parameters[name] = false;
|
|
259
|
-
} else if (typeof value === "number") {
|
|
260
|
-
parameters[name] = 0;
|
|
261
|
-
} else if (typeof value === "string") {
|
|
262
|
-
parameters[name] = "";
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
}
|
package/src/AnimationTrack.ts
DELETED
|
@@ -1,176 +0,0 @@
|
|
|
1
|
-
import { cloneAnimationValue, deserializeKeyframe, lerpNumber, lerpVec3, serializeKeyframe, slerpQuat, validateKeyframes, type AnimationValue, type Keyframe, type SerializedKeyframe } from "./Keyframe.js";
|
|
2
|
-
|
|
3
|
-
export type TrackValueType = "scalar" | "vector3" | "quaternion" | "number-array" | "boolean" | "string";
|
|
4
|
-
|
|
5
|
-
export type AnimationTrackDescriptor<T extends AnimationValue = AnimationValue> = {
|
|
6
|
-
readonly target: string;
|
|
7
|
-
readonly valueType: TrackValueType;
|
|
8
|
-
readonly keyframes: readonly Keyframe<T>[];
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type SerializedAnimationTrack<T extends AnimationValue = AnimationValue> = {
|
|
12
|
-
readonly target: string;
|
|
13
|
-
readonly valueType: TrackValueType;
|
|
14
|
-
readonly keyframes: readonly SerializedKeyframe<T>[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class AnimationTrack<T extends AnimationValue = AnimationValue> {
|
|
18
|
-
readonly target: string;
|
|
19
|
-
readonly valueType: TrackValueType;
|
|
20
|
-
readonly keyframes: readonly Keyframe<T>[];
|
|
21
|
-
readonly duration: number;
|
|
22
|
-
|
|
23
|
-
constructor(descriptor: AnimationTrackDescriptor<T>) {
|
|
24
|
-
if (descriptor.target.trim().length === 0) {
|
|
25
|
-
throw new Error("AnimationTrack target cannot be empty.");
|
|
26
|
-
}
|
|
27
|
-
validateKeyframes(descriptor.keyframes);
|
|
28
|
-
this.target = descriptor.target;
|
|
29
|
-
this.valueType = descriptor.valueType;
|
|
30
|
-
this.keyframes = descriptor.keyframes.map((keyframe) => ({ ...keyframe, value: cloneAnimationValue(keyframe.value) }));
|
|
31
|
-
this.duration = this.keyframes[this.keyframes.length - 1]!.time;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
sample(time: number): T {
|
|
35
|
-
if (!Number.isFinite(time)) {
|
|
36
|
-
throw new Error("AnimationTrack sample time must be finite.");
|
|
37
|
-
}
|
|
38
|
-
if (time <= this.keyframes[0]!.time) {
|
|
39
|
-
return cloneAnimationValue(this.keyframes[0]!.value);
|
|
40
|
-
}
|
|
41
|
-
const last = this.keyframes[this.keyframes.length - 1]!;
|
|
42
|
-
if (time >= last.time) {
|
|
43
|
-
return cloneAnimationValue(last.value);
|
|
44
|
-
}
|
|
45
|
-
for (let index = 0; index < this.keyframes.length - 1; index += 1) {
|
|
46
|
-
const a = this.keyframes[index]!;
|
|
47
|
-
const b = this.keyframes[index + 1]!;
|
|
48
|
-
if (time >= a.time && time <= b.time) {
|
|
49
|
-
if (a.interpolation === "step" || b.time === a.time) {
|
|
50
|
-
return cloneAnimationValue(a.value);
|
|
51
|
-
}
|
|
52
|
-
const t = (time - a.time) / (b.time - a.time);
|
|
53
|
-
if (a.interpolation === "cubicspline") {
|
|
54
|
-
return interpolateCubicSpline(this.valueType, a.value, b.value, a.outTangent, b.inTangent, t, b.time - a.time) as T;
|
|
55
|
-
}
|
|
56
|
-
return interpolate(this.valueType, a.value, b.value, t) as T;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return cloneAnimationValue(last.value);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
toJSON(): SerializedAnimationTrack<T> {
|
|
63
|
-
return {
|
|
64
|
-
target: this.target,
|
|
65
|
-
valueType: this.valueType,
|
|
66
|
-
keyframes: this.keyframes.map((keyframe) => serializeKeyframe(keyframe))
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
static fromJSON<T extends AnimationValue = AnimationValue>(serialized: SerializedAnimationTrack<T>): AnimationTrack<T> {
|
|
71
|
-
return new AnimationTrack({
|
|
72
|
-
target: serialized.target,
|
|
73
|
-
valueType: serialized.valueType,
|
|
74
|
-
keyframes: serialized.keyframes.map((keyframe) => deserializeKeyframe(keyframe))
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function interpolateCubicSpline(
|
|
80
|
-
type: TrackValueType,
|
|
81
|
-
a: AnimationValue,
|
|
82
|
-
b: AnimationValue,
|
|
83
|
-
outTangent: AnimationValue | undefined,
|
|
84
|
-
inTangent: AnimationValue | undefined,
|
|
85
|
-
t: number,
|
|
86
|
-
deltaTime: number
|
|
87
|
-
): AnimationValue {
|
|
88
|
-
if (outTangent === undefined || inTangent === undefined) {
|
|
89
|
-
throw new Error("CUBICSPLINE keyframes require outTangent on the first keyframe and inTangent on the next keyframe.");
|
|
90
|
-
}
|
|
91
|
-
switch (type) {
|
|
92
|
-
case "scalar":
|
|
93
|
-
return cubicScalar(a as number, b as number, outTangent as number, inTangent as number, t, deltaTime);
|
|
94
|
-
case "vector3":
|
|
95
|
-
return [
|
|
96
|
-
cubicScalar((a as [number, number, number])[0], (b as [number, number, number])[0], (outTangent as [number, number, number])[0], (inTangent as [number, number, number])[0], t, deltaTime),
|
|
97
|
-
cubicScalar((a as [number, number, number])[1], (b as [number, number, number])[1], (outTangent as [number, number, number])[1], (inTangent as [number, number, number])[1], t, deltaTime),
|
|
98
|
-
cubicScalar((a as [number, number, number])[2], (b as [number, number, number])[2], (outTangent as [number, number, number])[2], (inTangent as [number, number, number])[2], t, deltaTime)
|
|
99
|
-
];
|
|
100
|
-
case "quaternion":
|
|
101
|
-
return normalizeCubicQuaternion(a as [number, number, number, number], b as [number, number, number, number], outTangent as [number, number, number, number], inTangent as [number, number, number, number], t, deltaTime);
|
|
102
|
-
case "number-array":
|
|
103
|
-
return cubicNumberArray(a as readonly number[], b as readonly number[], outTangent as readonly number[], inTangent as readonly number[], t, deltaTime);
|
|
104
|
-
case "boolean":
|
|
105
|
-
case "string":
|
|
106
|
-
throw new Error(`CUBICSPLINE interpolation is not supported for ${type} animation tracks.`);
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function cubicScalar(a: number, b: number, outTangent: number, inTangent: number, t: number, deltaTime: number): number {
|
|
111
|
-
const t2 = t * t;
|
|
112
|
-
const t3 = t2 * t;
|
|
113
|
-
return (
|
|
114
|
-
(2 * t3 - 3 * t2 + 1) * a +
|
|
115
|
-
(t3 - 2 * t2 + t) * deltaTime * outTangent +
|
|
116
|
-
(-2 * t3 + 3 * t2) * b +
|
|
117
|
-
(t3 - t2) * deltaTime * inTangent
|
|
118
|
-
);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function cubicNumberArray(
|
|
122
|
-
a: readonly number[],
|
|
123
|
-
b: readonly number[],
|
|
124
|
-
outTangent: readonly number[],
|
|
125
|
-
inTangent: readonly number[],
|
|
126
|
-
t: number,
|
|
127
|
-
deltaTime: number
|
|
128
|
-
): readonly number[] {
|
|
129
|
-
if (a.length !== b.length || a.length !== outTangent.length || b.length !== inTangent.length) {
|
|
130
|
-
throw new Error("CUBICSPLINE number-array keyframes require matching value and tangent lengths.");
|
|
131
|
-
}
|
|
132
|
-
return a.map((value, index) => cubicScalar(value, b[index]!, outTangent[index]!, inTangent[index]!, t, deltaTime));
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function normalizeCubicQuaternion(
|
|
136
|
-
a: readonly [number, number, number, number],
|
|
137
|
-
b: readonly [number, number, number, number],
|
|
138
|
-
outTangent: readonly [number, number, number, number],
|
|
139
|
-
inTangent: readonly [number, number, number, number],
|
|
140
|
-
t: number,
|
|
141
|
-
deltaTime: number
|
|
142
|
-
): readonly [number, number, number, number] {
|
|
143
|
-
return slerpQuat(
|
|
144
|
-
[0, 0, 0, 1],
|
|
145
|
-
[
|
|
146
|
-
cubicScalar(a[0], b[0], outTangent[0], inTangent[0], t, deltaTime),
|
|
147
|
-
cubicScalar(a[1], b[1], outTangent[1], inTangent[1], t, deltaTime),
|
|
148
|
-
cubicScalar(a[2], b[2], outTangent[2], inTangent[2], t, deltaTime),
|
|
149
|
-
cubicScalar(a[3], b[3], outTangent[3], inTangent[3], t, deltaTime)
|
|
150
|
-
],
|
|
151
|
-
1
|
|
152
|
-
);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
function interpolate(type: TrackValueType, a: AnimationValue, b: AnimationValue, t: number): AnimationValue {
|
|
156
|
-
switch (type) {
|
|
157
|
-
case "scalar":
|
|
158
|
-
return lerpNumber(a as number, b as number, t);
|
|
159
|
-
case "vector3":
|
|
160
|
-
return lerpVec3(a as [number, number, number], b as [number, number, number], t);
|
|
161
|
-
case "quaternion":
|
|
162
|
-
return slerpQuat(a as [number, number, number, number], b as [number, number, number, number], t);
|
|
163
|
-
case "number-array":
|
|
164
|
-
return interpolateNumberArray(a as readonly number[], b as readonly number[], t);
|
|
165
|
-
case "boolean":
|
|
166
|
-
case "string":
|
|
167
|
-
return t < 1 ? a : b;
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function interpolateNumberArray(a: readonly number[], b: readonly number[], t: number): readonly number[] {
|
|
172
|
-
if (a.length !== b.length) {
|
|
173
|
-
throw new Error("number-array animation keyframes require matching lengths.");
|
|
174
|
-
}
|
|
175
|
-
return a.map((value, index) => lerpNumber(value, b[index]!, t));
|
|
176
|
-
}
|
package/src/BlendTree.ts
DELETED
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
export type BlendTreeChild<T> = {
|
|
2
|
-
readonly value: T;
|
|
3
|
-
readonly threshold: number;
|
|
4
|
-
};
|
|
5
|
-
|
|
6
|
-
export type BlendTree2DChild<T> = {
|
|
7
|
-
readonly value: T;
|
|
8
|
-
readonly position: readonly [number, number];
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type BlendTreeWeight<T> = {
|
|
12
|
-
readonly value: T;
|
|
13
|
-
readonly weight: number;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
export class BlendTree1D<T> {
|
|
17
|
-
readonly children: readonly BlendTreeChild<T>[];
|
|
18
|
-
|
|
19
|
-
constructor(children: readonly BlendTreeChild<T>[]) {
|
|
20
|
-
if (children.length === 0) {
|
|
21
|
-
throw new Error("BlendTree1D requires at least one child.");
|
|
22
|
-
}
|
|
23
|
-
this.children = [...children].sort((a, b) => a.threshold - b.threshold);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
weights(parameter: number): readonly BlendTreeWeight<T>[] {
|
|
27
|
-
if (!Number.isFinite(parameter)) {
|
|
28
|
-
throw new Error("Blend parameter must be finite.");
|
|
29
|
-
}
|
|
30
|
-
if (this.children.length === 1 || parameter <= this.children[0]!.threshold) {
|
|
31
|
-
return [{ value: this.children[0]!.value, weight: 1 }];
|
|
32
|
-
}
|
|
33
|
-
const last = this.children[this.children.length - 1]!;
|
|
34
|
-
if (parameter >= last.threshold) {
|
|
35
|
-
return [{ value: last.value, weight: 1 }];
|
|
36
|
-
}
|
|
37
|
-
for (let index = 0; index < this.children.length - 1; index += 1) {
|
|
38
|
-
const a = this.children[index]!;
|
|
39
|
-
const b = this.children[index + 1]!;
|
|
40
|
-
if (parameter >= a.threshold && parameter <= b.threshold) {
|
|
41
|
-
const span = b.threshold - a.threshold;
|
|
42
|
-
const t = span === 0 ? 0 : (parameter - a.threshold) / span;
|
|
43
|
-
return [{ value: a.value, weight: 1 - t }, { value: b.value, weight: t }];
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
return [{ value: last.value, weight: 1 }];
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export class BlendTree2D<T> {
|
|
51
|
-
readonly children: readonly BlendTree2DChild<T>[];
|
|
52
|
-
|
|
53
|
-
constructor(children: readonly BlendTree2DChild<T>[]) {
|
|
54
|
-
if (children.length === 0) {
|
|
55
|
-
throw new Error("BlendTree2D requires at least one child.");
|
|
56
|
-
}
|
|
57
|
-
for (const child of children) {
|
|
58
|
-
if (!Number.isFinite(child.position[0]) || !Number.isFinite(child.position[1])) {
|
|
59
|
-
throw new Error("BlendTree2D child positions must be finite.");
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
this.children = [...children];
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
weights(parameter: readonly [number, number]): readonly BlendTreeWeight<T>[] {
|
|
66
|
-
if (!Number.isFinite(parameter[0]) || !Number.isFinite(parameter[1])) {
|
|
67
|
-
throw new Error("BlendTree2D parameters must be finite.");
|
|
68
|
-
}
|
|
69
|
-
if (this.children.length === 1) {
|
|
70
|
-
return [{ value: this.children[0]!.value, weight: 1 }];
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const distances = this.children.map((child) => {
|
|
74
|
-
const dx = parameter[0] - child.position[0];
|
|
75
|
-
const dy = parameter[1] - child.position[1];
|
|
76
|
-
return Math.hypot(dx, dy);
|
|
77
|
-
});
|
|
78
|
-
const exactIndex = distances.findIndex((distance) => distance === 0);
|
|
79
|
-
if (exactIndex >= 0) {
|
|
80
|
-
return [{ value: this.children[exactIndex]!.value, weight: 1 }];
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const inverseDistances = distances.map((distance) => 1 / distance);
|
|
84
|
-
const total = inverseDistances.reduce((sum, weight) => sum + weight, 0);
|
|
85
|
-
return this.children
|
|
86
|
-
.map((child, index) => ({ value: child.value, weight: inverseDistances[index]! / total }))
|
|
87
|
-
.filter((weight) => weight.weight > 0);
|
|
88
|
-
}
|
|
89
|
-
}
|
package/src/Bone.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { composeMat4, identityMat4, type Mat4, type Quat, type Vec3 } from "./Keyframe.js";
|
|
2
|
-
|
|
3
|
-
export type BoneDescriptor = {
|
|
4
|
-
readonly name: string;
|
|
5
|
-
readonly parentIndex: number;
|
|
6
|
-
readonly translation?: Vec3;
|
|
7
|
-
readonly rotation?: Quat;
|
|
8
|
-
readonly scale?: Vec3;
|
|
9
|
-
readonly inverseBindMatrix?: Mat4;
|
|
10
|
-
/** Tag this bone as the start (or a member) of a spring-bone chain (secondary dynamics). */
|
|
11
|
-
readonly springChain?: boolean;
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export class Bone {
|
|
15
|
-
readonly name: string;
|
|
16
|
-
readonly parentIndex: number;
|
|
17
|
-
translation: Vec3;
|
|
18
|
-
rotation: Quat;
|
|
19
|
-
scale: Vec3;
|
|
20
|
-
inverseBindMatrix: Mat4;
|
|
21
|
-
/** True when this bone participates in a spring-bone chain solved by `createSpringChain`. */
|
|
22
|
-
springChain: boolean;
|
|
23
|
-
|
|
24
|
-
constructor(descriptor: BoneDescriptor) {
|
|
25
|
-
if (descriptor.name.trim().length === 0) {
|
|
26
|
-
throw new Error("Bone name cannot be empty.");
|
|
27
|
-
}
|
|
28
|
-
this.name = descriptor.name;
|
|
29
|
-
this.parentIndex = descriptor.parentIndex;
|
|
30
|
-
this.translation = descriptor.translation ?? [0, 0, 0];
|
|
31
|
-
this.rotation = descriptor.rotation ?? [0, 0, 0, 1];
|
|
32
|
-
this.scale = descriptor.scale ?? [1, 1, 1];
|
|
33
|
-
this.inverseBindMatrix = descriptor.inverseBindMatrix ?? identityMat4();
|
|
34
|
-
this.springChain = descriptor.springChain ?? false;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
localMatrix(): Mat4 {
|
|
38
|
-
return composeMat4(this.translation, this.rotation, this.scale);
|
|
39
|
-
}
|
|
40
|
-
}
|
package/src/CrowdAnimation.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
export interface CrowdAnimationAgent {
|
|
2
|
-
readonly id: string;
|
|
3
|
-
readonly clip: string;
|
|
4
|
-
readonly phase: number;
|
|
5
|
-
readonly speed: number;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export interface CrowdAnimationSample {
|
|
9
|
-
readonly id: string;
|
|
10
|
-
readonly clip: string;
|
|
11
|
-
readonly time: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function sampleCrowdAnimation(agents: readonly CrowdAnimationAgent[], elapsedSeconds: number): readonly CrowdAnimationSample[] {
|
|
15
|
-
if (!Number.isFinite(elapsedSeconds) || elapsedSeconds < 0) {
|
|
16
|
-
throw new Error("Crowd animation elapsedSeconds must be finite and non-negative.");
|
|
17
|
-
}
|
|
18
|
-
return agents.map((agent) => {
|
|
19
|
-
if (!agent.id.trim() || !agent.clip.trim()) throw new Error("Crowd animation agents require id and clip.");
|
|
20
|
-
if (!Number.isFinite(agent.phase) || !Number.isFinite(agent.speed) || agent.speed < 0) {
|
|
21
|
-
throw new Error(`Crowd animation agent ${agent.id} has invalid phase or speed.`);
|
|
22
|
-
}
|
|
23
|
-
return { id: agent.id, clip: agent.clip, time: agent.phase + elapsedSeconds * agent.speed };
|
|
24
|
-
});
|
|
25
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { AnimationValue } from "./Keyframe.js";
|
|
2
|
-
|
|
3
|
-
export type ECSAnimationComponent = Record<string, AnimationValue | undefined>;
|
|
4
|
-
|
|
5
|
-
export class ECSAnimationBridge {
|
|
6
|
-
private readonly components = new Map<string, ECSAnimationComponent>();
|
|
7
|
-
|
|
8
|
-
register(entityName: string, component: ECSAnimationComponent): void {
|
|
9
|
-
if (entityName.trim().length === 0) {
|
|
10
|
-
throw new Error("ECS animation target name cannot be empty.");
|
|
11
|
-
}
|
|
12
|
-
this.components.set(entityName, component);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
unregister(entityName: string): void {
|
|
16
|
-
this.components.delete(entityName);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
setAnimationValue(target: string, value: AnimationValue): void {
|
|
20
|
-
const [entityName, property] = target.split(".");
|
|
21
|
-
if (!entityName || !property) {
|
|
22
|
-
throw new Error(`Invalid ECS animation target ${target}.`);
|
|
23
|
-
}
|
|
24
|
-
const component = this.components.get(entityName);
|
|
25
|
-
if (!component) {
|
|
26
|
-
throw new Error(`Missing ECS animation target ${entityName}.`);
|
|
27
|
-
}
|
|
28
|
-
component[property] = Array.isArray(value) ? ([...value] as unknown as AnimationValue) : value;
|
|
29
|
-
}
|
|
30
|
-
}
|