three-vat 3.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tsl.d.ts CHANGED
@@ -1,5 +1,5 @@
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-BxRuW47G.js';
2
+ import { e as VATClock, k as VATPlaybackTexture, b as VATCarrier, V as VAT, h as VATInstance, f as VATCrowd } from './carrier-VLSJLgjX.js';
3
3
  import 'three';
4
4
 
5
5
  /**
@@ -225,6 +225,12 @@ interface CreateVATMeshOptions {
225
225
  * call make its own is identical on either path.
226
226
  */
227
227
  time?: VATTimeUniform;
228
+ /**
229
+ * The GPU's real texture ceiling — {@link getMaxTextureSize} — which the
230
+ * playback texture checks the crowd against, one row per instance. Defaults
231
+ * to `MAX_TEXTURE_SIZE`, a desktop figure (`VATPlaybackTextureOptions`).
232
+ */
233
+ maxTextureSize?: number;
228
234
  }
229
235
  /**
230
236
  * Turn a baked VAT and a list of instances into a crowd ready to render: an
package/dist/tsl.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { isBatchedCarrier, assertVATCarrier, assertBakedNormal } from './chunk-I4STYOD5.js';
2
- import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT } from './chunk-J5IEUGSB.js';
2
+ import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS, RIG_TEXELS, vertexWidthOf, RIG_TEXELS_PER_SLOT } from './chunk-OEKKEHRQ.js';
3
3
  import { InstancedMesh } from 'three';
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 } from 'three/tsl';
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
 
6
6
  function getMaxTextureSize(renderer) {
7
7
  const backend = renderer.backend;
@@ -103,20 +103,23 @@ function resolveBand(clip, playback, time) {
103
103
  const started = local.greaterThanEqual(0);
104
104
  const finished = started.and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(loops.greaterThanEqual(playback.repetitions));
105
105
  const isPingPong = playback.loopMode.equal(LoopMode.PingPong);
106
- const bounce = loops.mod(2);
106
+ const bounce = loops.sub(loops.mul(0.5).floor().mul(2));
107
107
  const pingPongPhase = bounce.lessThan(1).select(bounce, float(2).sub(bounce));
108
108
  const endPhase = playback.endMode.equal(EndMode.Clamp).select(float(1), float(0));
109
109
  const phase = started.select(
110
110
  finished.select(endPhase, isPingPong.select(pingPongPhase, loops.fract())),
111
111
  float(0)
112
112
  );
113
- const wraps = started.select(
113
+ const looping = started.select(
114
114
  finished.select(bool(false), isPingPong.select(bool(false), bool(true))),
115
115
  bool(false)
116
116
  );
117
- const f = phase.mul(wraps.select(frames, last));
117
+ const holds = looping.and(playback.endMode.equal(EndMode.Clamp)).and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(loops.floor().add(1).greaterThanEqual(playback.repetitions));
118
+ const wraps = looping.and(holds.not());
119
+ const f = phase.mul(looping.select(frames, last));
118
120
  const f0 = f.floor().min(last);
119
- const f1 = wraps.select(f0.add(1).mod(frames), f0.add(1).min(last));
121
+ const next = f0.add(1);
122
+ const f1 = wraps.select(next.greaterThanEqual(frames).select(float(0), next), next.min(last));
120
123
  const bandRow = (offset) => int(offset).add(clip.startFrame);
121
124
  return {
122
125
  row0: bandRow(f0),
@@ -164,18 +167,50 @@ function vatDecode(vat, options = {}) {
164
167
  }
165
168
  }
166
169
  }
167
- function vertexDecode({ positionTexture, normalTexture }, rows) {
170
+ function octDecode(stored) {
171
+ const e = stored.mul(2).sub(1);
172
+ const ex = e.x;
173
+ const ey = e.y;
174
+ const z = float(1).sub(ex.abs()).sub(ey.abs());
175
+ const overshoot = z.negate().max(0);
176
+ const back = (component) => component.sub(component.greaterThanEqual(0).select(overshoot, overshoot.negate()));
177
+ return vec3(back(ex), back(ey), z).normalize();
178
+ }
179
+ function texelOf(vat, vertexRow) {
180
+ const { rowsPerFrame } = vat;
181
+ if (rowsPerFrame === 1) return { column: vertexRow, rowOf: (row) => row };
182
+ const width = int(vertexWidthOf(vat));
183
+ const line = vertexRow.div(width);
184
+ const spanned = /* @__PURE__ */ new Map();
185
+ return {
186
+ column: vertexRow.mod(width),
187
+ rowOf: (row) => {
188
+ let at = spanned.get(row);
189
+ if (!at) spanned.set(row, at = row.mul(int(rowsPerFrame)).add(line));
190
+ return at;
191
+ }
192
+ };
193
+ }
194
+ function vertexDecode(vat, rows) {
195
+ const { positionTexture, normalTexture } = vat;
168
196
  const vertexRow = int(vertexIndex);
197
+ const { column, rowOf } = texelOf(vat, vertexRow);
169
198
  const band = (tex, of) => {
170
- const s0 = textureLoad(tex, ivec2(vertexRow, of.row0)).xyz;
171
- const s1 = textureLoad(tex, ivec2(vertexRow, of.row1)).xyz;
199
+ const s0 = textureLoad(tex, ivec2(column, rowOf(of.row0))).xyz;
200
+ const s1 = textureLoad(tex, ivec2(column, rowOf(of.row1))).xyz;
172
201
  return mix(s0, s1, of.blend);
173
202
  };
174
203
  const sample = (tex) => mix(band(tex, rows.live), band(tex, rows.outgoing), rows.weight);
204
+ const bandNormal = (tex, of) => {
205
+ const s0 = octDecode(textureLoad(tex, ivec2(column, rowOf(of.row0))).xy);
206
+ const s1 = octDecode(textureLoad(tex, ivec2(column, rowOf(of.row1))).xy);
207
+ return mix(s0, s1, of.blend);
208
+ };
209
+ const sampleNormal = (tex) => mix(bandNormal(tex, rows.live), bandNormal(tex, rows.outgoing), rows.weight);
175
210
  return {
176
211
  encoding: "delta",
177
212
  position: sample(positionTexture),
178
- normal: normalTexture ? sample(normalTexture).normalize() : null
213
+ normal: normalTexture ? sampleNormal(normalTexture).normalize() : null
179
214
  };
180
215
  }
