three-vat 3.0.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 became 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>
@@ -141,7 +141,8 @@ interface VATFrame {
141
141
  * Whether {@link rowNext} crossed the clip's last row back into its first.
142
142
  * True only while a clip is genuinely looping: a ping-pong bounces rather
143
143
  * than wraps, and a finished one-shot must not wrap at all or the corpse
144
- * 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).
145
146
  */
146
147
  wraps: boolean;
147
148
  /** Whether the repetitions have run out and the instance is holding an end pose. */
@@ -230,12 +231,20 @@ interface VATPlaybackTextureOptions {
230
231
  * Rows to reserve — the crowd's ceiling, rather than its current population.
231
232
  * Defaults to the number of instances given, which is a crowd placed once.
232
233
  *
233
- * At least that many, and at most `MAX_TEXTURE_SIZE`; both are refused by
234
- * name. Fixed once the texture is made (ADR-0022): a texture does not grow
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
235
236
  * in place, and growing one means rebuilding it and rebinding it on every
236
237
  * patched material — `docs/usage.md` carries that recipe.
237
238
  */
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;
239
248
  }
240
249
  /**
241
250
  * Write a crowd's instance playback into a new playback texture. Call once,
@@ -245,8 +254,8 @@ interface VATPlaybackTextureOptions {
245
254
  * The layout below is the shared contract, spelled once in {@link PACK_TEXELS}.
246
255
  * Both decode paths read exactly these texels of row `instanceIndex` —
247
256
  * `ROW_PRELUDE` in `src/webgl.ts` with `texelFetch`, `texturePlayback` in
248
- * `src/tsl.ts` with `textureLoad` — the first three always, the last two only
249
- * while a transition is running.
257
+ * `src/tsl.ts` with `textureLoad` — all five every frame, whether or not the
258
+ * instance is transitioning (see {@link PACK_TEXELS}).
250
259
  *
251
260
  * | Texel | r | g | b | a |
252
261
  * | ------------------------- | -------------- | ----------- | ----------- | -------- |
@@ -445,9 +454,13 @@ interface VATClip extends VATClipDefaults {
445
454
  * `geometry.groups[].materialIndex`, giving one draw call per material.
446
455
  *
447
456
  * The typed array behind either texture's `image.data` is the bake's choice,
448
- * not part of this contract: `Float32Array` today, and a narrower encoding may
449
- * change it in a minor release. Move the buffer, hand it to {@link makeVATTexture};
450
- * 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.
451
464
  */
