@volter/editor-blender 0.1.136 → 0.1.138

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.
@@ -1,256 +0,0 @@
1
- /**
2
- * A CHARACTER'S ANIMATION IN A GAME: layered, weighted actions on one armature's bones, and a
3
- * look-at for targets only the game knows.
4
- *
5
- * LAYERS: the base layer plays on every bone; a named layer plays on the bones from one bone down
6
- * (`from: 'spine_01'`: the upper body), and takes those bones from the base. So legs can run while
7
- * the upper body aims. A layer holds any number of actions at weights (`blend`); setting one action
8
- * is a blend of that one, crossfaded (`set`).
9
- *
10
- * LOOK-AT: turns one bone so its axis points at a point, after the actions, by a weight and within
11
- * an angle. The axis is the bone's FORWARD unless given: the direction in the bone that pointed the
12
- * way the character faces (Blender's convention, the armature's -Y) in the pose the file was in. A
13
- * bone's own Y runs along it, out of the top of a head, so it is not what "look" means. It is the game's, not Blender's: Blender's own IK and constraints are what an action was
14
- * authored with; this aims at what exists only while the game runs (an aim target, the player).
15
- *
16
- * Everything advances on the game's clock (`update(dt)`).
17
- */
18
- import * as THREE from 'three';
19
-
20
- export interface LayerOptions {
21
- /** The layer: `base` (default) or a name of the game's choosing. */
22
- readonly layer?: string;
23
- /** For a layer other than `base`: the bone it plays from, with every bone below it. */
24
- readonly from?: string;
25
- /** Seconds for weights to reach what was set (default 0.2 for `set`, 0 for `blend`). */
26
- readonly fade?: number;
27
- /** Repeat (default true); `false` plays once and holds the last frame. */
28
- readonly loop?: boolean;
29
- /** Playback rate, 1 is the action's own. */
30
- readonly speed?: number;
31
- /** Start again from the first frame when this action already plays on the layer. */
32
- readonly restart?: boolean;
33
- }
34
-
35
- export interface LookAtOptions {
36
- /** How far toward the target, 0 to 1 (default 1). */
37
- readonly weight?: number;
38
- /** The bone's local axis that should point at the target: `forward` (default, see the header)
39
- * or one of the bone's own axes (`y` is a Blender bone's length). */
40
- readonly axis?: 'forward' | 'x' | 'y' | 'z' | '-x' | '-y' | '-z';
41
- /** The most it turns away from the animated pose, in degrees (default 75). */
42
- readonly limit?: number;
43
- }
44
-
45
- interface Entry {
46
- readonly name: string;
47
- action: THREE.AnimationAction | null;
48
- weight: number;
49
- target: number;
50
- rate: number;
51
- loop: boolean;
52
- speed: number;
53
- }
54
-
55
- interface Layer {
56
- readonly name: string;
57
- from: string | null;
58
- /** The bones it plays on, by uuid; null for the base (everything other layers do not hold). */
59
- bones: Set<string> | null;
60
- readonly entries: Map<string, Entry>;
61
- }
62
-
63
- const AXES: Record<Exclude<NonNullable<LookAtOptions['axis']>, 'forward'>, THREE.Vector3> = {
64
- x: new THREE.Vector3(1, 0, 0), y: new THREE.Vector3(0, 1, 0), z: new THREE.Vector3(0, 0, 1),
65
- '-x': new THREE.Vector3(-1, 0, 0), '-y': new THREE.Vector3(0, -1, 0), '-z': new THREE.Vector3(0, 0, -1),
66
- };
67
-
68
- export class LayeredMixer {
69
- readonly #mixer: THREE.AnimationMixer;
70
- readonly #layers = new Map<string, Layer>();
71
- readonly #masked = new Map<string, THREE.AnimationClip>();
72
- readonly #looks = new Map<string, { bone: THREE.Bone; target: THREE.Vector3 | THREE.Object3D; options: LookAtOptions }>();
73
- /** Each bone's forward in its own space, from the pose the file was in (before any action). */
74
- readonly #forward = new Map<string, THREE.Vector3>();
75
-
76
- /** `clipFor` answers an action's full clip, or null while it is not available yet (a bake in
77
- * flight); the mixer asks again on later updates. */
78
- constructor(
79
- readonly root: THREE.Object3D,
80
- readonly bones: ReadonlyMap<string, THREE.Bone>,
81
- private readonly clipFor: (action: string) => THREE.AnimationClip | null,
82
- ) {
83
- this.#mixer = new THREE.AnimationMixer(root);
84
- // The character faces the armature's -Y, as in Blender; each bone's forward is that direction
85
- // in the bone's frame at the pose the file was in.
86
- root.updateWorldMatrix(true, true);
87
- const rootWorld = root.getWorldQuaternion(new THREE.Quaternion());
88
- const facing = new THREE.Vector3(0, -1, 0).applyQuaternion(rootWorld);
89
- for (const [name, bone] of bones) {
90
- const boneWorld = bone.getWorldQuaternion(new THREE.Quaternion());
91
- this.#forward.set(name, facing.clone().applyQuaternion(boneWorld.invert()).normalize());
92
- }
93
- this.#layers.set('base', { name: 'base', from: null, bones: null, entries: new Map() });
94
- }
95
-
96
- /** Play one action on a layer, crossfading the layer's others out. */
97
- set(action: string, options: LayerOptions = {}): string | null {
98
- const layer = this.#layer(options);
99
- if (typeof layer === 'string') return layer;
100
- const held = layer.entries.get(action);
101
- if (held && options.restart) held.action?.reset();
102
- this.#weigh(layer, { [action]: 1 }, options.fade ?? 0.2, options);
103
- return null;
104
- }
105
-
106
- /** Set a layer's actions at weights; an action left out fades to nothing. */
107
- blend(weights: Readonly<Record<string, number>>, options: LayerOptions = {}): string | null {
108
- const layer = this.#layer(options);
109
- if (typeof layer === 'string') return layer;
110
- this.#weigh(layer, weights, options.fade ?? 0, options);
111
- return null;
112
- }
113
-
114
- /** Fade out what a layer plays (the base: everything the character plays). */
115
- stop(options: LayerOptions = {}): void {
116
- const layer = this.#layers.get(options.layer ?? 'base');
117
- if (layer) this.#weigh(layer, {}, options.fade ?? 0.2, options);
118
- }
119
-
120
- /** The action that weighs most on a layer, as set (what a script asked for), or null. */
121
- playing(layer = 'base'): string | null {
122
- let best: Entry | null = null;
123
- for (const entry of this.#layers.get(layer)?.entries.values() ?? []) if (entry.target > 0 && (!best || entry.target > best.target)) best = entry;
124
- return best?.name ?? null;
125
- }
126
-
127
- /** Turn `bone` toward `target` (world point or object) every update until set to null. */
128
- lookAt(bone: string, target: THREE.Vector3 | THREE.Object3D | null, options: LookAtOptions = {}): string | null {
129
- if (target === null) { this.#looks.delete(bone); return null; }
130
- const found = this.bones.get(bone);
131
- if (!found) return `no bone "${bone}"`;
132
- this.#looks.set(bone, { bone: found, target, options: { ...options, axis: options.axis ?? 'forward' } });
133
- return null;
134
- }
135
-
136
- update(dt: number): void {
137
- for (const layer of this.#layers.values()) {
138
- for (const [name, entry] of [...layer.entries]) {
139
- if (!entry.action) entry.action = this.#action(layer, name, entry);
140
- entry.weight = entry.rate <= 0 ? entry.target
141
- : entry.weight < entry.target ? Math.min(entry.target, entry.weight + dt * entry.rate)
142
- : Math.max(entry.target, entry.weight - dt * entry.rate);
143
- if (entry.action) entry.action.setEffectiveWeight(entry.weight);
144
- if (entry.weight === 0 && entry.target === 0) {
145
- entry.action?.stop();
146
- layer.entries.delete(name);
147
- }
148
- }
149
- }
150
- this.#mixer.update(dt);
151
- for (const look of this.#looks.values()) this.#turn(look.bone, look.target, look.options);
152
- }
153
-
154
- dispose(): void {
155
- this.#mixer.stopAllAction();
156
- this.#looks.clear();
157
- }
158
-
159
- // ---------------------------------------------------------------- internals
160
-
161
- #layer(options: LayerOptions): Layer | string {
162
- const name = options.layer ?? 'base';
163
- if (name === 'base') return this.#layers.get('base')!;
164
- let layer = this.#layers.get(name);
165
- if (!layer || (options.from !== undefined && options.from !== layer.from)) {
166
- if (!options.from) return `layer "${name}" needs the bone it plays from (\`from\`)`;
167
- const root = this.bones.get(options.from);
168
- if (!root) return `no bone "${options.from}" for layer "${name}"`;
169
- const bones = new Set<string>();
170
- root.traverse((child) => { if ((child as THREE.Bone).isBone) bones.add(child.uuid); });
171
- if (layer) for (const entry of layer.entries.values()) entry.action?.stop();
172
- layer = { name, from: options.from, bones, entries: layer?.entries ?? new Map() };
173
- for (const entry of layer.entries.values()) entry.action = null;
174
- this.#layers.set(name, layer);
175
- // The base gives up these bones: its actions are rebuilt over what it still holds.
176
- for (const entry of this.#layers.get('base')!.entries.values()) { entry.action?.stop(); entry.action = null; }
177
- }
178
- return layer;
179
- }
180
-
181
- #weigh(layer: Layer, weights: Readonly<Record<string, number>>, fade: number, options: LayerOptions): void {
182
- const rate = fade > 0 ? 1 / fade : 0;
183
- for (const entry of layer.entries.values()) { entry.target = 0; entry.rate = rate; }
184
- for (const [name, raw] of Object.entries(weights)) {
185
- const target = Math.max(0, Math.min(1, raw));
186
- const entry = layer.entries.get(name) ?? { name, action: null, weight: 0, target: 0, rate, loop: true, speed: 1 };
187
- entry.target = target;
188
- entry.rate = rate;
189
- entry.loop = options.loop ?? entry.loop;
190
- entry.speed = options.speed ?? entry.speed;
191
- if (entry.action) this.#configure(entry.action, entry);
192
- layer.entries.set(name, entry);
193
- }
194
- }
195
-
196
- /** The action of `name` on `layer`: its clip limited to the layer's bones. */
197
- #action(layer: Layer, name: string, entry: Entry): THREE.AnimationAction | null {
198
- const full = this.clipFor(name);
199
- if (!full) return null;
200
- const allowed = layer.bones ?? this.#baseBones();
201
- const key = `${name}\u0000${layer.name}\u0000${allowed.size}`;
202
- let clip = this.#masked.get(key);
203
- if (!clip) {
204
- const tracks = full.tracks.filter((track) => allowed.has(track.name.slice(0, track.name.indexOf('.'))));
205
- clip = new THREE.AnimationClip(`${name}@${layer.name}`, full.duration, tracks);
206
- this.#masked.set(key, clip);
207
- }
208
- const action = this.#mixer.clipAction(clip);
209
- this.#configure(action, entry);
210
- action.setEffectiveWeight(entry.weight);
211
- action.play();
212
- return action;
213
- }
214
-
215
- #configure(action: THREE.AnimationAction, entry: Entry): void {
216
- action.setLoop(entry.loop ? THREE.LoopRepeat : THREE.LoopOnce, Number.POSITIVE_INFINITY);
217
- action.clampWhenFinished = !entry.loop;
218
- action.setEffectiveTimeScale(entry.speed);
219
- }
220
-
221
- /** Every bone no named layer holds. */
222
- #baseBones(): Set<string> {
223
- const taken = new Set<string>();
224
- for (const layer of this.#layers.values()) for (const uuid of layer.bones ?? []) taken.add(uuid);
225
- const bones = new Set<string>();
226
- for (const bone of this.bones.values()) if (!taken.has(bone.uuid)) bones.add(bone.uuid);
227
- return bones;
228
- }
229
-
230
- #turn(bone: THREE.Bone, target: THREE.Vector3 | THREE.Object3D, options: LookAtOptions): void {
231
- const weight = Math.max(0, Math.min(1, options.weight ?? 1));
232
- if (weight === 0) return;
233
- bone.updateWorldMatrix(true, false);
234
- // A script's Vector3 may come from its own copy of three, so it is recognised by its flag.
235
- const at = (target as THREE.Vector3).isVector3 ? new THREE.Vector3().copy(target as THREE.Vector3) : (target as THREE.Object3D).getWorldPosition(new THREE.Vector3());
236
- const position = new THREE.Vector3();
237
- const world = new THREE.Quaternion();
238
- bone.matrixWorld.decompose(position, world, new THREE.Vector3());
239
- const desired = at.sub(position);
240
- if (desired.lengthSq() < 1e-10) return;
241
- desired.normalize();
242
- const axis = options.axis ?? 'forward';
243
- const local = axis === 'forward' ? this.#forward.get(bone.name) ?? AXES.y : AXES[axis];
244
- const current = local.clone().applyQuaternion(world).normalize();
245
- const turn = new THREE.Quaternion().setFromUnitVectors(current, desired);
246
- const limit = THREE.MathUtils.degToRad(options.limit ?? 75);
247
- const angle = 2 * Math.acos(Math.min(1, Math.abs(turn.w)));
248
- const amount = angle > limit ? (limit / angle) * weight : weight;
249
- const applied = new THREE.Quaternion().slerp(turn, amount);
250
- const next = applied.multiply(world);
251
- const parent = new THREE.Quaternion();
252
- bone.parent?.getWorldQuaternion(parent);
253
- bone.quaternion.copy(parent.invert().multiply(next));
254
- bone.updateMatrixWorld(true);
255
- }
256
- }