three-vat 3.0.0 → 4.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
@@ -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 a texture whose width is the rig's, not
88
+ the mesh's. 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
  /**
@@ -465,30 +478,69 @@ interface VATBase {
465
478
  clips: VATClip[];
466
479
  /** Union of every baked frame's bounds; use as the geometry bounding box. */
467
480
  bounds: Box3;
468
- /** Vertex count (texture width). */
481
+ /**
482
+ * Vertices in the merged geometry — the vertex encoding's texture width
483
+ * wherever they fit one row, which is every bake below the ceiling
484
+ * ({@link DeltaVAT.rowsPerFrame}).
485
+ */
469
486
  vertexCount: number;
470
- /** Total frame rows across all clips (texture height). */
487
+ /**
488
+ * Total frames across all clips — the texture's height in rows, times
489
+ * {@link DeltaVAT.rowsPerFrame} under the vertex encoding. The unit every
490
+ * `startFrame` and `frames` in the clip table counts in.
491
+ */
471
492
  totalFrames: number;
472
493
  }
473
494
  /**
474
495
  * A VAT under the **vertex encoding**: a row holds where every vertex ended up,
475
496
  * 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).
497
+ * source-agnostic encoding (ADR-0008), the member every bake produced before
498
+ * there was a second one (ADR-0018), and what the default falls back to where
499
+ * the rig encoding refuses an asset (ADR-0027).
478
500
  */
479
501
  interface DeltaVAT extends VATBase {
480
502
  /** Which encoding a row holds — the discriminant of {@link VAT}. */
481
503
  encoding: 'delta';
482
- /** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
504
+ /**
505
+ * `RGBA16F` texture of per-vertex position deltas (`x = vertex`, `y = frame`),
506
+ * eight bytes a texel — half of what RGBA float cost, for 0.061% of the delta
507
+ * and nothing at all at the rest pose (#73, ADR-0002's amendment). A
508
+ * half-float sampler hands the shader floats, so neither decode unpacks
509
+ * anything; a bake whose delta would pass half-float's 65 504 ceiling is
510
+ * refused rather than clipped.
511
+ */
483
512
  positionTexture: DataTexture;
484
513
  /**
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.
514
+ * `RG8` texture of per-vertex absolute normals (`x = vertex`, `y = frame`),
515
+ * each texel an octahedral unit vector in two unsigned bytes — a quarter of
516
+ * the position texel beside it, for ~0.95° of angular error (#29,
517
+ * `src/octahedral.ts`).
518
+ * Decode it with `decodeOctahedral`; both shaders do.
519
+ *
520
+ * `null` when the bake was told to skip it (`bakeNormals: false`) — dropping
521
+ * the layer for a crowd that never reads a normal. Neither decode path
522
+ * samples it when it is absent; a smooth-shaded lit material paired with such
523
+ * a VAT is refused rather than lit by its rest pose.
490
524
  */
491
525
  normalTexture: DataTexture | null;
526
+ /**
527
+ * Texture rows one frame takes (ADR-0030). `1` wherever the vertex count
528
+ * fits the bake's `maxTextureSize`, and then both layers are `vertexCount`
529
+ * wide and `totalFrames` tall, as every bake before this field was. Past it,
530
+ * a frame's vertices continue onto the next row: the fewest rows that hold
531
+ * them, `ceil(vertexCount / rowsPerFrame)` texels wide, and vertex `v` of
532
+ * frame `f` at column `v mod width`, row `f × rowsPerFrame + floor(v /
533
+ * width)`. Both decode paths read it; neither adds a line where it is `1`.
534
+ */
535
+ rowsPerFrame: number;
536
+ /**
537
+ * Why the default encoding fell back to this one (ADR-0029): the message of
538
+ * the rig encoding's refusal, naming what the rig could not store and where
539
+ * — an animated morph, its clip and its part, say. `null` where the vertex
540
+ * encoding was asked for by name (`encoding: 'delta'`). The bake prints
541
+ * nothing on a fallback; this is where the reason is kept.
542
+ */
543
+ fallback: string | null;
492
544
  }
