three-vat 1.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,7 +16,7 @@ Bake a glTF `AnimationClip` into GPU textures and animate **hundreds or thousand
16
16
  npm install three-vat three
17
17
  ```
18
18
 
19
- `three` (>= 0.185) is a peer dependency.
19
+ `three` (>= 0.186) is a peer dependency.
20
20
 
21
21
  ## One crowd, start to finish
22
22
 
@@ -46,10 +46,13 @@ const vat = bakeVAT(gltf.scene, gltf.animations, {
46
46
  maxTextureSize: getMaxTextureSize(renderer), // this GPU's real ceiling
47
47
  })
48
48
 
49
- // One entry per character: which clip it plays, its phase, its rate.
49
+ // One entry per character: which clip it plays, when it started, its rate.
50
+ // Everything but `startTime` is optional — a clip baked from a configured
51
+ // `AnimationAction` carries its own loop, repetition count, end behaviour and
52
+ // speed, and an instance overrides only what it wants to differ.
50
53
  const instances = Array.from({ length: 500 }, (_, i) => ({
51
54
  clip: vat.clips[i % vat.clips.length],
52
- timeOffset: Math.random() * 2, // desync, so the crowd is not in lockstep
55
+ startTime: -Math.random() * 2, // began a moment ago, so the crowd is not in lockstep
53
56
  speed: 0.9 + Math.random() * 0.2,
54
57
  }))
55
58
 
@@ -78,6 +81,11 @@ Two things worth knowing the first time:
78
81
  - **Materials are never merged.** A 500-robot crowd with 3 materials is 3 draw
79
82
  calls — not 1, and not 500. VAT collapses instance count, not material count.
80
83
 
84
+ **A skinned character?** There is a second encoding: `{ encoding: 'rig' }` bakes
85
+ the posed rig instead of the posed vertices — two orders of magnitude less
86
+ texture, a bake in milliseconds, and faster on a phone, for an asset a rig can
87
+ express. [The rig encoding](./docs/usage.md#the-rig-encoding-encoding-rig).
88
+
81
89
  <details>
82
90
  <summary><b>Does it work with my model?</b></summary>
83
91
 
@@ -97,6 +105,65 @@ of `gltf.animations` rather than spending texture rows on them.
97
105
 
98
106
  </details>
99
107
 
108
+ <details>
109
+ <summary><b>Per-clip playback defaults</b></summary>
110
+
111
+ `bakeVAT` takes an `AnimationClip` **or** an `AnimationAction`, in the same
112
+ array. Configure the action the way three already taught you, and every instance
113
+ of that clip inherits it — and overrides any field it names.
114
+
115
+ ```ts
116
+ const death = mixer.clipAction(deathClip)
117
+ death.loop = THREE.LoopOnce
118
+
119
+ const vat = bakeVAT(gltf.scene, [walkClip, death, idleClip])
120
+ ```
121
+
122
+ `loop`, `repetitions` and `timeScale` are read; `time` and `paused` are ignored,
123
+ because a VAT has no playhead of its own to seed; a non-unit `weight` or an
124
+ additive `blendMode` throws, because one baked band cannot be several actions
125
+ blended at once.
126
+
127
+ **Both inputs clamp, where three rewinds.** `clampWhenFinished` defaults to
128
+ `false` in three, which means an untouched action says nothing about the end
129
+ either — and a crowd's answer to nothing is to hold the last frame, because a
130
+ corpse standing back up is the worse default. So the end mode is not read off
131
+ the action; an instance asks for three's rewind with `endMode: EndMode.Rewind`,
132
+ which is the finer grain anyway.
133
+
134
+ [Declaring the defaults at the bake](./docs/usage.md#declaring-the-defaults-at-the-bake).
135
+
136
+ </details>
137
+
138
+ <details>
139
+ <summary><b>Upgrading from 1.x</b></summary>
140
+
141
+ The breaks that reach a 1.x caller, no shims — the package had no users on the
142
+ 1.x playback contract, so 2.0 spells it one way rather than two. The full list
143
+ is [the CHANGELOG's 2.0.0 entry](./CHANGELOG.md).
144
+
145
+ ```ts
146
+ { clip, timeOffset: 1.4, speed: 1 } // 1.x — every field required
147
+ { clip, startTime: -1.4 } // 2.0 — desync is a start time in the past
148
+ ```
149
+
150
+ `timeOffset` is gone (`startTime: -timeOffset / speed`), and with it
151
+ `aTimeOffset`. The pack is three `vec4`s — clip, playback, fade — in a
152
+ **playback texture** keyed by the instance's logical index, not instanced
153
+ attributes, which are indexed by the *drawn* slot:
154
+ `addInstancedVATAttributes` is removed, `createVATPlaybackTexture(instances)`
155
+ replaces `addVATInstanceAttributes`, and `setVATInstance(playback, id, instance)`
156
+ takes that texture — `createVATMesh` returns it as `playback` — rather than a
157
+ geometry. A VAT texture's `image.data` is now opaque. Node 20, where
158
+ 1.x said 18, and `three >= 0.186`, where `batchIndirectIndex` is exported;
159
+ browsers are unaffected.
160
+
161
+ New, and none of it breaking: per-instance loop modes, one-shots,
162
+ `setVATInstance` after the crowd is built, and a crowd on a `BatchedMesh`.
163
+ [By hand, on either path](./docs/usage.md#by-hand-on-either-path).
164
+
165
+ </details>
166
+
100
167
  <details>
101
168
  <summary><b>Going bigger: texture limits, bake cost, Web Workers</b></summary>
102
169
 
@@ -117,7 +184,8 @@ worker recipe, the draw-call arithmetic, and the primitives underneath
117
184
  <details>
118
185
  <summary><b>What it does not do</b></summary>
119
186
 
120
- No clip crossfade (instances cut between clips), no LOD, no baking CLI or file
187
+ No clip crossfade (an instance blends out of a frozen pose, not between two
188
+ clips that are both playing), no LOD, no baking CLI or file
121
189
  format, no React/drei binding, glTF input only. Each is a decision rather than a
122
190
  gap, and each is written up with its reasoning in
123
191
  **[docs/usage.md](./docs/usage.md#what-10-does-not-do)**, alongside the
@@ -0,0 +1,515 @@
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 displacement (metres) of any vertex from the rest pose across the
378
+ * clip — the same number under either encoding, because the rig bake skins
379
+ * every vertex on the CPU for the bounds anyway and measures it there.
380
+ * Near-zero means the clip baked as a frozen pose — the diagnostic for a
381
+ * mis-targeted or genuinely static clip.
382
+ */
383
+ maxDelta: number;
384
+ }
385
+ /**
386
+ * A baked Vertex Animation Texture: the position `DataTexture` (and the normal
387
+ * one, unless the bake was told to skip it), the geometry they are indexed by,
388
+ * and the clip table and bounds needed to decode and render them. Produced
389
+ * exactly one way — {@link bakeVAT}, at runtime, from a loaded glTF (ADR-0010).
390
+ *
391
+ * The merged vertex ordering is the baker's own invention and the textures are
392
+ * indexed by it (`x = gl_VertexID`), so the caller cannot bring its own
393
+ * geometry — it must render the one baked here. `materials` is ordered to match
394
+ * `geometry.groups[].materialIndex`, giving one draw call per material.
395
+ *
396
+ * The typed array behind either texture's `image.data` is the bake's choice,
397
+ * not part of this contract: `Float32Array` today, and a narrower encoding may
398
+ * change it in a minor release. Move the buffer, hand it to {@link makeVATTexture};
399
+ * do not read numbers out of it.
400
+ */
401
+ interface VATBase {
402
+ /**
403
+ * Merged rest-pose geometry, in whichever space the encoding stores it: root
404
+ * space under the vertex encoding, where its `position` is the delta
405
+ * reference; each part's own local space under the rig encoding, where the
406
+ * slot carries the placement and also keeps `skinIndex` and `skinWeight`.
407
+ * Carries `normal` always, and `uv`, `color` and `tangent` when every source
408
+ * mesh carried them.
409
+ */
410
+ geometry: BufferGeometry;
411
+ /** Source materials, indexed by `geometry.groups[].materialIndex`. */
412
+ materials: Material[];
413
+ /** Clip table: name → `{ startFrame, frames, fps, ... }`. */
414
+ clips: VATClip[];
415
+ /** Union of every baked frame's bounds; use as the geometry bounding box. */
416
+ bounds: Box3;
417
+ /** Vertex count (texture width). */
418
+ vertexCount: number;
419
+ /** Total frame rows across all clips (texture height). */
420
+ totalFrames: number;
421
+ }
422
+ /**
423
+ * A VAT under the **vertex encoding**: a row holds where every vertex ended up,
424
+ * as a position delta and, unless the bake was told to skip it, a normal. The
425
+ * source-agnostic encoding (ADR-0008) and the default; the member every bake
426
+ * produced before there was a second one (ADR-0018).
427
+ */
428
+ interface DeltaVAT extends VATBase {
429
+ /** Which encoding a row holds — the discriminant of {@link VAT}. */
430
+ encoding: 'delta';
431
+ /** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
432
+ positionTexture: DataTexture;
433
+ /**
434
+ * RGBA float texture of per-vertex absolute normals (`x = vertex`, `y = frame`),
435
+ * or `null` when the bake was told to skip it (`bakeNormals: false`) — halving
436
+ * the VAT for a crowd that never reads a normal. Neither decode path samples
437
+ * it when it is absent; a smooth-shaded lit material paired with such a VAT is
438
+ * refused rather than lit by its rest pose.
439
+ */
440
+ normalTexture: DataTexture | null;
441
+ }
442
+ /**
443
+ * A VAT under the **rig encoding** (ADR-0018): a row holds the posed rig — one
444
+ * **slot** per bone, as a rotation, a translation and a uniform scale — and the
445
+ * vertex shader skins the rest-pose geometry from it. Two orders of magnitude
446
+ * smaller than the vertex encoding and bake-cheap, at the price of source
447
+ * agnosticism: what a rig cannot express is refused at the bake.
448
+ *
449
+ * No normal texture, and none missing: normals and tangents come out of the
450
+ * skin matrix, as in three's own skinning.
451
+ */
452
+ interface RigVAT extends VATBase {
453
+ /** Which encoding a row holds — the discriminant of {@link VAT}. */
454
+ encoding: 'rig';
455
+ /**
456
+ * RGBA float **rig texture**: two texels per slot — `(qx, qy, qz, qw)` then
457
+ * `(tx, ty, tz, s)` — laid out as `x = slot × 2 + texel`, `y = frame`, clips
458
+ * stacked as bands exactly as in the position texture. Consecutive rows of
459
+ * one slot sit on one quaternion hemisphere; the decode still flips the
460
+ * second row onto the first's side, because a looping clip blends a band's
461
+ * last row into its first and those are not neighbours.
462
+ */
463
+ rigTexture: DataTexture;
464
+ /** Slots in the rig — the texture is `slotCount × 2` texels wide. */
465
+ slotCount: number;
466
+ }
467
+ /**
468
+ * A baked VAT, discriminated on `encoding`. A decode that samples a texture
469
+ * narrows on `encoding` first, so an encoding it does not decode is refused by
470
+ * name rather than sampled as a texture the VAT does not have (ADR-0018).
471
+ */
472
+ type VAT = DeltaVAT | RigVAT;
473
+ /**
474
+ * The shared playback clock: one `{ value }` in seconds, read by every material
475
+ * of every VAT mesh driven by it. Set it once per frame. Deliberately the
476
+ * narrowest shape both decode paths satisfy — a WebGL `IUniform<number>` and a
477
+ * TSL uniform node are both one of these — so `createVATMesh` returns the same
478
+ * thing on either renderer.
479
+ */
480
+ interface VATClock {
481
+ value: number;
482
+ }
483
+ /**
484
+ * A **crowd** ready to render: the mesh to add to the scene, and the clock to
485
+ * advance. What `createVATMesh` returns on either decode path, so moving a
486
+ * crowd between renderers is an import change and nothing else. Named for what
487
+ * it is rather than for its `mesh` field — the clock is half of it.
488
+ */
489
+ interface VATCrowd {
490
+ /** Add to the scene. Its instance matrices are yours to write. */
491
+ mesh: InstancedMesh;
492
+ /** The shared playback clock — set `.value` once per frame. */
493
+ time: VATClock;
494
+ /**
495
+ * The crowd's **playback texture**: what carries each instance's clip, phase
496
+ * and policy to the shader, and what `setVATInstance` writes one row of
497
+ * (ADR-0016). Exposed rather than hidden behind the mesh because it is the
498
+ * object a caller has to hold to change an instance after the crowd is
499
+ * built — a `BufferGeometry` cannot carry a texture, so there is nowhere
500
+ * else honest to keep it.
501
+ */
502
+ playback: VATPlaybackTexture;
503
+ }
504
+
505
+ /**
506
+ * A mesh a VAT crowd can ride.
507
+ *
508
+ * `InstancedMesh` is what `createVATMesh` builds on either path.
509
+ * `BatchedMesh` is reached through the primitives, and buys per-instance
510
+ * frustum culling and depth sorting from three.js itself — see
511
+ * docs/usage.md, which also says what it does *not* buy.
512
+ */
513
+ type VATCarrier = InstancedMesh | BatchedMesh;
514
+
515
+ export { type DeltaVAT as D, EndMode as E, INFINITE_REPETITIONS as I, LoopMode as L, MAX_FADE_DURATION as M, type RigVAT as R, type VAT as V, type VATBase as a, type VATCarrier as b, type VATClip as c, type VATClipDefaults as d, type VATClock as e, type VATCrowd as f, type VATFadeFrom as g, type VATFrame as h, type VATInstance as i, type VATPlaybackTexture as j, createVATPlaybackTexture as k, endsAt as l, resolveVATFrame as r, setVATInstance as s };