three-vat 2.1.0 → 3.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
@@ -148,7 +148,7 @@ is [the CHANGELOG's 2.0.0 entry](./CHANGELOG.md).
148
148
  ```
149
149
 
150
150
  `timeOffset` is gone (`startTime: -timeOffset / speed`), and with it
151
- `aTimeOffset`. The pack is three `vec4`s — clip, playback, fade — in a
151
+ `aTimeOffset`. The pack became three `vec4`s — clip, playback, fade — in a
152
152
  **playback texture** keyed by the instance's logical index, not instanced
153
153
  attributes, which are indexed by the *drawn* slot:
154
154
  `addInstancedVATAttributes` is removed, `createVATPlaybackTexture(instances)`
@@ -184,9 +184,8 @@ worker recipe, the draw-call arithmetic, and the primitives underneath
184
184
  <details>
185
185
  <summary><b>What it does not do</b></summary>
186
186
 
187
- No clip crossfade (an instance blends out of a frozen pose, not between two
188
- clips that are both playing), no LOD, no baking CLI or file
189
- format, no React/drei binding, glTF input only. Each is a decision rather than a
187
+ No LOD, no baking CLI or file format, no React/drei binding, glTF input
188
+ only. Each is a decision rather than a
190
189
  gap, and each is written up with its reasoning in
191
190
  **[docs/usage.md](./docs/usage.md#what-10-does-not-do)**, alongside the
192
191
  trade-offs against `SkinnedMesh` and bone-texture instancing.
@@ -40,44 +40,20 @@ type EndMode = (typeof EndMode)[keyof typeof EndMode];
40
40
  */
41
41
  declare const INFINITE_REPETITIONS = -1;
42
42
  /**
43
- * The longest fade {@link setVATInstance} will honour, in seconds.
43
+ * One clip playing: which band, from when, how fast and under what policy.
44
44
  *
45
- * The cap exists because of what this fade *is*: one frozen pose of the
46
- * outgoing clip, blended away — not a second playback running alongside the
47
- * first. Over a tenth of a second that is invisible; over half a second the
48
- * instance visibly skates, because whatever it was doing stopped dead the
49
- * moment the transition began. A longer fade would not be a better fade, it
50
- * would be a more visible bug, so the number is clamped rather than trusted.
51
- *
52
- * Provisional, like the fade itself: a real two-clip crossfade (#30) replaces
53
- * both, and nothing else should be built on top of them.
54
- */
55
- declare const MAX_FADE_DURATION = 0.25;
56
- /**
57
- * The frozen pose a fade blends away from: one phase of the clip an instance
58
- * was playing when its animation changed, and the band that phase indexes.
59
- *
60
- * Not a second playback state — there is no start time and no speed here,
61
- * because nothing about it moves. That is the whole of the freeze, and the
62
- * whole of its limit; see {@link MAX_FADE_DURATION}.
63
- */
64
- interface VATFadeFrom {
65
- /** First texture row of the outgoing clip's band. */
66
- startFrame: number;
67
- /** Rows in that band. */
68
- frames: number;
69
- /** The phase of that band the instance was at, in `[0, 1]`. */
70
- phase: number;
71
- }
72
- /**
73
- * Per-instance playback state consumed by both decode paths.
45
+ * The unit the pack carries twice — as the animation an instance is playing,
46
+ * and as the one it is blending out of ({@link VATInstance.from}). Both are
47
+ * resolved by {@link resolveVATFrame} through the very same arithmetic, which
48
+ * is the whole difference between a crossfade and the pose freeze it replaced
49
+ * (ADR-0025): the outgoing half is a clip still *playing*, not a photograph.
74
50
  *
75
51
  * Every policy field is optional because the clip already answers it: a bake
76
52
  * handed a configured `AnimationAction` records the answer in the clip table
77
- * ({@link VATClipDefaults}), and an instance that says nothing inherits it. A
53
+ * ({@link VATClipDefaults}), and a state that says nothing inherits it. A
78
54
  * crowd of a thousand deaths says "once, clamped" once, at the bake.
79
55
  */
80
- interface VATInstance {
56
+ interface VATPlaybackState {
81
57
  /**
82
58
  * The clip band to play, straight out of `vat.clips`. Its playback defaults
83
59
  * come along with it; a clip table assembled by hand may carry none, and then
@@ -119,19 +95,33 @@ interface VATInstance {
119
95
  * `false`; see {@link EndMode}.
120
96
  */
121
97
  endMode?: EndMode;
98
+ }
99
+ /**
100
+ * Per-instance playback state consumed by both decode paths: the clip this
101
+ * instance is playing, and — while it is transitioning — the one it is
102
+ * crossfading out of.
103
+ */
104
+ interface VATInstance extends VATPlaybackState {
122
105
  /**
123
- * The frozen outgoing pose to fade away from. Normally you do not write this
124
- * yourself: {@link setVATInstance} freezes whatever the instance was playing
125
- * and fills it in when you ask for a {@link fadeDuration}.
106
+ * The outgoing playback state to blend away from: a clip *still playing*, in
107
+ * every respect an instance except that it carries no transition of its own.
108
+ *
109
+ * Normally you do not write this yourself — {@link setVATInstance} reads the
110
+ * instance's current pack back, whole, and fills it in when you ask for a
111
+ * {@link fadeDuration}. Write it by hand when you are assembling a crowd the
112
+ * library does not build for you; what you pass is what is written.
126
113
  */
127
- from?: VATFadeFrom;
114
+ from?: VATPlaybackState;
128
115
  /**
129
- * Seconds to blend {@link from} away over, capped at {@link MAX_FADE_DURATION}.
130
- * Wall-clock seconds from {@link startTime}: the clip's `speed` does not
131
- * stretch a fade.
116
+ * Seconds to blend {@link from} away over. Uncapped, and wall-clock seconds
117
+ * from {@link startTime}: the incoming clip's `speed` does not stretch a
118
+ * transition.
132
119
  *
133
- * Ignored without a `from` to fade away from — and at creation there is
134
- * nothing to fade away from, so this is `setVATInstance`'s field in practice.
120
+ * Zero, or absent, is a cut: no outgoing band is written. A negative or
121
+ * non-finite duration is refused when the instance is written.
122
+ *
123
+ * Ignored without a `from` to blend away from — and at creation there is
124
+ * nothing to blend away from, so this is `setVATInstance`'s field in practice.
135
125
  */
136
126
  fadeDuration?: number;
137
127
  }
@@ -159,26 +149,39 @@ interface VATFrame {
159
149
  /** How far through the clip this is, in `[0, 1]` — what {@link row} is derived from. */
160
150
  phase: number;
161
151
  /**
162
- * The frozen outgoing row a fade blends away from. Equal to {@link row} when
163
- * the instance is not fading, so a reader that ignores {@link fade} — the
164
- * demo's texture-panel cursors among them — never points at a row this
165
- * instance is not sampling.
152
+ * The band this instance is blending out of, resolved at the same moment —
153
+ * or `null` when it is not transitioning, which is almost always. Not a
154
+ * weight of zero, so a reader with no interest in transitions ignores one
155
+ * field rather than testing one.
166
156
  */
167
- fadeRow: number;
157
+ outgoing: VATOutgoingFrame | null;
158
+ }
159
+ /**
160
+ * The outgoing half of a crossfade: the very frame the outgoing clip would be
161
+ * showing if nothing had interrupted it, and how much of it is still showing.
162
+ *
163
+ * It is a {@link VATFrame} because it is one — resolved by
164
+ * {@link resolveVATFrame} from the outgoing playback state, through the same
165
+ * arithmetic, so an outgoing one-shot that runs out mid-transition clamps
166
+ * exactly as it would have. Its own `outgoing` is always `null`: the pack holds
167
+ * two bands, and the recursion is one level deep.
168
+ */
169
+ interface VATOutgoingFrame extends VATFrame {
168
170
  /**
169
- * How much of {@link fadeRow} is still showing: `1` at the moment of the
170
- * write, falling to `0` across `fadeDuration`, and `0` for an instance that
171
- * is not fading. The decode mixes the sampled clip toward the frozen pose by
172
- * exactly this weight — the weight of the fade, not the fade itself, which is
173
- * the pose-freeze fade `CONTEXT.md` names.
171
+ * How much of this band is still showing: `1` at the moment of the write,
172
+ * falling to `0` across `fadeDuration`, and `0` once the transition is over.
173
+ * Wall clock — `1 - clamp((time - startTime) / fadeDuration, 0, 1)` — so a
174
+ * half-speed incoming clip does not stretch the transition.
174
175
  */
175
- fadeWeight: number;
176
+ weight: number;
176
177
  }
177
178
  /**
178
179
  * What the vertex shader computes, as a pure function of `(instance, time)` —
179
180
  * the **one definition** of the playback semantics. Both decode paths
180
- * transcribe it (`DECODE_PRELUDE` in src/webgl.ts, `vatDecode` in src/tsl.ts);
181
- * neither invents it.
181
+ * transcribe it — its band half in `vatBand` (src/webgl.ts) and `resolveBand`
182
+ * (src/tsl.ts), one function of a clip and playback texel pair, *called twice*
183
+ * where an instance is transitioning; its crossfade weight beside the call, in
184
+ * `vatRows` and `vatDecode` — and neither invents it.
182
185
  *
183
186
  * It exists in TypeScript because the arithmetic is otherwise reachable only
184
187
  * inside a GLSL string and a TSL node graph, neither of which CI can evaluate
@@ -191,7 +194,7 @@ interface VATFrame {
191
194
  */
192
195
  declare function resolveVATFrame(instance: VATInstance, time: number): VATFrame;
193
196
  /**
194
- * What carries the pack to the shader: one `DataTexture`, three texels wide,
197
+ * What carries the pack to the shader: one `DataTexture`, five texels wide,
195
198
  * one row per instance, read by the instance's *logical* index (ADR-0016).
196
199
  *
197
200
  * Held by the caller rather than hidden behind the geometry, because a
@@ -207,24 +210,56 @@ interface VATPlaybackTexture {
207
210
  * float, {@link PACK_WIDTH} texels wide.
208
211
  */
209
212
  texture: DataTexture;
210
- /** Instances it carries — the rows of {@link texture}. */
213
+ /**
214
+ * Rows of {@link texture} — the crowd's **capacity**, which is what the
215
+ * decode indexes and what {@link setVATInstance} bounds-checks against.
216
+ *
217
+ * Not a live population: for a crowd that spawns and dies, most of these
218
+ * rows may be reserved and empty at any moment, and the library has no
219
+ * notion of which (ADR-0022). The caller owns the indices, because the
220
+ * carrier already hands that numbering out.
221
+ */
211
222
  count: number;
212
223
  }
224
+ /**
225
+ * How a playback texture is sized when the instances it is handed are not the
226
+ * whole story.
227
+ */
228
+ interface VATPlaybackTextureOptions {
229
+ /**
230
+ * Rows to reserve — the crowd's ceiling, rather than its current population.
231
+ * Defaults to the number of instances given, which is a crowd placed once.
232
+ *
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
235
+ * in place, and growing one means rebuilding it and rebinding it on every
236
+ * patched material — `docs/usage.md` carries that recipe.
237
+ */
238
+ capacity?: number;
239
+ }
213
240
  /**
214
241
  * Write a crowd's instance playback into a new playback texture. Call once,
215
242
  * before rendering, and bind the result into the decode — `createVATMesh` does
216
243
  * both for you.
217
244
  *
218
245
  * The layout below is the shared contract, spelled once in {@link PACK_TEXELS}.
219
- * Both decode paths read exactly these three texels of row `instanceIndex` —
220
- * `DECODE_PRELUDE` in `src/webgl.ts` with `texelFetch`, `texturePlayback` in
221
- * `src/tsl.ts` with `textureLoad`.
246
+ * Both decode paths read exactly these texels of row `instanceIndex` —
247
+ * `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.
222
250
  *
223
- * | Texel | r | g | b | a |
224
- * | ---------------- | ------------------- | ------------ | ------------- | ------------- |
225
- * | `x = 0` clip | clip start row | clip frames | clip fps | speed |
226
- * | `x = 1` playback | start time | loop mode | repetitions | end mode |
227
- * | `x = 2` fade | from clip start row | from frames | from phase | fade duration |
251
+ * | Texel | r | g | b | a |
252
+ * | ------------------------- | -------------- | ----------- | ----------- | -------- |
253
+ * | `x = 0` clip | clip start row | clip frames | clip fps | speed |
254
+ * | `x = 1` playback | start time | loop mode | repetitions | end mode |
255
+ * | `x = 2` crossfade | fade duration | 0 | 0 | 0 |
256
+ * | `x = 3` outgoing clip | clip start row | clip frames | clip fps | speed |
257
+ * | `x = 4` outgoing playback | start time | loop mode | repetitions | end mode |
258
+ *
259
+ * The outgoing pair is a full playback state — the same two texels, in the same
260
+ * order, with the same meaning — because that is the whole difference between a
261
+ * freeze and a crossfade (ADR-0025). The crossfade texel's three spare
262
+ * components are written as zero and read by nothing.
228
263
  *
229
264
  * **A texture, not three instanced attributes.** An attribute with divisor 1 is
230
265
  * indexed by the *drawn slot*, and the drawn slot stops being the instance the
@@ -233,25 +268,35 @@ interface VATPlaybackTexture {
233
268
  * (ADR-0016). A row keyed by the logical index is what three itself does for
234
269
  * the same problem, in `_matricesTexture`.
235
270
  *
236
- * **Three texels, not thirteen floats.** The move to a texture touched no
271
+ * **RGBA-shaped texels, not loose floats.** The move to a texture touched no
237
272
  * decode arithmetic, because the layout did not change with it: the pack was
238
- * already three RGBA-shaped `vec4`s (ADR-0009). Published 1.x is the other
239
- * story — five one-float attributes there, so a 1.x caller meets both changes
240
- * at once.
273
+ * already RGBA-shaped `vec4`s (ADR-0009). Published 1.x is the other story —
274
+ * five one-float attributes there, so a 1.x caller meets both changes at once.
241
275
  *
242
276
  * **`FloatType`, and it stays that way.** A `startTime` in seconds does not
243
- * survive half precision — one second of resolution at 2 048 s — so a narrower
244
- * encoding for the VAT textures does not reach this one.
277
+ * survive half precision — one second of resolution at 2 048 s — and there are
278
+ * now two of them per row, so a narrower encoding for the VAT textures does not
279
+ * reach this one.
245
280
  *
246
281
  * The policy fields, and the clip texel's speed, come from the instance where
247
282
  * it names them and from the clip's baked defaults where it does not — resolved
248
283
  * in the one place those tiers are spelled — and both decode paths read them as
249
- * {@link resolveVATFrame} defines them. The fade texel is written as zeroes,
250
- * which is what "not fading" is: a crowd being created has no pose to fade away
251
- * from. Fades belong to {@link setVATInstance}, where an instance's animation
252
- * changes and there is something to fade out of.
284
+ * {@link resolveVATFrame} defines them. The crossfade texel and the outgoing
285
+ * pair are written as zeroes, which is what "not transitioning" is: a crowd
286
+ * being created has no animation to blend away from. Transitions belong to
287
+ * {@link setVATInstance}, where an instance's animation changes and there is
288
+ * something to blend out of.
289
+ *
290
+ * **A crowd that spawns and dies gives a capacity** instead of a census
291
+ * ({@link VATPlaybackTextureOptions}, ADR-0022): the rows are reserved once,
292
+ * from the ceiling, and filled with {@link setVATInstance} as instances appear.
293
+ * The list may then be empty — a level that starts with nothing alive in it —
294
+ * and the reserved rows hold a first frame, held. The library allocates no
295
+ * indices and follows no `setInstanceCount`: the carrier already numbers the
296
+ * instances, and a texture does not grow in place. `docs/usage.md` carries both,
297
+ * with **row recycling** — the hazard of reusing a row an instance has died on.
253
298
  */
254
- declare function createVATPlaybackTexture(instances: VATInstance[]): VATPlaybackTexture;
299
+ declare function createVATPlaybackTexture(instances: VATInstance[], options?: VATPlaybackTextureOptions): VATPlaybackTexture;
255
300
  /**
256
301
  * Change one instance's animation, after the crowd is built. The single write
257
302
  * the whole event-driven half of this library is made of: an enemy hit at
@@ -274,15 +319,21 @@ declare function createVATPlaybackTexture(instances: VATInstance[]): VATPlayback
274
319
  * one small write rather than a full re-upload. Everything else about the
275
320
  * instance — its matrix, its clip's defaults — is untouched. On the TSL path
276
321
  * the range is recorded and ignored: three's WebGPU backend re-uploads the
277
- * whole image on `needsUpdate`, which is 48 bytes per instance once per frame
322
+ * whole image on `needsUpdate`, which is 80 bytes per instance once per frame
278
323
  * in which anything changed (docs/usage.md says what that costs).
279
324
  *
280
- * Ask for a `fadeDuration` and the pose the instance is in *at `startTime`* is
281
- * frozen and blended away over that many wall-clock seconds, so the change does
282
- * not pop. It is a frozen pose and not a second playback: see
283
- * {@link MAX_FADE_DURATION} for what that costs and how far it can be pushed.
284
- * One pose, too — writing an instance that is *already* fading freezes the clip
285
- * it had switched to and drops the older pose, because the pack holds one.
325
+ * Ask for a `fadeDuration` and the animation the instance was playing **keeps
326
+ * playing**, blended away over that many wall-clock seconds from `startTime`,
327
+ * so the change is a transition rather than a pop (ADR-0025). Uncapped: a tenth
328
+ * of a second for a death, half a second for a walk into a run, and both clips
329
+ * move throughout. Zero, or none at all, is a cut.
330
+ *
331
+ * Two bands, and no more. Writing an instance that is *already* mid-transition
332
+ * replaces the outgoing band with the one it was switching to and drops the
333
+ * older band at whatever weight it still had — a pop proportional to how early
334
+ * the interruption came, and the one visible discontinuity a caller can
335
+ * produce. `startTime + fadeDuration` is when the transition ends, for a caller
336
+ * who would rather wait it out.
286
337
  *
287
338
  * A written instance is a pure function of the clock from here on, so what
288
339
  * happens *after* it is a matter of scheduling one more of these writes —
@@ -512,4 +563,4 @@ interface VATCrowd {
512
563
  */
513
564
  type VATCarrier = InstancedMesh | BatchedMesh;
514
565
 
515
- export { type DeltaVAT as D, EndMode as E, INFINITE_REPETITIONS as I, LoopMode as L, MAX_FADE_DURATION as M, type RigVAT as R, type VAT as V, type VATBase as a, type VATCarrier as b, type VATClip as c, type VATClipDefaults as d, type VATClock as e, type VATCrowd as f, type VATFadeFrom as g, type VATFrame as h, type VATInstance as i, type VATPlaybackTexture as j, createVATPlaybackTexture as k, endsAt as l, resolveVATFrame as r, setVATInstance as s };
566
+ export { type DeltaVAT as D, EndMode as E, INFINITE_REPETITIONS as I, LoopMode as L, type RigVAT as R, type VAT as V, type VATBase as a, type VATCarrier as b, type VATClip as c, type VATClipDefaults as d, type VATClock as e, type VATCrowd as f, type VATFrame as g, type VATInstance as h, type VATOutgoingFrame as i, type VATPlaybackState as j, type VATPlaybackTexture as k, type VATPlaybackTextureOptions as l, createVATPlaybackTexture as m, endsAt as n, resolveVATFrame as r, setVATInstance as s };
@@ -15,9 +15,11 @@ function makeVATTexture(data, width, height, type = FloatType) {
15
15
  var PACK_TEXELS = {
16
16
  clip: 0,
17
17
  playback: 1,
18
- fade: 2
18
+ crossfade: 2,
19
+ outgoingClip: 3,
20
+ outgoingPlayback: 4
19
21
  };
20
- var PACK_WIDTH = 3;
22
+ var PACK_WIDTH = 5;
21
23
  var PACK_STRIDE = PACK_WIDTH * 4;
22
24
  var LoopMode = {
23
25
  /** Play the clip end to end, forever (or `repetitions` times). */
@@ -34,7 +36,6 @@ var EndMode = {
34
36
  Rewind: 1
35
37
  };
36
38
  var INFINITE_REPETITIONS = -1;
37
- var MAX_FADE_DURATION = 0.25;
38
39
  function defaultRepetitions(loopMode) {
39
40
  return loopMode === LoopMode.Repeat ? INFINITE_REPETITIONS : 1;
40
41
  }
@@ -56,15 +57,12 @@ function resolvedPlaybackOf(instance) {
56
57
  speed: instance.speed ?? clip.speed ?? LIBRARY_PLAYBACK_DEFAULTS.speed
57
58
  };
58
59
  }
59
- function fadeOf(instance) {
60
- const duration = Math.min(instance.fadeDuration ?? 0, MAX_FADE_DURATION);
60
+ function crossfadeOf(instance) {
61
61
  const from = instance.from;
62
- if (!from || duration <= 0 || from.frames <= 0) return null;
63
- return { from, duration };
64
- }
65
- function fadeRowOf(from) {
66
- return from.startFrame + Math.max(Math.min(Math.floor(from.phase * from.frames), from.frames - 1), 0);
62
+ if (!from || !asksToBlend(instance)) return null;
63
+ return { from, duration: instance.fadeDuration };
67
64
  }
65
+ var asksToBlend = (instance) => (instance.fadeDuration ?? 0) > 0;
68
66
  function resolveVATFrame(instance, time) {
69
67
  const { clip } = instance;
70
68
  const frames = clip.frames;
@@ -95,8 +93,8 @@ function resolveVATFrame(instance, time) {
95
93
  const f0 = Math.min(Math.floor(f), last);
96
94
  const f1 = wraps ? (f0 + 1) % frames : Math.min(f0 + 1, last);
97
95
  const row = clip.startFrame + f0;
98
- const fading = fadeOf(instance);
99
- const elapsed = fading ? (time - instance.startTime) / fading.duration : 0;
96
+ const crossfade = crossfadeOf(instance);
97
+ const elapsed = crossfade ? (time - instance.startTime) / crossfade.duration : 0;
100
98
  return {
101
99
  row,
102
100
  rowNext: clip.startFrame + f1,
@@ -104,50 +102,85 @@ function resolveVATFrame(instance, time) {
104
102
  wraps,
105
103
  finished,
106
104
  phase,
107
- fadeRow: fading ? fadeRowOf(fading.from) : row,
108
- fadeWeight: fading ? 1 - Math.min(Math.max(elapsed, 0), 1) : 0
105
+ outgoing: crossfade ? { ...resolveVATFrame(crossfade.from, time), weight: 1 - Math.min(Math.max(elapsed, 0), 1) } : null
109
106
  };
110
107
  }
111
- function createVATPlaybackTexture(instances) {
112
- const count = instances.length;
108
+ var RESERVED_ROW = {
109
+ clip: { startFrame: 0, frames: 1, fps: 1 },
110
+ startTime: 0,
111
+ speed: 0
112
+ };
113
+ function createVATPlaybackTexture(instances, options = {}) {
114
+ const live = instances.length;
115
+ const count = options.capacity ?? live;
116
+ if (count < live) {
117
+ throw new Error(
118
+ `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.`
119
+ );
120
+ }
113
121
  if (count < 1) {
114
122
  throw new Error(
115
- "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"
123
+ "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"
116
124
  );
117
125
  }
118
126
  if (count > MAX_TEXTURE_SIZE) {
119
127
  throw new Error(
120
- `three-vat: a crowd of ${count} instances needs ${count} rows of playback texture, past the ${MAX_TEXTURE_SIZE}-row ceiling \u2014 the playback texture holds one row per instance, so MAX_TEXTURE_SIZE is the instance ceiling.`
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.`
121
129
  );
122
130
  }
123
131
  const data = new Float32Array(count * PACK_STRIDE);
124
- for (let i = 0; i < count; i++) writePack(data, i, instances[i]);
132
+ for (let i = 0; i < count; i++) writePack(data, i, instances[i] ?? RESERVED_ROW);
125
133
  return { texture: makeVATTexture(data, PACK_WIDTH, count), count };
126
134
  }
127
135
  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";
128
136
  var rowStart = (index) => index * PACK_STRIDE;
129
137
  var texelStart = (index, field) => rowStart(index) + field * 4;
130
- function writePack(data, index, instance) {
131
- const clip = texelStart(index, PACK_TEXELS.clip);
132
- const playback = texelStart(index, PACK_TEXELS.playback);
133
- const fade = texelStart(index, PACK_TEXELS.fade);
134
- const policy = resolvedPlaybackOf(instance);
138
+ function checkedPolicyOf(state, index, what) {
139
+ const policy = resolvedPlaybackOf(state);
135
140
  if (policy.speed < 0) {
136
- throw new Error(`three-vat: instance ${index} has speed ${policy.speed}; ${FORWARD_ONLY_REASON}.`);
141
+ throw new Error(`three-vat: ${what} ${index} has speed ${policy.speed}; ${FORWARD_ONLY_REASON}.`);
137
142
  }
138
- data[clip] = instance.clip.startFrame;
139
- data[clip + 1] = instance.clip.frames;
140
- data[clip + 2] = instance.clip.fps;
143
+ return policy;
144
+ }
145
+ function putBand(data, index, at, state, policy) {
146
+ const clip = texelStart(index, at.clip);
147
+ const playback = texelStart(index, at.playback);
148
+ data[clip] = state.clip.startFrame;
149
+ data[clip + 1] = state.clip.frames;
150
+ data[clip + 2] = state.clip.fps;
141
151
  data[clip + 3] = policy.speed;
142
- data[playback] = instance.startTime;
152
+ data[playback] = state.startTime;
143
153
  data[playback + 1] = policy.loopMode;
144
154
  data[playback + 2] = policy.repetitions;
145
155
  data[playback + 3] = policy.endMode;
146
- const fading = fadeOf(instance);
147
- data[fade] = fading ? fading.from.startFrame : 0;
148
- data[fade + 1] = fading ? fading.from.frames : 0;
149
- data[fade + 2] = fading ? fading.from.phase : 0;
150
- data[fade + 3] = fading ? fading.duration : 0;
156
+ }
157
+ var LIVE_PAIR = { clip: PACK_TEXELS.clip, playback: PACK_TEXELS.playback };
158
+ var OUTGOING_PAIR = { clip: PACK_TEXELS.outgoingClip, playback: PACK_TEXELS.outgoingPlayback };
159
+ function checkedCrossfadeOf(instance, index) {
160
+ const duration = instance.fadeDuration;
161
+ if (duration !== void 0 && !(Number.isFinite(duration) && duration >= 0)) {
162
+ throw new Error(
163
+ `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.`
164
+ );
165
+ }
166
+ return crossfadeOf(instance);
167
+ }
168
+ function writePack(data, index, instance) {
169
+ const crossfade = checkedCrossfadeOf(instance, index);
170
+ const live = checkedPolicyOf(instance, index, "instance");
171
+ const outgoing = crossfade ? { state: crossfade.from, policy: checkedPolicyOf(crossfade.from, index, "the outgoing band of instance") } : null;
172
+ const crossfadeTexel = texelStart(index, PACK_TEXELS.crossfade);
173
+ putBand(data, index, LIVE_PAIR, instance, live);
174
+ if (outgoing) putBand(data, index, OUTGOING_PAIR, outgoing.state, outgoing.policy);
175
+ else clearTexels(data, index, OUTGOING_PAIR);
176
+ data[crossfadeTexel] = crossfade ? crossfade.duration : 0;
177
+ data[crossfadeTexel + 1] = 0;
178
+ data[crossfadeTexel + 2] = 0;
179
+ data[crossfadeTexel + 3] = 0;
180
+ }
181
+ function clearTexels(data, index, at) {
182
+ data.fill(0, texelStart(index, at.clip), texelStart(index, at.clip) + 4);
183
+ data.fill(0, texelStart(index, at.playback), texelStart(index, at.playback) + 4);
151
184
  }
152
185
  function readPack(data, index) {
153
186
  const clip = texelStart(index, PACK_TEXELS.clip);
@@ -163,35 +196,17 @@ function readPack(data, index) {
163
196
  }
164
197
  function assertInstance(playback, index) {
165
198
  if (!Number.isInteger(index) || index < 0 || index >= playback.count) {
166
- throw new Error(`three-vat: instance ${index} is outside this crowd of ${playback.count}`);
199
+ throw new Error(`three-vat: instance ${index} is outside this crowd's ${playback.count} rows`);
167
200
  }
168
201
  }
169
202
  function setVATInstance(playback, index, instance) {
170
203
  assertInstance(playback, index);
171
204
  const data = playback.texture.image.data;
172
- const fading = instance.from === void 0 && (instance.fadeDuration ?? 0) > 0 ? { ...instance, from: freezeOf(data, index, instance.startTime) } : instance;
173
- writePack(data, index, fading);
205
+ const transitioning = instance.from === void 0 && asksToBlend(instance) ? { ...instance, from: readPack(data, index) } : instance;
206
+ writePack(data, index, transitioning);
174
207
  playback.texture.addUpdateRange(rowStart(index), PACK_STRIDE);
175
208
  playback.texture.needsUpdate = true;
176
209
  }
177
- function freezeOf(data, index, time) {
178
- const outgoing = readPack(data, index);
179
- const { row } = resolveVATFrame(outgoing, time);
180
- return {
181
- startFrame: outgoing.clip.startFrame,
182
- frames: outgoing.clip.frames,
183
- // The row the instance is actually displaying at that moment — so a fade
184
- // out of a finished one-shot freezes the end pose it was holding, not the
185
- // first row of a clip it stopped playing seconds ago.
186
- //
187
- // Named as the phase at the *centre* of that row rather than the playback
188
- // phase itself, because {@link fadeRowOf} is what reads it back and the two
189
- // do not spread a phase the same way: a bouncing ping-pong is up to a row
190
- // apart between them, and rounding in a shader could cost another. Half a
191
- // row of slack costs nothing and lands all three decodes on this row.
192
- phase: (row - outgoing.clip.startFrame + 0.5) / outgoing.clip.frames
193
- };
194
- }
195
210
  function endsAt(instance) {
196
211
  const { repetitions, speed } = resolvedPlaybackOf(instance);
197
212
  if (repetitions === INFINITE_REPETITIONS || speed <= 0) return null;
@@ -208,4 +223,4 @@ var RIG_TEXELS = {
208
223
  };
209
224
  var RIG_TEXELS_PER_SLOT = 2;
210
225
 
211
- export { EndMode, FORWARD_ONLY_REASON, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_FADE_DURATION, MAX_TEXTURE_SIZE, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance };
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 };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
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';
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';
4
4
 
5
5
  /**
6
6
  * What {@link bakeVAT} takes for each animation: the clip itself, or an
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { LoopMode, MAX_TEXTURE_SIZE, makeVATTexture, RIG_TEXELS_PER_SLOT, RIG_TEXELS, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS } from './chunk-3PWAY6MD.js';
2
- export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_FADE_DURATION, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-3PWAY6MD.js';
1
+ import { LoopMode, MAX_TEXTURE_SIZE, makeVATTexture, RIG_TEXELS_PER_SLOT, RIG_TEXELS, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS } from './chunk-J5IEUGSB.js';
2
+ export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-J5IEUGSB.js';
3
3
  import { LoopRepeat, LoopOnce, LoopPingPong, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, Quaternion, BufferGeometry, BufferAttribute, AdditiveAnimationBlendMode, PropertyBinding } from 'three';
4
4
 
5
5
  var LOOP_MODES = /* @__PURE__ */ new Map([