452
465
  interface VATBase {
453
466
  /**
@@ -473,22 +486,43 @@ interface VATBase {
473
486
  /**
474
487
  * A VAT under the **vertex encoding**: a row holds where every vertex ended up,
475
488
  * as a position delta and, unless the bake was told to skip it, a normal. The
476
- * source-agnostic encoding (ADR-0008) and the default; the member every bake
477
- * 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).
478
492
  */
479
493
  interface DeltaVAT extends VATBase {
480
494
  /** Which encoding a row holds — the discriminant of {@link VAT}. */
481
495
  encoding: 'delta';
482
- /** 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
+ */
483
504
  positionTexture: DataTexture;
484
505
  /**
485
- * RGBA float texture of per-vertex absolute normals (`x = vertex`, `y = frame`),
486
- * or `null` when the bake was told to skip it (`bakeNormals: false`) — halving
487
- * the VAT for a crowd that never reads a normal. Neither decode path samples
488
- * it when it is absent; a smooth-shaded lit material paired with such a VAT is
489
- * 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.
490
516
  */
491
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;
492
526
  }
493
527
  /**
494
528
  * A VAT under the **rig encoding** (ADR-0018): a row holds the posed rig — one
@@ -1,15 +1,30 @@
1
- import { FloatType, DataTexture, RGBAFormat, NearestFilter } from 'three';
1
+ import { FloatType, DataTexture, RGBAFormat, RGFormat, UnsignedByteType, HalfFloatType, NearestFilter } from 'three';
2
2
 
3
3
  // src/vat-texture.ts
4
4
  var MAX_TEXTURE_SIZE = 16384;
5
- function makeVATTexture(data, width, height, type = FloatType) {
6
- const tex = new DataTexture(data, width, height, RGBAFormat, type);
5
+ var HALF_FLOAT_MAX = 65504;
6
+ function sampledExactly(tex) {
7
7
  tex.minFilter = NearestFilter;
8
8
  tex.magFilter = NearestFilter;
9
9
  tex.generateMipmaps = false;
10
10
  tex.needsUpdate = true;
11
11
  return tex;
12
12
  }
13
+ function makeVATTexture(data, width, height, type = FloatType) {
14
+ const half = type === HalfFloatType;
15
+ const wanted = half ? Uint16Array : Float32Array;
16
+ if (!(data instanceof wanted)) {
17
+ throw new Error(
18
+ `three-vat: a ${half ? "HalfFloatType" : "FloatType"} texture is built from a ${wanted.name}, not a ${data.constructor.name} \u2014 the array and the type describe the same texels and have to agree`
19
+ );
20
+ }
21
+ return sampledExactly(new DataTexture(data, width, height, RGBAFormat, type));
22
+ }
23
+ function makeVATNormalTexture(data, width, height) {
24
+ const tex = new DataTexture(data, width, height, RGFormat, UnsignedByteType);
25
+ tex.unpackAlignment = 1;
26
+ return sampledExactly(tex);
27
+ }
13
28
 
14
29
  // src/instance-playback.ts
15
30
  var PACK_TEXELS = {
@@ -74,24 +89,27 @@ function resolveVATFrame(instance, time) {
74
89
  const started = local >= 0;
75
90
  const finished = started && repetitions !== INFINITE_REPETITIONS && loops >= repetitions;
76
91
  let phase;
77
- let wraps;
92
+ let looping;
78
93
  if (!started) {
79
94
  phase = 0;
80
- wraps = false;
95
+ looping = false;
81
96
  } else if (finished) {
82
97
  phase = endMode === EndMode.Clamp ? 1 : 0;
83
- wraps = false;
98
+ looping = false;
84
99
  } else if (loopMode === LoopMode.PingPong) {
85
- const m = loops % 2;
100
+ const m = loops - 2 * Math.floor(loops * 0.5);
86
101
  phase = m < 1 ? m : 2 - m;
87
- wraps = false;
102
+ looping = false;
88
103
  } else {
89
104
  phase = loops % 1;
90
- wraps = true;
105
+ looping = true;
91
106
  }
92
- const f = phase * (wraps ? frames : last);
107
+ const holds = looping && endMode === EndMode.Clamp && repetitions !== INFINITE_REPETITIONS && Math.floor(loops) + 1 >= repetitions;
108
+ const wraps = looping && !holds;
109
+ const f = phase * (looping ? frames : last);
93
110
  const f0 = Math.min(Math.floor(f), last);
94
- const f1 = wraps ? (f0 + 1) % frames : Math.min(f0 + 1, last);
111
+ const next = f0 + 1;
112
+ const f1 = wraps ? next >= frames ? 0 : next : Math.min(next, last);
95
113
  const row = clip.startFrame + f0;
96
114
  const crossfade = crossfadeOf(instance);
97
115
  const elapsed = crossfade ? (time - instance.startTime) / crossfade.duration : 0;
@@ -123,9 +141,14 @@ function createVATPlaybackTexture(instances, options = {}) {
123
141
  "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. Pass a capacity to reserve rows for a crowd that has not spawned yet"
124
142
  );
125
143
  }
126
- if (count > MAX_TEXTURE_SIZE) {
144
+ const ceiling = options.maxTextureSize ?? MAX_TEXTURE_SIZE;
145
+ if (count > ceiling) {
146
+ const [source, hint] = options.maxTextureSize === void 0 ? [
147
+ "MAX_TEXTURE_SIZE, the default when no maxTextureSize option is given",
148
+ " Pass getMaxTextureSize(renderer) as maxTextureSize to check against this GPU's own limit."
149
+ ] : ["the maxTextureSize option", ""];
127
150
  throw new Error(
128
- `three-vat: ${count} rows of playback texture is past the ${MAX_TEXTURE_SIZE}-row ceiling \u2014 the playback texture holds one row per instance, so MAX_TEXTURE_SIZE is the instance ceiling.`
151
+ `three-vat: ${count} rows of playback texture is past the ${ceiling}-row ceiling of ${source} \u2014 the playback texture holds one row per instance, so the texture ceiling is the instance ceiling.` + hint
129
152
  );
130
153
  }
131
154
  const data = new Float32Array(count * PACK_STRIDE);
@@ -223,4 +246,4 @@ var RIG_TEXELS = {
223
246
  };
224
247
  var RIG_TEXELS_PER_SLOT = 2;
225
248
 
226
- export { EndMode, FORWARD_ONLY_REASON, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_TEXTURE_SIZE, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance };
249
+ export { EndMode, FORWARD_ONLY_REASON, HALF_FLOAT_MAX, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_TEXTURE_SIZE, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
- import { AnimationClip, AnimationAction, Object3D, TypedArray, TextureDataType, DataTexture } from 'three';
2
- import { D as DeltaVAT, R as RigVAT, V as VAT } from './carrier-BxRuW47G.js';
3
- export { E as EndMode, I as INFINITE_REPETITIONS, L as LoopMode, a as VATBase, b as VATCarrier, c as VATClip, d as VATClipDefaults, e as VATClock, f as VATCrowd, g as VATFrame, h as VATInstance, i as VATOutgoingFrame, j as VATPlaybackState, k as VATPlaybackTexture, l as VATPlaybackTextureOptions, m as createVATPlaybackTexture, n as endsAt, r as resolveVATFrame, s as setVATInstance } from './carrier-BxRuW47G.js';
1
+ import { AnimationClip, AnimationAction, Object3D, DataTexture, FloatType, HalfFloatType } from 'three';
2
+ import { D as DeltaVAT, R as RigVAT, V as VAT } from './carrier-D4pkftSk.js';
3
+ export { E as EndMode, I as INFINITE_REPETITIONS, L as LoopMode, a as VATBase, b as VATCarrier, c as VATClip, d as VATClipDefaults, e as VATClock, f as VATCrowd, g as VATFrame, h as VATInstance, i as VATOutgoingFrame, j as VATPlaybackState, k as VATPlaybackTexture, l as VATPlaybackTextureOptions, m as createVATPlaybackTexture, n as endsAt, r as resolveVATFrame, s as setVATInstance } from './carrier-D4pkftSk.js';
4
4
 
5
5
  /**
6
6
  * What {@link bakeVAT} takes for each animation: the clip itself, or an
@@ -24,8 +24,8 @@ interface BakeOptions {
24
24
  /**
25
25
  * Bake the normal texture. Default `true`.
26
26
  *
27
- * Turning it off halves the VAT — `verts x frames x 16 B x 2` becomes `x 1` —
28
- * and is correct for exactly two material setups:
27
+ * Turning it off drops the normal layer — `verts x frames x (8 B + 2 B)`
28
+ * becomes `x 8 B` — and is correct for exactly two material setups:
29
29
  *
30
30
  * - **Unlit** (`MeshBasicMaterial`, and its node twin), which never reads a
31
31
  * normal, so the texture was pure waste.
@@ -44,11 +44,16 @@ interface BakeOptions {
44
44
  */
45
45
  bakeNormals?: boolean;
46
46
  /**
47
- * What a frame row holds (ADR-0018). Default `'delta'`.
47
+ * What a frame row holds (ADR-0018). Default `'auto'` (ADR-0027).
48
48
  *
49
+ * - **`'auto'`** — the rig encoding where the asset allows it, and the
50
+ * vertex encoding where the rig encoding refuses it. Read `vat.encoding` to
51
+ * learn which one a bake chose. The default since 4.0: the rig texture is
52
+ * two orders of magnitude smaller, bakes in milliseconds, and decodes
53
+ * faster where memory bandwidth is scarce, as on a phone.
49
54
  * - **`'delta'`** — the vertex encoding: where every vertex ended up, as a
50
55
  * position delta and a normal. Source-agnostic — skinning, morph targets
51
- * and node animation alike — and the default.
56
+ * and node animation alike.
52
57
  * - **`'rig'`** — the rig encoding: the posed rig, one **slot** per bone as a
53
58
  * rotation, a translation and a uniform scale, skinned in the vertex shader
54
59
  * from the rest-pose geometry. Two orders of magnitude smaller, no vertex
@@ -59,7 +64,24 @@ interface BakeOptions {
59
64
  * node-animated part is one slot of weight one; parts reading the same
60
65
  * bones through the same bind matrix share slots.
61
66
  */
62
- encoding?: 'delta' | 'rig';
67
+ encoding?: 'auto' | 'delta' | 'rig';
68
+ /**
69
+ * Collapse materials that differ only in their flat colour into one, so the
70
+ * crowd draws once instead of once per material (ADR-0028). Default `false`.
71
+ *
72
+ * A material is **flat** when it has a `color`, no texture map of any kind,
73
+ * and `vertexColors` off. Two or more flat materials that agree on every
74
+ * other property — type, roughness, metalness, emissive, side, opacity and
75
+ * the rest — become one clone of the first, white, with `vertexColors` on,
76
+ * and each part's colour moves into the merged geometry's `color` attribute.
77
+ * A material that is not flat, or is the only one of its kind, is left
78
+ * exactly as it was. `vat.materials` therefore holds a material you did not
79
+ * create wherever a merge happened.
80
+ *
81
+ * Off by default because it is only ever right for flat-shaded assets: a
82
+ * textured character has nothing to collapse, and merging never guesses.
83
+ */
84
+ mergeFlatMaterials?: boolean;
63
85
  }
