three-vat 2.1.0 → 4.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.
package/README.md CHANGED
@@ -47,7 +47,7 @@ const vat = bakeVAT(gltf.scene, gltf.animations, {
47
47
  })
48
48
 
49
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
50
+ // Only `clip` and `startTime` are required — a clip baked from a configured
51
51
  // `AnimationAction` carries its own loop, repetition count, end behaviour and
52
52
  // speed, and an instance overrides only what it wants to differ.
53
53
  const instances = Array.from({ length: 500 }, (_, i) => ({
@@ -78,13 +78,15 @@ Two things worth knowing the first time:
78
78
  - **Render `vat.geometry`, not your source mesh.** The merged vertex ordering is
79
79
  the baker's, and the textures are indexed by it. `createVATMesh` does this for
80
80
  you; by hand, clone that geometry and no other.
81
- - **Materials are never merged.** A 500-robot crowd with 3 materials is 3 draw
82
- calls — not 1, and not 500. VAT collapses instance count, not material count.
81
+ - **Materials are never merged unless you ask.** A 500-robot crowd with 3
82
+ materials is 3 draw calls — not 1, and not 500. `mergeFlatMaterials: true`
83
+ makes flat colours one material, and the crowd one draw call.
83
84
 
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).
85
+ **A skinned character?** The bake picks the rig encoding for it by itself: the
86
+ posed rig instead of the posed vertices, for two orders of magnitude less
87
+ texture, a bake in milliseconds, and no vertex ceiling for a phone's 4096 to
88
+ refuse. Assets a rig cannot express fall back to vertices, and `vat.fallback`
89
+ says why. [The rig encoding](./docs/usage.md#the-rig-encoding-encoding-rig).
88
90
 
89
91
  <details>
90
92
  <summary><b>Does it work with my model?</b></summary>
@@ -148,9 +150,9 @@ is [the CHANGELOG's 2.0.0 entry](./CHANGELOG.md).
148
150
  ```
149
151
 
150
152
  `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:
153
+ `aTimeOffset`. The pack moved into a **playback texture**, five texels a row
154
+ since 3.0 (clip, playback, crossfade, outgoing clip, outgoing playback), keyed
155
+ by the instance's logical index rather than the *drawn* slot. So
154
156
  `addInstancedVATAttributes` is removed, `createVATPlaybackTexture(instances)`
155
157
  replaces `addVATInstanceAttributes`, and `setVATInstance(playback, id, instance)`
156
158
  takes that texture — `createVATMesh` returns it as `playback` — rather than a
@@ -167,16 +169,18 @@ New, and none of it breaking: per-instance loop modes, one-shots,
167
169
  <details>
168
170
  <summary><b>Going bigger: texture limits, bake cost, Web Workers</b></summary>
169
171
 
170
- The VAT is one `vertexCount` × `totalFrames` texture pair, so both axes hit the
171
- GPU's texture ceiling. Always pass `maxTextureSize: getMaxTextureSize(renderer)`
172
+ A vertex-encoded VAT is one `vertexCount` × `totalFrames` texture pair, so both
173
+ axes hit the GPU's texture ceiling; a rig-encoded one is two texels a bone wide,
174
+ so in practice only its frames do. Always pass `maxTextureSize: getMaxTextureSize(renderer)`
172
175
  as above: the default is a desktop-shaped guess, and mobile is often 4096.
173
176
 
174
- The bake is CPU work done once at load — about 100 ms for the demo's robot, and
175
- seconds for a 20k-vertex skinned character with many clips. It never touches the
176
- renderer, so it moves into a Web Worker as-is.
177
+ The bake is CPU work done once at load. Under the vertex encoding that is about
178
+ 100 ms for the demo's robot, and seconds for a 20k-vertex skinned character with
179
+ many clips; the rig encoding bakes several to a few hundred times faster. It
180
+ never touches the renderer, so `bakeVATInWorker` runs it in a Web Worker as-is.
177
181
 
178
182
  **[docs/usage.md](./docs/usage.md)** has the measured bake-cost table, the
179
- worker recipe, the draw-call arithmetic, and the primitives underneath
183
+ worker bake, the draw-call arithmetic, and the primitives underneath
180
184
  `createVATMesh` for when you are not rendering onto a plain `InstancedMesh`.
181
185
 
182
186
  </details>
@@ -184,9 +188,8 @@ worker recipe, the draw-call arithmetic, and the primitives underneath
184
188
  <details>
185
189
  <summary><b>What it does not do</b></summary>
186
190
 
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
189
- format, no React/drei binding, glTF input only. Each is a decision rather than a
191
+ No LOD, no baking CLI or file format, no React/drei binding, glTF input
192
+ only. Each is a decision rather than a
190
193
  gap, and each is written up with its reasoning in
191
194
  **[docs/usage.md](./docs/usage.md#what-10-does-not-do)**, alongside the
192
195
  trade-offs against `SkinnedMesh` and bone-texture instancing.
@@ -40,44 +40,20 @@ type EndMode = (typeof EndMode)[keyof typeof EndMode];
40
40
  */
41
41
  declare const INFINITE_REPETITIONS = -1;
42
42
  /**
43
- * The longest fade {@link setVATInstance} will honour, in seconds.
43
+ * One clip playing: which band, from when, how fast and under what policy.
44
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.
45
+ * The unit the pack carries twice — as the animation an instance is playing,
46
+ * and as the one it is blending out of ({@link VATInstance.from}). Both are
47
+ * resolved by {@link resolveVATFrame} through the very same arithmetic, which
48
+ * is the whole difference between a crossfade and the pose freeze it replaced
49
+ * (ADR-0025): the outgoing half is a clip still *playing*, not a photograph.
74
50
  *
75
51
  * Every policy field is optional because the clip already answers it: a bake
76
52
  * handed a configured `AnimationAction` records the answer in the clip table
77
- * ({@link VATClipDefaults}), and an instance that says nothing inherits it. A
53
+ * ({@link VATClipDefaults}), and a state that says nothing inherits it. A
78
54
  * crowd of a thousand deaths says "once, clamped" once, at the bake.
79
55
  */
80
- interface VATInstance {
56
+ interface VATPlaybackState {
81
57
  /**
82
58
  * The clip band to play, straight out of `vat.clips`. Its playback defaults
83
59
  * come along with it; a clip table assembled by hand may carry none, and then
@@ -119,19 +95,33 @@ interface VATInstance {
119
95
  * `false`; see {@link EndMode}.
120
96
  */
121
97
  endMode?: EndMode;
98
+ }
99
+ /**
100
+ * Per-instance playback state consumed by both decode paths: the clip this
101
+ * instance is playing, and — while it is transitioning — the one it is
102
+ * crossfading out of.
103
+ */
104
+ interface VATInstance extends VATPlaybackState {
122
105
  /**
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}.
106
+ * The outgoing playback state to blend away from: a clip *still playing*, in
107
+ * every respect an instance except that it carries no transition of its own.
108
+ *
109
+ * Normally you do not write this yourself — {@link setVATInstance} reads the
110
+ * instance's current pack back, whole, and fills it in when you ask for a
111
+ * {@link fadeDuration}. Write it by hand when you are assembling a crowd the
112
+ * library does not build for you; what you pass is what is written.
126
113
  */
127
- from?: VATFadeFrom;
114
+ from?: VATPlaybackState;
128
115
  /**
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.
116
+ * Seconds to blend {@link from} away over. Uncapped, and wall-clock seconds
117
+ * from {@link startTime}: the incoming clip's `speed` does not stretch a
118
+ * transition.
119
+ *
120
+ * Zero, or absent, is a cut: no outgoing band is written. A negative or
121
+ * non-finite duration is refused when the instance is written.
132
122
  *
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.
123
+ * Ignored without a `from` to blend away from — and at creation there is
124
+ * nothing to blend away from, so this is `setVATInstance`'s field in practice.
135
125
  */
136
126
  fadeDuration?: number;
137
127
  }
@@ -151,7 +141,8 @@ interface VATFrame {
151
141
  * Whether {@link rowNext} crossed the clip's last row back into its first.
152
142
  * True only while a clip is genuinely looping: a ping-pong bounces rather
153
143
  * than wraps, and a finished one-shot must not wrap at all or the corpse
154
- * stands back up for a frame.
144
+ * stands back up for a frame — nor, across its final repetition, may any
145
+ * clip that ends by clamping (#88).
155
146
  */
156
147
  wraps: boolean;
157
148
  /** Whether the repetitions have run out and the instance is holding an end pose. */
@@ -159,26 +150,39 @@ interface VATFrame {
159
150
  /** How far through the clip this is, in `[0, 1]` — what {@link row} is derived from. */
160
151
  phase: number;
161
152
  /**
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.
153
+ * The band this instance is blending out of, resolved at the same moment —
154
+ * or `null` when it is not transitioning, which is almost always. Not a
155
+ * weight of zero, so a reader with no interest in transitions ignores one
156
+ * field rather than testing one.
166
157
  */
167
- fadeRow: number;
158
+ outgoing: VATOutgoingFrame | null;
159
+ }
160
+ /**
161
+ * The outgoing half of a crossfade: the very frame the outgoing clip would be
162
+ * showing if nothing had interrupted it, and how much of it is still showing.
163
+ *
164
+ * It is a {@link VATFrame} because it is one — resolved by
165
+ * {@link resolveVATFrame} from the outgoing playback state, through the same
166
+ * arithmetic, so an outgoing one-shot that runs out mid-transition clamps
167
+ * exactly as it would have. Its own `outgoing` is always `null`: the pack holds
168
+ * two bands, and the recursion is one level deep.
169
+ */
170
+ interface VATOutgoingFrame extends VATFrame {
168
171
  /**
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.
172
+ * How much of this band is still showing: `1` at the moment of the write,
173
+ * falling to `0` across `fadeDuration`, and `0` once the transition is over.
174
+ * Wall clock — `1 - clamp((time - startTime) / fadeDuration, 0, 1)` — so a
175
+ * half-speed incoming clip does not stretch the transition.
174
176
  */
175
- fadeWeight: number;
177
+ weight: number;
176
178
  }
177
179
  /**
178
180
  * What the vertex shader computes, as a pure function of `(instance, time)` —
179
181
  * 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
+ * transcribe it — its band half in `vatBand` (src/webgl.ts) and `resolveBand`
183
+ * (src/tsl.ts), one function of a clip and playback texel pair, *called twice*
184
+ * where an instance is transitioning; its crossfade weight beside the call, in
185
+ * `vatRows` and `vatDecode` — and neither invents it.
182
186
  *
183
187
  * It exists in TypeScript because the arithmetic is otherwise reachable only
184
188
  * inside a GLSL string and a TSL node graph, neither of which CI can evaluate
@@ -191,7 +195,7 @@ interface VATFrame {
191
195
  */
192
196
  declare function resolveVATFrame(instance: VATInstance, time: number): VATFrame;
193
197
  /**
194
- * What carries the pack to the shader: one `DataTexture`, three texels wide,
198
+ * What carries the pack to the shader: one `DataTexture`, five texels wide,
195
199
  * one row per instance, read by the instance's *logical* index (ADR-0016).
196
200
  *
197
201
  * Held by the caller rather than hidden behind the geometry, because a
@@ -207,24 +211,64 @@ interface VATPlaybackTexture {
207
211
  * float, {@link PACK_WIDTH} texels wide.
208
212
  */
209
213
  texture: DataTexture;
210
- /** Instances it carries — the rows of {@link texture}. */
214
+ /**
215
+ * Rows of {@link texture} — the crowd's **capacity**, which is what the
216
+ * decode indexes and what {@link setVATInstance} bounds-checks against.
217
+ *
218
+ * Not a live population: for a crowd that spawns and dies, most of these
219
+ * rows may be reserved and empty at any moment, and the library has no
220
+ * notion of which (ADR-0022). The caller owns the indices, because the
221
+ * carrier already hands that numbering out.
222
+ */
211
223
  count: number;
212
224
  }
225
+ /**
226
+ * How a playback texture is sized when the instances it is handed are not the
227
+ * whole story.
228
+ */
229
+ interface VATPlaybackTextureOptions {
230
+ /**
231
+ * Rows to reserve — the crowd's ceiling, rather than its current population.
232
+ * Defaults to the number of instances given, which is a crowd placed once.
233
+ *
234
+ * At least that many, and at most {@link maxTextureSize}; both are refused
235
+ * by name. Fixed once the texture is made (ADR-0022): a texture does not grow
236
+ * in place, and growing one means rebuilding it and rebinding it on every
237
+ * patched material — `docs/usage.md` carries that recipe.
238
+ */
239
+ capacity?: number;
240
+ /**
241
+ * The GPU's real texture ceiling, and so the crowd's: the playback texture
242
+ * is one row per instance. Pass `getMaxTextureSize(renderer)` from
243
+ * `three-vat/webgl` or `three-vat/tsl`. Defaults to `MAX_TEXTURE_SIZE`, a
244
+ * desktop figure: a phone reporting 4096 refuses a crowd of 5000 at upload,
245
+ * with nothing naming the cause, unless the limit is given here (ADR-0022).
246
+ */
247
+ maxTextureSize?: number;
248
+ }
213
249
  /**
214
250
  * Write a crowd's instance playback into a new playback texture. Call once,
215
251
  * before rendering, and bind the result into the decode — `createVATMesh` does
216
252
  * both for you.
217
253
  *
218
254
  * 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`.
255
+ * Both decode paths read exactly these texels of row `instanceIndex` —
256
+ * `ROW_PRELUDE` in `src/webgl.ts` with `texelFetch`, `texturePlayback` in
257
+ * `src/tsl.ts` with `textureLoad` — all five every frame, whether or not the
258
+ * instance is transitioning (see {@link PACK_TEXELS}).
259
+ *
260
+ * | Texel | r | g | b | a |
261
+ * | ------------------------- | -------------- | ----------- | ----------- | -------- |
262
+ * | `x = 0` clip | clip start row | clip frames | clip fps | speed |
263
+ * | `x = 1` playback | start time | loop mode | repetitions | end mode |
264
+ * | `x = 2` crossfade | fade duration | 0 | 0 | 0 |
265
+ * | `x = 3` outgoing clip | clip start row | clip frames | clip fps | speed |
266
+ * | `x = 4` outgoing playback | start time | loop mode | repetitions | end mode |
222
267
  *
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 |
268
+ * The outgoing pair is a full playback state — the same two texels, in the same
269
+ * order, with the same meaning — because that is the whole difference between a
270
+ * freeze and a crossfade (ADR-0025). The crossfade texel's three spare
271
+ * components are written as zero and read by nothing.
228
272
  *
229
273
  * **A texture, not three instanced attributes.** An attribute with divisor 1 is
230
274
  * indexed by the *drawn slot*, and the drawn slot stops being the instance the
@@ -233,25 +277,35 @@ interface VATPlaybackTexture {
233
277
  * (ADR-0016). A row keyed by the logical index is what three itself does for
234
278
  * the same problem, in `_matricesTexture`.
235
279
  *
236
- * **Three texels, not thirteen floats.** The move to a texture touched no
280
+ * **RGBA-shaped texels, not loose floats.** The move to a texture touched no
237
281
  * 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.
282
+ * already RGBA-shaped `vec4`s (ADR-0009). Published 1.x is the other story —
283
+ * five one-float attributes there, so a 1.x caller meets both changes at once.
241
284
  *
242
285
  * **`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.
286
+ * survive half precision — one second of resolution at 2 048 s — and there are
287
+ * now two of them per row, so a narrower encoding for the VAT textures does not
288
+ * reach this one.
245
289
  *
246
290
  * The policy fields, and the clip texel's speed, come from the instance where
247
291
  * it names them and from the clip's baked defaults where it does not — resolved
248
292
  * 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.
293
+ * {@link resolveVATFrame} defines them. The crossfade texel and the outgoing
294
+ * pair are written as zeroes, which is what "not transitioning" is: a crowd
295
+ * being created has no animation to blend away from. Transitions belong to
296
+ * {@link setVATInstance}, where an instance's animation changes and there is
297
+ * something to blend out of.
298
+ *
299
+ * **A crowd that spawns and dies gives a capacity** instead of a census
300
+ * ({@link VATPlaybackTextureOptions}, ADR-0022): the rows are reserved once,
301
+ * from the ceiling, and filled with {@link setVATInstance} as instances appear.
302
+ * The list may then be empty — a level that starts with nothing alive in it —
303
+ * and the reserved rows hold a first frame, held. The library allocates no
304
+ * indices and follows no `setInstanceCount`: the carrier already numbers the
305
+ * instances, and a texture does not grow in place. `docs/usage.md` carries both,
306
+ * with **row recycling** — the hazard of reusing a row an instance has died on.
253
307
  */
254
- declare function createVATPlaybackTexture(instances: VATInstance[]): VATPlaybackTexture;
308
+ declare function createVATPlaybackTexture(instances: VATInstance[], options?: VATPlaybackTextureOptions): VATPlaybackTexture;
255
309
  /**
256
310
  * Change one instance's animation, after the crowd is built. The single write
257
311
  * the whole event-driven half of this library is made of: an enemy hit at
@@ -274,15 +328,21 @@ declare function createVATPlaybackTexture(instances: VATInstance[]): VATPlayback
274
328
  * one small write rather than a full re-upload. Everything else about the
275
329
  * instance — its matrix, its clip's defaults — is untouched. On the TSL path
276
330
  * 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
331
+ * whole image on `needsUpdate`, which is 80 bytes per instance once per frame
278
332
  * in which anything changed (docs/usage.md says what that costs).
279
333
  *
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.
334
+ * Ask for a `fadeDuration` and the animation the instance was playing **keeps
335
+ * playing**, blended away over that many wall-clock seconds from `startTime`,
336
+ * so the change is a transition rather than a pop (ADR-0025). Uncapped: a tenth
337
+ * of a second for a death, half a second for a walk into a run, and both clips
338
+ * move throughout. Zero, or none at all, is a cut.
339
+ *
340
+ * Two bands, and no more. Writing an instance that is *already* mid-transition
341
+ * replaces the outgoing band with the one it was switching to and drops the
342
+ * older band at whatever weight it still had — a pop proportional to how early
343
+ * the interruption came, and the one visible discontinuity a caller can
344
+ * produce. `startTime + fadeDuration` is when the transition ends, for a caller
345
+ * who would rather wait it out.
286
346
  *
287
347
  * A written instance is a pure function of the clock from here on, so what
288
348
  * happens *after* it is a matter of scheduling one more of these writes —
@@ -394,9 +454,13 @@ interface VATClip extends VATClipDefaults {
394
454
  * `geometry.groups[].materialIndex`, giving one draw call per material.
395
455
  *
396
456
  * 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.
457
+ * not part of this contract, and it is not the same array on every layer: the
458
+ * position texture holds a `Uint16Array` of half-floats (#73), the rig texture
459
+ * a `Float32Array`, the normal texture a `Uint8Array` of octahedral pairs
460
+ * (#29), and a narrower encoding may change any of them again in a minor
461
+ * release. Move the buffer, hand it back to the builder for that layer —
462
+ * {@link makeVATTexture} or {@link makeVATNormalTexture} — and do not read
463
+ * numbers out of it.
400
464
  */
401
465
  interface VATBase {
402
466
  /**
@@ -422,22 +486,43 @@ interface VATBase {
422
486
  /**
423
487
  * A VAT under the **vertex encoding**: a row holds where every vertex ended up,
424
488
  * 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).
489
+ * source-agnostic encoding (ADR-0008), the member every bake produced before
490
+ * there was a second one (ADR-0018), and what the default falls back to where
491
+ * the rig encoding refuses an asset (ADR-0027).
427
492
  */
428
493
  interface DeltaVAT extends VATBase {
429
494
  /** Which encoding a row holds — the discriminant of {@link VAT}. */
430
495
  encoding: 'delta';
431
- /** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
496
+ /**
497
+ * `RGBA16F` texture of per-vertex position deltas (`x = vertex`, `y = frame`),
498
+ * eight bytes a texel — half of what RGBA float cost, for 0.061% of the delta
499
+ * and nothing at all at the rest pose (#73, ADR-0002's amendment). A
500
+ * half-float sampler hands the shader floats, so neither decode unpacks
501
+ * anything; a bake whose delta would pass half-float's 65 504 ceiling is
502
+ * refused rather than clipped.
503
+ */
432
504
  positionTexture: DataTexture;
433
505
  /**
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.
506
+ * `RG8` texture of per-vertex absolute normals (`x = vertex`, `y = frame`),
507
+ * each texel an octahedral unit vector in two unsigned bytes — a quarter of
508
+ * the position texel beside it, for ~0.95° of angular error (#29,
509
+ * `src/octahedral.ts`).
510
+ * Decode it with `decodeOctahedral`; both shaders do.
511
+ *
512
+ * `null` when the bake was told to skip it (`bakeNormals: false`) — dropping
513
+ * the layer for a crowd that never reads a normal. Neither decode path
514
+ * samples it when it is absent; a smooth-shaded lit material paired with such
515
+ * a VAT is refused rather than lit by its rest pose.
439
516
  */
440
517
  normalTexture: DataTexture | null;
518
+ /**
519
+ * Why the default encoding fell back to this one (ADR-0029): the message of
520
+ * the rig encoding's refusal, naming what the rig could not store and where
521
+ * — an animated morph, its clip and its part, say. `null` where the vertex
522
+ * encoding was asked for by name (`encoding: 'delta'`). The bake prints
523
+ * nothing on a fallback; this is where the reason is kept.
524
+ */
525
+ fallback: string | null;
441
526
  }
442
527
  /**
443
528
  * A VAT under the **rig encoding** (ADR-0018): a row holds the posed rig — one
@@ -512,4 +597,4 @@ interface VATCrowd {
512
597
  */
513
598
  type VATCarrier = InstancedMesh | BatchedMesh;
514
599
 
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 };
600
+ export { type DeltaVAT as D, EndMode as E, INFINITE_REPETITIONS as I, LoopMode as L, 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 VATFrame as g, type VATInstance as h, type VATOutgoingFrame as i, type VATPlaybackState as j, type VATPlaybackTexture as k, type VATPlaybackTextureOptions as l, createVATPlaybackTexture as m, endsAt as n, resolveVATFrame as r, setVATInstance as s };