493
545
  /**
494
546
  * A VAT under the **rig encoding** (ADR-0018): a row holds the posed rig — one
@@ -1,15 +1,44 @@
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 vertexLayoutFor(vertexCount, totalFrames, maxTextureSize) {
7
+ const rowsPerFrame = Math.max(1, Math.ceil(vertexCount / maxTextureSize));
8
+ const width = vertexWidthOf({ vertexCount, rowsPerFrame });
9
+ const height = totalFrames * rowsPerFrame;
10
+ if (height > maxTextureSize) {
11
+ throw new Error(
12
+ `three-vat: totalFrames ${totalFrames} at ${rowsPerFrame} rows a frame is ${height} rows, which exceeds maxTextureSize ${maxTextureSize} \u2014 the vertex encoding spans each frame's ${vertexCount} vertices across ${rowsPerFrame} rows of ${width}; lower fps or bake fewer clips`
13
+ );
14
+ }
15
+ return { rowsPerFrame, width, height, frameStride: rowsPerFrame * width };
16
+ }
17
+ function vertexWidthOf({ vertexCount, rowsPerFrame }) {
18
+ return Math.ceil(vertexCount / rowsPerFrame);
19
+ }
20
+ function sampledExactly(tex) {
7
21
  tex.minFilter = NearestFilter;
8
22
  tex.magFilter = NearestFilter;
9
23
  tex.generateMipmaps = false;
10
24
  tex.needsUpdate = true;
11
25
  return tex;
12
26
  }
27
+ function makeVATTexture(data, width, height, type = FloatType) {
28
+ const half = type === HalfFloatType;
29
+ const wanted = half ? Uint16Array : Float32Array;
30
+ if (!(data instanceof wanted)) {
31
+ throw new Error(
32
+ `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`
33
+ );
34
+ }
35
+ return sampledExactly(new DataTexture(data, width, height, RGBAFormat, type));
36
+ }
37
+ function makeVATNormalTexture(data, width, height) {
38
+ const tex = new DataTexture(data, width, height, RGFormat, UnsignedByteType);
39
+ tex.unpackAlignment = 1;
40
+ return sampledExactly(tex);
41
+ }
13
42
 
14
43
  // src/instance-playback.ts
15
44
  var PACK_TEXELS = {
@@ -74,24 +103,27 @@ function resolveVATFrame(instance, time) {
74
103
  const started = local >= 0;
75
104
  const finished = started && repetitions !== INFINITE_REPETITIONS && loops >= repetitions;
76
105
  let phase;
77
- let wraps;
106
+ let looping;
78
107
  if (!started) {
79
108
  phase = 0;
80
- wraps = false;
109
+ looping = false;
81
110
  } else if (finished) {
82
111
  phase = endMode === EndMode.Clamp ? 1 : 0;
83
- wraps = false;
112
+ looping = false;
84
113
  } else if (loopMode === LoopMode.PingPong) {
85
- const m = loops % 2;
114
+ const m = loops - 2 * Math.floor(loops * 0.5);
86
115
  phase = m < 1 ? m : 2 - m;
87
- wraps = false;
116
+ looping = false;
88
117
  } else {
89
118
  phase = loops % 1;
90
- wraps = true;
119
+ looping = true;
91
120
  }
92
- const f = phase * (wraps ? frames : last);
121
+ const holds = looping && endMode === EndMode.Clamp && repetitions !== INFINITE_REPETITIONS && Math.floor(loops) + 1 >= repetitions;
122
+ const wraps = looping && !holds;
123
+ const f = phase * (looping ? frames : last);
93
124
  const f0 = Math.min(Math.floor(f), last);
94
- const f1 = wraps ? (f0 + 1) % frames : Math.min(f0 + 1, last);
125
+ const next = f0 + 1;
126
+ const f1 = wraps ? next >= frames ? 0 : next : Math.min(next, last);
95
127
  const row = clip.startFrame + f0;
96
128
  const crossfade = crossfadeOf(instance);
97
129
  const elapsed = crossfade ? (time - instance.startTime) / crossfade.duration : 0;
@@ -123,9 +155,14 @@ function createVATPlaybackTexture(instances, options = {}) {
123
155
  "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
156
  );
125
157
  }
126
- if (count > MAX_TEXTURE_SIZE) {
158
+ const ceiling = options.maxTextureSize ?? MAX_TEXTURE_SIZE;
159
+ if (count > ceiling) {
160
+ const [source, hint] = options.maxTextureSize === void 0 ? [
161
+ "MAX_TEXTURE_SIZE, the default when no maxTextureSize option is given",
162
+ " Pass getMaxTextureSize(renderer) as maxTextureSize to check against this GPU's own limit."
163
+ ] : ["the maxTextureSize option", ""];
127
164
  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.`
165
+ `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
166
  );
130
167
  }
131
168
  const data = new Float32Array(count * PACK_STRIDE);
@@ -223,4 +260,4 @@ var RIG_TEXELS = {
223
260
  };
224
261
  var RIG_TEXELS_PER_SLOT = 2;
225
262
 
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 };
263
+ 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, vertexLayoutFor, vertexWidthOf };
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-VLSJLgjX.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-VLSJLgjX.js';
4
4
 
5
5
  /**
6
6
  * What {@link bakeVAT} takes for each animation: the clip itself, or an
@@ -15,17 +15,21 @@ interface BakeOptions {
15
15
  /** Sample rate in frames per second. Default `30`. */
