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.
@@ -0,0 +1,249 @@
1
+ import { FloatType, DataTexture, RGBAFormat, RGFormat, UnsignedByteType, HalfFloatType, NearestFilter } from 'three';
2
+
3
+ // src/vat-texture.ts
4
+ var MAX_TEXTURE_SIZE = 16384;
5
+ var HALF_FLOAT_MAX = 65504;
6
+ function sampledExactly(tex) {
7
+ tex.minFilter = NearestFilter;
8
+ tex.magFilter = NearestFilter;
9
+ tex.generateMipmaps = false;
10
+ tex.needsUpdate = true;
11
+ return tex;
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
+ }
28
+
29
+ // src/instance-playback.ts
30
+ var PACK_TEXELS = {
31
+ clip: 0,
32
+ playback: 1,
33
+ crossfade: 2,
34
+ outgoingClip: 3,
35
+ outgoingPlayback: 4
36
+ };
37
+ var PACK_WIDTH = 5;
38
+ var PACK_STRIDE = PACK_WIDTH * 4;
39
+ var LoopMode = {
40
+ /** Play the clip end to end, forever (or `repetitions` times). */
41
+ Repeat: 0,
42
+ /** Play the clip through once. */
43
+ Once: 1,
44
+ /** Play forward, then backward, without baking the reversed frames. */
45
+ PingPong: 2
46
+ };
47
+ var EndMode = {
48
+ /** Hold the last frame. */
49
+ Clamp: 0,
50
+ /** Return to the first frame. */
51
+ Rewind: 1
52
+ };
53
+ var INFINITE_REPETITIONS = -1;
54
+ function defaultRepetitions(loopMode) {
55
+ return loopMode === LoopMode.Repeat ? INFINITE_REPETITIONS : 1;
56
+ }
57
+ var LIBRARY_PLAYBACK_DEFAULTS = {
58
+ loopMode: LoopMode.Repeat,
59
+ repetitions: defaultRepetitions(LoopMode.Repeat),
60
+ endMode: EndMode.Clamp,
61
+ speed: 1
62
+ };
63
+ function resolvedPlaybackOf(instance) {
64
+ const { clip } = instance;
65
+ const clipLoopMode = clip.loopMode ?? LIBRARY_PLAYBACK_DEFAULTS.loopMode;
66
+ const loopMode = instance.loopMode ?? clipLoopMode;
67
+ const clipRepetitions = loopMode === clipLoopMode ? clip.repetitions : void 0;
68
+ return {
69
+ loopMode,
70
+ repetitions: instance.repetitions ?? clipRepetitions ?? defaultRepetitions(loopMode),
71
+ endMode: instance.endMode ?? clip.endMode ?? LIBRARY_PLAYBACK_DEFAULTS.endMode,
72
+ speed: instance.speed ?? clip.speed ?? LIBRARY_PLAYBACK_DEFAULTS.speed
73
+ };
74
+ }
75
+ function crossfadeOf(instance) {
76
+ const from = instance.from;
77
+ if (!from || !asksToBlend(instance)) return null;
78
+ return { from, duration: instance.fadeDuration };
79
+ }
80
+ var asksToBlend = (instance) => (instance.fadeDuration ?? 0) > 0;
81
+ function resolveVATFrame(instance, time) {
82
+ const { clip } = instance;
83
+ const frames = clip.frames;
84
+ const last = frames - 1;
85
+ const duration = frames / clip.fps;
86
+ const { loopMode, repetitions, endMode, speed } = resolvedPlaybackOf(instance);
87
+ const local = (time - instance.startTime) * speed;
88
+ const loops = local / duration;
89
+ const started = local >= 0;
90
+ const finished = started && repetitions !== INFINITE_REPETITIONS && loops >= repetitions;
91
+ let phase;
92
+ let looping;
93
+ if (!started) {
94
+ phase = 0;
95
+ looping = false;
96
+ } else if (finished) {
97
+ phase = endMode === EndMode.Clamp ? 1 : 0;
98
+ looping = false;
99
+ } else if (loopMode === LoopMode.PingPong) {
100
+ const m = loops - 2 * Math.floor(loops * 0.5);
101
+ phase = m < 1 ? m : 2 - m;
102
+ looping = false;
103
+ } else {
104
+ phase = loops % 1;
105
+ looping = true;
106
+ }
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);
110
+ const f0 = Math.min(Math.floor(f), last);
111
+ const next = f0 + 1;
112
+ const f1 = wraps ? next >= frames ? 0 : next : Math.min(next, last);
113
+ const row = clip.startFrame + f0;
114
+ const crossfade = crossfadeOf(instance);
115
+ const elapsed = crossfade ? (time - instance.startTime) / crossfade.duration : 0;
116
+ return {
117
+ row,
118
+ rowNext: clip.startFrame + f1,
119
+ mix: f - f0,
120
+ wraps,
121
+ finished,
122
+ phase,
123
+ outgoing: crossfade ? { ...resolveVATFrame(crossfade.from, time), weight: 1 - Math.min(Math.max(elapsed, 0), 1) } : null
124
+ };
125
+ }
126
+ var RESERVED_ROW = {
127
+ clip: { startFrame: 0, frames: 1, fps: 1 },
128
+ startTime: 0,
129
+ speed: 0
130
+ };
131
+ function createVATPlaybackTexture(instances, options = {}) {
132
+ const live = instances.length;
133
+ const count = options.capacity ?? live;
134
+ if (count < live) {
135
+ throw new Error(
136
+ `three-vat: a capacity of ${count} cannot hold the ${live} instances it was given \u2014 capacity is the crowd ceiling, so it is at least the crowd you start with.`
137
+ );
138
+ }
139
+ if (count < 1) {
140
+ throw new Error(
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"
142
+ );
143
+ }
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", ""];
150
+ throw new Error(
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
152
+ );
153
+ }
154
+ const data = new Float32Array(count * PACK_STRIDE);
155
+ for (let i = 0; i < count; i++) writePack(data, i, instances[i] ?? RESERVED_ROW);
156
+ return { texture: makeVATTexture(data, PACK_WIDTH, count), count };
157
+ }
158
+ var FORWARD_ONLY_REASON = "a baked band plays forward from its own first row, so a negative speed would freeze it on that row rather than run it backwards \u2014 bake a reversed clip instead. A speed of 0 is a held first row, and is fine";
159
+ var rowStart = (index) => index * PACK_STRIDE;
160
+ var texelStart = (index, field) => rowStart(index) + field * 4;
161
+ function checkedPolicyOf(state, index, what) {
162
+ const policy = resolvedPlaybackOf(state);
163
+ if (policy.speed < 0) {
164
+ throw new Error(`three-vat: ${what} ${index} has speed ${policy.speed}; ${FORWARD_ONLY_REASON}.`);
165
+ }
166
+ return policy;
167
+ }
168
+ function putBand(data, index, at, state, policy) {
169
+ const clip = texelStart(index, at.clip);
170
+ const playback = texelStart(index, at.playback);
171
+ data[clip] = state.clip.startFrame;
172
+ data[clip + 1] = state.clip.frames;
173
+ data[clip + 2] = state.clip.fps;
174
+ data[clip + 3] = policy.speed;
175
+ data[playback] = state.startTime;
176
+ data[playback + 1] = policy.loopMode;
177
+ data[playback + 2] = policy.repetitions;
178
+ data[playback + 3] = policy.endMode;
179
+ }
180
+ var LIVE_PAIR = { clip: PACK_TEXELS.clip, playback: PACK_TEXELS.playback };
181
+ var OUTGOING_PAIR = { clip: PACK_TEXELS.outgoingClip, playback: PACK_TEXELS.outgoingPlayback };
182
+ function checkedCrossfadeOf(instance, index) {
183
+ const duration = instance.fadeDuration;
184
+ if (duration !== void 0 && !(Number.isFinite(duration) && duration >= 0)) {
185
+ throw new Error(
186
+ `three-vat: instance ${index} has fadeDuration ${duration}; a transition lasts a finite number of seconds, and 0 (or no fadeDuration at all) is the cut.`
187
+ );
188
+ }
189
+ return crossfadeOf(instance);
190
+ }
191
+ function writePack(data, index, instance) {
192
+ const crossfade = checkedCrossfadeOf(instance, index);
193
+ const live = checkedPolicyOf(instance, index, "instance");
194
+ const outgoing = crossfade ? { state: crossfade.from, policy: checkedPolicyOf(crossfade.from, index, "the outgoing band of instance") } : null;
195
+ const crossfadeTexel = texelStart(index, PACK_TEXELS.crossfade);
196
+ putBand(data, index, LIVE_PAIR, instance, live);
197
+ if (outgoing) putBand(data, index, OUTGOING_PAIR, outgoing.state, outgoing.policy);
198
+ else clearTexels(data, index, OUTGOING_PAIR);
199
+ data[crossfadeTexel] = crossfade ? crossfade.duration : 0;
200
+ data[crossfadeTexel + 1] = 0;
201
+ data[crossfadeTexel + 2] = 0;
202
+ data[crossfadeTexel + 3] = 0;
203
+ }
204
+ function clearTexels(data, index, at) {
205
+ data.fill(0, texelStart(index, at.clip), texelStart(index, at.clip) + 4);
206
+ data.fill(0, texelStart(index, at.playback), texelStart(index, at.playback) + 4);
207
+ }
208
+ function readPack(data, index) {
209
+ const clip = texelStart(index, PACK_TEXELS.clip);
210
+ const playback = texelStart(index, PACK_TEXELS.playback);
211
+ return {
212
+ clip: { startFrame: data[clip], frames: data[clip + 1], fps: data[clip + 2] },
213
+ startTime: data[playback],
214
+ speed: data[clip + 3],
215
+ loopMode: data[playback + 1],
216
+ repetitions: data[playback + 2],
217
+ endMode: data[playback + 3]
218
+ };
219
+ }
220
+ function assertInstance(playback, index) {
221
+ if (!Number.isInteger(index) || index < 0 || index >= playback.count) {
222
+ throw new Error(`three-vat: instance ${index} is outside this crowd's ${playback.count} rows`);
223
+ }
224
+ }
225
+ function setVATInstance(playback, index, instance) {
226
+ assertInstance(playback, index);
227
+ const data = playback.texture.image.data;
228
+ const transitioning = instance.from === void 0 && asksToBlend(instance) ? { ...instance, from: readPack(data, index) } : instance;
229
+ writePack(data, index, transitioning);
230
+ playback.texture.addUpdateRange(rowStart(index), PACK_STRIDE);
231
+ playback.texture.needsUpdate = true;
232
+ }
233
+ function endsAt(instance) {
234
+ const { repetitions, speed } = resolvedPlaybackOf(instance);
235
+ if (repetitions === INFINITE_REPETITIONS || speed <= 0) return null;
236
+ const duration = instance.clip.frames / instance.clip.fps;
237
+ return instance.startTime + duration * repetitions / speed;
238
+ }
239
+
240
+ // src/rig-texture.ts
241
+ var RIG_TEXELS = {
242
+ /** `(qx, qy, qz, qw)` — the slot's rotation, on one hemisphere with its neighbouring rows. */
243
+ rotation: 0,
244
+ /** `(tx, ty, tz, s)` — where the slot puts the origin, and its uniform scale in the spare component. */
245
+ placement: 1
246
+ };
247
+ var RIG_TEXELS_PER_SLOT = 2;
248
+
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-BXaLAPRO.js';
3
- export { E as EndMode, I as INFINITE_REPETITIONS, L as LoopMode, M as MAX_FADE_DURATION, a as VATBase, b as VATCarrier, c as VATClip, d as VATClipDefaults, e as VATClock, f as VATCrowd, g as VATFadeFrom, h as VATFrame, i as VATInstance, j as VATPlaybackTexture, k as createVATPlaybackTexture, l as endsAt, r as resolveVATFrame, s as setVATInstance } from './carrier-BXaLAPRO.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 };