three-vat 4.1.0 → 4.2.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/dist/tsl.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { Node } from 'three/webgpu';
2
- import { e as VATClock, k as VATPlaybackTexture, b as VATCarrier, V as VAT, h as VATInstance, f as VATCrowd } from './carrier-VLSJLgjX.js';
2
+ import { V as VATCarrier } from './carrier-DmKhB4QL.js';
3
+ import { V as VATClock, a as VATPlaybackTexture, b as VAT, c as VATInstance, d as VATCrowd } from './types-CDPXKe0_.js';
3
4
  import 'three';
4
5
 
5
6
  /**
package/dist/tsl.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isBatchedCarrier, assertVATCarrier, assertBakedNormal } from './chunk-I4STYOD5.js';
2
- import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS, RIG_TEXELS, vertexWidthOf, RIG_TEXELS_PER_SLOT } from './chunk-OEKKEHRQ.js';
2
+ import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS, RIG_TEXELS, vertexWidthOf, RIG_TEXELS_PER_SLOT } from './chunk-4IKOXUSX.js';
3
3
  import { InstancedMesh } from 'three';
4
4
  import { uniform, Fn, positionLocal, normalLocal, tangentLocal, positionGeometry, batch, instancedMesh, float, bool, int, vertexIndex, attribute, mat3, tangentGeometry, normalGeometry, vec4, batchIndirectIndex, instanceIndex, hash, mix, textureLoad, ivec2, dot, mat4, vec3 } from 'three/tsl';
5
5
 
@@ -20,6 +20,7 @@ function texturePlayback(texture, instance) {
20
20
  packTexel(texture, PACK_TEXELS.playback, instance)
21
21
  ),
22
22
  crossfadeDuration: crossfade.x,
23
+ crossfadeStart: crossfade.y,
23
24
  outgoing: outgoingBand(
24
25
  packTexel(texture, PACK_TEXELS.outgoingClip, instance),
25
26
  packTexel(texture, PACK_TEXELS.outgoingPlayback, instance)
@@ -73,7 +74,7 @@ function hashedPlayback(clip, desync, instance) {
73
74
  endMode: float(clip.endMode)
74
75
  }
75
76
  };
76
- return { live, crossfadeDuration: float(0), outgoing: live };
77
+ return { live, crossfadeDuration: float(0), crossfadeStart: live.playback.startTime, outgoing: live };
77
78
  }
78
79
  function vatNodes(vat, options = {}) {
79
80
  const { time = uniform(0), carrier } = options;
@@ -98,25 +99,27 @@ function vatNodes(vat, options = {}) {
98
99
  function resolveBand(clip, playback, time) {
99
100
  const frames = clip.frames;
100
101
  const last = frames.sub(1);
101
- const local = time.sub(playback.startTime).mul(clip.speed);
102
- const loops = local.div(clip.duration);
102
+ const reversed = clip.speed.lessThan(0);
103
+ const local = time.sub(playback.startTime).mul(clip.speed.abs());
104
+ const loops = local.max(0).div(clip.duration);
103
105
  const started = local.greaterThanEqual(0);
104
106
  const finished = started.and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(loops.greaterThanEqual(playback.repetitions));
105
107
  const isPingPong = playback.loopMode.equal(LoopMode.PingPong);
106
108
  const bounce = loops.sub(loops.mul(0.5).floor().mul(2));
107
109
  const pingPongPhase = bounce.lessThan(1).select(bounce, float(2).sub(bounce));
108
110
  const endPhase = playback.endMode.equal(EndMode.Clamp).select(float(1), float(0));
109
- const phase = started.select(
110
- finished.select(endPhase, isPingPong.select(pingPongPhase, loops.fract())),
111
- float(0)
112
- );
113
- const looping = started.select(
114
- finished.select(bool(false), isPingPong.select(bool(false), bool(true))),
115
- bool(false)
116
- );
117
- const holds = looping.and(playback.endMode.equal(EndMode.Clamp)).and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(loops.floor().add(1).greaterThanEqual(playback.repetitions));
111
+ const cascade = finished.select(endPhase, isPingPong.select(pingPongPhase, loops.fract()));
112
+ const looping = finished.select(bool(false), isPingPong.select(bool(false), bool(true)));
113
+ const forwardHolds = playback.endMode.equal(EndMode.Clamp).and(loops.floor().add(1).greaterThanEqual(playback.repetitions));
114
+ const holds = looping.and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(reversed.select(loops.lessThan(1), forwardHolds));
118
115
  const wraps = looping.and(holds.not());
119
- const f = phase.mul(looping.select(frames, last));
116
+ const mirrored = float(1).sub(cascade);
117
+ const phase = reversed.select(
118
+ wraps.and(mirrored.greaterThanEqual(1)).select(float(0), mirrored),
119
+ cascade
120
+ );
121
+ const spread = phase.mul(looping.select(frames, last));
122
+ const f = holds.select(spread.min(last), spread);
120
123
  const f0 = f.floor().min(last);
121
124
  const next = f0.add(1);
122
125
  const f1 = wraps.select(next.greaterThanEqual(frames).select(float(0), next), next.min(last));
@@ -135,7 +138,7 @@ function vatDecode(vat, options = {}) {
135
138
  const instance = instanceIdOf(carrier);
136
139
  const playback = playbackTexture ? texturePlayback(playbackTexture.texture, instance) : hashedPlayback(clipAt(vat, clipIndex), desync, instance);
137
140
  const live = resolveBand(playback.live.clip, playback.live.playback, time);
138
- const elapsed = time.sub(playback.live.playback.startTime);
141
+ const elapsed = time.sub(playback.crossfadeStart);
139
142
  const duration = playback.crossfadeDuration;
140
143
  const weight = duration.greaterThan(0).select(
141
144
  float(1).sub(elapsed.div(duration).clamp(0, 1)),
@@ -1,4 +1,4 @@
1
- import { DataTexture, BufferGeometry, Material, Box3, InstancedMesh, BatchedMesh } from 'three';
1
+ import { DataTexture, BufferGeometry, Material, Box3, InstancedMesh } from 'three';
2
2
 
3
3
  /**
4
4
  * How an instance repeats its clip — the numbers `THREE.LoopRepeat`,
@@ -70,10 +70,14 @@ interface VATPlaybackState {
70
70
  */
71
71
  startTime: number;
72
72
  /**
73
- * Playback rate multiplier, `>= 0`. Defaults to the clip's speed, then to
74
- * `1`. A negative rate is refused when the instance is written: a VAT plays
75
- * forward, and a clip that must run backwards is baked as a reversed clip.
76
- * `0` is legal — the instance holds its clip's first row.
73
+ * Playback rate multiplier. Defaults to the clip's speed, then to `1`.
74
+ *
75
+ * A negative rate plays the band backwards at its magnitude, with nothing
76
+ * baked for it (ADR-0033): the pose at every moment is the one forward
77
+ * playback shows at the mirrored point of the clip, so a reversed one-shot
78
+ * starts on its last frame, and the end modes read in the direction of play
79
+ * — `Clamp` holds the pose it stopped on, `Rewind` returns to the one it
80
+ * started from. `0` holds the clip's first row, and is not reversed.
77
81
  */
78
82
  speed?: number;
79
83
  /** How the clip repeats. Defaults to the clip's, then {@link LoopMode.Repeat}. */
@@ -114,7 +118,7 @@ interface VATInstance extends VATPlaybackState {
114
118
  from?: VATPlaybackState;
115
119
  /**
116
120
  * 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
121
+ * from {@link fadeStart}: the incoming clip's `speed` does not stretch a
118
122
  * transition.
119
123
  *
120
124
  * Zero, or absent, is a cut: no outgoing band is written. A negative or
@@ -124,6 +128,18 @@ interface VATInstance extends VATPlaybackState {
124
128
  * nothing to blend away from, so this is `setVATInstance`'s field in practice.
125
129
  */
126
130
  fadeDuration?: number;
131
+ /**
132
+ * The clock time, in seconds, at which the blend out of {@link from} began.
133
+ * Defaults to {@link startTime}, which is when every transition you write
134
+ * yourself begins, so you normally leave it out: it is normally written by a
135
+ * turn, whose live band's start time is fixed by pose continuity and cannot
136
+ * also place the blend (ADR-0036).
137
+ *
138
+ * Moves the weight and nothing else: both bands resolve exactly as they
139
+ * would without it. A non-finite start is refused when the instance is
140
+ * written.
141
+ */
142
+ fadeStart?: number;
127
143
  }
128
144
  /**
129
145
  * Where in its VAT an instance is at a given moment: the two frame rows to
@@ -139,10 +155,11 @@ interface VATFrame {
139
155
  mix: number;
140
156
  /**
141
157
  * Whether {@link rowNext} crossed the clip's last row back into its first.
142
- * True only while a clip is genuinely looping: a ping-pong bounces rather
158
+ * True only while a clip is genuinely looping, or scheduled to start doing so: a ping-pong bounces rather
143
159
  * than wraps, and a finished one-shot must not wrap at all or the corpse
144
160
  * stands back up for a frame — nor, across its final repetition, may any
145
- * clip that ends by clamping (#88).
161
+ * clip that ends by clamping (#88), nor a reversed finite play across its
162
+ * first interval (ADR-0033).
146
163
  */
147
164
  wraps: boolean;
148
165
  /** Whether the repetitions have run out and the instance is holding an end pose. */
@@ -171,8 +188,9 @@ interface VATOutgoingFrame extends VATFrame {
171
188
  /**
172
189
  * How much of this band is still showing: `1` at the moment of the write,
173
190
  * falling to `0` across `fadeDuration`, and `0` once the transition is over.
174
- * Wall clock — `1 - clamp((time - startTime) / fadeDuration, 0, 1)` — so a
175
- * half-speed incoming clip does not stretch the transition.
191
+ * Wall clock — `1 - clamp((time - fadeStart) / fadeDuration, 0, 1)`, where
192
+ * `fadeStart` defaults to the live `startTime` — so a half-speed incoming
193
+ * clip does not stretch the transition.
176
194
  */
177
195
  weight: number;
178
196
  }
@@ -261,14 +279,16 @@ interface VATPlaybackTextureOptions {
261
279
  * | ------------------------- | -------------- | ----------- | ----------- | -------- |
262
280
  * | `x = 0` clip | clip start row | clip frames | clip fps | speed |
263
281
  * | `x = 1` playback | start time | loop mode | repetitions | end mode |
264
- * | `x = 2` crossfade | fade duration | 0 | 0 | 0 |
282
+ * | `x = 2` crossfade | fade duration | fade start | 0 | 0 |
265
283
  * | `x = 3` outgoing clip | clip start row | clip frames | clip fps | speed |
266
284
  * | `x = 4` outgoing playback | start time | loop mode | repetitions | end mode |
267
285
  *
268
286
  * The outgoing pair is a full playback state — the same two texels, in the same
269
287
  * order, with the same meaning — because that is the whole difference between a
270
- * freeze and a crossfade (ADR-0025). The crossfade texel's three spare
271
- * components are written as zero and read by nothing.
288
+ * freeze and a crossfade (ADR-0025). The crossfade texel's `g` is the blend
289
+ * start, the instance's `fadeStart` or else its start time, so every write
290
+ * fills it (ADR-0036); its two spare components are written as zero and read
291
+ * by nothing.
272
292
  *
273
293
  * **A texture, not three instanced attributes.** An attribute with divisor 1 is
274
294
  * indexed by the *drawn slot*, and the drawn slot stops being the instance the
@@ -290,9 +310,10 @@ interface VATPlaybackTextureOptions {
290
310
  * The policy fields, and the clip texel's speed, come from the instance where
291
311
  * it names them and from the clip's baked defaults where it does not — resolved
292
312
  * in the one place those tiers are spelled — and both decode paths read them as
293
- * {@link resolveVATFrame} defines them. The crossfade texel and the outgoing
313
+ * {@link resolveVATFrame} defines them. The crossfade duration and the outgoing
294
314
  * pair are written as zeroes, which is what "not transitioning" is: a crowd
295
- * being created has no animation to blend away from. Transitions belong to
315
+ * being created has no animation to blend away from. The blend start beside
316
+ * the duration is filled anyway, as on every write. Transitions belong to
296
317
  * {@link setVATInstance}, where an instance's animation changes and there is
297
318
  * something to blend out of.
298
319
  *
@@ -332,8 +353,9 @@ declare function createVATPlaybackTexture(instances: VATInstance[], options?: VA
332
353
  * in which anything changed (docs/usage.md says what that costs).
333
354
  *
334
355
  * Ask for a `fadeDuration` and the animation the instance was playing **keeps
335
- * playing**, blended away over that many wall-clock seconds from `startTime`,
336
- * so the change is a transition rather than a pop (ADR-0025). Uncapped: a tenth
356
+ * playing**, blended away over that many wall-clock seconds from `startTime`
357
+ * (or from a `fadeStart`, which a turn writes), so the change is a transition
358
+ * rather than a pop (ADR-0025, ADR-0036). Uncapped: a tenth
337
359
  * of a second for a death, half a second for a walk into a run, and both clips
338
360
  * move throughout. Zero, or none at all, is a cut.
339
361
  *
@@ -341,8 +363,9 @@ declare function createVATPlaybackTexture(instances: VATInstance[], options?: VA
341
363
  * replaces the outgoing band with the one it was switching to and drops the
342
364
  * older band at whatever weight it still had — a pop proportional to how early
343
365
  * the interruption came, and the one visible discontinuity a caller can
344
- * produce. `startTime + fadeDuration` is when the transition ends, for a caller
345
- * who would rather wait it out.
366
+ * produce. `startTime + fadeDuration` is when the transition ends — or
367
+ * `fadeStart + fadeDuration`, where one is written — for a caller who would
368
+ * rather wait it out.
346
369
  *
347
370
  * A written instance is a pure function of the clock from here on, so what
348
371
  * happens *after* it is a matter of scheduling one more of these writes —
@@ -355,10 +378,62 @@ declare function createVATPlaybackTexture(instances: VATInstance[], options?: VA
355
378
  * to — and the playback texture is the object such a caller holds (ADR-0016).
356
379
  */
357
380
  declare function setVATInstance(playback: VATPlaybackTexture, index: number, instance: VATInstance): void;
381
+ /**
382
+ * Turn one instance round at the pose it is showing (ADR-0036): from `time`
383
+ * on, it retraces its path, showing at `time + x` the pose it showed at
384
+ * `time − x`. A walker backs up; a door halfway open closes from halfway.
385
+ *
386
+ * ```ts
387
+ * // the moment the player lets go — and nothing per frame afterwards
388
+ * const back = turnVATInstance(playback, doorId, time.value)
389
+ * const shut = endsAt(back) // when it is closed again, or null if endless
390
+ * ```
391
+ *
392
+ * The row is read back out of the playback texture, so there is no CPU copy
393
+ * of the instance to keep. The turned instance is written through the same
394
+ * pack writer as {@link setVATInstance}, only its row is flagged for upload,
395
+ * and it is returned so {@link endsAt} can schedule what comes next.
396
+ *
397
+ * - A play with a count runs back to where it began and holds that pose. It
398
+ * is written with `Clamp` whatever its end mode, because a `Rewind` would
399
+ * snap back to the pose it turned at — so a second turn gives back the path
400
+ * but not a `Rewind`, which the pack does not remember.
401
+ * - An endless play has no beginning, only a desync start time, and retraces
402
+ * endlessly.
403
+ * - A turn retraces motion, never waiting. A finished play turns from the
404
+ * moment it finished, not from the end of its hold — and from the pose its
405
+ * path finished on, which is not the one `Clamp` holds for a fractional
406
+ * count or an even ping-pong, so those jump back onto their path. A play finished on the
407
+ * pose it started from (`Rewind`), or still waiting for its start time, has
408
+ * nothing to retrace and holds where it is, for good.
409
+ * - Direction is the way the pose is moving. For a ping-pong the turn chooses
410
+ * the sign, the count and the start time that reproduce the retraced path,
411
+ * an even count included. The speed's magnitude is kept.
412
+ *
413
+ * Distinct from reverse playback, a negative `speed` written with
414
+ * {@link setVATInstance}, which plays a clip backwards from its start rather
415
+ * than from where the instance is.
416
+ *
417
+ * A turn does not ease: the pose is continuous and the velocity reverses at
418
+ * once, as three's `timeScale = -timeScale` does.
419
+ *
420
+ * An instance mid-crossfade retraces the blend too (#120). The weight flows
421
+ * back toward the clip it was leaving: that clip, retraced, is the live band
422
+ * of what the turn writes, the clip it was entering, retraced, is its `from`,
423
+ * and a `fadeStart` places the blend to end when the retrace passes the
424
+ * moment the original began. From then on it plays the clip it was leaving,
425
+ * retraced, alone, and `endsAt` answers for that. Within the blend both bands
426
+ * are mirrored whole, so a band that had finished on `Clamp` holds as long
427
+ * again before it retraces. Not a band finished on `Rewind`, which holds its
428
+ * start pose for good, nor one clamped off its path, which jumps back onto
429
+ * it, both as they would alone. A transition already over is dropped.
430
+ */
431
+ declare function turnVATInstance(playback: VATPlaybackTexture, index: number, time: number): VATInstance;
358
432
  /**
359
433
  * The exact clock time this instance stops animating — when
360
434
  * {@link resolveVATFrame} first reports `finished` — or `null` for an animation
361
- * that never gets there: an endless loop, or a speed of zero.
435
+ * that never gets there: an endless loop, or a speed of zero. The same moment
436
+ * for `speed: -1` as for `speed: 1`.
362
437
  *
363
438
  * This is what makes chaining one clip to the next a single scheduled write
364
439
  * rather than a per-frame poll:
@@ -412,9 +487,9 @@ interface VATClipDefaults {
412
487
  */
413
488
  endMode: EndMode;
414
489
  /**
415
- * Playback rate, from the action's `timeScale`. `>= 0`: a band is sampled
416
- * forward from its own first row, so a negative rate is refused at the bake
417
- * rather than held on that row for ever. `0` is a held first row, on purpose.
490
+ * Playback rate, from the action's `timeScale`. A negative rate plays the
491
+ * band backwards, as it does on an instance (ADR-0033). `0` is a held first
492
+ * row, on purpose.
418
493
  */
419
494
  speed: number;
420
495
  }
@@ -605,14 +680,4 @@ interface VATCrowd {
605
680
  playback: VATPlaybackTexture;
606
681
  }
607
682
 
608
- /**
609
- * A mesh a VAT crowd can ride.
610
- *
611
- * `InstancedMesh` is what `createVATMesh` builds on either path.
612
- * `BatchedMesh` is reached through the primitives, and buys per-instance
613
- * frustum culling and depth sorting from three.js itself — see
614
- * docs/usage.md, which also says what it does *not* buy.
615
- */
616
- type VATCarrier = InstancedMesh | BatchedMesh;
617
-
618
- 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 };
683
+ export { type DeltaVAT as D, EndMode as E, INFINITE_REPETITIONS as I, LoopMode as L, type RigVAT as R, type VATClock as V, type VATPlaybackTexture as a, type VAT as b, type VATInstance as c, type VATCrowd as d, type VATBase as e, type VATClip as f, type VATClipDefaults as g, type VATFrame as h, type VATOutgoingFrame as i, type VATPlaybackState as j, type VATPlaybackTextureOptions as k, createVATPlaybackTexture as l, endsAt as m, resolveVATFrame as r, setVATInstance as s, turnVATInstance as t };
package/dist/webgl.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { IUniform, MeshDepthMaterial, WebGLRenderer, Material } from 'three';
2
- import { b as VATCarrier, V as VAT, k as VATPlaybackTexture, h as VATInstance, f as VATCrowd } from './carrier-VLSJLgjX.js';
2
+ import { V as VATCarrier } from './carrier-DmKhB4QL.js';
3
+ import { b as VAT, a as VATPlaybackTexture, c as VATInstance, d as VATCrowd } from './types-CDPXKe0_.js';
3
4
 
4
5
  /**
5
6
  * The real maximum texture dimension this GPU accepts, for
package/dist/webgl.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { assertBakedNormal, assertVATCarrier, isBatchedCarrier } from './chunk-I4STYOD5.js';
2
- import { PACK_TEXELS, EndMode, LoopMode, RIG_TEXELS_PER_SLOT, RIG_TEXELS, INFINITE_REPETITIONS, createVATPlaybackTexture, vertexWidthOf } from './chunk-OEKKEHRQ.js';
2
+ import { PACK_TEXELS, EndMode, LoopMode, RIG_TEXELS_PER_SLOT, RIG_TEXELS, INFINITE_REPETITIONS, createVATPlaybackTexture, vertexWidthOf } from './chunk-4IKOXUSX.js';
3
3
  import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial, Material } from 'three';
4
4
 
5
5
  function getMaxTextureSize(renderer) {
@@ -40,7 +40,7 @@ var ROW_PRELUDE = (
40
40
  };
41
41
 
42
42
  // resolveVATFrame for one (clip texel, playback texel) pair, branch for
43
- // branch: not started, finished, ping-pong, repeat \u2014 the resolver's own
43
+ // branch: finished, ping-pong, repeat \u2014 the resolver's own
44
44
  // order, each case falling out into the shared phase-to-row arithmetic below
45
45
  // rather than returning early, so all of them land on the same two rows.
46
46
  //
@@ -53,9 +53,12 @@ var ROW_PRELUDE = (
53
53
  float duration = frames / vatClip.z;
54
54
  // Local time: how far into its own animation this instance is. A start time
55
55
  // in the past is what desyncs a crowd; a start time in the future has not
56
- // begun, which is not the same thing as having finished.
57
- float local = ( uVatTime - vatPlayback.x ) * vatClip.w;
58
- float loops = local / duration;
56
+ // begun, which is not the same thing as having finished. The speed's sign is
57
+ // the direction and its magnitude the rate (ADR-0033).
58
+ bool reversed = vatClip.w < 0.0;
59
+ float local = ( uVatTime - vatPlayback.x ) * abs( vatClip.w );
60
+ // Not yet started sits on the pose its start time will show.
61
+ float loops = max( local, 0.0 ) / duration;
59
62
  float repetitions = vatPlayback.z;
60
63
 
61
64
  bool started = local >= 0.0;
@@ -63,10 +66,7 @@ var ROW_PRELUDE = (
63
66
 
64
67
  float phase;
65
68
  bool looping;
66
- if ( !started ) {
67
- phase = 0.0;
68
- looping = false;
69
- } else if ( finished ) {
69
+ if ( finished ) {
70
70
  // Held at an end pose, and in neither case sampling past it.
71
71
  phase = vatPlayback.w == ${glslFloat(EndMode.Clamp)} ? 1.0 : 0.0;
72
72
  looping = false;
@@ -81,11 +81,20 @@ var ROW_PRELUDE = (
81
81
  }
82
82
 
83
83
  // Except across the final repetition of a clip that clamps, which holds its
84
- // last row rather than blending back toward its first (#88).
85
- bool holds = looping && vatPlayback.w == ${glslFloat(EndMode.Clamp)} && repetitions != ${glslFloat(INFINITE_REPETITIONS)} && floor( loops ) + 1.0 >= repetitions;
84
+ // last row rather than blending back toward its first (#88) \u2014 and, reversed,
85
+ // across the first interval of a play that runs out.
86
+ bool holds = looping && repetitions != ${glslFloat(INFINITE_REPETITIONS)} && ( reversed ? loops < 1.0 : vatPlayback.w == ${glslFloat(EndMode.Clamp)} && floor( loops ) + 1.0 >= repetitions );
86
87
  bool wraps = looping && !holds;
87
88
 
88
- float f = phase * ( looping ? frames : last );
89
+ // The mirror, as a select on the phase and never a branch: every instance
90
+ // pays for it, reversed or not (#72). A mirrored 1 across the seam is the
91
+ // seam, the first row again.
92
+ float mirrored = 1.0 - phase;
93
+ phase = reversed ? ( wraps && mirrored >= 1.0 ? 0.0 : mirrored ) : phase;
94
+
95
+ // A held interval sits on the last row, with nothing to blend toward.
96
+ float spread = phase * ( looping ? frames : last );
97
+ float f = holds ? min( spread, last ) : spread;
89
98
  float f0 = min( floor( f ), last );
90
99
  // A compare, not a mod: a mod divides, and at the last row a quotient a hair
91
100
  // under 1 leaves f1 at frames, one row past the band (#79).
@@ -137,10 +146,12 @@ var ROW_PRELUDE = (
137
146
  // The crossfade's weight, transcribed from the resolver: wall clock, not
138
147
  // clip time \u2014 the incoming clip's speed does not stretch a transition \u2014 and
139
148
  // a duration of zero is what a cut is, which is what the pack writes when
140
- // there is no band to blend away.
149
+ // there is no band to blend away. Measured from the blend start the same
150
+ // texel carries in g, which is the live start time unless a turn placed it
151
+ // (ADR-0036): one component swapped for another, so no fetch and no branch.
141
152
  rows.weight = 0.0;
142
153
  if ( vatCrossfade.x > 0.0 ) {
143
- rows.weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatCrossfade.x, 0.0, 1.0 );
154
+ rows.weight = 1.0 - clamp( ( uVatTime - vatCrossfade.y ) / vatCrossfade.x, 0.0, 1.0 );
144
155
  }
145
156
 
146
157
  return rows;
@@ -0,0 +1,53 @@
1
+ import { b as VAT } from './types-CDPXKe0_.js';
2
+ import { Texture } from 'three';
3
+ import { GLTFParser } from 'three/examples/jsm/loaders/GLTFLoader.js';
4
+
5
+ /** One image of the source file, as the file held it. */
6
+ interface SourceImage {
7
+ bytes: Uint8Array;
8
+ mimeType: string;
9
+ name?: string;
10
+ }
11
+ /**
12
+ * How a source texture reaches its image: through its own `source`, through
13
+ * one of the {@link IMAGE_EXTENSIONS}, or both, one the fallback of the other. Copied through
14
+ * in the same shape, so a KTX2 texture stays KTX2.
15
+ */
16
+ interface SourceTexture {
17
+ source?: SourceImage;
18
+ extensions: Record<string, SourceImage>;
19
+ /** The image-format extensions the source file required. */
20
+ required: string[];
21
+ }
22
+ /** Each source texture's images, keyed by the image a loaded texture holds, which its clones share. */
23
+ type SourceImages = Map<Texture['source'], SourceTexture>;
24
+ /**
25
+ * How a `.gltf`'s image file beside it is read: its bytes, now or later, for
26
+ * the URI exactly as the `.gltf` writes it.
27
+ */
28
+ type ReadImageFile = (uri: string) => ArrayBuffer | Promise<ArrayBuffer>;
29
+ /**
30
+ * Every texture the loaded materials hold, keyed by its image, with that
31
+ * image's bytes as the file holds them, for {@link writeBakedFile} to copy
32
+ * into the baked file as they are (ADR-0034). `parser` is the loaded glTF's
33
+ * own (`gltf.parser`): an image in the binary chunk is read through its
34
+ * `getDependency('bufferView', i)`, and a `data:` URI is decoded where it
35
+ * stands. An image in a file beside a `.gltf` is read through `readFile`,
36
+ * which by default fetches it from where the loader found the `.gltf`.
37
+ */
38
+ declare function readSourceImages(parser: GLTFParser, readFile?: ReadImageFile): Promise<SourceImages>;
39
+
40
+ /**
41
+ * Write `vat` as a baked file. Resolves to the `.glb`'s bytes. In Node, a
42
+ * `FileReader` has to be installed first (src/file-reader.ts): the exporter
43
+ * reads its own output back through one.
44
+ *
45
+ * A textured material's images are copied from `images`, the source file's
46
+ * own bytes (src/write-materials.ts). A rig-encoded VAT is written with a
47
+ * preview skin, and only as `bakeVAT` returned it ({@link previewSkin}).
48
+ */
49
+ declare function writeBakedFile(vat: VAT, { images }?: {
50
+ images?: SourceImages;
51
+ }): Promise<Uint8Array>;
52
+
53
+ export { type ReadImageFile, type SourceImage, type SourceImages, type SourceTexture, readSourceImages, writeBakedFile };
package/dist/write.js ADDED
@@ -0,0 +1,3 @@
1
+ export { readSourceImages, writeBakedFile } from './chunk-7TT7B5WS.js';
2
+ import './chunk-P6AFXEBT.js';
3
+ import './chunk-4IKOXUSX.js';
package/package.json CHANGED
@@ -1,81 +1,88 @@
1
- {
2
- "name": "three-vat",
3
- "version": "4.1.0",
4
- "description": "Bake glTF animation clips into vertex animation textures for zero-CPU instanced crowds in three.js.",
5
- "type": "module",
6
- "license": "MIT",
7
- "author": "Mike Fernandez <mike.fernandez.sec@gmail.com>",
8
- "repository": {
9
- "type": "git",
10
- "url": "git+https://github.com/MikeFernandez-Pro/three-vat.git"
11
- },
12
- "bugs": {
13
- "url": "https://github.com/MikeFernandez-Pro/three-vat/issues"
14
- },
15
- "homepage": "https://github.com/MikeFernandez-Pro/three-vat#readme",
16
- "publishConfig": {
17
- "access": "public"
18
- },
19
- "packageManager": "pnpm@10.19.0",
20
- "sideEffects": false,
21
- "files": [
22
- "dist"
23
- ],
24
- "exports": {
25
- ".": {
26
- "types": "./dist/index.d.ts",
27
- "import": "./dist/index.js"
28
- },
29
- "./webgl": {
30
- "types": "./dist/webgl.d.ts",
31
- "import": "./dist/webgl.js"
32
- },
33
- "./tsl": {
34
- "types": "./dist/tsl.d.ts",
35
- "import": "./dist/tsl.js"
36
- }
37
- },
38
- "scripts": {
39
- "dev": "pnpm --filter three-vat-example dev",
40
- "test": "vitest run && pnpm --filter three-vat-example test",
41
- "build": "tsup",
42
- "typecheck": "tsc --noEmit && tsc --noEmit -p release/tsconfig.json && pnpm --filter three-vat-example typecheck",
43
- "build:watch": "tsup --watch",
44
- "test:watch": "vitest",
45
- "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
46
- },
47
- "keywords": [
48
- "three",
49
- "threejs",
50
- "vat",
51
- "vertex-animation-texture",
52
- "instancing",
53
- "instancedmesh",
54
- "crowd",
55
- "animation",
56
- "skinning",
57
- "webgpu",
58
- "tsl"
59
- ],
60
- "peerDependencies": {
61
- "three": ">=0.186.0"
62
- },
63
- "devDependencies": {
64
- "@types/three": "^0.186.0",
65
- "gifenc": "1.0.3",
66
- "playwright-core": "1.63.0",
67
- "pngjs": "7.0.0",
68
- "three": "^0.186.0",
69
- "tsup": "^8.3.0",
70
- "typescript": "^5.6.0",
71
- "vite": "^7.1.0",
72
- "vitest": "^2.1.0"
73
- },
74
- "engines": {
75
- "node": ">=20"
76
- },
77
- "workspaces": [
78
- ".",
79
- "examples"
80
- ]
81
- }
1
+ {
2
+ "name": "three-vat",
3
+ "version": "4.2.0",
4
+ "description": "Bake glTF animation clips into vertex animation textures for zero-CPU instanced crowds in three.js.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Mike Fernandez <mike.fernandez.sec@gmail.com>",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/MikeFernandez-Pro/three-vat.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/MikeFernandez-Pro/three-vat/issues"
14
+ },
15
+ "homepage": "https://github.com/MikeFernandez-Pro/three-vat#readme",
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "packageManager": "pnpm@10.19.0",
20
+ "sideEffects": false,
21
+ "bin": {
22
+ "three-vat": "./dist/bin.js"
23
+ },
24
+ "files": [
25
+ "dist"
26
+ ],
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./webgl": {
33
+ "types": "./dist/webgl.d.ts",
34
+ "import": "./dist/webgl.js"
35
+ },
36
+ "./tsl": {
37
+ "types": "./dist/tsl.d.ts",
38
+ "import": "./dist/tsl.js"
39
+ },
40
+ "./write": {
41
+ "types": "./dist/write.d.ts",
42
+ "import": "./dist/write.js"
43
+ }
44
+ },
45
+ "scripts": {
46
+ "dev": "pnpm --filter three-vat-example dev",
47
+ "test": "vitest run && pnpm --filter three-vat-example test",
48
+ "build": "tsup",
49
+ "typecheck": "tsc --noEmit && tsc --noEmit -p release/tsconfig.json && pnpm --filter three-vat-example typecheck",
50
+ "build:watch": "tsup --watch",
51
+ "test:watch": "vitest",
52
+ "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
53
+ },
54
+ "keywords": [
55
+ "three",
56
+ "threejs",
57
+ "vat",
58
+ "vertex-animation-texture",
59
+ "instancing",
60
+ "instancedmesh",
61
+ "crowd",
62
+ "animation",
63
+ "skinning",
64
+ "webgpu",
65
+ "tsl"
66
+ ],
67
+ "peerDependencies": {
68
+ "three": ">=0.186.0"
69
+ },
70
+ "devDependencies": {
71
+ "@types/three": "^0.186.0",
72
+ "gifenc": "1.0.3",
73
+ "playwright-core": "1.63.0",
74
+ "pngjs": "7.0.0",
75
+ "three": "^0.186.0",
76
+ "tsup": "^8.3.0",
77
+ "typescript": "^5.6.0",
78
+ "vite": "^7.1.0",
79
+ "vitest": "^2.1.0"
80
+ },
81
+ "engines": {
82
+ "node": ">=20"
83
+ },
84
+ "workspaces": [
85
+ ".",
86
+ "examples"
87
+ ]
88
+ }