181
216
  function compose(q, ts) {
@@ -247,7 +282,7 @@ function rigDecode({ rigTexture, geometry }, rows) {
247
282
  }
248
283
  function createVATMesh(vat, instances, options = {}) {
249
284
  const time = options.time ?? uniform(0);
250
- const playback = createVATPlaybackTexture(instances);
285
+ const playback = createVATPlaybackTexture(instances, { maxTextureSize: options.maxTextureSize });
251
286
  for (const source of vat.materials) assertBakedNormal(vat, source);
252
287
  const materials = vat.materials.map((source) => source.clone());
253
288
  const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
package/dist/webgl.d.ts CHANGED
@@ -1,5 +1,5 @@
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-BxRuW47G.js';
2
+ import { b as VATCarrier, V as VAT, k as VATPlaybackTexture, h as VATInstance, f as VATCrowd } from './carrier-VLSJLgjX.js';
3
3
 
4
4
  /**
5
5
  * The real maximum texture dimension this GPU accepts, for
@@ -137,6 +137,12 @@ interface CreateVATMeshOptions {
137
137
  * untwisted shadow.
138
138
  */
139
139
  hook?: VATPostDecodeHook;
140
+ /**
141
+ * The GPU's real texture ceiling — {@link getMaxTextureSize} — which the
142
+ * playback texture checks the crowd against, one row per instance. Defaults
143
+ * to `MAX_TEXTURE_SIZE`, a desktop figure (`VATPlaybackTextureOptions`).
144
+ */
145
+ maxTextureSize?: number;
140
146
  }
141
147
  /**
142
148
  * Turn a baked VAT and a list of instances into a crowd ready to render: an
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 } from './chunk-J5IEUGSB.js';
2
+ import { PACK_TEXELS, EndMode, LoopMode, RIG_TEXELS_PER_SLOT, RIG_TEXELS, INFINITE_REPETITIONS, createVATPlaybackTexture, vertexWidthOf } from './chunk-OEKKEHRQ.js';
3
3
  import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial, Material } from 'three';
4
4
 
5
5
  function getMaxTextureSize(renderer) {
@@ -62,26 +62,35 @@ var ROW_PRELUDE = (
62
62
  bool finished = started && repetitions != ${glslFloat(INFINITE_REPETITIONS)} && loops >= repetitions;
63
63
 
64
64
  float phase;
65
- bool wraps;
65
+ bool looping;
66
66
  if ( !started ) {
67
67
  phase = 0.0;
68
- wraps = false;
68
+ looping = false;
69
69
  } else 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
- wraps = false;
72
+ looping = false;
73
73
  } else if ( vatPlayback.y == ${glslFloat(LoopMode.PingPong)} ) {
74
- float m = mod( loops, 2.0 );
74
+ // Halved rather than divided, so the remainder cannot land below zero.
75
+ float m = loops - 2.0 * floor( loops * 0.5 );
75
76
  phase = m < 1.0 ? m : 2.0 - m;
76
- wraps = false; // a ping-pong bounces; it does not wrap
77
+ looping = false; // a ping-pong bounces; it does not wrap
77
78
  } else {
78
79
  phase = fract( loops );
79
- wraps = true; // and here the interpolation crossing back is correct
80
+ looping = true; // and here the interpolation crossing back is correct
80
81
  }
81
82
 
82
- float f = phase * ( wraps ? frames : last );
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;
86
+ bool wraps = looping && !holds;
87
+
88
+ float f = phase * ( looping ? frames : last );
83
89
  float f0 = min( floor( f ), last );
84
- float f1 = wraps ? mod( f0 + 1.0, frames ) : min( f0 + 1.0, last );
90
+ // A compare, not a mod: a mod divides, and at the last row a quotient a hair
91
+ // under 1 leaves f1 at frames, one row past the band (#79).
92
+ float next = f0 + 1.0;
93
+ float f1 = wraps ? ( next >= frames ? 0.0 : next ) : min( next, last );
85
94
 
86
95
  VatBand band;
87
96
  band.row0 = int( vatClip.x + f0 );
@@ -112,10 +121,10 @@ var ROW_PRELUDE = (
112
121
  // normal decode. Every vertex of an instance reads the same texels of the
113
122
  // same row, whichever branch it takes, so the texture cache absorbs them;
114
123
  // the demo bench under ADR-0016's 5% bound is what said so for three of
115
- // them, and ADR-0025's is what says so for a transitioning instance's five.
124
+ // them, and #72's idle-crowd bench is what says so for all five.
116
125
  //
117
- // Three texels, always \u2014 the crossfade one sits third exactly so that an
118
- // instance that is not transitioning reads what it has always read.
126
+ // Three texels here, the live pair and the crossfade; the other two are
127
+ // vatOutgoingBand's, fetched every frame by every sampler that calls it.
119
128
  vec4 vatClip = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.clip}, vatInstance ), 0 );
120
129
  vec4 vatPlayback = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.playback}, vatInstance ), 0 );
121
130
  vec4 vatCrossfade = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.crossfade}, vatInstance ), 0 );
@@ -166,17 +175,23 @@ var ROW_PRELUDE = (
166
175
  }
167
176
  `
168
177
  );
169
- var VERTEX_PRELUDE = (
178
+ function texelOf(vat) {
179
+ const { rowsPerFrame } = vat;
180
+ if (rowsPerFrame === 1) return (row) => `ivec2( gl_VertexID, ${row} )`;
181
+ const width = vertexWidthOf(vat);
182
+ return (row) => `ivec2( gl_VertexID % ${width}, ${row} * ${rowsPerFrame} + gl_VertexID / ${width} )`;
183
+ }
184
+ var vertexPrelude = (texel) => (
170
185
  /* glsl */
171
186
  `
172
187
  uniform highp sampler2D uVatPosTex;
173
188
 
174
- // One band of one layer: the two rows this band sits between, mixed. The
175
- // same function for the live band and the outgoing one, as vatBand is the same
176
- // function for both pairs.
189
+ // One band of the position layer: the two rows this band sits between,
190
+ // mixed. The same function for the live band and the outgoing one, as
191
+ // vatBand is the same function for both pairs.
177
192
  vec3 vatBandSample( const in sampler2D tex, const in VatBand band ) {
178
- vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, band.row0 ), 0 ).xyz;
179
- vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, band.row1 ), 0 ).xyz;
193
+ vec3 s0 = texelFetch( tex, ${texel("band.row0")}, 0 ).xyz;
194
+ vec3 s1 = texelFetch( tex, ${texel("band.row1")}, 0 ).xyz;
180
195
  return mix( s0, s1, band.blend );
181
196
  }
182
197
 
@@ -199,10 +214,37 @@ var VERTEX_PRELUDE = (
199
214
  }
200
215
  `
201
216
  );