64
86
  /**
65
87
  * Bake animations into a VAT by sampling the posed subtree frame by frame on
@@ -111,16 +133,17 @@ interface BakeOptions {
111
133
  * Renderer-agnostic — touches no WebGL/WebGPU context — so it runs identically
112
134
  * at runtime, in a Web Worker, and in Node.
113
135
  *
114
- * All of that is the **vertex encoding**, the default. `encoding: 'rig'`
115
- * stores the posed rig instead — one slot per bone, two texels, skinned in the
116
- * vertex shader from the rest-pose geometry (ADR-0018) — under the same clip
117
- * table and the same playback contract, at a fraction of the memory; see
118
- * {@link BakeOptions.encoding} for what it refuses. Overloaded on the option's
119
- * literal, so a call that names no encoding, or the vertex encoding, still
120
- * holds the narrow {@link DeltaVAT} it always did.
136
+ * All of that is the **vertex encoding**. `encoding: 'rig'` stores the posed
137
+ * rig instead — one slot per bone, two texels, skinned in the vertex shader
138
+ * from the rest-pose geometry (ADR-0018) — under the same clip table and the
139
+ * same playback contract, at a fraction of the memory; see
140
+ * {@link BakeOptions.encoding} for what it refuses. The default is the rig
141
+ * where the asset allows it and the vertices where it does not (ADR-0027), so
142
+ * a bake that names no encoding returns the {@link VAT} union: narrow on
143
+ * `vat.encoding`, or name the encoding and get the narrow member back.
121
144
  */