16
16
  fps?: number;
17
17
  /**
18
- * Largest texture dimension the target GPU accepts. Both VAT axes are checked
19
- * against it: `vertexCount` (width) and `totalFrames` (height). Defaults to
18
+ * Largest texture dimension the target GPU accepts. Under the rig encoding
19
+ * both axes are checked against it: the rig's width and `totalFrames`. Under
20
+ * the vertex encoding a frame whose vertices outnumber it spans several rows
21
+ * instead of refusing (ADR-0030), so the height alone is checked: `totalFrames
22
+ * × rowsPerFrame`. Defaults to
20
23
  * {@link MAX_TEXTURE_SIZE}; pass the renderer's real limit to avoid baking a
21
- * VAT that allocates on your desktop and fails on a phone.
24
+ * VAT that allocates on your desktop and fails on a phone, and so that a
25
+ * frame spans rows only where this GPU needs it to.
22
26
  */
23
27
  maxTextureSize?: number;
24
28
  /**
25
29
  * Bake the normal texture. Default `true`.
26
30
  *
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:
31
+ * Turning it off drops the normal layer — `verts x frames x (8 B + 2 B)`
32
+ * becomes `x 8 B` — and is correct for exactly two material setups:
29
33
  *
30
34
  * - **Unlit** (`MeshBasicMaterial`, and its node twin), which never reads a
31
35
  * normal, so the texture was pure waste.
@@ -44,11 +48,16 @@ interface BakeOptions {
44
48
  */
45
49
  bakeNormals?: boolean;
46
50
  /**
47
- * What a frame row holds (ADR-0018). Default `'delta'`.
51
+ * What a frame row holds (ADR-0018). Default `'auto'` (ADR-0027).
48
52
  *
53
+ * - **`'auto'`** — the rig encoding where the asset allows it, and the
54
+ * vertex encoding where the rig encoding refuses it. Read `vat.encoding` to
55
+ * learn which one a bake chose. The default since 4.0: the rig texture is
56
+ * two orders of magnitude smaller, bakes in milliseconds, and decodes
57
+ * faster where memory bandwidth is scarce, as on a phone.
49
58
  * - **`'delta'`** — the vertex encoding: where every vertex ended up, as a
50
59
  * position delta and a normal. Source-agnostic — skinning, morph targets
51
- * and node animation alike — and the default.
60
+ * and node animation alike.
52
61
  * - **`'rig'`** — the rig encoding: the posed rig, one **slot** per bone as a
53
62
  * rotation, a translation and a uniform scale, skinned in the vertex shader
54
63
  * from the rest-pose geometry. Two orders of magnitude smaller, no vertex
@@ -59,7 +68,24 @@ interface BakeOptions {
59
68
  * node-animated part is one slot of weight one; parts reading the same
60
69
  * bones through the same bind matrix share slots.
61
70
  */