202
- var NORMAL_PRELUDE = (
217
+ var normalPrelude = (texel) => (
203
218
  /* glsl */
204
219
  `
205
220
  uniform highp sampler2D uVatNrmTex;
221
+
222
+ vec2 vatOctSign( const in vec2 v ) {
223
+ return vec2( v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0 );
224
+ }
225
+
226
+ vec3 vatOctDecode( const in vec2 stored ) {
227
+ vec2 e = stored * 2.0 - 1.0;
228
+ float z = 1.0 - abs( e.x ) - abs( e.y );
229
+ vec2 xy = e - vatOctSign( e ) * max( -z, 0.0 );
230
+ return normalize( vec3( xy, z ) );
231
+ }
232
+
233
+ // One band of the normal layer \u2014 vatBandSample, over decoded normals.
234
+ vec3 vatBandSampleNormal( const in VatBand band ) {
235
+ vec3 s0 = vatOctDecode( texelFetch( uVatNrmTex, ${texel("band.row0")}, 0 ).xy );
236
+ vec3 s1 = vatOctDecode( texelFetch( uVatNrmTex, ${texel("band.row1")}, 0 ).xy );
237
+ return mix( s0, s1, band.blend );
238
+ }
239
+
240
+ // vatSample, for the one layer whose texel is not what it decodes to. The
241
+ // outgoing band is resolved and mixed unconditionally here too, for the
242
+ // reason spelled out on vatSample.
243
+ vec3 vatSampleNormal( const in int vatInstance ) {
244
+ VatRows rows = vatRows( vatInstance );
245
+ VatBand outgoing = vatOutgoingBand( vatInstance, rows.weight > 0.0 );
246
+ return mix( vatBandSampleNormal( rows.live ), vatBandSampleNormal( outgoing ), rows.weight );
247
+ }
206
248
  `
207
249
  );
208
250
  var RIG_PRELUDE = (
@@ -320,7 +362,7 @@ var vertexPosition = (id) => (
320
362
  var vertexNormal = (id) => (
321
363
  /* glsl */
322
364
  `
323
- vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
365
+ vec3 objectNormal = normalize( vatSampleNormal( ${INSTANCE_ID[id]} ) );
324
366
  #ifdef USE_TANGENT
325
367
  vec3 objectTangent = vec3( tangent.xyz );
326
368
  #endif
@@ -343,9 +385,12 @@ var rigNormal = (id) => (
343
385
  #endif
344
386
  `
345
387
  );
346
- function vertexDecode({ positionTexture, normalTexture }, id) {
388
+ function vertexDecode(vat, id) {
389
+ const { positionTexture, normalTexture, rowsPerFrame } = vat;
390
+ const texel = texelOf(vat);
391
+ const span = rowsPerFrame === 1 ? "" : `:rows${rowsPerFrame}x${vertexWidthOf(vat)}`;
347
392
  return {
348
- prelude: (normalTexture ? NORMAL_PRELUDE : "") + VERTEX_PRELUDE,
393
+ prelude: (normalTexture ? normalPrelude(texel) : "") + vertexPrelude(texel),
349
394
  bind(uniforms) {
350
395
  uniforms.uVatPosTex = { value: positionTexture };
351
396
  if (normalTexture) uniforms.uVatNrmTex = { value: normalTexture };
@@ -358,7 +403,7 @@ function vertexDecode({ positionTexture, normalTexture }, id) {
358
403
  normal: normalTexture ? vertexNormal(id) : null,
359
404
  // A normal-less VAT injects a different vertex shader off the same material
360
405
  // parameters, so the variant is in the key (see `patchVATMaterial`).
361
- key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}`,
406
+ key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}${span}`,
362
407
  instanceIndex: INSTANCE_ID[id]
363
408
  };
364
409
  }
@@ -473,7 +518,7 @@ function createVATDepthMaterial(vat, uniforms, playback, options) {
473
518
  }
474
519
  function createVATMesh(vat, instances, options = {}) {
475
520
  const uniforms = options.time ? { uVatTime: options.time } : createVATUniforms();
476
- const playback = createVATPlaybackTexture(instances);
521
+ const playback = createVATPlaybackTexture(instances, { maxTextureSize: options.maxTextureSize });
477
522
  const patch = { hook: options.hook };
478
523
  const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback, patch));
479
524
  const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-vat",
3
- "version": "3.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Bake glTF animation clips into vertex animation textures for zero-CPU instanced crowds in three.js.",
5
5
  "type": "module",
6
6
  "license": "MIT",