@aura3d/animation 1.3.3 → 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
package/src/AnimationEvents.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export type AnimationEventMarker = {
|
|
2
|
-
readonly name: string;
|
|
3
|
-
readonly time: number;
|
|
4
|
-
readonly payload?: unknown;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export type AnimationEvent = AnimationEventMarker & {
|
|
8
|
-
readonly clipName: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export function eventsBetween(markers: readonly AnimationEventMarker[], clipName: string, fromTime: number, toTime: number, duration: number, looped: boolean): readonly AnimationEvent[] {
|
|
12
|
-
const events: AnimationEvent[] = [];
|
|
13
|
-
if (duration <= 0) {
|
|
14
|
-
return events;
|
|
15
|
-
}
|
|
16
|
-
if (!looped || toTime >= fromTime) {
|
|
17
|
-
for (const marker of markers) {
|
|
18
|
-
if (marker.time > fromTime && marker.time <= toTime) {
|
|
19
|
-
events.push({ ...marker, clipName });
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
return events;
|
|
23
|
-
}
|
|
24
|
-
const lateEvents: AnimationEvent[] = [];
|
|
25
|
-
const earlyEvents: AnimationEvent[] = [];
|
|
26
|
-
for (const marker of markers) {
|
|
27
|
-
if (marker.time > fromTime && marker.time <= duration) {
|
|
28
|
-
lateEvents.push({ ...marker, clipName });
|
|
29
|
-
} else if (marker.time >= 0 && marker.time <= toTime) {
|
|
30
|
-
earlyEvents.push({ ...marker, clipName });
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
return [...lateEvents, ...earlyEvents];
|
|
34
|
-
}
|
package/src/AnimationLayer.ts
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { AnimationAction } from "./AnimationAction.js";
|
|
2
|
-
|
|
3
|
-
export type AnimationLayerSnapshot = {
|
|
4
|
-
readonly name: string;
|
|
5
|
-
readonly weight: number;
|
|
6
|
-
readonly additive: boolean;
|
|
7
|
-
readonly mask: readonly string[];
|
|
8
|
-
readonly actions: readonly string[];
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type AnimationLayerOptions = {
|
|
12
|
-
readonly weight?: number;
|
|
13
|
-
readonly additive?: boolean;
|
|
14
|
-
readonly mask?: readonly string[];
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
export class AnimationLayer {
|
|
18
|
-
readonly name: string;
|
|
19
|
-
weight: number;
|
|
20
|
-
additive: boolean;
|
|
21
|
-
readonly mask: readonly string[];
|
|
22
|
-
readonly actions: AnimationAction[] = [];
|
|
23
|
-
private readonly baseWeights = new WeakMap<AnimationAction, number>();
|
|
24
|
-
|
|
25
|
-
constructor(name: string, weightOrOptions: number | AnimationLayerOptions = 1) {
|
|
26
|
-
if (name.trim().length === 0) {
|
|
27
|
-
throw new Error("AnimationLayer name cannot be empty.");
|
|
28
|
-
}
|
|
29
|
-
this.name = name;
|
|
30
|
-
const options = typeof weightOrOptions === "number" ? { weight: weightOrOptions } : weightOrOptions;
|
|
31
|
-
this.weight = options.weight ?? 1;
|
|
32
|
-
this.additive = options.additive ?? false;
|
|
33
|
-
this.mask = [...(options.mask ?? [])];
|
|
34
|
-
if (!Number.isFinite(this.weight) || this.weight < 0) {
|
|
35
|
-
throw new Error("AnimationLayer weight must be finite and non-negative.");
|
|
36
|
-
}
|
|
37
|
-
for (const entry of this.mask) {
|
|
38
|
-
if (entry.trim().length === 0) {
|
|
39
|
-
throw new Error("AnimationLayer mask entries cannot be empty.");
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
add(action: AnimationAction): void {
|
|
45
|
-
if (!this.actions.includes(action)) {
|
|
46
|
-
this.actions.push(action);
|
|
47
|
-
this.baseWeights.set(action, action.weight);
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
applyWeight(): void {
|
|
52
|
-
for (const action of this.actions) {
|
|
53
|
-
action.setWeight((this.baseWeights.get(action) ?? action.weight) * this.weight);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
capturesTarget(target: string): boolean {
|
|
58
|
-
if (this.mask.length === 0) {
|
|
59
|
-
return true;
|
|
60
|
-
}
|
|
61
|
-
return this.mask.some((entry) => target === entry || target.startsWith(`${entry}.`));
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
snapshot(): AnimationLayerSnapshot {
|
|
65
|
-
return {
|
|
66
|
-
name: this.name,
|
|
67
|
-
weight: this.weight,
|
|
68
|
-
additive: this.additive,
|
|
69
|
-
mask: [...this.mask],
|
|
70
|
-
actions: this.actions.map((action) => action.clip.name)
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
}
|
package/src/AnimationMixer.ts
DELETED
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
import { AnimationAction, type AnimationActionSnapshot } from "./AnimationAction.js";
|
|
2
|
-
import type { AnimationClip } from "./AnimationClip.js";
|
|
3
|
-
import type { AnimationEvent } from "./AnimationEvents.js";
|
|
4
|
-
import type { AnimationLayer, AnimationLayerSnapshot } from "./AnimationLayer.js";
|
|
5
|
-
import { cloneAnimationValue, normalizeQuat, slerpQuat, type AnimationValue } from "./Keyframe.js";
|
|
6
|
-
import { DEFAULT_INERTIALIZATION_HALF_LIFE, inertializedTransitionWeight } from "./Inertialization.js";
|
|
7
|
-
import { applyRootMotion, extractRootMotion, type RootMotionSample, type RootMotionTarget } from "./RootMotion.js";
|
|
8
|
-
|
|
9
|
-
export type AnimationTarget = {
|
|
10
|
-
setAnimationValue?: (target: string, value: AnimationValue) => void;
|
|
11
|
-
applyRootMotion?: (sample: RootMotionSample) => void;
|
|
12
|
-
position?: [number, number, number];
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type AnimationMixerOptions = {
|
|
16
|
-
readonly applyRootMotion?: boolean;
|
|
17
|
-
readonly rootMotionTrack?: string;
|
|
18
|
-
readonly rootMotionScale?: number;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export type AnimationMixerSnapshot = {
|
|
22
|
-
readonly timeScale: number;
|
|
23
|
-
readonly actionCount: number;
|
|
24
|
-
readonly actions: readonly AnimationActionSnapshot[];
|
|
25
|
-
readonly layers: readonly AnimationLayerSnapshot[];
|
|
26
|
-
readonly values: Readonly<Record<string, AnimationValue>>;
|
|
27
|
-
readonly applyErrors: readonly AnimationApplyError[];
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export type AnimationApplyError = {
|
|
31
|
-
readonly target: string;
|
|
32
|
-
readonly message: string;
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
type MixerLayer = Pick<AnimationLayer, "name" | "weight" | "additive" | "mask" | "actions" | "capturesTarget" | "snapshot">;
|
|
36
|
-
type WeightedAccumulator = { value: AnimationValue; weight: number; type: string };
|
|
37
|
-
type TargetAccumulator = { type: string; base?: WeightedAccumulator; additive?: AnimationValue };
|
|
38
|
-
|
|
39
|
-
export class AnimationMixer {
|
|
40
|
-
readonly target: AnimationTarget | undefined;
|
|
41
|
-
timeScale = 1;
|
|
42
|
-
private readonly actions: AnimationAction[] = [];
|
|
43
|
-
private readonly layers: AnimationLayer[] = [];
|
|
44
|
-
private readonly values = new Map<string, AnimationValue>();
|
|
45
|
-
private applyErrors: AnimationApplyError[] = [];
|
|
46
|
-
private readonly eventListeners = new Set<(event: AnimationEvent) => void>();
|
|
47
|
-
private disposed = false;
|
|
48
|
-
private inertialTransition: { from: AnimationAction; to: AnimationAction; elapsed: number; halfLife: number } | undefined;
|
|
49
|
-
|
|
50
|
-
constructor(target?: AnimationTarget, private readonly options: AnimationMixerOptions = {}) {
|
|
51
|
-
this.target = target;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
play(clip: AnimationClip): AnimationAction {
|
|
55
|
-
this.assertAlive();
|
|
56
|
-
const action = new AnimationAction(clip).play();
|
|
57
|
-
this.actions.push(action);
|
|
58
|
-
return action;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
addAction(action: AnimationAction): void {
|
|
62
|
-
this.assertAlive();
|
|
63
|
-
if (!this.actions.includes(action)) {
|
|
64
|
-
this.actions.push(action);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
addLayer(layer: AnimationLayer): void {
|
|
69
|
-
this.assertAlive();
|
|
70
|
-
if (!this.layers.includes(layer)) {
|
|
71
|
-
this.layers.push(layer);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
stopAll(): void {
|
|
76
|
-
this.assertAlive();
|
|
77
|
-
for (const action of this.actions) {
|
|
78
|
-
action.stop();
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
crossFade(from: AnimationAction, to: AnimationAction, duration: number): void {
|
|
83
|
-
this.assertAlive();
|
|
84
|
-
from.fadeTo(0, duration);
|
|
85
|
-
to.play().fadeTo(1, duration);
|
|
86
|
-
this.addAction(to);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Inertialized transition between two actions: instead of the linear weight ramp of
|
|
91
|
-
* {@link crossFade}, the source action's weight follows a critically-damped decay (zero initial
|
|
92
|
-
* slope, momentum-preserving) so the blend carries through instead of dissolving. The transition
|
|
93
|
-
* is advanced deterministically inside {@link update}; weights always sum to 1 and the source is
|
|
94
|
-
* stopped once it has effectively faded. Pass a `halfLife` (seconds) to tune snappiness.
|
|
95
|
-
*/
|
|
96
|
-
inertialCrossFade(from: AnimationAction, to: AnimationAction, halfLife = DEFAULT_INERTIALIZATION_HALF_LIFE): void {
|
|
97
|
-
this.assertAlive();
|
|
98
|
-
to.play();
|
|
99
|
-
from.weight = 1;
|
|
100
|
-
to.weight = 0;
|
|
101
|
-
this.addAction(to);
|
|
102
|
-
this.inertialTransition = { from, to, elapsed: 0, halfLife };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
update(delta: number): readonly AnimationEvent[] {
|
|
106
|
-
this.assertAlive();
|
|
107
|
-
if (!Number.isFinite(delta) || delta < 0) {
|
|
108
|
-
throw new Error("AnimationMixer delta must be finite and non-negative.");
|
|
109
|
-
}
|
|
110
|
-
this.advanceInertialTransition(delta * this.timeScale);
|
|
111
|
-
const events: AnimationEvent[] = [];
|
|
112
|
-
const accumulators = new Map<string, TargetAccumulator>();
|
|
113
|
-
for (const action of this.actions) {
|
|
114
|
-
const previousTime = action.time;
|
|
115
|
-
const actionEvents = action.update(delta * this.timeScale);
|
|
116
|
-
if (actionEvents.length > 0) {
|
|
117
|
-
events.push(...actionEvents);
|
|
118
|
-
}
|
|
119
|
-
if (!action.playing && action.weight <= 0) {
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
this.applyActionRootMotion(action, previousTime);
|
|
123
|
-
for (const track of action.clip.tracks) {
|
|
124
|
-
const sample = track.sample(action.time);
|
|
125
|
-
for (const layer of this.layersForAction(action)) {
|
|
126
|
-
if (layer.capturesTarget(track.target)) {
|
|
127
|
-
blendInto(accumulators, track.target, track.valueType, sample, action.weight * layer.weight, layer.additive);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
this.values.clear();
|
|
133
|
-
this.applyErrors = [];
|
|
134
|
-
for (const [target, accumulator] of accumulators) {
|
|
135
|
-
const value = finalizeTargetBlend(accumulator);
|
|
136
|
-
this.values.set(target, value);
|
|
137
|
-
try {
|
|
138
|
-
this.target?.setAnimationValue?.(target, value);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
this.applyErrors.push({
|
|
141
|
-
target,
|
|
142
|
-
message: error instanceof Error ? error.message : String(error)
|
|
143
|
-
});
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
for (const event of events) {
|
|
147
|
-
for (const listener of Array.from(this.eventListeners)) {
|
|
148
|
-
listener(event);
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
return events;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
getValue(target: string): AnimationValue | undefined {
|
|
155
|
-
const value = this.values.get(target);
|
|
156
|
-
return value === undefined ? undefined : cloneAnimationValue(value);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
onEvent(listener: (event: AnimationEvent) => void): () => void {
|
|
160
|
-
this.assertAlive();
|
|
161
|
-
this.eventListeners.add(listener);
|
|
162
|
-
return () => this.eventListeners.delete(listener);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
snapshot(): AnimationMixerSnapshot {
|
|
166
|
-
const values: Record<string, AnimationValue> = {};
|
|
167
|
-
for (const [target, value] of this.values) {
|
|
168
|
-
values[target] = cloneAnimationValue(value);
|
|
169
|
-
}
|
|
170
|
-
return {
|
|
171
|
-
timeScale: this.timeScale,
|
|
172
|
-
actionCount: this.actions.length,
|
|
173
|
-
actions: this.actions.map((action) => action.snapshot()),
|
|
174
|
-
layers: this.layers.map((layer) => layer.snapshot()),
|
|
175
|
-
values,
|
|
176
|
-
applyErrors: this.applyErrors.map((error) => ({ ...error }))
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
dispose(): void {
|
|
181
|
-
this.actions.length = 0;
|
|
182
|
-
this.layers.length = 0;
|
|
183
|
-
this.values.clear();
|
|
184
|
-
this.applyErrors = [];
|
|
185
|
-
this.eventListeners.clear();
|
|
186
|
-
this.inertialTransition = undefined;
|
|
187
|
-
this.disposed = true;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
private assertAlive(): void {
|
|
191
|
-
if (this.disposed) {
|
|
192
|
-
throw new Error("AnimationMixer has been disposed.");
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
private advanceInertialTransition(scaledDelta: number): void {
|
|
197
|
-
const transition = this.inertialTransition;
|
|
198
|
-
if (!transition) return;
|
|
199
|
-
transition.elapsed += scaledDelta;
|
|
200
|
-
const fromWeight = inertializedTransitionWeight(transition.elapsed, transition.halfLife);
|
|
201
|
-
transition.from.weight = fromWeight;
|
|
202
|
-
transition.to.weight = 1 - fromWeight;
|
|
203
|
-
// Retire the transition once the source has effectively faded (offset within 1e-3).
|
|
204
|
-
if (fromWeight <= 1e-3) {
|
|
205
|
-
transition.from.weight = 0;
|
|
206
|
-
transition.to.weight = 1;
|
|
207
|
-
transition.from.stop();
|
|
208
|
-
this.inertialTransition = undefined;
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
private applyActionRootMotion(action: AnimationAction, previousTime: number): void {
|
|
213
|
-
if (!this.options.applyRootMotion || action.weight <= 0) return;
|
|
214
|
-
const rootMotionTarget = this.options.rootMotionTrack ?? "root.position";
|
|
215
|
-
if (!action.clip.tracks.some((track) => track.target === rootMotionTarget)) return;
|
|
216
|
-
const sample = extractRootMotion(action.clip, {
|
|
217
|
-
fromTime: previousTime,
|
|
218
|
-
toTime: action.time,
|
|
219
|
-
loop: action.loopMode === "repeat",
|
|
220
|
-
target: rootMotionTarget
|
|
221
|
-
});
|
|
222
|
-
const scaledSample = action.weight === 1
|
|
223
|
-
? sample
|
|
224
|
-
: {
|
|
225
|
-
...sample,
|
|
226
|
-
delta: [
|
|
227
|
-
sample.delta[0] * action.weight,
|
|
228
|
-
sample.delta[1] * action.weight,
|
|
229
|
-
sample.delta[2] * action.weight
|
|
230
|
-
] as [number, number, number]
|
|
231
|
-
};
|
|
232
|
-
this.target?.applyRootMotion?.(scaledSample);
|
|
233
|
-
if (this.target?.position) {
|
|
234
|
-
applyRootMotion(this.target as RootMotionTarget, scaledSample, this.options.rootMotionScale ?? 1);
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
private layersForAction(action: AnimationAction): readonly MixerLayer[] {
|
|
239
|
-
if (this.layers.length === 0) {
|
|
240
|
-
return UNMASKED_LAYERS;
|
|
241
|
-
}
|
|
242
|
-
const owningLayers = this.layers.filter((layer) => layer.actions.includes(action));
|
|
243
|
-
return owningLayers.length > 0 ? owningLayers : UNMASKED_LAYERS;
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
const UNMASKED_LAYER: MixerLayer = {
|
|
248
|
-
name: "default",
|
|
249
|
-
weight: 1,
|
|
250
|
-
additive: false,
|
|
251
|
-
mask: [],
|
|
252
|
-
actions: [],
|
|
253
|
-
capturesTarget: () => true,
|
|
254
|
-
snapshot: () => ({ name: "default", weight: 1, additive: false, mask: [], actions: [] })
|
|
255
|
-
};
|
|
256
|
-
const UNMASKED_LAYERS: readonly MixerLayer[] = [UNMASKED_LAYER];
|
|
257
|
-
|
|
258
|
-
function blendInto(accumulators: Map<string, TargetAccumulator>, target: string, type: string, value: AnimationValue, weight: number, additive: boolean): void {
|
|
259
|
-
if (weight <= 0) {
|
|
260
|
-
return;
|
|
261
|
-
}
|
|
262
|
-
const current = accumulators.get(target);
|
|
263
|
-
if (!current) {
|
|
264
|
-
const accumulator: TargetAccumulator = { type };
|
|
265
|
-
if (additive) {
|
|
266
|
-
accumulator.additive = additiveContribution(type, value, weight);
|
|
267
|
-
} else {
|
|
268
|
-
accumulator.base = { value: cloneAnimationValue(value), weight, type };
|
|
269
|
-
}
|
|
270
|
-
accumulators.set(target, accumulator);
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (current.type !== type) {
|
|
274
|
-
throw new Error(`Cannot blend ${type} animation track into existing ${current.type} target ${target}.`);
|
|
275
|
-
}
|
|
276
|
-
if (additive) {
|
|
277
|
-
current.additive = current.additive === undefined
|
|
278
|
-
? additiveContribution(type, value, weight)
|
|
279
|
-
: combineAdditive(type, current.additive, value, weight);
|
|
280
|
-
return;
|
|
281
|
-
}
|
|
282
|
-
if (!current.base) {
|
|
283
|
-
current.base = { value: cloneAnimationValue(value), weight, type };
|
|
284
|
-
return;
|
|
285
|
-
}
|
|
286
|
-
blendBase(current.base, type, value, weight);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function blendBase(current: WeightedAccumulator, type: string, value: AnimationValue, weight: number): void {
|
|
290
|
-
const total = current.weight + weight;
|
|
291
|
-
const t = weight / total;
|
|
292
|
-
if (type === "scalar") {
|
|
293
|
-
current.value = (current.value as number) + ((value as number) - (current.value as number)) * t;
|
|
294
|
-
} else if (type === "vector3") {
|
|
295
|
-
const a = current.value as [number, number, number];
|
|
296
|
-
const b = value as [number, number, number];
|
|
297
|
-
current.value = [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t];
|
|
298
|
-
} else if (type === "quaternion") {
|
|
299
|
-
current.value = slerpQuat(current.value as [number, number, number, number], value as [number, number, number, number], t);
|
|
300
|
-
} else if (type === "number-array") {
|
|
301
|
-
const a = current.value as readonly number[];
|
|
302
|
-
const b = value as readonly number[];
|
|
303
|
-
if (a.length !== b.length) {
|
|
304
|
-
throw new Error("Cannot blend number-array animation values with different lengths.");
|
|
305
|
-
}
|
|
306
|
-
current.value = a.map((component, index) => component + (b[index]! - component) * t);
|
|
307
|
-
} else {
|
|
308
|
-
current.value = cloneAnimationValue(value);
|
|
309
|
-
}
|
|
310
|
-
current.weight = total;
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
function finalizeTargetBlend(accumulator: TargetAccumulator): AnimationValue {
|
|
314
|
-
const base = accumulator.base
|
|
315
|
-
? finalizeBaseBlend(accumulator.base)
|
|
316
|
-
: accumulator.additive !== undefined
|
|
317
|
-
? additiveNeutral(accumulator.type, accumulator.additive)
|
|
318
|
-
: undefined;
|
|
319
|
-
if (base === undefined) {
|
|
320
|
-
throw new Error("Animation target accumulator has no sampled value.");
|
|
321
|
-
}
|
|
322
|
-
if (accumulator.additive === undefined) {
|
|
323
|
-
return base;
|
|
324
|
-
}
|
|
325
|
-
return applyAdditive(accumulator.type, base, accumulator.additive);
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
function finalizeBaseBlend(accumulator: WeightedAccumulator): AnimationValue {
|
|
329
|
-
if (accumulator.type === "quaternion") {
|
|
330
|
-
return normalizeQuat(accumulator.value as [number, number, number, number]);
|
|
331
|
-
}
|
|
332
|
-
return cloneAnimationValue(accumulator.value);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function additiveContribution(type: string, value: AnimationValue, weight: number): AnimationValue {
|
|
336
|
-
if (type === "scalar") {
|
|
337
|
-
return (value as number) * weight;
|
|
338
|
-
}
|
|
339
|
-
if (type === "vector3") {
|
|
340
|
-
const vector = value as [number, number, number];
|
|
341
|
-
return [vector[0] * weight, vector[1] * weight, vector[2] * weight];
|
|
342
|
-
}
|
|
343
|
-
if (type === "number-array") {
|
|
344
|
-
return (value as readonly number[]).map((component) => component * weight);
|
|
345
|
-
}
|
|
346
|
-
if (type === "quaternion") {
|
|
347
|
-
return slerpQuat([0, 0, 0, 1], normalizeQuat(value as [number, number, number, number]), weight);
|
|
348
|
-
}
|
|
349
|
-
throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
function combineAdditive(type: string, current: AnimationValue, value: AnimationValue, weight: number): AnimationValue {
|
|
353
|
-
return applyAdditive(type, current, additiveContribution(type, value, weight));
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function applyAdditive(type: string, base: AnimationValue, delta: AnimationValue): AnimationValue {
|
|
357
|
-
if (type === "scalar") {
|
|
358
|
-
return (base as number) + (delta as number);
|
|
359
|
-
}
|
|
360
|
-
if (type === "vector3") {
|
|
361
|
-
const a = base as [number, number, number];
|
|
362
|
-
const b = delta as [number, number, number];
|
|
363
|
-
return [a[0] + b[0], a[1] + b[1], a[2] + b[2]];
|
|
364
|
-
}
|
|
365
|
-
if (type === "number-array") {
|
|
366
|
-
const a = base as readonly number[];
|
|
367
|
-
const b = delta as readonly number[];
|
|
368
|
-
if (a.length !== b.length) {
|
|
369
|
-
throw new Error("Cannot add number-array animation values with different lengths.");
|
|
370
|
-
}
|
|
371
|
-
return a.map((component, index) => component + b[index]!);
|
|
372
|
-
}
|
|
373
|
-
if (type === "quaternion") {
|
|
374
|
-
return normalizeQuat(multiplyQuat(base as [number, number, number, number], delta as [number, number, number, number]));
|
|
375
|
-
}
|
|
376
|
-
throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
function additiveNeutral(type: string, value: AnimationValue): AnimationValue {
|
|
380
|
-
if (type === "scalar") {
|
|
381
|
-
return 0;
|
|
382
|
-
}
|
|
383
|
-
if (type === "vector3") {
|
|
384
|
-
return [0, 0, 0];
|
|
385
|
-
}
|
|
386
|
-
if (type === "number-array") {
|
|
387
|
-
return (value as readonly number[]).map(() => 0);
|
|
388
|
-
}
|
|
389
|
-
if (type === "quaternion") {
|
|
390
|
-
return [0, 0, 0, 1];
|
|
391
|
-
}
|
|
392
|
-
throw new Error("Additive animation layers require numeric, vector, quaternion, or number-array tracks.");
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
function multiplyQuat(a: [number, number, number, number], b: [number, number, number, number]): [number, number, number, number] {
|
|
396
|
-
return [
|
|
397
|
-
a[3] * b[0] + a[0] * b[3] + a[1] * b[2] - a[2] * b[1],
|
|
398
|
-
a[3] * b[1] - a[0] * b[2] + a[1] * b[3] + a[2] * b[0],
|
|
399
|
-
a[3] * b[2] + a[0] * b[1] - a[1] * b[0] + a[2] * b[3],
|
|
400
|
-
a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]
|
|
401
|
-
];
|
|
402
|
-
}
|
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
export {
|
|
2
|
-
AnimationStateMachine as AnimationStateGraph,
|
|
3
|
-
AnimationStateMachine,
|
|
4
|
-
type AnimationState,
|
|
5
|
-
type AnimationStateBlend,
|
|
6
|
-
type AnimationStateMachineGraphSnapshot,
|
|
7
|
-
type AnimationStateMachineGraphState,
|
|
8
|
-
type AnimationStateMachineGraphTransition,
|
|
9
|
-
type StateTransition
|
|
10
|
-
} from "./AnimationStateMachine.js";
|
|
11
|
-
|
|
12
|
-
import { AnimationStateMachine, type AnimationState } from "./AnimationStateMachine.js";
|
|
13
|
-
|
|
14
|
-
export interface AnimationAnimationStateGraphOptions {
|
|
15
|
-
readonly idleState?: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface AnimationAnimationStateGraphSample {
|
|
19
|
-
readonly frame: number;
|
|
20
|
-
readonly state: string;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function createAnimationAnimationStateGraph(options: AnimationAnimationStateGraphOptions = {}): AnimationStateMachine {
|
|
24
|
-
const idle = options.idleState ?? "idle";
|
|
25
|
-
const states: readonly AnimationState[] = [
|
|
26
|
-
{
|
|
27
|
-
name: idle,
|
|
28
|
-
transitions: [
|
|
29
|
-
transition("speak", "isSpeaking", 50),
|
|
30
|
-
transition("gesture", "gesture", 40, ["gesture"]),
|
|
31
|
-
transition("walk", "isWalking", 30),
|
|
32
|
-
transition("action", "action", 60, ["action"]),
|
|
33
|
-
transition("listen", "isListening", 20)
|
|
34
|
-
]
|
|
35
|
-
},
|
|
36
|
-
{ name: "listen", transitions: [transition("speak", "isSpeaking", 50), transition(idle, "isListening", 0, undefined, false)] },
|
|
37
|
-
{ name: "speak", transitions: [transition("gesture", "gesture", 60, ["gesture"]), transition(idle, "isSpeaking", 0, undefined, false)] },
|
|
38
|
-
{ name: "gesture", duration: 0.4, oneShot: true, onComplete: idle },
|
|
39
|
-
{ name: "walk", transitions: [transition(idle, "isWalking", 0, undefined, false), transition("action", "action", 60, ["action"])] },
|
|
40
|
-
{ name: "action", duration: 0.5, oneShot: true, onComplete: idle }
|
|
41
|
-
];
|
|
42
|
-
return new AnimationStateMachine(states, idle);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export function sampleAnimationAnimationStateGraph(
|
|
46
|
-
graph: AnimationStateMachine,
|
|
47
|
-
inputs: readonly Readonly<Record<string, number | boolean | string>>[],
|
|
48
|
-
delta = 1 / 30
|
|
49
|
-
): readonly AnimationAnimationStateGraphSample[] {
|
|
50
|
-
return inputs.map((parameters, frame) => {
|
|
51
|
-
for (const [key, value] of Object.entries(parameters)) graph.setParameter(key, value);
|
|
52
|
-
return { frame, state: graph.update(delta) };
|
|
53
|
-
});
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export interface LocomotionAnimationStateGraphOptions {
|
|
57
|
-
readonly idleState?: string;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Reusable locomotion state graph (idle <-> walk <-> run) for character-controller and
|
|
62
|
-
* Animation Studio templates. Parameters: `isMoving` (walk), `isRunning` (run). Mirrors
|
|
63
|
-
* {@link createAnimationAnimationStateGraph} so both share the deterministic state-machine core.
|
|
64
|
-
*/
|
|
65
|
-
export function createLocomotionAnimationStateGraph(options: LocomotionAnimationStateGraphOptions = {}): AnimationStateMachine {
|
|
66
|
-
const idle = options.idleState ?? "idle";
|
|
67
|
-
const states: readonly AnimationState[] = [
|
|
68
|
-
{
|
|
69
|
-
name: idle,
|
|
70
|
-
transitions: [transition("run", "isRunning", 60), transition("walk", "isMoving", 40)]
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
name: "walk",
|
|
74
|
-
transitions: [transition("run", "isRunning", 60), transition(idle, "isMoving", 0, undefined, false)]
|
|
75
|
-
},
|
|
76
|
-
{
|
|
77
|
-
name: "run",
|
|
78
|
-
transitions: [transition("walk", "isRunning", 0, undefined, false), transition(idle, "isMoving", 0, undefined, false)]
|
|
79
|
-
}
|
|
80
|
-
];
|
|
81
|
-
return new AnimationStateMachine(states, idle);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function transition(
|
|
85
|
-
to: string,
|
|
86
|
-
parameter: string,
|
|
87
|
-
priority: number,
|
|
88
|
-
consumeParameters?: readonly string[],
|
|
89
|
-
expected = true
|
|
90
|
-
) {
|
|
91
|
-
return {
|
|
92
|
-
to,
|
|
93
|
-
priority,
|
|
94
|
-
label: `${parameter}->${to}`,
|
|
95
|
-
consumeParameters,
|
|
96
|
-
condition: (parameters: Readonly<Record<string, number | boolean | string>>) => {
|
|
97
|
-
const value = parameters[parameter];
|
|
98
|
-
if (expected !== true) return value === expected;
|
|
99
|
-
if (typeof value === "boolean") return value;
|
|
100
|
-
if (typeof value === "string") return value.trim().length > 0;
|
|
101
|
-
if (typeof value === "number") return value > 0;
|
|
102
|
-
return false;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
}
|