122
- declare function bakeVAT(root: Object3D, animations: BakeInput[], options?: BakeOptions & {
123
- encoding?: 'delta';
145
+ declare function bakeVAT(root: Object3D, animations: BakeInput[], options: BakeOptions & {
146
+ encoding: 'delta';
124
147
  }): DeltaVAT;
125
148
  declare function bakeVAT(root: Object3D, animations: BakeInput[], options: BakeOptions & {
126
149
  encoding: 'rig';
@@ -135,21 +158,159 @@ declare function bakeVAT(root: Object3D, animations: BakeInput[], options?: Bake
135
158
  * Node, and in a Web Worker) so it cannot query the real limit itself: pass
136
159
  * `getMaxTextureSize(renderer)` from `three-vat/webgl` or `three-vat/tsl`
137
160
  * whenever a renderer exists.
138
- * - **The instance ceiling**, because the playback texture is one row per
139
- * instance (`createVATPlaybackTexture`, ADR-0016). That one takes no
140
- * override, the crowd being built long after the bake was sized.
141
- *
142
- * Not a guarantee either way: plenty of mobile GPUs report 4096 or 8192, and
143
- * a crowd between that and this number is refused by the driver at upload
144
- * rather than here. The bake is where the real limit is worth passing, because
145
- * it is where the numbers get large.
161
+ * - **The instance ceiling's fallback**, because the playback texture is one
162
+ * row per instance (`createVATPlaybackTexture`, ADR-0016), used when the
163
+ * caller does not pass `maxTextureSize` to it or to `createVATMesh`. The
164
+ * crowd is built beside the renderer, so the real limit is to hand there
165
+ * (ADR-0022's amendment).
166
+ *
167
+ * Not a guarantee either way: plenty of mobile GPUs report 4096 or 8192. The
168
+ * playback texture names this number as its source when it falls back to it,
169
+ * so a crowd refused against it says where the real limit belongs.
146
170
  */
147
171
  declare const MAX_TEXTURE_SIZE = 16384;
148
172
  /**
149
- * Build a VAT `DataTexture` with the fixed sampling flags every path relies on:
150
- * RGBA, nearest filtering, no mipmaps. Frame interpolation is done manually in
151
- * the shader, so linear filtering must stay off.
173
+ * Build an RGBA VAT `DataTexture` — the position texture, the rig texture and
174
+ * the playback texture, which is every layer whose texel is four numbers that
175
+ * have to stay numbers.
176
+ *
177
+ * `type` says which kind of number, and the two callers that pass it disagree
178
+ * deliberately (ADR-0002's amendment):
179
+ *
180
+ * - **`HalfFloatType`, with a `Uint16Array`** — the position layer (#73). It
181
+ * stores *deltas*, and half-float's error is proportional to what it holds:
182
+ * 0.061% of the delta, zero at the rest pose. Eight bytes a texel instead of
183
+ * sixteen. Half-float tops out at 65 504, which is the one thing a delta can
184
+ * exceed, so `bakeVAT` range-checks before it writes.
185
+ * - **`FloatType`, with a `Float32Array`** — the rig texture, whose texels are
186
+ * a quaternion and an *absolute* translation, and the playback texture, whose
187
+ * `startTime` in seconds does not survive half precision.
188
+ *
189
+ * `data` is one of those two arrays and not a `TypedArray`, deliberately: the
190
+ * normal texture is two unsigned bytes a texel ({@link makeVATNormalTexture}),
191
+ * and handing its buffer to this function would upload byte pairs as float32
192
+ * bits — a crowd rendering garbage rather than a call that failed. The narrow
193
+ * parameter turns that into a compile error, which is what a stale copy of the
194
+ * Web Worker recipe in `docs/usage.md` deserves. Pairing either array with the
195
+ * *other's* `type` is the same fault wearing a legal signature — half a texel
196
+ * read as a whole one — so that pairing is checked here and refused, rather
197
+ * than left to the driver.
198
+ */
199
+ declare function makeVATTexture(data: Float32Array | Uint16Array, width: number, height: number, type?: typeof FloatType | typeof HalfFloatType): DataTexture;
200
+ /**
201
+ * Build the normal texture: two unsigned bytes a texel, an octahedral unit
202
+ * vector (`src/octahedral.ts`, #29). `RG8` where the position layer is
203
+ * `RGBA16F`, because a normal is a direction and eight bits of each of two
204
+ * channels carry one to under a degree.
205
+ *
206
+ * `unpackAlignment` is the one thing this needs that the float builder does
207
+ * not: a row of `RG8` is `2 × width` bytes, so an odd vertex count makes every
208
+ * row an odd multiple of two, and the default alignment of 4 would have the
209
+ * driver start each row at the wrong offset. An RGBA half-float row is `8 ×
210
+ * width` bytes and an RGBA float row `16 × width` — a multiple of 4 at any
211
+ * width either way, which is why this never came up on those.
212
+ */
213
+ declare function makeVATNormalTexture(data: Uint8Array, width: number, height: number): DataTexture;
214
+
215
+ /** A vector the decode can write into — `THREE.Vector3` is one. */
216
+ interface Vec3Out {
217
+ x: number;
218
+ y: number;
219
+ z: number;
220
+ }
221
+ /**
222
+ * Write a normal into two bytes of a VAT normal texture's buffer, at `offset`
223
+ * and `offset + 1` — `(row * vertexCount + vertex) * 2` for the texel of one
224
+ * vertex at one frame.
225
+ *
226
+ * Takes a direction, not a unit vector: the first step is a division by the L1
227
+ * norm, so the encoding is scale-invariant and an unnormalised normal encodes
228
+ * to the same texel its normalised twin does. A zero-length normal has no
229
+ * direction to divide out, and is written as the texel +Z lands on rather than
230
+ * as NaN — a degenerate vertex then shades like a flat one instead of turning
231
+ * the mesh black.
232
+ */
233
+ declare function encodeOctahedral(x: number, y: number, z: number, out: Uint8Array | number[], offset: number): void;
234
+ /**
235
+ * Decode a normal texel — the two bytes {@link encodeOctahedral} wrote — back
236
+ * to a unit vector. Pass `out` to decode a whole layer without allocating.
237
+ *
238
+ * `u` and `v` are the stored bytes, `0..255`; a sampler hands the shaders the
239
+ * same numbers already divided by 255, which is the `/ 255` below and the only
240
+ * difference between this and the two transcriptions of it.
241
+ *
242
+ * The fold is undone without a branch, by the identity that a negative `z`
243
+ * means exactly `|x| + |y| - 1` of overshoot to take back off both components,
244
+ * each toward its own zero. Branchless because #72 measured what a branch in
245
+ * the vertex decode costs: the compiler holds registers for the side it skips,
246
+ * and an idle crowd pays for them.
247
+ */
248
+ declare function decodeOctahedral<T extends Vec3Out>(u: number, v: number, out: T): T;
249
+ declare function decodeOctahedral(u: number, v: number): Vec3Out;
250
+
251
+ /**
252
+ * What `bakeVATInWorker` talks to: a `Worker`, or anything else that posts and
253
+ * receives messages the same way (a `MessagePort`, say). The worker on the
254
+ * other end must call {@link serveVATBakes}.
255
+ */
256
+ interface VATBakeWorker {
257
+ postMessage(message: unknown, transfer: Transferable[]): void;
258
+ addEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void): void;
259
+ removeEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void): void;
260
+ }
261
+ /**
262
+ * Where `serveVATBakes` listens: the worker's own global scope by default, or
263
+ * any other end of a message channel.
264
+ */
265
+ interface VATBakeScope {
266
+ postMessage(message: unknown, transfer: Transferable[]): void;
267
+ addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
268
+ removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
269
+ }
270
+ /**
271
+ * {@link bakeVAT}, in a Web Worker: the same arguments, the same VAT, and a
272
+ * page that keeps drawing frames while it bakes.
273
+ *
274
+ * ```ts
275
+ * // bake.worker.ts — the whole file
276
+ * import { serveVATBakes } from 'three-vat'
277
+ * serveVATBakes()
278
+ * ```
279
+ *
280
+ * ```ts
281
+ * // the page
282
+ * const worker = new Worker(new URL('./bake.worker.ts', import.meta.url), { type: 'module' })
283
+ * const vat = await bakeVATInWorker(worker, gltf.scene, gltf.animations, {
284
+ * maxTextureSize: getMaxTextureSize(renderer),
285
+ * })
286
+ * ```
287
+ *
288
+ * The subtree is copied, not moved: the caller's scene is untouched, and a
289
+ * source geometry without normals is not given them as it would be by a bake
290
+ * on this thread. Materials never cross — the VAT comes back holding the
291
+ * caller's own, in `materialIndex` order. A refusal rejects the promise with
292
+ * the message `bakeVAT` would have thrown. One worker serves any number of
293
+ * bakes, one at a time. A track on what the bake does not read, such as a
294
+ * material's colour, stays on the page.
295
+ *
296
+ * What cannot be copied is refused before anything is sent: a bone outside
297
+ * the subtree, a keyframe track the bake reads that has a custom interpolant
298
+ * (glTF's cubic spline is carried), an attribute that is neither a
299
+ * `BufferAttribute` nor an interleaved one.
300
+ */
301
+ declare function bakeVATInWorker(worker: VATBakeWorker, root: Object3D, animations: BakeInput[], options: BakeOptions & {
302
+ encoding: 'delta';
303
+ }): Promise<DeltaVAT>;
304
+ declare function bakeVATInWorker(worker: VATBakeWorker, root: Object3D, animations: BakeInput[], options: BakeOptions & {
305
+ encoding: 'rig';
306
+ }): Promise<RigVAT>;
307
+ declare function bakeVATInWorker(worker: VATBakeWorker, root: Object3D, animations: BakeInput[], options?: BakeOptions): Promise<VAT>;
308
+ /**
309
+ * The worker's half of {@link bakeVATInWorker}: answer every bake the page
310
+ * sends, by calling `bakeVAT` here. Call it once, at the top of the worker
311
+ * module. Messages that are not bakes are left alone, so the worker may do
312
+ * other work besides. Returns a function that stops listening.
152
313
  */
153
- declare function makeVATTexture(data: TypedArray, width: number, height: number, type?: TextureDataType): DataTexture;
314
+ declare function serveVATBakes(scope?: VATBakeScope): () => void;
154
315
 
155
- export { type BakeInput, type BakeOptions, DeltaVAT, MAX_TEXTURE_SIZE, RigVAT, VAT, bakeVAT, makeVATTexture };
316
+ export { type BakeInput, type BakeOptions, DeltaVAT, MAX_TEXTURE_SIZE, RigVAT, VAT, type VATBakeScope, type VATBakeWorker, type Vec3Out, bakeVAT, bakeVATInWorker, decodeOctahedral, encodeOctahedral, makeVATNormalTexture, makeVATTexture, serveVATBakes };