62
- encoding?: 'delta' | 'rig';
71
+ encoding?: 'auto' | 'delta' | 'rig';
72
+ /**
73
+ * Collapse materials that differ only in their flat colour into one, so the
74
+ * crowd draws once instead of once per material (ADR-0028). Default `false`.
75
+ *
76
+ * A material is **flat** when it has a `color`, no texture map of any kind,
77
+ * and `vertexColors` off. Two or more flat materials that agree on every
78
+ * other property — type, roughness, metalness, emissive, side, opacity and
79
+ * the rest — become one clone of the first, white, with `vertexColors` on,
80
+ * and each part's colour moves into the merged geometry's `color` attribute.
81
+ * A material that is not flat, or is the only one of its kind, is left
82
+ * exactly as it was. `vat.materials` therefore holds a material you did not
83
+ * create wherever a merge happened.
84
+ *
85
+ * Off by default because it is only ever right for flat-shaded assets: a
86
+ * textured character has nothing to collapse, and merging never guesses.
87
+ */
88
+ mergeFlatMaterials?: boolean;
63
89
  }
64
90
  /**
65
91
  * Bake animations into a VAT by sampling the posed subtree frame by frame on
@@ -111,16 +137,17 @@ interface BakeOptions {
111
137
  * Renderer-agnostic — touches no WebGL/WebGPU context — so it runs identically
112
138
  * at runtime, in a Web Worker, and in Node.
113
139
  *
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.
140
+ * All of that is the **vertex encoding**. `encoding: 'rig'` stores the posed
141
+ * rig instead — one slot per bone, two texels, skinned in the vertex shader
142
+ * from the rest-pose geometry (ADR-0018) — under the same clip table and the
143
+ * same playback contract, at a fraction of the memory; see
144
+ * {@link BakeOptions.encoding} for what it refuses. The default is the rig
145
+ * where the asset allows it and the vertices where it does not (ADR-0027), so
146
+ * a bake that names no encoding returns the {@link VAT} union: narrow on
147
+ * `vat.encoding`, or name the encoding and get the narrow member back.
121
148
  */
