three-vat 1.0.0 → 2.0.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,472 @@
1
+ import { DataTexture, BufferGeometry, Material, Box3, InstancedMesh, BatchedMesh } from 'three';
2
+
3
+ /**
4
+ * How an instance repeats its clip — the numbers `THREE.LoopRepeat`,
5
+ * `THREE.LoopOnce` and `THREE.LoopPingPong` name, in three's own order, as
6
+ * values a `Float32Array` can carry.
7
+ *
8
+ * What each one *means* is {@link resolveVATFrame}, in one place, transcribed
9
+ * by both decode paths.
10
+ */
11
+ declare const LoopMode: {
12
+ /** Play the clip end to end, forever (or `repetitions` times). */
13
+ readonly Repeat: 0;
14
+ /** Play the clip through once. */
15
+ readonly Once: 1;
16
+ /** Play forward, then backward, without baking the reversed frames. */
17
+ readonly PingPong: 2;
18
+ };
19
+ type LoopMode = (typeof LoopMode)[keyof typeof LoopMode];
20
+ /**
21
+ * What an instance does once it has finished — three's `clampWhenFinished`, as
22
+ * a pair of numbers.
23
+ *
24
+ * `Clamp` is first because it is this library's default, where three's
25
+ * `clampWhenFinished` defaults to `false`. A one-shot in a crowd — a death, an
26
+ * impact — almost always has to *stay* in its final state, and a rewinding
27
+ * corpse standing back up is the failure a crowd library should not ship by
28
+ * default. The divergence is deliberate.
29
+ */
30
+ declare const EndMode: {
31
+ /** Hold the last frame. */
32
+ readonly Clamp: 0;
33
+ /** Return to the first frame. */
34
+ readonly Rewind: 1;
35
+ };
36
+ type EndMode = (typeof EndMode)[keyof typeof EndMode];
37
+ /**
38
+ * An endless repeat count, as the pack spells it. `Infinity` does not survive a
39
+ * `Float32Array` usefully, so it is converted once, here, at the boundary.
40
+ */
41
+ declare const INFINITE_REPETITIONS = -1;
42
+ /**
43
+ * The longest fade {@link setVATInstance} will honour, in seconds.
44
+ *
45
+ * The cap exists because of what this fade *is*: one frozen pose of the
46
+ * outgoing clip, blended away — not a second playback running alongside the
47
+ * first. Over a tenth of a second that is invisible; over half a second the
48
+ * instance visibly skates, because whatever it was doing stopped dead the
49
+ * moment the transition began. A longer fade would not be a better fade, it
50
+ * would be a more visible bug, so the number is clamped rather than trusted.
51
+ *
52
+ * Provisional, like the fade itself: a real two-clip crossfade (#30) replaces
53
+ * both, and nothing else should be built on top of them.
54
+ */
55
+ declare const MAX_FADE_DURATION = 0.25;
56
+ /**
57
+ * The frozen pose a fade blends away from: one phase of the clip an instance
58
+ * was playing when its animation changed, and the band that phase indexes.
59
+ *
60
+ * Not a second playback state — there is no start time and no speed here,
61
+ * because nothing about it moves. That is the whole of the freeze, and the
62
+ * whole of its limit; see {@link MAX_FADE_DURATION}.
63
+ */
64
+ interface VATFadeFrom {
65
+ /** First texture row of the outgoing clip's band. */
66
+ startFrame: number;
67
+ /** Rows in that band. */
68
+ frames: number;
69
+ /** The phase of that band the instance was at, in `[0, 1]`. */
70
+ phase: number;
71
+ }
72
+ /**
73
+ * Per-instance playback state consumed by both decode paths.
74
+ *
75
+ * Every policy field is optional because the clip already answers it: a bake
76
+ * handed a configured `AnimationAction` records the answer in the clip table
77
+ * ({@link VATClipDefaults}), and an instance that says nothing inherits it. A
78
+ * crowd of a thousand deaths says "once, clamped" once, at the bake.
79
+ */
80
+ interface VATInstance {
81
+ /**
82
+ * The clip band to play, straight out of `vat.clips`. Its playback defaults
83
+ * come along with it; a clip table assembled by hand may carry none, and then
84
+ * the library defaults below apply.
85
+ */
86
+ clip: Pick<VAT['clips'][number], 'startFrame' | 'frames' | 'fps'> & Partial<VATClipDefaults>;
87
+ /**
88
+ * Absolute clock time, in seconds, at which this animation began. May be in
89
+ * the past — and **desync is exactly that**: give each instance of a crowd its
90
+ * own start time a little way back and they stop moving in lockstep.
91
+ *
92
+ * The one field with no clip-level default: when an animation began is a fact
93
+ * about the instance and nothing else.
94
+ */
95
+ startTime: number;
96
+ /**
97
+ * Playback rate multiplier, `>= 0`. Defaults to the clip's speed, then to
98
+ * `1`. A negative rate is refused when the instance is written: a VAT plays
99
+ * forward, and a clip that must run backwards is baked as a reversed clip.
100
+ * `0` is legal — the instance holds its clip's first row.
101
+ */
102
+ speed?: number;
103
+ /** How the clip repeats. Defaults to the clip's, then {@link LoopMode.Repeat}. */
104
+ loopMode?: LoopMode;
105
+ /**
106
+ * How many times to play the clip, or {@link INFINITE_REPETITIONS}. Defaults
107
+ * to the clip's count, then to endless for {@link LoopMode.Repeat} and a
108
+ * single play for anything else — the counts three's own `LoopRepeat` and
109
+ * `LoopOnce` imply.
110
+ *
111
+ * A count belongs to the mode it was configured under: replace the clip's
112
+ * `loopMode` and say nothing here, and the count comes from the new mode
113
+ * rather than from the clip — a one-shot over a looping clip finishes.
114
+ */
115
+ repetitions?: number;
116
+ /**
117
+ * What to do once the repetitions run out. Defaults to the clip's, then
118
+ * {@link EndMode.Clamp} — where three's `clampWhenFinished` defaults to
119
+ * `false`; see {@link EndMode}.
120
+ */
121
+ endMode?: EndMode;
122
+ /**
123
+ * The frozen outgoing pose to fade away from. Normally you do not write this
124
+ * yourself: {@link setVATInstance} freezes whatever the instance was playing
125
+ * and fills it in when you ask for a {@link fadeDuration}.
126
+ */
127
+ from?: VATFadeFrom;
128
+ /**
129
+ * Seconds to blend {@link from} away over, capped at {@link MAX_FADE_DURATION}.
130
+ * Wall-clock seconds from {@link startTime}: the clip's `speed` does not
131
+ * stretch a fade.
132
+ *
133
+ * Ignored without a `from` to fade away from — and at creation there is
134
+ * nothing to fade away from, so this is `setVATInstance`'s field in practice.
135
+ */
136
+ fadeDuration?: number;
137
+ }
138
+ /**
139
+ * Where in its VAT an instance is at a given moment: the two frame rows to
140
+ * sample, the blend between them, and the two facts a decode cannot re-derive
141
+ * from the rows alone.
142
+ */
143
+ interface VATFrame {
144
+ /** The frame row to sample — an absolute texture row, clip band included. */
145
+ row: number;
146
+ /** The row it interpolates toward. */
147
+ rowNext: number;
148
+ /** Blend between {@link row} and {@link rowNext}, in `[0, 1)`. */
149
+ mix: number;
150
+ /**
151
+ * Whether {@link rowNext} crossed the clip's last row back into its first.
152
+ * True only while a clip is genuinely looping: a ping-pong bounces rather
153
+ * than wraps, and a finished one-shot must not wrap at all or the corpse
154
+ * stands back up for a frame.
155
+ */
156
+ wraps: boolean;
157
+ /** Whether the repetitions have run out and the instance is holding an end pose. */
158
+ finished: boolean;
159
+ /** How far through the clip this is, in `[0, 1]` — what {@link row} is derived from. */
160
+ phase: number;
161
+ /**
162
+ * The frozen outgoing row a fade blends away from. Equal to {@link row} when
163
+ * the instance is not fading, so a reader that ignores {@link fade} — the
164
+ * demo's texture-panel cursors among them — never points at a row this
165
+ * instance is not sampling.
166
+ */
167
+ fadeRow: number;
168
+ /**
169
+ * How much of {@link fadeRow} is still showing: `1` at the moment of the
170
+ * write, falling to `0` across `fadeDuration`, and `0` for an instance that
171
+ * is not fading. The decode mixes the sampled clip toward the frozen pose by
172
+ * exactly this weight — the weight of the fade, not the fade itself, which is
173
+ * the pose-freeze fade `CONTEXT.md` names.
174
+ */
175
+ fadeWeight: number;
176
+ }
177
+ /**
178
+ * What the vertex shader computes, as a pure function of `(instance, time)` —
179
+ * the **one definition** of the playback semantics. Both decode paths
180
+ * transcribe it (`DECODE_PRELUDE` in src/webgl.ts, `vatDecode` in src/tsl.ts);
181
+ * neither invents it.
182
+ *
183
+ * It exists in TypeScript because the arithmetic is otherwise reachable only
184
+ * inside a GLSL string and a TSL node graph, neither of which CI can evaluate
185
+ * without a GPU — and because a caller scheduling what happens after a one-shot
186
+ * needs to ask the same question the shader answers.
187
+ *
188
+ * There is no accumulated state anywhere in here: an instance is written once,
189
+ * at the moment its animation changes, and every frame after that is this
190
+ * function of the shared clock.
191
+ */
192
+ declare function resolveVATFrame(instance: VATInstance, time: number): VATFrame;
193
+ /**
194
+ * What carries the pack to the shader: one `DataTexture`, three texels wide,
195
+ * one row per instance, read by the instance's *logical* index (ADR-0016).
196
+ *
197
+ * Held by the caller rather than hidden behind the geometry, because a
198
+ * `BufferGeometry` has nowhere to put a texture and a side channel — a
199
+ * `WeakMap`, or a `userData` the first `clone()` loses — would not say what
200
+ * {@link setVATInstance} writes into. `createVATMesh` hands one back on either
201
+ * decode path; build your own with {@link createVATPlaybackTexture} when you
202
+ * are assembling a crowd by hand.
203
+ */
204
+ interface VATPlaybackTexture {
205
+ /**
206
+ * The texture both decode paths bind: `x = field`, `y = instance`, RGBA
207
+ * float, {@link PACK_WIDTH} texels wide.
208
+ */
209
+ texture: DataTexture;
210
+ /** Instances it carries — the rows of {@link texture}. */
211
+ count: number;
212
+ }
213
+ /**
214
+ * Write a crowd's instance playback into a new playback texture. Call once,
215
+ * before rendering, and bind the result into the decode — `createVATMesh` does
216
+ * both for you.
217
+ *
218
+ * The layout below is the shared contract, spelled once in {@link PACK_TEXELS}.
219
+ * Both decode paths read exactly these three texels of row `instanceIndex` —
220
+ * `DECODE_PRELUDE` in `src/webgl.ts` with `texelFetch`, `texturePlayback` in
221
+ * `src/tsl.ts` with `textureLoad`.
222
+ *
223
+ * | Texel | r | g | b | a |
224
+ * | ---------------- | ------------------- | ------------ | ------------- | ------------- |
225
+ * | `x = 0` clip | clip start row | clip frames | clip fps | speed |
226
+ * | `x = 1` playback | start time | loop mode | repetitions | end mode |
227
+ * | `x = 2` fade | from clip start row | from frames | from phase | fade duration |
228
+ *
229
+ * **A texture, not three instanced attributes.** An attribute with divisor 1 is
230
+ * indexed by the *drawn slot*, and the drawn slot stops being the instance the
231
+ * moment a renderer culls or sorts per instance — on a `BatchedMesh` every
232
+ * vertex would read element 0 and the whole crowd would play instance 0's clip
233
+ * (ADR-0016). A row keyed by the logical index is what three itself does for
234
+ * the same problem, in `_matricesTexture`.
235
+ *
236
+ * **Three texels, not thirteen floats.** The move to a texture touched no
237
+ * decode arithmetic, because the layout did not change with it: the pack was
238
+ * already three RGBA-shaped `vec4`s (ADR-0009). Published 1.x is the other
239
+ * story — five one-float attributes there, so a 1.x caller meets both changes
240
+ * at once.
241
+ *
242
+ * **`FloatType`, and it stays that way.** A `startTime` in seconds does not
243
+ * survive half precision — one second of resolution at 2 048 s — so a narrower
244
+ * encoding for the VAT textures does not reach this one.
245
+ *
246
+ * The policy fields, and the clip texel's speed, come from the instance where
247
+ * it names them and from the clip's baked defaults where it does not — resolved
248
+ * in the one place those tiers are spelled — and both decode paths read them as
249
+ * {@link resolveVATFrame} defines them. The fade texel is written as zeroes,
250
+ * which is what "not fading" is: a crowd being created has no pose to fade away
251
+ * from. Fades belong to {@link setVATInstance}, where an instance's animation
252
+ * changes and there is something to fade out of.
253
+ */
254
+ declare function createVATPlaybackTexture(instances: VATInstance[]): VATPlaybackTexture;
255
+ /**
256
+ * Change one instance's animation, after the crowd is built. The single write
257
+ * the whole event-driven half of this library is made of: an enemy hit at
258
+ * `t = 12.3s` becomes a dying enemy here, and the CPU does not touch it again.
259
+ *
260
+ * ```ts
261
+ * // the moment it is hit — and nothing per frame afterwards
262
+ * setVATInstance(playback, enemyId, {
263
+ * clip: vat.clips[2], // "once, clamped" came with the bake
264
+ * startTime: time.value,
265
+ * fadeDuration: 0.1, // blend out of whatever it was doing
266
+ * })
267
+ * ```
268
+ *
269
+ * `playback` is the crowd's playback texture — `createVATMesh` hands it back
270
+ * beside the mesh and the clock — and `index` the instance's index in the array
271
+ * the crowd was built from, the same one `setMatrixAt` takes.
272
+ *
273
+ * Only that instance's row is marked for upload, so a crowd of a thousand costs
274
+ * one small write rather than a full re-upload. Everything else about the
275
+ * instance — its matrix, its clip's defaults — is untouched. On the TSL path
276
+ * the range is recorded and ignored: three's WebGPU backend re-uploads the
277
+ * whole image on `needsUpdate`, which is 48 bytes per instance once per frame
278
+ * in which anything changed (docs/usage.md says what that costs).
279
+ *
280
+ * Ask for a `fadeDuration` and the pose the instance is in *at `startTime`* is
281
+ * frozen and blended away over that many wall-clock seconds, so the change does
282
+ * not pop. It is a frozen pose and not a second playback: see
283
+ * {@link MAX_FADE_DURATION} for what that costs and how far it can be pushed.
284
+ * One pose, too — writing an instance that is *already* fading freezes the clip
285
+ * it had switched to and drops the older pose, because the pack holds one.
286
+ *
287
+ * A written instance is a pure function of the clock from here on, so what
288
+ * happens *after* it is a matter of scheduling one more of these writes —
289
+ * {@link endsAt} says exactly when. Chaining stays yours: the GPU never learns
290
+ * about a next clip.
291
+ *
292
+ * This is deliberately a function over a playback texture rather than an
293
+ * `InstancedMesh` method. The primitives stay composable for a crowd rendered
294
+ * onto something else, which is the escape hatch ADR-0009 and ADR-0014 commit
295
+ * to — and the playback texture is the object such a caller holds (ADR-0016).
296
+ */
297
+ declare function setVATInstance(playback: VATPlaybackTexture, index: number, instance: VATInstance): void;
298
+ /**
299
+ * The exact clock time this instance stops animating — when
300
+ * {@link resolveVATFrame} first reports `finished` — or `null` for an animation
301
+ * that never gets there: an endless loop, or a speed of zero.
302
+ *
303
+ * This is what makes chaining one clip to the next a single scheduled write
304
+ * rather than a per-frame poll:
305
+ *
306
+ * ```ts
307
+ * const hit = { clip: vat.clips[1], startTime: now, loopMode: LoopMode.Once }
308
+ * setVATInstance(playback, id, hit)
309
+ *
310
+ * const at = endsAt(hit)
311
+ * if (at !== null) schedule(at, () => setVATInstance(playback, id, { clip: walk, startTime: at }))
312
+ * ```
313
+ *
314
+ * Nothing about the chain reaches the GPU: it reads one pack, and the next clip
315
+ * does not exist to it until that write happens.
316
+ */
317
+ declare function endsAt(instance: VATInstance): number | null;
318
+
319
+ /**
320
+ * The playback policy a clip carries for every instance that plays it —
321
+ * declared once, at the bake, instead of repeated at every instance. Hand
322
+ * `bakeVAT` a configured `AnimationAction` rather than a bare `AnimationClip`
323
+ * and these come from it; hand it a clip and they are the library defaults
324
+ * (repeat, forever, speed 1, clamping when finished).
325
+ *
326
+ * Defaults, never decisions: an instance overrides any of them, field by field
327
+ * ({@link VATInstance}). Nothing in the texels changes between "once" and
328
+ * "forever" — a bake produces poses, and this is the policy those poses are
329
+ * played under.
330
+ */
331
+ interface VATClipDefaults {
332
+ /** How the clip repeats, from the action's `loop`. */
333
+ loopMode: LoopMode;
334
+ /**
335
+ * How many times it plays, from the action's `repetitions` — `Infinity`
336
+ * converted to `INFINITE_REPETITIONS`, because a `Float32Array` cannot carry
337
+ * the former.
338
+ */
339
+ repetitions: number;
340
+ /**
341
+ * What it does once finished — {@link EndMode.Clamp} from the bake, whichever
342
+ * of the two inputs it came from.
343
+ *
344
+ * three defaults `clampWhenFinished` to `false`, and a crowd's answer to a
345
+ * one-shot is to hold the last frame: a corpse standing back up is the worse
346
+ * default to ship (see {@link EndMode}). A bare `AnimationClip` says nothing,
347
+ * and an untouched action's `false` is not a statement either — it is what
348
+ * the field already holds — so the bake gives both the same answer rather
349
+ * than punishing the caller who configured an action. `clampWhenFinished =
350
+ * true` agrees with it; three's rewind is asked for per instance, with
351
+ * `endMode: EndMode.Rewind` (ADR-0017).
352
+ */
353
+ endMode: EndMode;
354
+ /**
355
+ * Playback rate, from the action's `timeScale`. `>= 0`: a band is sampled
356
+ * forward from its own first row, so a negative rate is refused at the bake
357
+ * rather than held on that row for ever. `0` is a held first row, on purpose.
358
+ */
359
+ speed: number;
360
+ }
361
+ /**
362
+ * One baked animation range within a VAT's stacked frame rows, and the playback
363
+ * defaults every instance of it inherits.
364
+ */
365
+ interface VATClip extends VATClipDefaults {
366
+ /** Clip name, taken from the source `AnimationClip`. */
367
+ name: string;
368
+ /** First frame row (y) of this clip in the texture. */
369
+ startFrame: number;
370
+ /** Number of frame rows baked for this clip. */
371
+ frames: number;
372
+ /** Effective frames-per-second of the bake (`frames / duration`). */
373
+ fps: number;
374
+ /** Source clip duration in seconds. */
375
+ duration: number;
376
+ /**
377
+ * Largest per-vertex position-delta magnitude (metres) across the clip.
378
+ * Near-zero means the clip baked as a frozen pose — the diagnostic for a
379
+ * mis-targeted or genuinely static clip.
380
+ */
381
+ maxDelta: number;
382
+ }
383
+ /**
384
+ * A baked Vertex Animation Texture: the position `DataTexture` (and the normal
385
+ * one, unless the bake was told to skip it), the geometry they are indexed by,
386
+ * and the clip table and bounds needed to decode and render them. Produced
387
+ * exactly one way — {@link bakeVAT}, at runtime, from a loaded glTF (ADR-0010).
388
+ *
389
+ * The merged vertex ordering is the baker's own invention and the textures are
390
+ * indexed by it (`x = gl_VertexID`), so the caller cannot bring its own
391
+ * geometry — it must render the one baked here. `materials` is ordered to match
392
+ * `geometry.groups[].materialIndex`, giving one draw call per material.
393
+ *
394
+ * The typed array behind either texture's `image.data` is the bake's choice,
395
+ * not part of this contract: `Float32Array` today, and a narrower encoding may
396
+ * change it in a minor release. Move the buffer, hand it to {@link makeVATTexture};
397
+ * do not read numbers out of it.
398
+ */
399
+ interface VAT {
400
+ /** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
401
+ positionTexture: DataTexture;
402
+ /**
403
+ * RGBA float texture of per-vertex absolute normals (`x = vertex`, `y = frame`),
404
+ * or `null` when the bake was told to skip it (`bakeNormals: false`) — halving
405
+ * the VAT for a crowd that never reads a normal. Neither decode path samples
406
+ * it when it is absent; a smooth-shaded lit material paired with such a VAT is
407
+ * refused rather than lit by its rest pose.
408
+ */
409
+ normalTexture: DataTexture | null;
410
+ /**
411
+ * Merged, root-space rest-pose geometry. Its `position` is the delta
412
+ * reference. Carries `normal` always, and `uv`, `color` and `tangent` when
413
+ * every source mesh carried them; skinning attributes and morph targets are
414
+ * dropped, the VAT having replaced them.
415
+ */
416
+ geometry: BufferGeometry;
417
+ /** Source materials, indexed by `geometry.groups[].materialIndex`. */
418
+ materials: Material[];
419
+ /** Clip table: name → `{ startFrame, frames, fps, ... }`. */
420
+ clips: VATClip[];
421
+ /** Union of every baked frame's bounds; use as the geometry bounding box. */
422
+ bounds: Box3;
423
+ /** Vertex count (texture width). */
424
+ vertexCount: number;
425
+ /** Total frame rows across all clips (texture height). */
426
+ totalFrames: number;
427
+ /** Position encoding. Only `'delta'` in v1. */
428
+ encoding: 'delta';
429
+ }
430
+ /**
431
+ * The shared playback clock: one `{ value }` in seconds, read by every material
432
+ * of every VAT mesh driven by it. Set it once per frame. Deliberately the
433
+ * narrowest shape both decode paths satisfy — a WebGL `IUniform<number>` and a
434
+ * TSL uniform node are both one of these — so `createVATMesh` returns the same
435
+ * thing on either renderer.
436
+ */
437
+ interface VATClock {
438
+ value: number;
439
+ }
440
+ /**
441
+ * A **crowd** ready to render: the mesh to add to the scene, and the clock to
442
+ * advance. What `createVATMesh` returns on either decode path, so moving a
443
+ * crowd between renderers is an import change and nothing else. Named for what
444
+ * it is rather than for its `mesh` field — the clock is half of it.
445
+ */
446
+ interface VATCrowd {
447
+ /** Add to the scene. Its instance matrices are yours to write. */
448
+ mesh: InstancedMesh;
449
+ /** The shared playback clock — set `.value` once per frame. */
450
+ time: VATClock;
451
+ /**
452
+ * The crowd's **playback texture**: what carries each instance's clip, phase
453
+ * and policy to the shader, and what `setVATInstance` writes one row of
454
+ * (ADR-0016). Exposed rather than hidden behind the mesh because it is the
455
+ * object a caller has to hold to change an instance after the crowd is
456
+ * built — a `BufferGeometry` cannot carry a texture, so there is nowhere
457
+ * else honest to keep it.
458
+ */
459
+ playback: VATPlaybackTexture;
460
+ }
461
+
462
+ /**
463
+ * A mesh a VAT crowd can ride.
464
+ *
465
+ * `InstancedMesh` is what `createVATMesh` builds on either path.
466
+ * `BatchedMesh` is reached through the primitives, and buys per-instance
467
+ * frustum culling and depth sorting from three.js itself — see
468
+ * docs/usage.md, which also says what it does *not* buy.
469
+ */
470
+ type VATCarrier = InstancedMesh | BatchedMesh;
471
+
472
+ export { EndMode as E, INFINITE_REPETITIONS as I, LoopMode as L, MAX_FADE_DURATION as M, type VAT as V, type VATCarrier as a, type VATClip as b, type VATClipDefaults as c, type VATClock as d, type VATCrowd as e, type VATFadeFrom as f, type VATFrame as g, type VATInstance as h, type VATPlaybackTexture as i, createVATPlaybackTexture as j, endsAt as k, resolveVATFrame as r, setVATInstance as s };
@@ -0,0 +1,202 @@
1
+ import { FloatType, DataTexture, RGBAFormat, NearestFilter } from 'three';
2
+
3
+ // src/vat-texture.ts
4
+ var MAX_TEXTURE_SIZE = 16384;
5
+ function makeVATTexture(data, width, height, type = FloatType) {
6
+ const tex = new DataTexture(data, width, height, RGBAFormat, type);
7
+ tex.minFilter = NearestFilter;
8
+ tex.magFilter = NearestFilter;
9
+ tex.generateMipmaps = false;
10
+ tex.needsUpdate = true;
11
+ return tex;
12
+ }
13
+
14
+ // src/instance-playback.ts
15
+ var PACK_TEXELS = {
16
+ clip: 0,
17
+ playback: 1,
18
+ fade: 2
19
+ };
20
+ var PACK_WIDTH = 3;
21
+ var PACK_STRIDE = PACK_WIDTH * 4;
22
+ var LoopMode = {
23
+ /** Play the clip end to end, forever (or `repetitions` times). */
24
+ Repeat: 0,
25
+ /** Play the clip through once. */
26
+ Once: 1,
27
+ /** Play forward, then backward, without baking the reversed frames. */
28
+ PingPong: 2
29
+ };
30
+ var EndMode = {
31
+ /** Hold the last frame. */
32
+ Clamp: 0,
33
+ /** Return to the first frame. */
34
+ Rewind: 1
35
+ };
36
+ var INFINITE_REPETITIONS = -1;
37
+ var MAX_FADE_DURATION = 0.25;
38
+ function defaultRepetitions(loopMode) {
39
+ return loopMode === LoopMode.Repeat ? INFINITE_REPETITIONS : 1;
40
+ }
41
+ var LIBRARY_PLAYBACK_DEFAULTS = {
42
+ loopMode: LoopMode.Repeat,
43
+ repetitions: defaultRepetitions(LoopMode.Repeat),
44
+ endMode: EndMode.Clamp,
45
+ speed: 1
46
+ };
47
+ function resolvedPlaybackOf(instance) {
48
+ const { clip } = instance;
49
+ const clipLoopMode = clip.loopMode ?? LIBRARY_PLAYBACK_DEFAULTS.loopMode;
50
+ const loopMode = instance.loopMode ?? clipLoopMode;
51
+ const clipRepetitions = loopMode === clipLoopMode ? clip.repetitions : void 0;
52
+ return {
53
+ loopMode,
54
+ repetitions: instance.repetitions ?? clipRepetitions ?? defaultRepetitions(loopMode),
55
+ endMode: instance.endMode ?? clip.endMode ?? LIBRARY_PLAYBACK_DEFAULTS.endMode,
56
+ speed: instance.speed ?? clip.speed ?? LIBRARY_PLAYBACK_DEFAULTS.speed
57
+ };
58
+ }
59
+ function fadeOf(instance) {
60
+ const duration = Math.min(instance.fadeDuration ?? 0, MAX_FADE_DURATION);
61
+ const from = instance.from;
62
+ if (!from || duration <= 0 || from.frames <= 0) return null;
63
+ return { from, duration };
64
+ }
65
+ function fadeRowOf(from) {
66
+ return from.startFrame + Math.max(Math.min(Math.floor(from.phase * from.frames), from.frames - 1), 0);
67
+ }
68
+ function resolveVATFrame(instance, time) {
69
+ const { clip } = instance;
70
+ const frames = clip.frames;
71
+ const last = frames - 1;
72
+ const duration = frames / clip.fps;
73
+ const { loopMode, repetitions, endMode, speed } = resolvedPlaybackOf(instance);
74
+ const local = (time - instance.startTime) * speed;
75
+ const loops = local / duration;
76
+ const started = local >= 0;
77
+ const finished = started && repetitions !== INFINITE_REPETITIONS && loops >= repetitions;
78
+ let phase;
79
+ let wraps;
80
+ if (!started) {
81
+ phase = 0;
82
+ wraps = false;
83
+ } else if (finished) {
84
+ phase = endMode === EndMode.Clamp ? 1 : 0;
85
+ wraps = false;
86
+ } else if (loopMode === LoopMode.PingPong) {
87
+ const m = loops % 2;
88
+ phase = m < 1 ? m : 2 - m;
89
+ wraps = false;
90
+ } else {
91
+ phase = loops % 1;
92
+ wraps = true;
93
+ }
94
+ const f = phase * (wraps ? frames : last);
95
+ const f0 = Math.min(Math.floor(f), last);
96
+ const f1 = wraps ? (f0 + 1) % frames : Math.min(f0 + 1, last);
97
+ const row = clip.startFrame + f0;
98
+ const fading = fadeOf(instance);
99
+ const elapsed = fading ? (time - instance.startTime) / fading.duration : 0;
100
+ return {
101
+ row,
102
+ rowNext: clip.startFrame + f1,
103
+ mix: f - f0,
104
+ wraps,
105
+ finished,
106
+ phase,
107
+ fadeRow: fading ? fadeRowOf(fading.from) : row,
108
+ fadeWeight: fading ? 1 - Math.min(Math.max(elapsed, 0), 1) : 0
109
+ };
110
+ }
111
+ function createVATPlaybackTexture(instances) {
112
+ const count = instances.length;
113
+ if (count < 1) {
114
+ throw new Error(
115
+ "three-vat: a crowd needs at least one instance \u2014 a playback texture is one row per instance, and there is no zero-row texture to carry none"
116
+ );
117
+ }
118
+ if (count > MAX_TEXTURE_SIZE) {
119
+ throw new Error(
120
+ `three-vat: a crowd of ${count} instances needs ${count} rows of playback texture, past the ${MAX_TEXTURE_SIZE}-row ceiling \u2014 the playback texture holds one row per instance, so MAX_TEXTURE_SIZE is the instance ceiling.`
121
+ );
122
+ }
123
+ const data = new Float32Array(count * PACK_STRIDE);
124
+ for (let i = 0; i < count; i++) writePack(data, i, instances[i]);
125
+ return { texture: makeVATTexture(data, PACK_WIDTH, count), count };
126
+ }
127
+ var FORWARD_ONLY_REASON = "a baked band plays forward from its own first row, so a negative speed would freeze it on that row rather than run it backwards \u2014 bake a reversed clip instead. A speed of 0 is a held first row, and is fine";
128
+ var rowStart = (index) => index * PACK_STRIDE;
129
+ var texelStart = (index, field) => rowStart(index) + field * 4;
130
+ function writePack(data, index, instance) {
131
+ const clip = texelStart(index, PACK_TEXELS.clip);
132
+ const playback = texelStart(index, PACK_TEXELS.playback);
133
+ const fade = texelStart(index, PACK_TEXELS.fade);
134
+ const policy = resolvedPlaybackOf(instance);
135
+ if (policy.speed < 0) {
136
+ throw new Error(`three-vat: instance ${index} has speed ${policy.speed}; ${FORWARD_ONLY_REASON}.`);
137
+ }
138
+ data[clip] = instance.clip.startFrame;
139
+ data[clip + 1] = instance.clip.frames;
140
+ data[clip + 2] = instance.clip.fps;
141
+ data[clip + 3] = policy.speed;
142
+ data[playback] = instance.startTime;
143
+ data[playback + 1] = policy.loopMode;
144
+ data[playback + 2] = policy.repetitions;
145
+ data[playback + 3] = policy.endMode;
146
+ const fading = fadeOf(instance);
147
+ data[fade] = fading ? fading.from.startFrame : 0;
148
+ data[fade + 1] = fading ? fading.from.frames : 0;
149
+ data[fade + 2] = fading ? fading.from.phase : 0;
150
+ data[fade + 3] = fading ? fading.duration : 0;
151
+ }
152
+ function readPack(data, index) {
153
+ const clip = texelStart(index, PACK_TEXELS.clip);
154
+ const playback = texelStart(index, PACK_TEXELS.playback);
155
+ return {
156
+ clip: { startFrame: data[clip], frames: data[clip + 1], fps: data[clip + 2] },
157
+ startTime: data[playback],
158
+ speed: data[clip + 3],
159
+ loopMode: data[playback + 1],
160
+ repetitions: data[playback + 2],
161
+ endMode: data[playback + 3]
162
+ };
163
+ }
164
+ function assertInstance(playback, index) {
165
+ if (!Number.isInteger(index) || index < 0 || index >= playback.count) {
166
+ throw new Error(`three-vat: instance ${index} is outside this crowd of ${playback.count}`);
167
+ }
168
+ }
169
+ function setVATInstance(playback, index, instance) {
170
+ assertInstance(playback, index);
171
+ const data = playback.texture.image.data;
172
+ const fading = instance.from === void 0 && (instance.fadeDuration ?? 0) > 0 ? { ...instance, from: freezeOf(data, index, instance.startTime) } : instance;
173
+ writePack(data, index, fading);
174
+ playback.texture.addUpdateRange(rowStart(index), PACK_STRIDE);
175
+ playback.texture.needsUpdate = true;
176
+ }
177
+ function freezeOf(data, index, time) {
178
+ const outgoing = readPack(data, index);
179
+ const { row } = resolveVATFrame(outgoing, time);
180
+ return {
181
+ startFrame: outgoing.clip.startFrame,
182
+ frames: outgoing.clip.frames,
183
+ // The row the instance is actually displaying at that moment — so a fade
184
+ // out of a finished one-shot freezes the end pose it was holding, not the
185
+ // first row of a clip it stopped playing seconds ago.
186
+ //
187
+ // Named as the phase at the *centre* of that row rather than the playback
188
+ // phase itself, because {@link fadeRowOf} is what reads it back and the two
189
+ // do not spread a phase the same way: a bouncing ping-pong is up to a row
190
+ // apart between them, and rounding in a shader could cost another. Half a
191
+ // row of slack costs nothing and lands all three decodes on this row.
192
+ phase: (row - outgoing.clip.startFrame + 0.5) / outgoing.clip.frames
193
+ };
194
+ }
195
+ function endsAt(instance) {
196
+ const { repetitions, speed } = resolvedPlaybackOf(instance);
197
+ if (repetitions === INFINITE_REPETITIONS || speed <= 0) return null;
198
+ const duration = instance.clip.frames / instance.clip.fps;
199
+ return instance.startTime + duration * repetitions / speed;
200
+ }
201
+
202
+ export { EndMode, FORWARD_ONLY_REASON, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_FADE_DURATION, MAX_TEXTURE_SIZE, PACK_TEXELS, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance };