three-vat 2.0.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.
@@ -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;
@@ -199,4 +214,13 @@ function endsAt(instance) {
199
214
  return instance.startTime + duration * repetitions / speed;
200
215
  }
201
216
 
202
- export { EndMode, FORWARD_ONLY_REASON, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_FADE_DURATION, MAX_TEXTURE_SIZE, PACK_TEXELS, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance };
217
+ // src/rig-texture.ts
218
+ var RIG_TEXELS = {
219
+ /** `(qx, qy, qz, qw)` — the slot's rotation, on one hemisphere with its neighbouring rows. */
220
+ rotation: 0,
221
+ /** `(tx, ty, tz, s)` — where the slot puts the origin, and its uniform scale in the spare component. */
222
+ placement: 1
223
+ };
224
+ var RIG_TEXELS_PER_SLOT = 2;
225
+
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 { V as VAT } from './carrier-BFCPmcQK.js';
3
- export { E as EndMode, I as INFINITE_REPETITIONS, L as LoopMode, M as MAX_FADE_DURATION, a as VATCarrier, b as VATClip, c as VATClipDefaults, d as VATClock, e as VATCrowd, f as VATFadeFrom, g as VATFrame, h as VATInstance, i as VATPlaybackTexture, j as createVATPlaybackTexture, k as endsAt, r as resolveVATFrame, s as setVATInstance } from './carrier-BFCPmcQK.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
@@ -37,8 +37,29 @@ interface BakeOptions {
37
37
  * Anything else that shades — a smooth-shaded lit material — would light the
38
38
  * crowd by its rest-pose normals, which is visibly wrong (ADR-0002). Both
39
39
  * decode paths refuse that pairing loudly rather than render it.
40
+ *
41
+ * Accepted and ignored under the rig encoding, which has no normal texture to
42
+ * drop: normals come out of the skin matrix there. Refusing a no-op would
43
+ * punish the caller who switched encodings and left their options alone.
40
44
  */
41
45
  bakeNormals?: boolean;
46
+ /**
47
+ * What a frame row holds (ADR-0018). Default `'delta'`.
48
+ *
49
+ * - **`'delta'`** — the vertex encoding: where every vertex ended up, as a
50
+ * position delta and a normal. Source-agnostic — skinning, morph targets
51
+ * and node animation alike — and the default.
52
+ * - **`'rig'`** — the rig encoding: the posed rig, one **slot** per bone as a
53
+ * rotation, a translation and a uniform scale, skinned in the vertex shader
54
+ * from the rest-pose geometry. Two orders of magnitude smaller, no vertex
55
+ * ceiling, and unable to store what a rig cannot express — refused at the
56
+ * bake by name, before a frame is sampled: a morph target whose influence a
57
+ * baked clip animates, or a bone (or rigid part) scaled unevenly. A morph
58
+ * influence no clip animates is folded into the rest pose; a rigid,
59
+ * node-animated part is one slot of weight one; parts reading the same
60
+ * bones through the same bind matrix share slots.
61
+ */
62
+ encoding?: 'delta' | 'rig';
42
63
  }
43
64
  /**
44
65
  * Bake animations into a VAT by sampling the posed subtree frame by frame on
@@ -89,8 +110,22 @@ interface BakeOptions {
89
110
  * approximation becomes visible, so the bake warns once, naming the bone.
90
111
  * Renderer-agnostic — touches no WebGL/WebGPU context — so it runs identically
91
112
  * at runtime, in a Web Worker, and in Node.
113
+ *
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.
92
121
  */
93
- declare function bakeVAT(root: Object3D, animations: BakeInput[], { fps, maxTextureSize, bakeNormals }?: BakeOptions): VAT;
122
+ declare function bakeVAT(root: Object3D, animations: BakeInput[], options?: BakeOptions & {
123
+ encoding?: 'delta';
124
+ }): DeltaVAT;
125
+ declare function bakeVAT(root: Object3D, animations: BakeInput[], options: BakeOptions & {
126
+ encoding: 'rig';
127
+ }): RigVAT;
128
+ declare function bakeVAT(root: Object3D, animations: BakeInput[], options?: BakeOptions): VAT;
94
129
 
95
130
  /**
96
131
  * The WebGL2 *spec floor for high-end desktop*, which two things lean on.
@@ -117,4 +152,4 @@ declare const MAX_TEXTURE_SIZE = 16384;
117
152
  */
118
153
  declare function makeVATTexture(data: TypedArray, width: number, height: number, type?: TextureDataType): DataTexture;
119
154
 
120
- export { type BakeInput, type BakeOptions, MAX_TEXTURE_SIZE, VAT, bakeVAT, makeVATTexture };
155
+ export { type BakeInput, type BakeOptions, DeltaVAT, MAX_TEXTURE_SIZE, RigVAT, VAT, bakeVAT, makeVATTexture };