122
- declare function bakeVAT(root: Object3D, animations: BakeInput[], options?: BakeOptions & {
123
- encoding?: 'delta';
149
+ declare function bakeVAT(root: Object3D, animations: BakeInput[], options: BakeOptions & {
150
+ encoding: 'delta';
124
151
  }): DeltaVAT;
125
152
  declare function bakeVAT(root: Object3D, animations: BakeInput[], options: BakeOptions & {
126
153
  encoding: 'rig';
@@ -135,21 +162,160 @@ declare function bakeVAT(root: Object3D, animations: BakeInput[], options?: Bake
135
162
  * Node, and in a Web Worker) so it cannot query the real limit itself: pass
136
163
  * `getMaxTextureSize(renderer)` from `three-vat/webgl` or `three-vat/tsl`
137
164
  * 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.
165
+ * - **The instance ceiling's fallback**, because the playback texture is one
166
+ * row per instance (`createVATPlaybackTexture`, ADR-0016), used when the
167
+ * caller does not pass `maxTextureSize` to it or to `createVATMesh`. The
168
+ * crowd is built beside the renderer, so the real limit is to hand there
169
+ * (ADR-0022's amendment).
170
+ *
171
+ * Not a guarantee either way: plenty of mobile GPUs report 4096 or 8192. The
172
+ * playback texture names this number as its source when it falls back to it,
173
+ * so a crowd refused against it says where the real limit belongs.
146
174
  */
147
175
  declare const MAX_TEXTURE_SIZE = 16384;
148
176
  /**
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.
177
+ * Build an RGBA VAT `DataTexture` — the position texture, the rig texture and
178
+ * the playback texture, which is every layer whose texel is four numbers that
179
+ * have to stay numbers.
180
+ *
181
+ * `type` says which kind of number, and the two callers that pass it disagree
182
+ * deliberately (ADR-0002's amendment):
183
+ *
184
+ * - **`HalfFloatType`, with a `Uint16Array`** — the position layer (#73). It
185
+ * stores *deltas*, and half-float's error is proportional to what it holds:
186
+ * 0.061% of the delta, zero at the rest pose. Eight bytes a texel instead of
187
+ * sixteen. Half-float tops out at 65 504, which is the one thing a delta can
188
+ * exceed, so `bakeVAT` range-checks before it writes.
189
+ * - **`FloatType`, with a `Float32Array`** — the rig texture, whose texels are
190
+ * a quaternion and an *absolute* translation, and the playback texture, whose
191
+ * `startTime` in seconds does not survive half precision.
192
+ *
193
+ * `data` is one of those two arrays and not a `TypedArray`, deliberately: the
194
+ * normal texture is two unsigned bytes a texel ({@link makeVATNormalTexture}),
195
+ * and handing its buffer to this function would upload byte pairs as float32
196
+ * bits — a crowd rendering garbage rather than a call that failed. The narrow
197
+ * parameter turns that into a compile error, which is what a stale copy of the
198
+ * Web Worker recipe in `docs/usage.md` deserves. Pairing either array with the
199
+ * *other's* `type` is the same fault wearing a legal signature — half a texel
200
+ * read as a whole one — so that pairing is checked here and refused, rather
201
+ * than left to the driver.
202
+ */
203
+ declare function makeVATTexture(data: Float32Array | Uint16Array, width: number, height: number, type?: typeof FloatType | typeof HalfFloatType): DataTexture;
204
+ /**
205
+ * Build the normal texture: two unsigned bytes a texel, an octahedral unit
206
+ * vector (`src/octahedral.ts`, #29). `RG8` where the position layer is
207
+ * `RGBA16F`, because a normal is a direction and eight bits of each of two
208
+ * channels carry one to under a degree.
209
+ *
210
+ * `unpackAlignment` is the one thing this needs that the float builder does
211
+ * not: a row of `RG8` is `2 × width` bytes, so an odd vertex count makes every
212
+ * row an odd multiple of two, and the default alignment of 4 would have the
213
+ * driver start each row at the wrong offset. An RGBA half-float row is `8 ×
214
+ * width` bytes and an RGBA float row `16 × width` — a multiple of 4 at any
215
+ * width either way, which is why this never came up on those.
216
+ */
217
+ declare function makeVATNormalTexture(data: Uint8Array, width: number, height: number): DataTexture;
218
+
219
+ /** A vector the decode can write into — `THREE.Vector3` is one. */
220
+ interface Vec3Out {
221
+ x: number;
222
+ y: number;
223
+ z: number;
224
+ }
225
+ /**
226
+ * Write a normal into two bytes of a VAT normal texture's buffer, at `offset`
227
+ * and `offset + 1` — `(row * vertexCount + vertex) * 2` in a one-row bake,
228
+ * `deltaTexel` in `src/test-utils.ts` in any, for the texel of one
229
+ * vertex at one frame.
230
+ *
231
+ * Takes a direction, not a unit vector: the first step is a division by the L1
232
+ * norm, so the encoding is scale-invariant and an unnormalised normal encodes
233
+ * to the same texel its normalised twin does. A zero-length normal has no
234
+ * direction to divide out, and is written as the texel +Z lands on rather than
235
+ * as NaN — a degenerate vertex then shades like a flat one instead of turning
236
+ * the mesh black.
237
+ */
238
+ declare function encodeOctahedral(x: number, y: number, z: number, out: Uint8Array | number[], offset: number): void;
239
+ /**
240
+ * Decode a normal texel — the two bytes {@link encodeOctahedral} wrote — back
241
+ * to a unit vector. Pass `out` to decode a whole layer without allocating.
242
+ *
243
+ * `u` and `v` are the stored bytes, `0..255`; a sampler hands the shaders the
244
+ * same numbers already divided by 255, which is the `/ 255` below and the only
245
+ * difference between this and the two transcriptions of it.
246
+ *
247
+ * The fold is undone without a branch, by the identity that a negative `z`
248
+ * means exactly `|x| + |y| - 1` of overshoot to take back off both components,
249
+ * each toward its own zero. Branchless because #72 measured what a branch in
250
+ * the vertex decode costs: the compiler holds registers for the side it skips,
251
+ * and an idle crowd pays for them.
252
+ */
253
+ declare function decodeOctahedral<T extends Vec3Out>(u: number, v: number, out: T): T;
254
+ declare function decodeOctahedral(u: number, v: number): Vec3Out;
255
+
256
+ /**
257
+ * What `bakeVATInWorker` talks to: a `Worker`, or anything else that posts and
258
+ * receives messages the same way (a `MessagePort`, say). The worker on the
259
+ * other end must call {@link serveVATBakes}.
260
+ */
261
+ interface VATBakeWorker {
262
+ postMessage(message: unknown, transfer: Transferable[]): void;
263
+ addEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void): void;
264
+ removeEventListener(type: 'message' | 'error', listener: (event: MessageEvent | ErrorEvent) => void): void;
265
+ }
266
+ /**
267
+ * Where `serveVATBakes` listens: the worker's own global scope by default, or
268
+ * any other end of a message channel.
269
+ */
270
+ interface VATBakeScope {
271
+ postMessage(message: unknown, transfer: Transferable[]): void;
272
+ addEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
273
+ removeEventListener(type: 'message', listener: (event: MessageEvent) => void): void;
274
+ }
275
+ /**
276
+ * {@link bakeVAT}, in a Web Worker: the same arguments, the same VAT, and a
277
+ * page that keeps drawing frames while it bakes.
278
+ *
279
+ * ```ts
280
+ * // bake.worker.ts — the whole file
281
+ * import { serveVATBakes } from 'three-vat'
282
+ * serveVATBakes()
283
+ * ```
284
+ *
285
+ * ```ts
286
+ * // the page
287
+ * const worker = new Worker(new URL('./bake.worker.ts', import.meta.url), { type: 'module' })
288
+ * const vat = await bakeVATInWorker(worker, gltf.scene, gltf.animations, {
289
+ * maxTextureSize: getMaxTextureSize(renderer),
290
+ * })
291
+ * ```
292
+ *
293
+ * The subtree is copied, not moved: the caller's scene is untouched, and a
294
+ * source geometry without normals is not given them as it would be by a bake
295
+ * on this thread. Materials never cross — the VAT comes back holding the
296
+ * caller's own, in `materialIndex` order. A refusal rejects the promise with
297
+ * the message `bakeVAT` would have thrown. One worker serves any number of
298
+ * bakes, one at a time. A track on what the bake does not read, such as a
299
+ * material's colour, stays on the page.
300
+ *
301
+ * What cannot be copied is refused before anything is sent: a bone outside
302
+ * the subtree, a keyframe track the bake reads that has a custom interpolant
303
+ * (glTF's cubic spline is carried), an attribute that is neither a
304
+ * `BufferAttribute` nor an interleaved one.
305
+ */
306
+ declare function bakeVATInWorker(worker: VATBakeWorker, root: Object3D, animations: BakeInput[], options: BakeOptions & {
307
+ encoding: 'delta';
308
+ }): Promise<DeltaVAT>;
309
+ declare function bakeVATInWorker(worker: VATBakeWorker, root: Object3D, animations: BakeInput[], options: BakeOptions & {
310
+ encoding: 'rig';
311
+ }): Promise<RigVAT>;
312
+ declare function bakeVATInWorker(worker: VATBakeWorker, root: Object3D, animations: BakeInput[], options?: BakeOptions): Promise<VAT>;
313
+ /**
314
+ * The worker's half of {@link bakeVATInWorker}: answer every bake the page
315
+ * sends, by calling `bakeVAT` here. Call it once, at the top of the worker
316
+ * module. Messages that are not bakes are left alone, so the worker may do
317
+ * other work besides. Returns a function that stops listening.
152
318
  */
153
- declare function makeVATTexture(data: TypedArray, width: number, height: number, type?: TextureDataType): DataTexture;
319
+ declare function serveVATBakes(scope?: VATBakeScope): () => void;
154
320
 
155
- export { type BakeInput, type BakeOptions, DeltaVAT, MAX_TEXTURE_SIZE, RigVAT, VAT, bakeVAT, makeVATTexture };
321
+ export { type BakeInput, type BakeOptions, DeltaVAT, MAX_TEXTURE_SIZE, RigVAT, VAT, type VATBakeScope, type VATBakeWorker, type Vec3Out, bakeVAT, bakeVATInWorker, decodeOctahedral, encodeOctahedral, makeVATNormalTexture, makeVATTexture, serveVATBakes };