@yali-engine/animation 0.0.0-stage → 0.4.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.
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Animation graphs (ADR-0089, `docs/animation-design.md` §8): a typed builder that writes the
3
+ * word stream `yali_anim_add_graph` compiles. Graphs are data (`BuiltGraph.words`; a JSON `.yanim`
4
+ * form comes with the editor). Parameters are named here and live in `anim.Graph.params` (16
5
+ * slots), so gameplay sets them with no call into the core (`Animation.setParam` by name).
6
+ *
7
+ * @example
8
+ * const g = graph({ params: ['speed', 'wave'] });
9
+ * const loco = g.blend1d('speed', [[0, g.clip(idle)], [1.5, g.clip(walk)], [4, g.clip(run)]]);
10
+ * const root = g.stateMachine({
11
+ * entry: 'loco',
12
+ * states: { loco, wave: g.clip(wave, { loop: false }) },
13
+ * transitions: [
14
+ * { from: 'loco', to: 'wave', when: [g.trigger('wave')], duration: 0.2 },
15
+ * { from: 'wave', to: 'loco', when: ['finished'], duration: 0.3 },
16
+ * ],
17
+ * });
18
+ * const id = anim.addGraph(skeleton, g.build(root));
19
+ */
20
+ /** Most parameters a graph has (`anim.Graph.params`). */
21
+ export declare const MAX_PARAMS = 16;
22
+ /** A node of a graph being built. */
23
+ export interface GraphNode {
24
+ readonly index: number;
25
+ }
26
+ /** A blend weight: a constant, or a parameter by name. */
27
+ export type Weight = number | {
28
+ readonly param: string;
29
+ };
30
+ /** A transition condition. `'finished'`: the source state's clip reached its end (normalized time ≥ 1). */
31
+ export type Condition = 'finished' | {
32
+ readonly param: string;
33
+ readonly op: '>' | '<' | '>=' | '<=' | '==' | '!=';
34
+ readonly value: number;
35
+ } | {
36
+ readonly trigger: string;
37
+ } | {
38
+ readonly normalizedTime: number;
39
+ };
40
+ /** How a transition hands over (ADR-0089). */
41
+ export type TransitionMode = 'inertialize' | 'crossfade';
42
+ /** A transition between states (`from: '*'`: from any state; listed order is priority). */
43
+ export interface Transition {
44
+ readonly from: string;
45
+ readonly to: string;
46
+ /** Every condition must hold (and). */
47
+ readonly when: readonly Condition[];
48
+ /** Seconds. */
49
+ readonly duration: number;
50
+ /** Default `'inertialize'` (dead blending). */
51
+ readonly mode?: TransitionMode;
52
+ }
53
+ /** A built graph: the words for `yali_anim_add_graph` and the parameter names by slot. */
54
+ export interface BuiltGraph {
55
+ readonly words: Uint32Array;
56
+ readonly params: readonly string[];
57
+ /** The root state machine's state names by index (`anim.Graph.state` is index + 1). */
58
+ readonly states: readonly string[];
59
+ }
60
+ /** Starts a graph over these named parameters. */
61
+ export declare function graph(options?: {
62
+ params?: readonly string[];
63
+ }): GraphBuilder;
64
+ /** Builds one graph: each method adds a node and returns it; `build(root)` writes the words. */
65
+ export declare class GraphBuilder {
66
+ #private;
67
+ readonly params: readonly string[];
68
+ constructor(params: readonly string[]);
69
+ /** Plays a clip (an `Animation.addClip` id): looping by default, at `speed`; `additive` makes it a delta from its first frame. */
70
+ clip(clip: number, options?: {
71
+ speed?: number;
72
+ loop?: boolean;
73
+ additive?: boolean;
74
+ }): GraphNode;
75
+ /** `a` blended towards `b` by `weight` (0 = a), per joint by `mask` (an `Animation.addMask` id). */
76
+ blend(a: GraphNode, b: GraphNode, weight: Weight, mask?: number): GraphNode;
77
+ /**
78
+ * A 1-D blend space: children at increasing positions along `param`; the two around the
79
+ * parameter blend, the rest are not sampled. Children play in step (one normalized phase).
80
+ */
81
+ blend1d(param: string, points: readonly (readonly [number, GraphNode])[]): GraphNode;
82
+ /** `add` (an additive clip's node) applied over `base` at `weight`, per joint by `mask`. */
83
+ additive(base: GraphNode, add: GraphNode, weight: Weight, mask?: number): GraphNode;
84
+ /** A parameter trigger condition (the parameter is set to 1 to fire; the graph clears it). */
85
+ trigger(param: string): Condition;
86
+ /** A state machine over named states; `entry` starts. */
87
+ stateMachine(sm: {
88
+ states: Readonly<Record<string, GraphNode>>;
89
+ entry: string;
90
+ transitions?: readonly Transition[];
91
+ }): GraphNode;
92
+ /** The graph's words, `root` its output. */
93
+ build(root: GraphNode): BuiltGraph;
94
+ }
package/dist/graph.js ADDED
@@ -0,0 +1,155 @@
1
+ import { YaliError } from '@yali-engine/core';
2
+ /**
3
+ * Animation graphs (ADR-0089, `docs/animation-design.md` §8): a typed builder that writes the
4
+ * word stream `yali_anim_add_graph` compiles. Graphs are data (`BuiltGraph.words`; a JSON `.yanim`
5
+ * form comes with the editor). Parameters are named here and live in `anim.Graph.params` (16
6
+ * slots), so gameplay sets them with no call into the core (`Animation.setParam` by name).
7
+ *
8
+ * @example
9
+ * const g = graph({ params: ['speed', 'wave'] });
10
+ * const loco = g.blend1d('speed', [[0, g.clip(idle)], [1.5, g.clip(walk)], [4, g.clip(run)]]);
11
+ * const root = g.stateMachine({
12
+ * entry: 'loco',
13
+ * states: { loco, wave: g.clip(wave, { loop: false }) },
14
+ * transitions: [
15
+ * { from: 'loco', to: 'wave', when: [g.trigger('wave')], duration: 0.2 },
16
+ * { from: 'wave', to: 'loco', when: ['finished'], duration: 0.3 },
17
+ * ],
18
+ * });
19
+ * const id = anim.addGraph(skeleton, g.build(root));
20
+ */
21
+ /** Most parameters a graph has (`anim.Graph.params`). */
22
+ export const MAX_PARAMS = 16;
23
+ const NODE = { clip: 0, blend: 1, blend1d: 2, additive: 3, stateMachine: 4 };
24
+ const COND = { finished: 0, gt: 1, lt: 2, ge: 3, le: 4, eq: 5, ne: 6, trigger: 7, normalizedTime: 8 };
25
+ const OPS = { '>': COND.gt, '<': COND.lt, '>=': COND.ge, '<=': COND.le, '==': COND.eq, '!=': COND.ne };
26
+ const f32bits = (() => {
27
+ const f = new Float32Array(1);
28
+ const u = new Uint32Array(f.buffer);
29
+ return (v) => {
30
+ f[0] = v;
31
+ return u[0];
32
+ };
33
+ })();
34
+ /** Starts a graph over these named parameters. */
35
+ export function graph(options = {}) {
36
+ return new GraphBuilder(options.params ?? []);
37
+ }
38
+ /** Builds one graph: each method adds a node and returns it; `build(root)` writes the words. */
39
+ export class GraphBuilder {
40
+ params;
41
+ #nodes = [];
42
+ /** Each state machine's state names, by node. */
43
+ #stateNames = new Map();
44
+ constructor(params) {
45
+ if (params.length > MAX_PARAMS)
46
+ throw new YaliError('ANIM_GRAPH', `${params.length} parameters; a graph has at most ${MAX_PARAMS}.`, 'Pack flags into fewer parameters.');
47
+ if (new Set(params).size !== params.length)
48
+ throw new YaliError('ANIM_GRAPH', 'Two parameters share a name.', 'Name each parameter once.');
49
+ this.params = params;
50
+ }
51
+ #param(name, path) {
52
+ const i = this.params.indexOf(name);
53
+ if (i < 0)
54
+ throw Object.assign(new YaliError('ANIM_GRAPH', `${path}: no parameter named ${JSON.stringify(name)}.`, `Declare it: graph({ params: [${[...this.params, name].map((p) => JSON.stringify(p)).join(', ')}] }).`), { path });
55
+ return i;
56
+ }
57
+ #weight(w, path) {
58
+ return typeof w === 'number' ? [0, f32bits(w)] : [1, this.#param(w.param, path)];
59
+ }
60
+ #add(words) {
61
+ this.#nodes.push(words);
62
+ return { index: this.#nodes.length - 1 };
63
+ }
64
+ #node(n, path) {
65
+ if (!n || n.index < 0 || n.index >= this.#nodes.length)
66
+ throw Object.assign(new YaliError('ANIM_GRAPH', `${path}: not a node of this graph.`, 'Pass a node this builder returned.'), { path });
67
+ return n.index;
68
+ }
69
+ /** Plays a clip (an `Animation.addClip` id): looping by default, at `speed`; `additive` makes it a delta from its first frame. */
70
+ clip(clip, options = {}) {
71
+ if (!Number.isInteger(clip) || clip < 1)
72
+ throw Object.assign(new YaliError('ANIM_GRAPH', `clip: ${clip} is not a clip id.`, 'Pass an id Animation.addClip returned (ids start at 1).'), { path: 'clip' });
73
+ return this.#add([NODE.clip, clip, f32bits(options.speed ?? 1), (options.loop === false ? 0 : 1) | (options.additive ? 2 : 0)]);
74
+ }
75
+ /** `a` blended towards `b` by `weight` (0 = a), per joint by `mask` (an `Animation.addMask` id). */
76
+ blend(a, b, weight, mask = 0) {
77
+ const [k, v] = this.#weight(weight, 'blend.weight');
78
+ return this.#add([NODE.blend, this.#node(a, 'blend.a'), this.#node(b, 'blend.b'), k, v, mask]);
79
+ }
80
+ /**
81
+ * A 1-D blend space: children at increasing positions along `param`; the two around the
82
+ * parameter blend, the rest are not sampled. Children play in step (one normalized phase).
83
+ */
84
+ blend1d(param, points) {
85
+ if (points.length === 0)
86
+ throw Object.assign(new YaliError('ANIM_GRAPH', 'blend1d: no points.', 'Give at least one [position, node].'), { path: 'blend1d.points' });
87
+ for (let i = 1; i < points.length; i++) {
88
+ if (!(points[i][0] > points[i - 1][0]))
89
+ throw Object.assign(new YaliError('ANIM_GRAPH', `blend1d: position ${points[i][0]} is not above ${points[i - 1][0]}.`, 'List points in increasing position.'), { path: `blend1d.points[${i}]` });
90
+ }
91
+ const words = [NODE.blend1d, this.#param(param, 'blend1d.param'), points.length];
92
+ points.forEach(([pos, n], i) => words.push(f32bits(pos), this.#node(n, `blend1d.points[${i}]`)));
93
+ return this.#add(words);
94
+ }
95
+ /** `add` (an additive clip's node) applied over `base` at `weight`, per joint by `mask`. */
96
+ additive(base, add, weight, mask = 0) {
97
+ const [k, v] = this.#weight(weight, 'additive.weight');
98
+ return this.#add([NODE.additive, this.#node(base, 'additive.base'), this.#node(add, 'additive.add'), k, v, mask]);
99
+ }
100
+ /** A parameter trigger condition (the parameter is set to 1 to fire; the graph clears it). */
101
+ trigger(param) {
102
+ this.#param(param, 'trigger');
103
+ return { trigger: param };
104
+ }
105
+ /** A state machine over named states; `entry` starts. */
106
+ stateMachine(sm) {
107
+ const names = Object.keys(sm.states);
108
+ const idx = (name, path) => {
109
+ const i = names.indexOf(name);
110
+ if (i < 0)
111
+ throw Object.assign(new YaliError('ANIM_GRAPH', `${path}: no state named ${JSON.stringify(name)}.`, `States: ${names.join(', ')}.`), { path });
112
+ return i;
113
+ };
114
+ const words = [NODE.stateMachine, names.length, idx(sm.entry, 'stateMachine.entry')];
115
+ names.forEach((n) => words.push(this.#node(sm.states[n], `stateMachine.states.${n}`)));
116
+ const ts = sm.transitions ?? [];
117
+ words.push(ts.length);
118
+ ts.forEach((t, i) => {
119
+ const path = `stateMachine.transitions[${i}]`;
120
+ if (!(t.duration >= 0))
121
+ throw Object.assign(new YaliError('ANIM_GRAPH', `${path}: duration ${t.duration}.`, 'Give a duration of 0 or more seconds.'), { path: `${path}.duration` });
122
+ words.push(t.from === '*' ? 0xffffffff : idx(t.from, `${path}.from`), idx(t.to, `${path}.to`), f32bits(t.duration), t.mode === 'crossfade' ? 1 : 0, t.when.length);
123
+ t.when.forEach((c, k) => {
124
+ const cp = `${path}.when[${k}]`;
125
+ if (c === 'finished')
126
+ words.push(COND.finished, 0, 0);
127
+ else if ('trigger' in c)
128
+ words.push(COND.trigger, this.#param(c.trigger, cp), 0);
129
+ else if ('normalizedTime' in c)
130
+ words.push(COND.normalizedTime, 0, f32bits(c.normalizedTime));
131
+ else
132
+ words.push(OPS[c.op], this.#param(c.param, cp), f32bits(c.value));
133
+ });
134
+ });
135
+ const node = this.#add(words);
136
+ this.#stateNames.set(node.index, names);
137
+ return node;
138
+ }
139
+ /** The graph's words, `root` its output. */
140
+ build(root) {
141
+ const r = this.#node(root, 'build.root');
142
+ const body = this.#nodes.flat();
143
+ const words = new Uint32Array(4 + this.#nodes.length + body.length);
144
+ words.set([1, this.#nodes.length, r, this.params.length]);
145
+ // An offset table, then the nodes.
146
+ let o = 4 + this.#nodes.length;
147
+ this.#nodes.forEach((n, i) => {
148
+ words[4 + i] = o;
149
+ words.set(n, o);
150
+ o += n.length;
151
+ });
152
+ return { words, params: this.params, states: this.#stateNames.get(r) ?? [] };
153
+ }
154
+ }
155
+ //# sourceMappingURL=graph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"graph.js","sourceRoot":"","sources":["../src/graph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,20 @@
1
+ /**
2
+ * `@yali-engine/animation`: skeletons, clips and skinned meshes (ADR-0083, ADR-0088), sampled and
3
+ * skinned by the core module's `anim` variant (ADR-0084). `@yali-engine/vite` loads that variant when an
4
+ * app depends on this package. Animation runs at the render rate; skinning runs in the vertex
5
+ * shader on every backend (WebGPU, compatibility mode, WebGL2).
6
+ *
7
+ * @example
8
+ * import { animationPlugin, type Animation, spawnAnimatedGltf } from '@yali-engine/animation';
9
+ * const app = App.create({ core, plugins: [transformsPlugin(), renderPlugin({ canvas }), assetsPlugin({ loaders: [] }), gltfPlugin(), animationPlugin()] });
10
+ * const model = assets.load<GltfAsset | null>('fox.glb');
11
+ * await model.ready;
12
+ * spawnAnimatedGltf(app.world, app.resource<Animation>('animation'), model.get()!, render.Mesh!, { clip: 'Run' });
13
+ */
14
+ export { Animation, type AnimationStats, type ClipTrack, type JointRest, type PaletteFrame } from './animation.ts';
15
+ export { ANIM_LOOP, ANIM_PAUSED, type AnimationComponents, BONE_DRIVE, BONE_FOLLOW, registerAnimation } from './components.ts';
16
+ export { type BuiltGraph, type Condition, graph, GraphBuilder, type GraphNode, MAX_PARAMS, type Transition, type TransitionMode, type Weight } from './graph.ts';
17
+ export { addGltfAnimation, type AnimatedGltf, type GltfAnimationAssets, spawnAnimatedGltf, type SpawnAnimatedOptions } from './gltf.ts';
18
+ export { type MorphBinding, type MorphMeshes, Morphs, type MorphTarget, type MorphTrack } from './morph.ts';
19
+ export { animationPlugin } from './plugin.ts';
20
+ export { buildRagdoll, type Ragdoll, type RagdollOptions, type RagdollPhysics, setRagdoll } from './ragdoll.ts';
package/dist/index.js ADDED
@@ -0,0 +1,21 @@
1
+ /**
2
+ * `@yali-engine/animation`: skeletons, clips and skinned meshes (ADR-0083, ADR-0088), sampled and
3
+ * skinned by the core module's `anim` variant (ADR-0084). `@yali-engine/vite` loads that variant when an
4
+ * app depends on this package. Animation runs at the render rate; skinning runs in the vertex
5
+ * shader on every backend (WebGPU, compatibility mode, WebGL2).
6
+ *
7
+ * @example
8
+ * import { animationPlugin, type Animation, spawnAnimatedGltf } from '@yali-engine/animation';
9
+ * const app = App.create({ core, plugins: [transformsPlugin(), renderPlugin({ canvas }), assetsPlugin({ loaders: [] }), gltfPlugin(), animationPlugin()] });
10
+ * const model = assets.load<GltfAsset | null>('fox.glb');
11
+ * await model.ready;
12
+ * spawnAnimatedGltf(app.world, app.resource<Animation>('animation'), model.get()!, render.Mesh!, { clip: 'Run' });
13
+ */
14
+ export { Animation } from "./animation.js";
15
+ export { ANIM_LOOP, ANIM_PAUSED, BONE_DRIVE, BONE_FOLLOW, registerAnimation } from "./components.js";
16
+ export { graph, GraphBuilder, MAX_PARAMS } from "./graph.js";
17
+ export { addGltfAnimation, spawnAnimatedGltf } from "./gltf.js";
18
+ export { Morphs } from "./morph.js";
19
+ export { animationPlugin } from "./plugin.js";
20
+ export { buildRagdoll, setRagdoll } from "./ragdoll.js";
21
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,EAAE,SAAS,EAA0E,MAAM,gBAAgB,CAAC;AACnH,OAAO,EAAE,SAAS,EAAE,WAAW,EAA4B,UAAU,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC/H,OAAO,EAAmC,KAAK,EAAE,YAAY,EAAkB,UAAU,EAAqD,MAAM,YAAY,CAAC;AACjK,OAAO,EAAE,gBAAgB,EAA+C,iBAAiB,EAA6B,MAAM,WAAW,CAAC;AACxI,OAAO,EAAuC,MAAM,EAAqC,MAAM,YAAY,CAAC;AAC5G,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAAE,YAAY,EAA0D,UAAU,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,49 @@
1
+ import type { Entity, World } from '@yali-engine/core';
2
+ /** What blending needs of `@yali-engine/render`'s `MeshStore` (duck-typed: no dependency). */
3
+ export interface MorphMeshes {
4
+ updateVertices(id: number, vertices: Float32Array): void;
5
+ }
6
+ /** One morph target: position offsets (xyz per vertex) and normal offsets (or null). */
7
+ export interface MorphTarget {
8
+ readonly positions: Float32Array;
9
+ readonly normals: Float32Array | null;
10
+ }
11
+ /** A clip's weights for one mesh: one weight per target per key. */
12
+ export interface MorphTrack {
13
+ readonly times: Float32Array;
14
+ readonly values: Float32Array;
15
+ readonly interpolation: 'LINEAR' | 'STEP';
16
+ }
17
+ /** A mesh whose morph targets follow an animator (`Morphs.add`). */
18
+ export interface MorphBinding {
19
+ /** The entity with `anim.Animator` whose clip and time drive the weights. */
20
+ readonly animator: Entity;
21
+ /** Its mesh id (`render.Mesh.mesh`). */
22
+ readonly mesh: number;
23
+ /** The vertices the targets add to (interleaved, `MESH_VERTEX_FLOATS` each). */
24
+ readonly base: Float32Array;
25
+ readonly targets: readonly MorphTarget[];
26
+ /** The weights when the playing clip has none for this mesh. */
27
+ readonly defaults: readonly number[];
28
+ /** By clip id, that clip's weights for this mesh. */
29
+ readonly tracks: ReadonlyMap<number, MorphTrack>;
30
+ }
31
+ /**
32
+ * Morph targets (ADR-0088), blended on the CPU each frame their weights change and rewritten
33
+ * into the geometry pool (`MeshStore.updateVertices`): a mesh's weights come from its animator's
34
+ * clip at its time. One weight state per mesh id: instances sharing a mesh share its morph.
35
+ */
36
+ export declare class Morphs {
37
+ #private;
38
+ /** Meshes rewritten last frame. */
39
+ written: number;
40
+ constructor(world: World);
41
+ /** Where blended vertices go (the render plugin's mesh store). */
42
+ setMeshes(meshes: MorphMeshes): void;
43
+ /** Whether mesh `id` already has a binding. */
44
+ has(mesh: number): boolean;
45
+ /** Binds a mesh's targets to an animator. */
46
+ add(b: MorphBinding): void;
47
+ /** Samples every bound mesh's weights and blends those that changed. Allocation-free once warm. */
48
+ update(): void;
49
+ }
package/dist/morph.js ADDED
@@ -0,0 +1,136 @@
1
+ import { registerAnimation } from "./components.js";
2
+ const FLOATS = 8;
3
+ /**
4
+ * Morph targets (ADR-0088), blended on the CPU each frame their weights change and rewritten
5
+ * into the geometry pool (`MeshStore.updateVertices`): a mesh's weights come from its animator's
6
+ * clip at its time. One weight state per mesh id: instances sharing a mesh share its morph.
7
+ */
8
+ export class Morphs {
9
+ #world;
10
+ #live = [];
11
+ #meshes = null;
12
+ #query = null;
13
+ #state = new Map();
14
+ /** Meshes rewritten last frame. */
15
+ written = 0;
16
+ constructor(world) {
17
+ this.#world = world;
18
+ }
19
+ /** Where blended vertices go (the render plugin's mesh store). */
20
+ setMeshes(meshes) {
21
+ this.#meshes = meshes;
22
+ }
23
+ /** Whether mesh `id` already has a binding. */
24
+ has(mesh) {
25
+ return this.#live.some((l) => l.mesh === mesh);
26
+ }
27
+ /** Binds a mesh's targets to an animator. */
28
+ add(b) {
29
+ const k = b.targets.length;
30
+ this.#live.push({ ...b, weights: new Float32Array(k), last: new Float32Array(k).fill(Number.NaN), out: new Float32Array(b.base.length) });
31
+ }
32
+ /** Samples every bound mesh's weights and blends those that changed. Allocation-free once warm. */
33
+ update() {
34
+ this.written = 0;
35
+ if (this.#live.length === 0 || !this.#meshes)
36
+ return;
37
+ const { Animator } = registerAnimation(this.#world);
38
+ this.#query ??= this.#world.query({ with: [Animator], columns: [[Animator, 'clip'], [Animator, 'time']] });
39
+ const q = this.#query;
40
+ const chunks = q.run();
41
+ for (let c = 0; c < chunks; c++) {
42
+ const k = q.chunk(c);
43
+ const clips = k.u32(0);
44
+ const times = k.f32(1);
45
+ for (let i = 0; i < k.length; i++) {
46
+ const e = k.entity(i);
47
+ const s = this.#state.get(e);
48
+ if (s) {
49
+ s.clip = clips[i];
50
+ s.time = times[i];
51
+ }
52
+ else
53
+ this.#state.set(e, { clip: clips[i], time: times[i] });
54
+ }
55
+ }
56
+ for (const l of this.#live) {
57
+ const s = this.#state.get(l.animator);
58
+ const track = s ? l.tracks.get(s.clip) : undefined;
59
+ if (track && s)
60
+ sample(track, s.time, l.weights);
61
+ else
62
+ for (let t = 0; t < l.weights.length; t++)
63
+ l.weights[t] = l.defaults[t] ?? 0;
64
+ let same = true;
65
+ for (let t = 0; t < l.weights.length && same; t++)
66
+ same = l.weights[t] === l.last[t];
67
+ if (same)
68
+ continue;
69
+ l.last.set(l.weights);
70
+ blend(l);
71
+ this.#meshes.updateVertices(l.mesh, l.out);
72
+ this.written++;
73
+ }
74
+ }
75
+ }
76
+ /** A track's weights at `time` (clamped to its keys). */
77
+ function sample(track, time, out) {
78
+ const t = track.times;
79
+ const n = t.length;
80
+ const k = out.length;
81
+ if (n === 0)
82
+ return;
83
+ if (time <= t[0] || n === 1) {
84
+ for (let w = 0; w < k; w++)
85
+ out[w] = track.values[w];
86
+ return;
87
+ }
88
+ if (time >= t[n - 1]) {
89
+ for (let w = 0; w < k; w++)
90
+ out[w] = track.values[(n - 1) * k + w];
91
+ return;
92
+ }
93
+ let lo = 0;
94
+ let hi = n - 1;
95
+ while (hi - lo > 1) {
96
+ const mid = (lo + hi) >> 1;
97
+ if (t[mid] <= time)
98
+ lo = mid;
99
+ else
100
+ hi = mid;
101
+ }
102
+ const f = track.interpolation === 'STEP' ? 0 : (time - t[lo]) / (t[hi] - t[lo]);
103
+ for (let w = 0; w < k; w++)
104
+ out[w] = track.values[lo * k + w] * (1 - f) + track.values[hi * k + w] * f;
105
+ }
106
+ /** `out` = base + Σ weight × target, normals renormalized. */
107
+ function blend(l) {
108
+ const out = l.out;
109
+ out.set(l.base);
110
+ const n = out.length / FLOATS;
111
+ for (let t = 0; t < l.targets.length; t++) {
112
+ const w = l.weights[t];
113
+ if (w === 0)
114
+ continue;
115
+ const { positions: dp, normals: dn } = l.targets[t];
116
+ for (let v = 0; v < n; v++) {
117
+ const o = v * FLOATS;
118
+ out[o] += w * dp[v * 3];
119
+ out[o + 1] += w * dp[v * 3 + 1];
120
+ out[o + 2] += w * dp[v * 3 + 2];
121
+ if (dn) {
122
+ out[o + 3] += w * dn[v * 3];
123
+ out[o + 4] += w * dn[v * 3 + 1];
124
+ out[o + 5] += w * dn[v * 3 + 2];
125
+ }
126
+ }
127
+ }
128
+ for (let v = 0; v < n; v++) {
129
+ const o = v * FLOATS;
130
+ const len = Math.hypot(out[o + 3], out[o + 4], out[o + 5]) || 1;
131
+ out[o + 3] /= len;
132
+ out[o + 4] /= len;
133
+ out[o + 5] /= len;
134
+ }
135
+ }
136
+ //# sourceMappingURL=morph.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"morph.js","sourceRoot":"","sources":["../src/morph.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ import { type YaliPlugin } from '@yali-engine/core';
2
+ /**
3
+ * `@yali-engine/animation` as a plugin (ADR-0083, ADR-0088): every frame, in Update, animators advance
4
+ * by the frame's game time and the core samples their clips, writes each skin's palette rows and
5
+ * moves each socket (before PostUpdate's transform propagation, so the frame draws the pose it
6
+ * computed). With `@yali-engine/render` present the scene reads the palette from here. Provides the
7
+ * `animation` resource (`Animation`: skeletons, clips, skins) and `animationComponents`.
8
+ * Fails at startup, with the fix, when the loaded core has no animation. Registers the tools
9
+ * `animation.stats` and `animation.play`, and the counters `animation.paletteRows` and
10
+ * `animation.morphsWritten`. Morph targets blend on the CPU when their weights change (ADR-0088).
11
+ *
12
+ * @example
13
+ * const app = App.create({ core, plugins: [transformsPlugin(), renderPlugin({ canvas }), assetsPlugin(), gltfPlugin(), animationPlugin()] });
14
+ * const model = spawnAnimatedGltf(app.world, app.resource<Animation>('animation'), asset, render.Mesh!, { clip: 'Walk' });
15
+ */
16
+ export declare function animationPlugin(): YaliPlugin;
package/dist/plugin.js ADDED
@@ -0,0 +1,111 @@
1
+ import { YaliError } from '@yali-engine/core';
2
+ import { Animation } from "./animation.js";
3
+ import { ANIM_LOOP, ANIM_PAUSED, registerAnimation } from "./components.js";
4
+ /**
5
+ * `@yali-engine/animation` as a plugin (ADR-0083, ADR-0088): every frame, in Update, animators advance
6
+ * by the frame's game time and the core samples their clips, writes each skin's palette rows and
7
+ * moves each socket (before PostUpdate's transform propagation, so the frame draws the pose it
8
+ * computed). With `@yali-engine/render` present the scene reads the palette from here. Provides the
9
+ * `animation` resource (`Animation`: skeletons, clips, skins) and `animationComponents`.
10
+ * Fails at startup, with the fix, when the loaded core has no animation. Registers the tools
11
+ * `animation.stats` and `animation.play`, and the counters `animation.paletteRows` and
12
+ * `animation.morphsWritten`. Morph targets blend on the CPU when their weights change (ADR-0088).
13
+ *
14
+ * @example
15
+ * const app = App.create({ core, plugins: [transformsPlugin(), renderPlugin({ canvas }), assetsPlugin(), gltfPlugin(), animationPlugin()] });
16
+ * const model = spawnAnimatedGltf(app.world, app.resource<Animation>('animation'), asset, render.Mesh!, { clip: 'Walk' });
17
+ */
18
+ export function animationPlugin() {
19
+ return {
20
+ name: '@yali-engine/animation',
21
+ version: '0.4.0',
22
+ dependencies: [{ name: '@yali-engine/transforms', range: '^0.4.0' }],
23
+ build(b) {
24
+ const world = b.app.world;
25
+ const animation = new Animation(world);
26
+ const components = registerAnimation(world);
27
+ b.provide('animation', animation);
28
+ b.provide('animationComponents', components);
29
+ const source = () => animation.palette();
30
+ let animators;
31
+ let render;
32
+ b.system({
33
+ name: 'animation.update',
34
+ stage: 'Update',
35
+ run: (f) => {
36
+ animation.update(f.time.virtualDelta);
37
+ if (render === undefined) {
38
+ try {
39
+ render = b.app.resource('render');
40
+ if (render.meshes)
41
+ animation.morphs.setMeshes(render.meshes);
42
+ }
43
+ catch {
44
+ render = null; // headless: nothing reads the palette
45
+ }
46
+ }
47
+ animation.morphs.update();
48
+ // The scene is replaced on device recovery: point the current one here every frame.
49
+ if (render?.scene && render.scene.paletteSource !== source)
50
+ render.scene.paletteSource = source;
51
+ },
52
+ });
53
+ b.counter('animation.paletteRows', () => animation.stats().paletteRows);
54
+ b.counter('animation.morphsWritten', () => animation.morphs.written);
55
+ b.tool({
56
+ name: 'animation.stats',
57
+ description: 'Animation asset counts (skeletons, clips, skins) and the palette rows skinned last frame.',
58
+ input: { type: 'object', properties: {} },
59
+ run: () => animation.stats(),
60
+ });
61
+ b.tool({
62
+ name: 'animation.play',
63
+ description: "Sets what an animator plays: its clip id, time (seconds), speed, looping, paused. Returns the animator's state.",
64
+ input: {
65
+ type: 'object',
66
+ properties: {
67
+ entity: { type: 'number', description: 'The entity with anim.Animator (a spawnAnimatedGltf root).' },
68
+ clip: { type: 'number', description: 'Clip id (0: none, the rest pose).' },
69
+ time: { type: 'number', description: 'Seconds into the clip.' },
70
+ speed: { type: 'number', description: '1 is normal speed.' },
71
+ loop: { type: 'boolean' },
72
+ paused: { type: 'boolean' },
73
+ },
74
+ required: ['entity'],
75
+ },
76
+ run: (args) => {
77
+ const e = args.entity;
78
+ const A = components.Animator;
79
+ animators ??= world.query({ with: [A], columns: [[A, 'skeleton'], [A, 'clip'], [A, 'time'], [A, 'speed'], [A, 'flags']] });
80
+ const cur = readAnimator(world, animators, e);
81
+ const flags = (cur.flags & ~(ANIM_LOOP | ANIM_PAUSED)) | ((args.loop ?? (cur.flags & ANIM_LOOP) !== 0) ? ANIM_LOOP : 0) | ((args.paused ?? (cur.flags & ANIM_PAUSED) !== 0) ? ANIM_PAUSED : 0);
82
+ const next = {
83
+ skeleton: cur.skeleton,
84
+ clip: args.clip ?? cur.clip,
85
+ time: args.time ?? cur.time,
86
+ speed: args.speed ?? cur.speed,
87
+ flags,
88
+ };
89
+ world.insert(e, A, next);
90
+ return { ...next, duration: animation.clipDuration(next.clip) };
91
+ },
92
+ });
93
+ },
94
+ };
95
+ }
96
+ /** An animator's fields, or a tool error naming the input at fault. */
97
+ function readAnimator(world, q, e) {
98
+ if (!world.isAlive(e))
99
+ throw Object.assign(new YaliError('ENTITY_NOT_ALIVE', `Entity ${e} is not alive.`, 'Pass the root spawnAnimatedGltf returned (world.query with ["anim.Animator"] lists them).'), { path: 'entity' });
100
+ const chunks = q.run();
101
+ for (let c = 0; c < chunks; c++) {
102
+ const k = q.chunk(c);
103
+ for (let i = 0; i < k.length; i++) {
104
+ if (k.entity(i) !== e)
105
+ continue;
106
+ return { skeleton: k.u32(0)[i], clip: k.u32(1)[i], time: k.f32(2)[i], speed: k.f32(3)[i], flags: k.u8(4)[i] };
107
+ }
108
+ }
109
+ throw Object.assign(new YaliError('MISSING_COMPONENT', `Entity ${e} has no anim.Animator.`, 'Pass the root spawnAnimatedGltf returned.'), { path: 'entity' });
110
+ }
111
+ //# sourceMappingURL=plugin.js.map