@certe/atmos-animation 0.1.0
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/LICENCE +674 -0
- package/README.md +131 -0
- package/dist/animation-clip.d.ts +25 -0
- package/dist/animation-clip.d.ts.map +1 -0
- package/dist/animation-clip.js +15 -0
- package/dist/animation-clip.js.map +1 -0
- package/dist/animation-handler.d.ts +31 -0
- package/dist/animation-handler.d.ts.map +1 -0
- package/dist/animation-handler.js +75 -0
- package/dist/animation-handler.js.map +1 -0
- package/dist/animation-mixer.d.ts +75 -0
- package/dist/animation-mixer.d.ts.map +1 -0
- package/dist/animation-mixer.js +274 -0
- package/dist/animation-mixer.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/keyframe-sampler.d.ts +14 -0
- package/dist/keyframe-sampler.d.ts.map +1 -0
- package/dist/keyframe-sampler.js +85 -0
- package/dist/keyframe-sampler.js.map +1 -0
- package/dist/pose.d.ts +16 -0
- package/dist/pose.d.ts.map +1 -0
- package/dist/pose.js +89 -0
- package/dist/pose.js.map +1 -0
- package/dist/register-builtins.d.ts +3 -0
- package/dist/register-builtins.d.ts.map +1 -0
- package/dist/register-builtins.js +18 -0
- package/dist/register-builtins.js.map +1 -0
- package/dist/skeleton.d.ts +32 -0
- package/dist/skeleton.d.ts.map +1 -0
- package/dist/skeleton.js +47 -0
- package/dist/skeleton.js.map +1 -0
- package/package.json +28 -0
- package/src/index.ts +15 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# 🎬 @certe/atmos-animation
|
|
2
|
+
|
|
3
|
+
Skeletal animation system for the Atmos Engine. Provides skeleton data structures, keyframe sampling, pose blending, and an `AnimationMixer` component for clip playback and cross-fading.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 🔑 Key Concepts
|
|
8
|
+
|
|
9
|
+
- **Skeleton** — Joint hierarchy with inverse bind matrices and a rest pose
|
|
10
|
+
- **AnimationClip** — Named collection of keyframe tracks targeting individual joints
|
|
11
|
+
- **AnimationMixer** — Component that plays clips, blends weighted layers, and outputs bone matrices for GPU skinning
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 🚀 Quick Start
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { AnimationMixer, createSkeleton, createAnimationClip } from '@certe/atmos-animation';
|
|
19
|
+
|
|
20
|
+
// Typically created automatically by instantiateModel() for glTF skinned meshes
|
|
21
|
+
const mixer = gameObject.addComponent(AnimationMixer);
|
|
22
|
+
mixer.skeleton = skeleton;
|
|
23
|
+
mixer.addClip(walkClip);
|
|
24
|
+
mixer.addClip(runClip);
|
|
25
|
+
|
|
26
|
+
// Play
|
|
27
|
+
const walkLayer = mixer.play(walkClip, { loop: true });
|
|
28
|
+
|
|
29
|
+
// Cross-fade to run over 0.3 seconds
|
|
30
|
+
const runLayer = mixer.play(runClip, { loop: true, weight: 0 });
|
|
31
|
+
mixer.crossFade(walkLayer, runLayer, 0.3);
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 📖 API Overview
|
|
37
|
+
|
|
38
|
+
### Skeleton
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createSkeleton, getInverseBindMatrix } from '@certe/atmos-animation';
|
|
42
|
+
|
|
43
|
+
const skeleton = createSkeleton(
|
|
44
|
+
joints, // Array<{ name, parentIndex }>
|
|
45
|
+
inverseBindMatrices, // Float32Array (jointCount × 16)
|
|
46
|
+
restT, restR, restS // Optional rest pose arrays
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
// Read one joint's IBM
|
|
50
|
+
const ibm = Mat4.create();
|
|
51
|
+
getInverseBindMatrix(ibm, skeleton, jointIndex);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### AnimationClip & Tracks
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
import { createAnimationClip } from '@certe/atmos-animation';
|
|
58
|
+
|
|
59
|
+
const clip = createAnimationClip('walk', [
|
|
60
|
+
{
|
|
61
|
+
jointIndex: 0,
|
|
62
|
+
channel: 'rotation',
|
|
63
|
+
interpolation: 'LINEAR',
|
|
64
|
+
times: new Float32Array([0, 0.5, 1.0]),
|
|
65
|
+
values: new Float32Array([/* quaternion keyframes */]),
|
|
66
|
+
},
|
|
67
|
+
]);
|
|
68
|
+
// clip.duration is auto-computed from max track time
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Keyframe Sampling
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import { sampleTrack } from '@certe/atmos-animation';
|
|
75
|
+
|
|
76
|
+
const out = new Float32Array(4); // 4 for rotation, 3 for translation/scale
|
|
77
|
+
sampleTrack(out, track, time);
|
|
78
|
+
// Binary search + LINEAR (lerp/slerp) or STEP interpolation
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### AnimationMixer Component
|
|
82
|
+
|
|
83
|
+
| Method | Description |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `play(clip, opts?)` | Play a clip, returns `AnimationLayer` |
|
|
86
|
+
| `playByName(name, opts?)` | Play by clip name with optional cross-fade |
|
|
87
|
+
| `crossFade(from, to, duration)` | Smooth transition between layers |
|
|
88
|
+
| `stop(layer)` | Stop and remove a layer |
|
|
89
|
+
| `resetToRestPose()` | Clear all layers |
|
|
90
|
+
| `addClip(clip)` | Register a clip for `playByName` |
|
|
91
|
+
|
|
92
|
+
| Property | Description |
|
|
93
|
+
|---|---|
|
|
94
|
+
| `skeleton` | The `Skeleton` to animate |
|
|
95
|
+
| `boneMatrices` | `Float32Array` output (jointCount × 16) for GPU upload |
|
|
96
|
+
| `clipNames` | Sorted list of registered clip names |
|
|
97
|
+
| `speed` | Global playback speed multiplier |
|
|
98
|
+
| `loop` | Default looping behavior |
|
|
99
|
+
|
|
100
|
+
### Blending Algorithm
|
|
101
|
+
|
|
102
|
+
Each frame, `onUpdate(dt)`:
|
|
103
|
+
|
|
104
|
+
1. Sample all active layers' tracks at their current time
|
|
105
|
+
2. Blend translation/scale as **delta-from-rest** weighted by layer weight
|
|
106
|
+
3. Blend rotation via **weighted quaternion accumulation** with shortest-path sign flip
|
|
107
|
+
4. Fill undriven joints with rest pose
|
|
108
|
+
5. Call `computeBoneMatrices()` to produce final GPU-ready matrices
|
|
109
|
+
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## 📁 Structure
|
|
113
|
+
|
|
114
|
+
```
|
|
115
|
+
packages/animation/src/
|
|
116
|
+
index.ts # Public API
|
|
117
|
+
skeleton.ts # Skeleton type + factory
|
|
118
|
+
animation-clip.ts # AnimationClip + KeyframeTrack
|
|
119
|
+
keyframe-sampler.ts # Binary search + lerp/slerp (zero-alloc)
|
|
120
|
+
pose.ts # computeBoneMatrices() — two-pass world × IBM
|
|
121
|
+
animation-mixer.ts # AnimationMixer component
|
|
122
|
+
animation-handler.ts # Animation event handling
|
|
123
|
+
register-builtins.ts # Component registry integration
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## 🔗 Dependencies
|
|
129
|
+
|
|
130
|
+
- `@certe/atmos-core` — Component lifecycle
|
|
131
|
+
- `@certe/atmos-math` — Vec3, Mat4, Quat for pose computation and interpolation
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/** Interpolation mode for a keyframe track. */
|
|
2
|
+
export type Interpolation = 'LINEAR' | 'STEP';
|
|
3
|
+
/** Which transform channel a track controls. */
|
|
4
|
+
export type AnimationChannel = 'translation' | 'rotation' | 'scale';
|
|
5
|
+
/** A single track of keyframes targeting one joint's transform channel. */
|
|
6
|
+
export interface KeyframeTrack {
|
|
7
|
+
jointIndex: number;
|
|
8
|
+
channel: AnimationChannel;
|
|
9
|
+
interpolation: Interpolation;
|
|
10
|
+
/** Keyframe timestamps in seconds. */
|
|
11
|
+
times: Float32Array;
|
|
12
|
+
/** Keyframe values (3 floats for T/S, 4 floats for R per keyframe). */
|
|
13
|
+
values: Float32Array;
|
|
14
|
+
}
|
|
15
|
+
/** A named animation clip containing multiple keyframe tracks. */
|
|
16
|
+
export interface AnimationClip {
|
|
17
|
+
name: string;
|
|
18
|
+
duration: number;
|
|
19
|
+
tracks: readonly KeyframeTrack[];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create an animation clip. Computes duration from the max time across all tracks.
|
|
23
|
+
*/
|
|
24
|
+
export declare function createAnimationClip(name: string, tracks: KeyframeTrack[]): AnimationClip;
|
|
25
|
+
//# sourceMappingURL=animation-clip.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation-clip.d.ts","sourceRoot":"","sources":["../src/animation-clip.ts"],"names":[],"mappings":"AAAA,+CAA+C;AAC/C,MAAM,MAAM,aAAa,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE9C,gDAAgD;AAChD,MAAM,MAAM,gBAAgB,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,CAAC;AAEpE,2EAA2E;AAC3E,MAAM,WAAW,aAAa;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,aAAa,EAAE,aAAa,CAAC;IAC7B,sCAAsC;IACtC,KAAK,EAAE,YAAY,CAAC;IACpB,uEAAuE;IACvE,MAAM,EAAE,YAAY,CAAC;CACtB;AAED,kEAAkE;AAClE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,SAAS,aAAa,EAAE,CAAC;CAClC;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,aAAa,EAAE,GACtB,aAAa,CASf"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create an animation clip. Computes duration from the max time across all tracks.
|
|
3
|
+
*/
|
|
4
|
+
export function createAnimationClip(name, tracks) {
|
|
5
|
+
let duration = 0;
|
|
6
|
+
for (const track of tracks) {
|
|
7
|
+
if (track.times.length > 0) {
|
|
8
|
+
const last = track.times[track.times.length - 1];
|
|
9
|
+
if (last > duration)
|
|
10
|
+
duration = last;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return { name, duration, tracks };
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=animation-clip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation-clip.js","sourceRoot":"","sources":["../src/animation-clip.ts"],"names":[],"mappings":"AAwBA;;GAEG;AACH,MAAM,UAAU,mBAAmB,CACjC,IAAY,EACZ,MAAuB;IAEvB,IAAI,QAAQ,GAAG,CAAC,CAAC;IACjB,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAE,CAAC;YAClD,IAAI,IAAI,GAAG,QAAQ;gBAAE,QAAQ,GAAG,IAAI,CAAC;QACvC,CAAC;IACH,CAAC;IACD,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimationHandler: lives on model root, delegates to child AnimationMixers.
|
|
3
|
+
* Provides a single control point for animation playback across all skinned meshes.
|
|
4
|
+
*/
|
|
5
|
+
import { Component } from '@certe/atmos-core';
|
|
6
|
+
export declare class AnimationHandler extends Component {
|
|
7
|
+
/** Which clip to auto-play on start. */
|
|
8
|
+
initialClip: string;
|
|
9
|
+
/** Default playback speed multiplier. */
|
|
10
|
+
speed: number;
|
|
11
|
+
/** Whether clips loop by default. */
|
|
12
|
+
loop: boolean;
|
|
13
|
+
/** Whether to auto-play initialClip on start. */
|
|
14
|
+
autoplay: boolean;
|
|
15
|
+
/** Recursively collect all AnimationMixers from this GameObject and descendants. */
|
|
16
|
+
private _collectMixers;
|
|
17
|
+
/** Union of all clip names across child mixers, sorted and deduplicated. */
|
|
18
|
+
get clipNames(): string[];
|
|
19
|
+
/** Name of the currently playing clip (from first mixer found), or empty string. */
|
|
20
|
+
get currentClip(): string;
|
|
21
|
+
/** Play a clip by name on all child mixers. */
|
|
22
|
+
play(name: string, opts?: {
|
|
23
|
+
speed?: number;
|
|
24
|
+
loop?: boolean;
|
|
25
|
+
crossFadeDuration?: number;
|
|
26
|
+
}): void;
|
|
27
|
+
/** Stop all layers on all child mixers. */
|
|
28
|
+
stop(): void;
|
|
29
|
+
onStart(): void;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=animation-handler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation-handler.d.ts","sourceRoot":"","sources":["../src/animation-handler.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAc,MAAM,mBAAmB,CAAC;AAG1D,qBAAa,gBAAiB,SAAQ,SAAS;IAC7C,wCAAwC;IACxC,WAAW,SAAM;IACjB,yCAAyC;IACzC,KAAK,SAAK;IACV,qCAAqC;IACrC,IAAI,UAAQ;IACZ,iDAAiD;IACjD,QAAQ,UAAQ;IAEhB,oFAAoF;IACpF,OAAO,CAAC,cAAc;IAWtB,4EAA4E;IAC5E,IAAI,SAAS,IAAI,MAAM,EAAE,CAMxB;IAED,oFAAoF;IACpF,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,+CAA+C;IAC/C,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QACxB,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,GAAG,IAAI;IAYR,2CAA2C;IAC3C,IAAI,IAAI,IAAI;IAQZ,OAAO,IAAI,IAAI;CAWhB"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimationHandler: lives on model root, delegates to child AnimationMixers.
|
|
3
|
+
* Provides a single control point for animation playback across all skinned meshes.
|
|
4
|
+
*/
|
|
5
|
+
import { Component } from '@certe/atmos-core';
|
|
6
|
+
import { AnimationMixer } from './animation-mixer.js';
|
|
7
|
+
export class AnimationHandler extends Component {
|
|
8
|
+
/** Which clip to auto-play on start. */
|
|
9
|
+
initialClip = '';
|
|
10
|
+
/** Default playback speed multiplier. */
|
|
11
|
+
speed = 1;
|
|
12
|
+
/** Whether clips loop by default. */
|
|
13
|
+
loop = true;
|
|
14
|
+
/** Whether to auto-play initialClip on start. */
|
|
15
|
+
autoplay = true;
|
|
16
|
+
/** Recursively collect all AnimationMixers from this GameObject and descendants. */
|
|
17
|
+
_collectMixers() {
|
|
18
|
+
const result = [];
|
|
19
|
+
const walk = (go) => {
|
|
20
|
+
const mixer = go.getComponent(AnimationMixer);
|
|
21
|
+
if (mixer)
|
|
22
|
+
result.push(mixer);
|
|
23
|
+
for (const child of go.children)
|
|
24
|
+
walk(child);
|
|
25
|
+
};
|
|
26
|
+
walk(this.gameObject);
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/** Union of all clip names across child mixers, sorted and deduplicated. */
|
|
30
|
+
get clipNames() {
|
|
31
|
+
const names = new Set();
|
|
32
|
+
for (const mixer of this._collectMixers()) {
|
|
33
|
+
for (const n of mixer.clipNames)
|
|
34
|
+
names.add(n);
|
|
35
|
+
}
|
|
36
|
+
return [...names].sort();
|
|
37
|
+
}
|
|
38
|
+
/** Name of the currently playing clip (from first mixer found), or empty string. */
|
|
39
|
+
get currentClip() {
|
|
40
|
+
const mixers = this._collectMixers();
|
|
41
|
+
return mixers.length > 0 ? mixers[0].currentClip : '';
|
|
42
|
+
}
|
|
43
|
+
/** Play a clip by name on all child mixers. */
|
|
44
|
+
play(name, opts) {
|
|
45
|
+
const speed = opts?.speed ?? this.speed;
|
|
46
|
+
const loop = opts?.loop ?? this.loop;
|
|
47
|
+
for (const mixer of this._collectMixers()) {
|
|
48
|
+
mixer.playByName(name, {
|
|
49
|
+
speed,
|
|
50
|
+
loop,
|
|
51
|
+
crossFadeDuration: opts?.crossFadeDuration,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** Stop all layers on all child mixers. */
|
|
56
|
+
stop() {
|
|
57
|
+
for (const mixer of this._collectMixers()) {
|
|
58
|
+
for (const layer of [...mixer.layers]) {
|
|
59
|
+
mixer.stop(layer);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
onStart() {
|
|
64
|
+
// Disable autoplay on all child mixers so they don't play independently
|
|
65
|
+
for (const mixer of this._collectMixers()) {
|
|
66
|
+
mixer.autoplay = false;
|
|
67
|
+
}
|
|
68
|
+
// Stop any existing playback (e.g. from previous play cycle) before starting
|
|
69
|
+
this.stop();
|
|
70
|
+
if (this.autoplay && this.initialClip) {
|
|
71
|
+
this.play(this.initialClip, { speed: this.speed, loop: this.loop });
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=animation-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation-handler.js","sourceRoot":"","sources":["../src/animation-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimationMixer component: manages animation playback, blending, and crossfade.
|
|
3
|
+
* Produces bone matrices each frame for GPU skinning.
|
|
4
|
+
*/
|
|
5
|
+
import { Component } from '@certe/atmos-core';
|
|
6
|
+
import type { Skeleton } from './skeleton.js';
|
|
7
|
+
import type { AnimationClip } from './animation-clip.js';
|
|
8
|
+
/** A single active animation layer. */
|
|
9
|
+
export interface AnimationLayer {
|
|
10
|
+
clip: AnimationClip;
|
|
11
|
+
time: number;
|
|
12
|
+
speed: number;
|
|
13
|
+
weight: number;
|
|
14
|
+
loop: boolean;
|
|
15
|
+
playing: boolean;
|
|
16
|
+
/** Internal: used for crossfade weight ramping. */
|
|
17
|
+
_fadeSpeed: number;
|
|
18
|
+
/** Internal: target weight for crossfade. */
|
|
19
|
+
_fadeTarget: number;
|
|
20
|
+
}
|
|
21
|
+
export declare class AnimationMixer extends Component {
|
|
22
|
+
/** Final bone matrices (jointCount * 16 floats), uploaded to GPU each frame. */
|
|
23
|
+
boneMatrices: Float32Array | null;
|
|
24
|
+
/** Which clip to auto-play on start. */
|
|
25
|
+
initialClip: string;
|
|
26
|
+
/** Default playback speed multiplier. */
|
|
27
|
+
speed: number;
|
|
28
|
+
/** Whether clips loop by default. */
|
|
29
|
+
loop: boolean;
|
|
30
|
+
/** Whether to auto-play initialClip on start. */
|
|
31
|
+
autoplay: boolean;
|
|
32
|
+
private _skeleton;
|
|
33
|
+
private _layers;
|
|
34
|
+
private _clips;
|
|
35
|
+
private _blendedT;
|
|
36
|
+
private _blendedR;
|
|
37
|
+
private _blendedS;
|
|
38
|
+
private _accumWeightR;
|
|
39
|
+
get skeleton(): Skeleton | null;
|
|
40
|
+
set skeleton(sk: Skeleton | null);
|
|
41
|
+
/** Reset bone matrices to rest pose and stop all layers. */
|
|
42
|
+
resetToRestPose(): void;
|
|
43
|
+
/** Play a clip, returning the layer handle. */
|
|
44
|
+
play(clip: AnimationClip, opts?: {
|
|
45
|
+
speed?: number;
|
|
46
|
+
weight?: number;
|
|
47
|
+
loop?: boolean;
|
|
48
|
+
}): AnimationLayer;
|
|
49
|
+
/**
|
|
50
|
+
* Crossfade from one layer to another over `duration` seconds.
|
|
51
|
+
* `from` fades to 0 weight, `to` fades to 1 weight.
|
|
52
|
+
*/
|
|
53
|
+
crossFade(from: AnimationLayer, to: AnimationLayer, duration: number): void;
|
|
54
|
+
/** Stop and remove a layer. */
|
|
55
|
+
stop(layer: AnimationLayer): void;
|
|
56
|
+
/** Get all active layers (read-only). */
|
|
57
|
+
get layers(): readonly AnimationLayer[];
|
|
58
|
+
/** Register a clip by name. */
|
|
59
|
+
addClip(clip: AnimationClip): void;
|
|
60
|
+
/** Sorted list of registered clip names. */
|
|
61
|
+
get clipNames(): string[];
|
|
62
|
+
/** Name of the currently playing clip (first layer), or empty string. */
|
|
63
|
+
get currentClip(): string;
|
|
64
|
+
/** Play a clip by name, with optional crossfade from the current clip. */
|
|
65
|
+
playByName(name: string, opts?: {
|
|
66
|
+
speed?: number;
|
|
67
|
+
loop?: boolean;
|
|
68
|
+
crossFadeDuration?: number;
|
|
69
|
+
}): AnimationLayer | null;
|
|
70
|
+
onStart(): void;
|
|
71
|
+
onUpdate(dt: number): void;
|
|
72
|
+
private _advanceLayers;
|
|
73
|
+
private _accumulateLayer;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=animation-mixer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"animation-mixer.d.ts","sourceRoot":"","sources":["../src/animation-mixer.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE9C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAC9C,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAIzD,uCAAuC;AACvC,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,OAAO,CAAC;IACd,OAAO,EAAE,OAAO,CAAC;IACjB,mDAAmD;IACnD,UAAU,EAAE,MAAM,CAAC;IACnB,6CAA6C;IAC7C,WAAW,EAAE,MAAM,CAAC;CACrB;AAQD,qBAAa,cAAe,SAAQ,SAAS;IAC3C,gFAAgF;IAChF,YAAY,EAAE,YAAY,GAAG,IAAI,CAAQ;IAEzC,wCAAwC;IACxC,WAAW,SAAM;IACjB,yCAAyC;IACzC,KAAK,SAAK;IACV,qCAAqC;IACrC,IAAI,UAAQ;IACZ,iDAAiD;IACjD,QAAQ,UAAQ;IAEhB,OAAO,CAAC,SAAS,CAAyB;IAC1C,OAAO,CAAC,OAAO,CAAwB;IACvC,OAAO,CAAC,MAAM,CAAoC;IAGlD,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,SAAS,CAA6B;IAC9C,OAAO,CAAC,SAAS,CAA6B;IAG9C,OAAO,CAAC,aAAa,CAA6B;IAElD,IAAI,QAAQ,IAAI,QAAQ,GAAG,IAAI,CAA2B;IAE1D,IAAI,QAAQ,CAAC,EAAE,EAAE,QAAQ,GAAG,IAAI,EAmB/B;IAED,4DAA4D;IAC5D,eAAe,IAAI,IAAI;IAUvB,+CAA+C;IAC/C,IAAI,CAAC,IAAI,EAAE,aAAa,EAAE,IAAI,CAAC,EAAE;QAC/B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,IAAI,CAAC,EAAE,OAAO,CAAC;KAChB,GAAG,cAAc;IAelB;;;OAGG;IACH,SAAS,CAAC,IAAI,EAAE,cAAc,EAAE,EAAE,EAAE,cAAc,EAAE,QAAQ,EAAE,MAAM,GAAG,IAAI;IAS3E,+BAA+B;IAC/B,IAAI,CAAC,KAAK,EAAE,cAAc,GAAG,IAAI;IAMjC,yCAAyC;IACzC,IAAI,MAAM,IAAI,SAAS,cAAc,EAAE,CAEtC;IAED,+BAA+B;IAC/B,OAAO,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI;IAIlC,4CAA4C;IAC5C,IAAI,SAAS,IAAI,MAAM,EAAE,CAExB;IAED,yEAAyE;IACzE,IAAI,WAAW,IAAI,MAAM,CAGxB;IAED,0EAA0E;IAC1E,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE;QAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;QACf,IAAI,CAAC,EAAE,OAAO,CAAC;QACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,GAAG,cAAc,GAAG,IAAI;IAczB,OAAO,IAAI,IAAI;IAMf,QAAQ,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI;IAgE1B,OAAO,CAAC,cAAc;IAkCtB,OAAO,CAAC,gBAAgB;CAqDzB"}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimationMixer component: manages animation playback, blending, and crossfade.
|
|
3
|
+
* Produces bone matrices each frame for GPU skinning.
|
|
4
|
+
*/
|
|
5
|
+
import { Component } from '@certe/atmos-core';
|
|
6
|
+
import { Quat } from '@certe/atmos-math';
|
|
7
|
+
import { sampleTrack } from './keyframe-sampler.js';
|
|
8
|
+
import { computeBoneMatrices } from './pose.js';
|
|
9
|
+
// Scratch arrays for blending (module-level, zero alloc)
|
|
10
|
+
const _sampledT = new Float32Array(3);
|
|
11
|
+
const _sampledR = new Float32Array(4);
|
|
12
|
+
const _sampledS = new Float32Array(3);
|
|
13
|
+
const _blendR = new Float32Array(4);
|
|
14
|
+
export class AnimationMixer extends Component {
|
|
15
|
+
/** Final bone matrices (jointCount * 16 floats), uploaded to GPU each frame. */
|
|
16
|
+
boneMatrices = null;
|
|
17
|
+
/** Which clip to auto-play on start. */
|
|
18
|
+
initialClip = '';
|
|
19
|
+
/** Default playback speed multiplier. */
|
|
20
|
+
speed = 1;
|
|
21
|
+
/** Whether clips loop by default. */
|
|
22
|
+
loop = true;
|
|
23
|
+
/** Whether to auto-play initialClip on start. */
|
|
24
|
+
autoplay = true;
|
|
25
|
+
_skeleton = null;
|
|
26
|
+
_layers = [];
|
|
27
|
+
_clips = new Map();
|
|
28
|
+
// Per-joint blended T/R/S (allocated when skeleton is set)
|
|
29
|
+
_blendedT = null;
|
|
30
|
+
_blendedR = null;
|
|
31
|
+
_blendedS = null;
|
|
32
|
+
// Accumulated rotation weight per joint (for rest-pose fill after blending)
|
|
33
|
+
_accumWeightR = null;
|
|
34
|
+
get skeleton() { return this._skeleton; }
|
|
35
|
+
set skeleton(sk) {
|
|
36
|
+
this._skeleton = sk;
|
|
37
|
+
if (sk) {
|
|
38
|
+
const jc = sk.jointCount;
|
|
39
|
+
this._blendedT = new Float32Array(jc * 3);
|
|
40
|
+
this._blendedR = new Float32Array(jc * 4);
|
|
41
|
+
this._blendedS = new Float32Array(jc * 3);
|
|
42
|
+
this.boneMatrices = new Float32Array(jc * 16);
|
|
43
|
+
// Compute rest-pose bone matrices immediately so GPU has valid data before first update
|
|
44
|
+
this._blendedT.set(sk.restT);
|
|
45
|
+
this._blendedR.set(sk.restR);
|
|
46
|
+
this._blendedS.set(sk.restS);
|
|
47
|
+
computeBoneMatrices(this.boneMatrices, sk, this._blendedT, this._blendedR, this._blendedS);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
this.boneMatrices = null;
|
|
51
|
+
this._blendedT = null;
|
|
52
|
+
this._blendedR = null;
|
|
53
|
+
this._blendedS = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Reset bone matrices to rest pose and stop all layers. */
|
|
57
|
+
resetToRestPose() {
|
|
58
|
+
const sk = this._skeleton;
|
|
59
|
+
if (!sk || !this.boneMatrices || !this._blendedT)
|
|
60
|
+
return;
|
|
61
|
+
this._layers.length = 0;
|
|
62
|
+
this._blendedT.set(sk.restT);
|
|
63
|
+
this._blendedR.set(sk.restR);
|
|
64
|
+
this._blendedS.set(sk.restS);
|
|
65
|
+
computeBoneMatrices(this.boneMatrices, sk, this._blendedT, this._blendedR, this._blendedS);
|
|
66
|
+
}
|
|
67
|
+
/** Play a clip, returning the layer handle. */
|
|
68
|
+
play(clip, opts) {
|
|
69
|
+
const layer = {
|
|
70
|
+
clip,
|
|
71
|
+
time: 0,
|
|
72
|
+
speed: opts?.speed ?? 1,
|
|
73
|
+
weight: opts?.weight ?? 1,
|
|
74
|
+
loop: opts?.loop ?? true,
|
|
75
|
+
playing: true,
|
|
76
|
+
_fadeSpeed: 0,
|
|
77
|
+
_fadeTarget: opts?.weight ?? 1,
|
|
78
|
+
};
|
|
79
|
+
this._layers.push(layer);
|
|
80
|
+
return layer;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Crossfade from one layer to another over `duration` seconds.
|
|
84
|
+
* `from` fades to 0 weight, `to` fades to 1 weight.
|
|
85
|
+
*/
|
|
86
|
+
crossFade(from, to, duration) {
|
|
87
|
+
const d = Math.max(duration, 0.001);
|
|
88
|
+
from._fadeTarget = 0;
|
|
89
|
+
from._fadeSpeed = from.weight / d;
|
|
90
|
+
to.weight = 0;
|
|
91
|
+
to._fadeTarget = 1;
|
|
92
|
+
to._fadeSpeed = 1 / d;
|
|
93
|
+
}
|
|
94
|
+
/** Stop and remove a layer. */
|
|
95
|
+
stop(layer) {
|
|
96
|
+
layer.playing = false;
|
|
97
|
+
const idx = this._layers.indexOf(layer);
|
|
98
|
+
if (idx >= 0)
|
|
99
|
+
this._layers.splice(idx, 1);
|
|
100
|
+
}
|
|
101
|
+
/** Get all active layers (read-only). */
|
|
102
|
+
get layers() {
|
|
103
|
+
return this._layers;
|
|
104
|
+
}
|
|
105
|
+
/** Register a clip by name. */
|
|
106
|
+
addClip(clip) {
|
|
107
|
+
this._clips.set(clip.name, clip);
|
|
108
|
+
}
|
|
109
|
+
/** Sorted list of registered clip names. */
|
|
110
|
+
get clipNames() {
|
|
111
|
+
return [...this._clips.keys()].sort();
|
|
112
|
+
}
|
|
113
|
+
/** Name of the currently playing clip (first layer), or empty string. */
|
|
114
|
+
get currentClip() {
|
|
115
|
+
const first = this._layers[0];
|
|
116
|
+
return first ? first.clip.name : '';
|
|
117
|
+
}
|
|
118
|
+
/** Play a clip by name, with optional crossfade from the current clip. */
|
|
119
|
+
playByName(name, opts) {
|
|
120
|
+
const clip = this._clips.get(name);
|
|
121
|
+
if (!clip)
|
|
122
|
+
return null;
|
|
123
|
+
const layer = this.play(clip, {
|
|
124
|
+
speed: opts?.speed ?? this.speed,
|
|
125
|
+
loop: opts?.loop ?? this.loop,
|
|
126
|
+
});
|
|
127
|
+
if (opts?.crossFadeDuration && this._layers.length > 1) {
|
|
128
|
+
const from = this._layers[this._layers.length - 2];
|
|
129
|
+
this.crossFade(from, layer, opts.crossFadeDuration);
|
|
130
|
+
}
|
|
131
|
+
return layer;
|
|
132
|
+
}
|
|
133
|
+
onStart() {
|
|
134
|
+
if (this.autoplay && this.initialClip) {
|
|
135
|
+
this.playByName(this.initialClip, { speed: this.speed, loop: this.loop });
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
onUpdate(dt) {
|
|
139
|
+
if (!this._skeleton || !this._blendedT)
|
|
140
|
+
return;
|
|
141
|
+
const jc = this._skeleton.jointCount;
|
|
142
|
+
// Advance layers and handle fades
|
|
143
|
+
this._advanceLayers(dt);
|
|
144
|
+
// Start from rest pose (T/S stay as rest, R zeroed for accumulation)
|
|
145
|
+
const sk = this._skeleton;
|
|
146
|
+
this._blendedT.set(sk.restT);
|
|
147
|
+
this._blendedS.set(sk.restS);
|
|
148
|
+
this._blendedR.fill(0);
|
|
149
|
+
// Track accumulated rotation weight per joint
|
|
150
|
+
if (!this._accumWeightR || this._accumWeightR.length !== jc) {
|
|
151
|
+
this._accumWeightR = new Float32Array(jc);
|
|
152
|
+
}
|
|
153
|
+
this._accumWeightR.fill(0);
|
|
154
|
+
// Accumulate weighted samples
|
|
155
|
+
for (const layer of this._layers) {
|
|
156
|
+
if (!layer.playing || layer.weight <= 0)
|
|
157
|
+
continue;
|
|
158
|
+
this._accumulateLayer(layer, jc);
|
|
159
|
+
}
|
|
160
|
+
// Fill in rest-pose quaternion for joints not fully covered by animations
|
|
161
|
+
const br = this._blendedR;
|
|
162
|
+
for (let j = 0; j < jc; j++) {
|
|
163
|
+
const aw = this._accumWeightR[j];
|
|
164
|
+
const rOff = j * 4;
|
|
165
|
+
if (aw < 0.001) {
|
|
166
|
+
// No animation touched this joint — use rest pose directly
|
|
167
|
+
br[rOff] = sk.restR[rOff];
|
|
168
|
+
br[rOff + 1] = sk.restR[rOff + 1];
|
|
169
|
+
br[rOff + 2] = sk.restR[rOff + 2];
|
|
170
|
+
br[rOff + 3] = sk.restR[rOff + 3];
|
|
171
|
+
}
|
|
172
|
+
else if (aw < 0.999) {
|
|
173
|
+
// Partially covered — blend in rest pose for remaining weight
|
|
174
|
+
const restW = 1 - aw;
|
|
175
|
+
const dot = br[rOff] * sk.restR[rOff] + br[rOff + 1] * sk.restR[rOff + 1] +
|
|
176
|
+
br[rOff + 2] * sk.restR[rOff + 2] + br[rOff + 3] * sk.restR[rOff + 3];
|
|
177
|
+
const sign = dot < 0 ? -1 : 1;
|
|
178
|
+
br[rOff] = br[rOff] + sk.restR[rOff] * restW * sign;
|
|
179
|
+
br[rOff + 1] = br[rOff + 1] + sk.restR[rOff + 1] * restW * sign;
|
|
180
|
+
br[rOff + 2] = br[rOff + 2] + sk.restR[rOff + 2] * restW * sign;
|
|
181
|
+
br[rOff + 3] = br[rOff + 3] + sk.restR[rOff + 3] * restW * sign;
|
|
182
|
+
}
|
|
183
|
+
Quat.normalize(br.subarray(rOff, rOff + 4), br.subarray(rOff, rOff + 4));
|
|
184
|
+
}
|
|
185
|
+
// Compute final bone matrices
|
|
186
|
+
computeBoneMatrices(this.boneMatrices, this._skeleton, this._blendedT, this._blendedR, this._blendedS);
|
|
187
|
+
}
|
|
188
|
+
_advanceLayers(dt) {
|
|
189
|
+
for (let i = this._layers.length - 1; i >= 0; i--) {
|
|
190
|
+
const layer = this._layers[i];
|
|
191
|
+
if (!layer.playing)
|
|
192
|
+
continue;
|
|
193
|
+
// Advance time
|
|
194
|
+
layer.time += dt * layer.speed;
|
|
195
|
+
// Loop or clamp
|
|
196
|
+
if (layer.loop) {
|
|
197
|
+
if (layer.clip.duration > 0) {
|
|
198
|
+
layer.time = layer.time % layer.clip.duration;
|
|
199
|
+
if (layer.time < 0)
|
|
200
|
+
layer.time += layer.clip.duration;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
else {
|
|
204
|
+
layer.time = Math.min(layer.time, layer.clip.duration);
|
|
205
|
+
}
|
|
206
|
+
// Apply fade
|
|
207
|
+
if (layer._fadeSpeed > 0) {
|
|
208
|
+
if (layer.weight < layer._fadeTarget) {
|
|
209
|
+
layer.weight = Math.min(layer.weight + layer._fadeSpeed * dt, layer._fadeTarget);
|
|
210
|
+
}
|
|
211
|
+
else if (layer.weight > layer._fadeTarget) {
|
|
212
|
+
layer.weight = Math.max(layer.weight - layer._fadeSpeed * dt, layer._fadeTarget);
|
|
213
|
+
}
|
|
214
|
+
// Remove fully faded-out layers
|
|
215
|
+
if (layer._fadeTarget === 0 && layer.weight <= 0) {
|
|
216
|
+
this._layers.splice(i, 1);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
_accumulateLayer(layer, jointCount) {
|
|
222
|
+
const w = layer.weight;
|
|
223
|
+
const bt = this._blendedT;
|
|
224
|
+
const br = this._blendedR;
|
|
225
|
+
const bs = this._blendedS;
|
|
226
|
+
const sk = this._skeleton;
|
|
227
|
+
// Sample each track and blend using delta-from-rest for T/S,
|
|
228
|
+
// weighted accumulation + rest fill for R (done in onUpdate post-pass).
|
|
229
|
+
for (const track of layer.clip.tracks) {
|
|
230
|
+
const ji = track.jointIndex;
|
|
231
|
+
if (ji < 0 || ji >= jointCount)
|
|
232
|
+
continue;
|
|
233
|
+
switch (track.channel) {
|
|
234
|
+
case 'translation': {
|
|
235
|
+
sampleTrack(_sampledT, track, layer.time);
|
|
236
|
+
const off = ji * 3;
|
|
237
|
+
// Delta from rest: bt starts at restT, add (sampled - rest) * weight
|
|
238
|
+
bt[off] = bt[off] + (_sampledT[0] - sk.restT[off]) * w;
|
|
239
|
+
bt[off + 1] = bt[off + 1] + (_sampledT[1] - sk.restT[off + 1]) * w;
|
|
240
|
+
bt[off + 2] = bt[off + 2] + (_sampledT[2] - sk.restT[off + 2]) * w;
|
|
241
|
+
break;
|
|
242
|
+
}
|
|
243
|
+
case 'rotation': {
|
|
244
|
+
sampleTrack(_sampledR, track, layer.time);
|
|
245
|
+
const rOff = ji * 4;
|
|
246
|
+
this._accumWeightR[ji] = this._accumWeightR[ji] + w;
|
|
247
|
+
// Weighted quaternion accumulation (shortest path)
|
|
248
|
+
_blendR[0] = br[rOff];
|
|
249
|
+
_blendR[1] = br[rOff + 1];
|
|
250
|
+
_blendR[2] = br[rOff + 2];
|
|
251
|
+
_blendR[3] = br[rOff + 3];
|
|
252
|
+
const dot = _blendR[0] * _sampledR[0] + _blendR[1] * _sampledR[1] +
|
|
253
|
+
_blendR[2] * _sampledR[2] + _blendR[3] * _sampledR[3];
|
|
254
|
+
const sign = dot < 0 ? -1 : 1;
|
|
255
|
+
br[rOff] = br[rOff] + _sampledR[0] * w * sign;
|
|
256
|
+
br[rOff + 1] = br[rOff + 1] + _sampledR[1] * w * sign;
|
|
257
|
+
br[rOff + 2] = br[rOff + 2] + _sampledR[2] * w * sign;
|
|
258
|
+
br[rOff + 3] = br[rOff + 3] + _sampledR[3] * w * sign;
|
|
259
|
+
break;
|
|
260
|
+
}
|
|
261
|
+
case 'scale': {
|
|
262
|
+
sampleTrack(_sampledS, track, layer.time);
|
|
263
|
+
const sOff = ji * 3;
|
|
264
|
+
// Delta from rest: bs starts at restS, add (sampled - rest) * weight
|
|
265
|
+
bs[sOff] = bs[sOff] + (_sampledS[0] - sk.restS[sOff]) * w;
|
|
266
|
+
bs[sOff + 1] = bs[sOff + 1] + (_sampledS[1] - sk.restS[sOff + 1]) * w;
|
|
267
|
+
bs[sOff + 2] = bs[sOff + 2] + (_sampledS[2] - sk.restS[sOff + 2]) * w;
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
//# sourceMappingURL=animation-mixer.js.map
|