@ikijs/engine 0.1.1 → 0.3.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/README.md +25 -31
- package/dist/index.d.mts +60 -2
- package/dist/index.d.ts +60 -2
- package/dist/index.js +82 -0
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +81 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +17 -3
package/README.md
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# @ikijs/engine
|
|
2
2
|
|
|
3
|
-
WebGL2 runtime that plays a [`.iki`](
|
|
3
|
+
WebGL2 runtime that plays a [`.iki`](https://github.com/zeikar/iki/tree/main/packages/format) puppet model in the browser.
|
|
4
4
|
|
|
5
5
|
The engine is **host-agnostic**: it depends only on
|
|
6
|
-
[`@ikijs/format`](
|
|
6
|
+
[`@ikijs/format`](https://github.com/zeikar/iki/tree/main/packages/format) and knows nothing about any particular app. A host
|
|
7
7
|
drives it by setting parameters (from lip-sync, gaze, blink, expressions); the
|
|
8
8
|
engine renders the result each frame.
|
|
9
9
|
|
|
@@ -38,15 +38,16 @@ clamped to the declared range; unknown ids and non-finite values are ignored.
|
|
|
38
38
|
|
|
39
39
|
## API
|
|
40
40
|
|
|
41
|
-
| Export | What it is
|
|
42
|
-
| ------------------------------------------------ |
|
|
43
|
-
| `IkiPlayer` | The renderer: `load` / `start` / `stop` / `setParameter` / `getParameter` / `getParameters` / `destroy`
|
|
44
|
-
| `IkiLoadResult` | `{ failedTextures, superseded }` returned by `load()`
|
|
45
|
-
| `ParameterStore` | The clamped parameter map the player drives
|
|
46
|
-
| `
|
|
47
|
-
| `
|
|
48
|
-
| `
|
|
49
|
-
| `
|
|
41
|
+
| Export | What it is |
|
|
42
|
+
| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `IkiPlayer` | The renderer: `load` / `start` / `stop` / `setParameter` / `getParameter` / `getParameters` / `destroy` |
|
|
44
|
+
| `IkiLoadResult` | `{ failedTextures, superseded }` returned by `load()` |
|
|
45
|
+
| `ParameterStore` | The clamped parameter map the player drives |
|
|
46
|
+
| `IkiMotion` | The three drivers below, built from a model and stepped as one; `drivenParameterIds` lists what they write |
|
|
47
|
+
| `IdleMotion` | Auto-blink / breath / gaze-drift driver |
|
|
48
|
+
| `PhysicsMotion` | Spring-mass-damper secondary motion (`model.physics`) |
|
|
49
|
+
| `HairChainMotion` | Multi-segment angular chain with gravity (`model.physicsChains`) |
|
|
50
|
+
| `translate` `rotate` `scale` `multiply` `toMat3` | The 2D affine helpers the engine itself uses |
|
|
50
51
|
|
|
51
52
|
## Motion drivers
|
|
52
53
|
|
|
@@ -56,37 +57,30 @@ per frame, and each writes through a sink you supply. That keeps them testable
|
|
|
56
57
|
and lets a host override or omit any of them.
|
|
57
58
|
|
|
58
59
|
```ts
|
|
59
|
-
import {
|
|
60
|
+
import { IkiMotion } from "@ikijs/engine";
|
|
60
61
|
|
|
61
62
|
// The drivers read the live pose and write the next one, both through the
|
|
62
63
|
// player — no host-side copy of the parameter state to keep in sync.
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const physics = new PhysicsMotion(
|
|
68
|
-
model.physics ?? [],
|
|
69
|
-
model.parameters,
|
|
70
|
-
read,
|
|
71
|
-
drive,
|
|
72
|
-
);
|
|
73
|
-
const chains = new HairChainMotion(
|
|
74
|
-
model.physicsChains ?? [],
|
|
75
|
-
model.parameters,
|
|
76
|
-
model.deformers ?? [],
|
|
77
|
-
read,
|
|
78
|
-
drive,
|
|
64
|
+
const motion = new IkiMotion(
|
|
65
|
+
model,
|
|
66
|
+
(id) => player.getParameter(id),
|
|
67
|
+
(id, value) => player.setParameter(id, value),
|
|
79
68
|
);
|
|
80
69
|
|
|
81
70
|
const tick = (now: number) => {
|
|
82
|
-
|
|
83
|
-
physics.update(now); // reads what idle just wrote
|
|
84
|
-
chains.update(now);
|
|
71
|
+
motion.update(now); // idle, then physics (lags what idle just wrote), then chains
|
|
85
72
|
requestAnimationFrame(tick);
|
|
86
73
|
};
|
|
87
74
|
requestAnimationFrame(tick);
|
|
88
75
|
```
|
|
89
76
|
|
|
77
|
+
To stop, stop calling `update()` — the drivers leave the pose where it was,
|
|
78
|
+
and `motion.drivenParameterIds` lists every parameter they wrote for a host
|
|
79
|
+
that wants to restore it. The three drivers are also exported individually
|
|
80
|
+
(`IdleMotion`, `PhysicsMotion`, `HairChainMotion`): the two physics drivers
|
|
81
|
+
take the same `read`/`sink` pair, `IdleMotion` only the `sink`, and each is
|
|
82
|
+
stepped with `update(nowMs)` in that order.
|
|
83
|
+
|
|
90
84
|
Both physics drivers integrate on a fixed 1/60 s sub-step with a clamped frame
|
|
91
85
|
delta, so a backgrounded tab or a long hitch cannot snap the rig.
|
|
92
86
|
|
package/dist/index.d.mts
CHANGED
|
@@ -189,7 +189,7 @@ interface IdleMotionOptions {
|
|
|
189
189
|
rng?: () => number;
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
|
-
* Pure-logic idle-animation driver. Animates the
|
|
192
|
+
* Pure-logic idle-animation driver. Animates the eight "life" parameters
|
|
193
193
|
* (eyes, breath, gaze, head sway) on an internal clock so tab-backgrounding
|
|
194
194
|
* or irregular frame delivery can't produce teleports or snap-close blinks.
|
|
195
195
|
*
|
|
@@ -201,6 +201,13 @@ interface IdleMotionOptions {
|
|
|
201
201
|
* The host is responsible for scheduling; this class has no timers or rAF.
|
|
202
202
|
*/
|
|
203
203
|
declare class IdleMotion {
|
|
204
|
+
/**
|
|
205
|
+
* Every parameter id {@link update} and `emitRestingPose` write, in write
|
|
206
|
+
* order. Published so a host (or IkiMotion) can restore them after it
|
|
207
|
+
* stops driving — keep in step with those two methods; the test suite
|
|
208
|
+
* pins this list to the actual emissions.
|
|
209
|
+
*/
|
|
210
|
+
readonly drivenParameterIds: readonly string[];
|
|
204
211
|
private readonly sink;
|
|
205
212
|
private readonly rng;
|
|
206
213
|
private clockMs;
|
|
@@ -232,6 +239,8 @@ declare class IdleMotion {
|
|
|
232
239
|
private swayX;
|
|
233
240
|
/** Vertical head sway in degrees, pure function of the internal clock. */
|
|
234
241
|
private swayY;
|
|
242
|
+
/** Head roll sway in degrees, pure function of the internal clock. */
|
|
243
|
+
private swayZ;
|
|
235
244
|
/** Ease gaze current toward target; pick a new target on the internal clock. */
|
|
236
245
|
private advanceGaze;
|
|
237
246
|
}
|
|
@@ -260,6 +269,8 @@ declare class IdleMotion {
|
|
|
260
269
|
* updated params on its own render loop (drivers and rendering are decoupled).
|
|
261
270
|
*/
|
|
262
271
|
declare class PhysicsMotion {
|
|
272
|
+
/** The output parameter of every rig, in rig order — what `emit` writes. */
|
|
273
|
+
readonly drivenParameterIds: readonly string[];
|
|
263
274
|
private readonly rigs;
|
|
264
275
|
private readonly read;
|
|
265
276
|
private readonly sink;
|
|
@@ -315,6 +326,8 @@ declare class PhysicsMotion {
|
|
|
315
326
|
* The host schedules updates; this class has no timers, rAF, DOM, or Date.now.
|
|
316
327
|
*/
|
|
317
328
|
declare class HairChainMotion {
|
|
329
|
+
/** Every segment's output, chain by chain — what `emitSegment` writes. */
|
|
330
|
+
readonly drivenParameterIds: readonly string[];
|
|
318
331
|
private readonly chainData;
|
|
319
332
|
private readonly params;
|
|
320
333
|
private readonly deformers;
|
|
@@ -370,4 +383,49 @@ declare class HairChainMotion {
|
|
|
370
383
|
private emitSegment;
|
|
371
384
|
}
|
|
372
385
|
|
|
373
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Bundles the three motion drivers — {@link IdleMotion}, {@link PhysicsMotion},
|
|
388
|
+
* {@link HairChainMotion} — into the one loop every host otherwise hand-builds:
|
|
389
|
+
* construct all three from the model, step them in the order physics and
|
|
390
|
+
* chains depend on, and know what they wrote.
|
|
391
|
+
*
|
|
392
|
+
* Usage:
|
|
393
|
+
* const motion = new IkiMotion(
|
|
394
|
+
* model,
|
|
395
|
+
* (id) => player.getParameter(id),
|
|
396
|
+
* (id, value) => player.setParameter(id, value),
|
|
397
|
+
* );
|
|
398
|
+
* // inside your rAF loop:
|
|
399
|
+
* motion.update(performance.now());
|
|
400
|
+
*
|
|
401
|
+
* The host schedules; this class has no timers, rAF, DOM, or Date.now.
|
|
402
|
+
*
|
|
403
|
+
* Stopping is the host's too: stop calling update(). The drivers leave the
|
|
404
|
+
* pose where it was — a host that wants it back writes its own resting values
|
|
405
|
+
* to `drivenParameterIds`.
|
|
406
|
+
*/
|
|
407
|
+
declare class IkiMotion {
|
|
408
|
+
/**
|
|
409
|
+
* Idle ids, then rig outputs, then chain-segment outputs, deduplicated and
|
|
410
|
+
* insertion-ordered. May name ids the model lacks (the player silently
|
|
411
|
+
* drops writes to unknown ids) — intersect with the model's parameters if
|
|
412
|
+
* you mirror into your own store.
|
|
413
|
+
*/
|
|
414
|
+
readonly drivenParameterIds: readonly string[];
|
|
415
|
+
private readonly idle;
|
|
416
|
+
private readonly physics;
|
|
417
|
+
private readonly chains;
|
|
418
|
+
constructor(model: IkiModel, read: (id: string) => number, sink: (id: string, value: number) => void);
|
|
419
|
+
/**
|
|
420
|
+
* Advance idle, then physics, then chains to the same wall-clock timestamp
|
|
421
|
+
* (milliseconds). Order is load-bearing: `PhysicsMotion` reads its input
|
|
422
|
+
* parameters (typically `ParamAngleX/Z`) through `read`, and the head sway
|
|
423
|
+
* idle wrote THIS frame is what the springs must lag behind; `HairChainMotion`
|
|
424
|
+
* then resolves its anchor deformer's world rotation from that same
|
|
425
|
+
* just-written pose (which may include a physics output). One timestamp for
|
|
426
|
+
* all three keeps their dt in lockstep.
|
|
427
|
+
*/
|
|
428
|
+
update(nowMs: number): void;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export { type Affine, HairChainMotion, IdleMotion, type IdleMotionOptions, type IkiLoadResult, IkiMotion, IkiPlayer, ParameterStore, PhysicsMotion, multiply, rotate, scale, toMat3, translate };
|
package/dist/index.d.ts
CHANGED
|
@@ -189,7 +189,7 @@ interface IdleMotionOptions {
|
|
|
189
189
|
rng?: () => number;
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
|
-
* Pure-logic idle-animation driver. Animates the
|
|
192
|
+
* Pure-logic idle-animation driver. Animates the eight "life" parameters
|
|
193
193
|
* (eyes, breath, gaze, head sway) on an internal clock so tab-backgrounding
|
|
194
194
|
* or irregular frame delivery can't produce teleports or snap-close blinks.
|
|
195
195
|
*
|
|
@@ -201,6 +201,13 @@ interface IdleMotionOptions {
|
|
|
201
201
|
* The host is responsible for scheduling; this class has no timers or rAF.
|
|
202
202
|
*/
|
|
203
203
|
declare class IdleMotion {
|
|
204
|
+
/**
|
|
205
|
+
* Every parameter id {@link update} and `emitRestingPose` write, in write
|
|
206
|
+
* order. Published so a host (or IkiMotion) can restore them after it
|
|
207
|
+
* stops driving — keep in step with those two methods; the test suite
|
|
208
|
+
* pins this list to the actual emissions.
|
|
209
|
+
*/
|
|
210
|
+
readonly drivenParameterIds: readonly string[];
|
|
204
211
|
private readonly sink;
|
|
205
212
|
private readonly rng;
|
|
206
213
|
private clockMs;
|
|
@@ -232,6 +239,8 @@ declare class IdleMotion {
|
|
|
232
239
|
private swayX;
|
|
233
240
|
/** Vertical head sway in degrees, pure function of the internal clock. */
|
|
234
241
|
private swayY;
|
|
242
|
+
/** Head roll sway in degrees, pure function of the internal clock. */
|
|
243
|
+
private swayZ;
|
|
235
244
|
/** Ease gaze current toward target; pick a new target on the internal clock. */
|
|
236
245
|
private advanceGaze;
|
|
237
246
|
}
|
|
@@ -260,6 +269,8 @@ declare class IdleMotion {
|
|
|
260
269
|
* updated params on its own render loop (drivers and rendering are decoupled).
|
|
261
270
|
*/
|
|
262
271
|
declare class PhysicsMotion {
|
|
272
|
+
/** The output parameter of every rig, in rig order — what `emit` writes. */
|
|
273
|
+
readonly drivenParameterIds: readonly string[];
|
|
263
274
|
private readonly rigs;
|
|
264
275
|
private readonly read;
|
|
265
276
|
private readonly sink;
|
|
@@ -315,6 +326,8 @@ declare class PhysicsMotion {
|
|
|
315
326
|
* The host schedules updates; this class has no timers, rAF, DOM, or Date.now.
|
|
316
327
|
*/
|
|
317
328
|
declare class HairChainMotion {
|
|
329
|
+
/** Every segment's output, chain by chain — what `emitSegment` writes. */
|
|
330
|
+
readonly drivenParameterIds: readonly string[];
|
|
318
331
|
private readonly chainData;
|
|
319
332
|
private readonly params;
|
|
320
333
|
private readonly deformers;
|
|
@@ -370,4 +383,49 @@ declare class HairChainMotion {
|
|
|
370
383
|
private emitSegment;
|
|
371
384
|
}
|
|
372
385
|
|
|
373
|
-
|
|
386
|
+
/**
|
|
387
|
+
* Bundles the three motion drivers — {@link IdleMotion}, {@link PhysicsMotion},
|
|
388
|
+
* {@link HairChainMotion} — into the one loop every host otherwise hand-builds:
|
|
389
|
+
* construct all three from the model, step them in the order physics and
|
|
390
|
+
* chains depend on, and know what they wrote.
|
|
391
|
+
*
|
|
392
|
+
* Usage:
|
|
393
|
+
* const motion = new IkiMotion(
|
|
394
|
+
* model,
|
|
395
|
+
* (id) => player.getParameter(id),
|
|
396
|
+
* (id, value) => player.setParameter(id, value),
|
|
397
|
+
* );
|
|
398
|
+
* // inside your rAF loop:
|
|
399
|
+
* motion.update(performance.now());
|
|
400
|
+
*
|
|
401
|
+
* The host schedules; this class has no timers, rAF, DOM, or Date.now.
|
|
402
|
+
*
|
|
403
|
+
* Stopping is the host's too: stop calling update(). The drivers leave the
|
|
404
|
+
* pose where it was — a host that wants it back writes its own resting values
|
|
405
|
+
* to `drivenParameterIds`.
|
|
406
|
+
*/
|
|
407
|
+
declare class IkiMotion {
|
|
408
|
+
/**
|
|
409
|
+
* Idle ids, then rig outputs, then chain-segment outputs, deduplicated and
|
|
410
|
+
* insertion-ordered. May name ids the model lacks (the player silently
|
|
411
|
+
* drops writes to unknown ids) — intersect with the model's parameters if
|
|
412
|
+
* you mirror into your own store.
|
|
413
|
+
*/
|
|
414
|
+
readonly drivenParameterIds: readonly string[];
|
|
415
|
+
private readonly idle;
|
|
416
|
+
private readonly physics;
|
|
417
|
+
private readonly chains;
|
|
418
|
+
constructor(model: IkiModel, read: (id: string) => number, sink: (id: string, value: number) => void);
|
|
419
|
+
/**
|
|
420
|
+
* Advance idle, then physics, then chains to the same wall-clock timestamp
|
|
421
|
+
* (milliseconds). Order is load-bearing: `PhysicsMotion` reads its input
|
|
422
|
+
* parameters (typically `ParamAngleX/Z`) through `read`, and the head sway
|
|
423
|
+
* idle wrote THIS frame is what the springs must lag behind; `HairChainMotion`
|
|
424
|
+
* then resolves its anchor deformer's world rotation from that same
|
|
425
|
+
* just-written pose (which may include a physics output). One timestamp for
|
|
426
|
+
* all three keeps their dt in lockstep.
|
|
427
|
+
*/
|
|
428
|
+
update(nowMs: number): void;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
export { type Affine, HairChainMotion, IdleMotion, type IdleMotionOptions, type IkiLoadResult, IkiMotion, IkiPlayer, ParameterStore, PhysicsMotion, multiply, rotate, scale, toMat3, translate };
|
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
HairChainMotion: () => HairChainMotion,
|
|
24
24
|
IdleMotion: () => IdleMotion,
|
|
25
|
+
IkiMotion: () => IkiMotion,
|
|
25
26
|
IkiPlayer: () => IkiPlayer,
|
|
26
27
|
ParameterStore: () => ParameterStore,
|
|
27
28
|
PhysicsMotion: () => PhysicsMotion,
|
|
@@ -1084,6 +1085,8 @@ var SWAY_X_AMP_B_DEG = 1.3;
|
|
|
1084
1085
|
var SWAY_X_PERIOD_B_MS = 9700;
|
|
1085
1086
|
var SWAY_Y_AMP_DEG = 1.6;
|
|
1086
1087
|
var SWAY_Y_PERIOD_MS = 7300;
|
|
1088
|
+
var SWAY_Z_AMP_DEG = 1.1;
|
|
1089
|
+
var SWAY_Z_PERIOD_MS = 11300;
|
|
1087
1090
|
function lerp(a, b, t) {
|
|
1088
1091
|
return a + (b - a) * t;
|
|
1089
1092
|
}
|
|
@@ -1096,6 +1099,22 @@ function randomInDisk(rng, radius) {
|
|
|
1096
1099
|
return [r * Math.cos(theta), r * Math.sin(theta)];
|
|
1097
1100
|
}
|
|
1098
1101
|
var IdleMotion = class {
|
|
1102
|
+
/**
|
|
1103
|
+
* Every parameter id {@link update} and `emitRestingPose` write, in write
|
|
1104
|
+
* order. Published so a host (or IkiMotion) can restore them after it
|
|
1105
|
+
* stops driving — keep in step with those two methods; the test suite
|
|
1106
|
+
* pins this list to the actual emissions.
|
|
1107
|
+
*/
|
|
1108
|
+
drivenParameterIds = [
|
|
1109
|
+
import_format.StandardParameter.EyeOpenLeft,
|
|
1110
|
+
import_format.StandardParameter.EyeOpenRight,
|
|
1111
|
+
import_format.StandardParameter.Breath,
|
|
1112
|
+
import_format.StandardParameter.EyeballX,
|
|
1113
|
+
import_format.StandardParameter.EyeballY,
|
|
1114
|
+
import_format.StandardParameter.AngleX,
|
|
1115
|
+
import_format.StandardParameter.AngleY,
|
|
1116
|
+
import_format.StandardParameter.AngleZ
|
|
1117
|
+
];
|
|
1099
1118
|
sink;
|
|
1100
1119
|
rng;
|
|
1101
1120
|
// Internal clock: advances by clamped dt, NOT by raw wall-clock jumps.
|
|
@@ -1157,6 +1176,7 @@ var IdleMotion = class {
|
|
|
1157
1176
|
this.sink(import_format.StandardParameter.EyeballY, this.gazeCurrentY);
|
|
1158
1177
|
this.sink(import_format.StandardParameter.AngleX, this.swayX());
|
|
1159
1178
|
this.sink(import_format.StandardParameter.AngleY, this.swayY());
|
|
1179
|
+
this.sink(import_format.StandardParameter.AngleZ, this.swayZ());
|
|
1160
1180
|
}
|
|
1161
1181
|
// ---------------------------------------------------------------------------
|
|
1162
1182
|
// Private helpers
|
|
@@ -1169,6 +1189,7 @@ var IdleMotion = class {
|
|
|
1169
1189
|
this.sink(import_format.StandardParameter.EyeballY, 0);
|
|
1170
1190
|
this.sink(import_format.StandardParameter.AngleX, 0);
|
|
1171
1191
|
this.sink(import_format.StandardParameter.AngleY, 0);
|
|
1192
|
+
this.sink(import_format.StandardParameter.AngleZ, 0);
|
|
1172
1193
|
}
|
|
1173
1194
|
/** Returns the current eye-open value (0..1) and advances blink state. */
|
|
1174
1195
|
advanceBlink() {
|
|
@@ -1200,6 +1221,10 @@ var IdleMotion = class {
|
|
|
1200
1221
|
swayY() {
|
|
1201
1222
|
return SWAY_Y_AMP_DEG * Math.sin(2 * Math.PI * this.clockMs / SWAY_Y_PERIOD_MS);
|
|
1202
1223
|
}
|
|
1224
|
+
/** Head roll sway in degrees, pure function of the internal clock. */
|
|
1225
|
+
swayZ() {
|
|
1226
|
+
return SWAY_Z_AMP_DEG * Math.sin(2 * Math.PI * this.clockMs / SWAY_Z_PERIOD_MS);
|
|
1227
|
+
}
|
|
1203
1228
|
/** Ease gaze current toward target; pick a new target on the internal clock. */
|
|
1204
1229
|
advanceGaze(dt) {
|
|
1205
1230
|
if (this.clockMs >= this.nextGazeRetargetMs) {
|
|
@@ -1222,6 +1247,8 @@ function signedNormalized(value, param) {
|
|
|
1222
1247
|
return clamp((value - rest) / den, -1, 1);
|
|
1223
1248
|
}
|
|
1224
1249
|
var PhysicsMotion = class {
|
|
1250
|
+
/** The output parameter of every rig, in rig order — what `emit` writes. */
|
|
1251
|
+
drivenParameterIds;
|
|
1225
1252
|
rigs;
|
|
1226
1253
|
read;
|
|
1227
1254
|
sink;
|
|
@@ -1235,6 +1262,7 @@ var PhysicsMotion = class {
|
|
|
1235
1262
|
this.sink = sink;
|
|
1236
1263
|
this.params = new Map(params.map((p) => [p.id, p]));
|
|
1237
1264
|
this.state = rigs.map(() => ({ x: 0, v: 0 }));
|
|
1265
|
+
this.drivenParameterIds = rigs.map((rig) => rig.output.parameter);
|
|
1238
1266
|
}
|
|
1239
1267
|
/**
|
|
1240
1268
|
* Advance every rig to the given wall-clock timestamp (milliseconds).
|
|
@@ -1305,6 +1333,8 @@ var PhysicsMotion = class {
|
|
|
1305
1333
|
var DEG2RAD = Math.PI / 180;
|
|
1306
1334
|
var RAD2DEG = 180 / Math.PI;
|
|
1307
1335
|
var HairChainMotion = class {
|
|
1336
|
+
/** Every segment's output, chain by chain — what `emitSegment` writes. */
|
|
1337
|
+
drivenParameterIds;
|
|
1308
1338
|
chainData;
|
|
1309
1339
|
params;
|
|
1310
1340
|
deformers;
|
|
@@ -1325,6 +1355,9 @@ var HairChainMotion = class {
|
|
|
1325
1355
|
),
|
|
1326
1356
|
state: chain.segments.map(() => ({ angle: 0, angularVelocity: 0 }))
|
|
1327
1357
|
}));
|
|
1358
|
+
this.drivenParameterIds = chains.flatMap(
|
|
1359
|
+
(c) => c.segments.map((s) => s.output.parameter)
|
|
1360
|
+
);
|
|
1328
1361
|
}
|
|
1329
1362
|
/**
|
|
1330
1363
|
* Advance every chain to the given wall-clock timestamp (milliseconds).
|
|
@@ -1437,10 +1470,59 @@ var HairChainMotion = class {
|
|
|
1437
1470
|
);
|
|
1438
1471
|
}
|
|
1439
1472
|
};
|
|
1473
|
+
|
|
1474
|
+
// src/iki-motion.ts
|
|
1475
|
+
var IkiMotion = class {
|
|
1476
|
+
/**
|
|
1477
|
+
* Idle ids, then rig outputs, then chain-segment outputs, deduplicated and
|
|
1478
|
+
* insertion-ordered. May name ids the model lacks (the player silently
|
|
1479
|
+
* drops writes to unknown ids) — intersect with the model's parameters if
|
|
1480
|
+
* you mirror into your own store.
|
|
1481
|
+
*/
|
|
1482
|
+
drivenParameterIds;
|
|
1483
|
+
idle;
|
|
1484
|
+
physics;
|
|
1485
|
+
chains;
|
|
1486
|
+
constructor(model, read, sink) {
|
|
1487
|
+
const rigs = model.physics ?? [];
|
|
1488
|
+
const chains = model.physicsChains ?? [];
|
|
1489
|
+
this.idle = new IdleMotion(sink);
|
|
1490
|
+
this.physics = new PhysicsMotion(rigs, model.parameters, read, sink);
|
|
1491
|
+
this.chains = new HairChainMotion(
|
|
1492
|
+
chains,
|
|
1493
|
+
model.parameters,
|
|
1494
|
+
model.deformers ?? [],
|
|
1495
|
+
read,
|
|
1496
|
+
sink
|
|
1497
|
+
);
|
|
1498
|
+
this.drivenParameterIds = [
|
|
1499
|
+
.../* @__PURE__ */ new Set([
|
|
1500
|
+
...this.idle.drivenParameterIds,
|
|
1501
|
+
...this.physics.drivenParameterIds,
|
|
1502
|
+
...this.chains.drivenParameterIds
|
|
1503
|
+
])
|
|
1504
|
+
];
|
|
1505
|
+
}
|
|
1506
|
+
/**
|
|
1507
|
+
* Advance idle, then physics, then chains to the same wall-clock timestamp
|
|
1508
|
+
* (milliseconds). Order is load-bearing: `PhysicsMotion` reads its input
|
|
1509
|
+
* parameters (typically `ParamAngleX/Z`) through `read`, and the head sway
|
|
1510
|
+
* idle wrote THIS frame is what the springs must lag behind; `HairChainMotion`
|
|
1511
|
+
* then resolves its anchor deformer's world rotation from that same
|
|
1512
|
+
* just-written pose (which may include a physics output). One timestamp for
|
|
1513
|
+
* all three keeps their dt in lockstep.
|
|
1514
|
+
*/
|
|
1515
|
+
update(nowMs) {
|
|
1516
|
+
this.idle.update(nowMs);
|
|
1517
|
+
this.physics.update(nowMs);
|
|
1518
|
+
this.chains.update(nowMs);
|
|
1519
|
+
}
|
|
1520
|
+
};
|
|
1440
1521
|
// Annotate the CommonJS export names for ESM import in node:
|
|
1441
1522
|
0 && (module.exports = {
|
|
1442
1523
|
HairChainMotion,
|
|
1443
1524
|
IdleMotion,
|
|
1525
|
+
IkiMotion,
|
|
1444
1526
|
IkiPlayer,
|
|
1445
1527
|
ParameterStore,
|
|
1446
1528
|
PhysicsMotion,
|