three-vat 3.0.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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-D4pkftSk.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, RIG_TEXELS_PER_SLOT } from './chunk-BSAJL5PG.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,6 +167,15 @@ function vatDecode(vat, options = {}) {
164
167
  }
165
168
  }
166
169
  }
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
+ }
167
179
  function vertexDecode({ positionTexture, normalTexture }, rows) {
168
180
  const vertexRow = int(vertexIndex);
169
181
  const band = (tex, of) => {
@@ -172,10 +184,16 @@ function vertexDecode({ positionTexture, normalTexture }, rows) {
172
184
  return mix(s0, s1, of.blend);
173
185
  };
174
186
  const sample = (tex) => mix(band(tex, rows.live), band(tex, rows.outgoing), rows.weight);
187
+ const bandNormal = (tex, of) => {
188
+ const s0 = octDecode(textureLoad(tex, ivec2(vertexRow, of.row0)).xy);
189
+ const s1 = octDecode(textureLoad(tex, ivec2(vertexRow, of.row1)).xy);
190
+ return mix(s0, s1, of.blend);
191
+ };
192
+ const sampleNormal = (tex) => mix(bandNormal(tex, rows.live), bandNormal(tex, rows.outgoing), rows.weight);
175
193
  return {
176
194
  encoding: "delta",
177
195
  position: sample(positionTexture),
178
- normal: normalTexture ? sample(normalTexture).normalize() : null
196
+ normal: normalTexture ? sampleNormal(normalTexture).normalize() : null
179
197
  };
180
198
  }
181
199
  function compose(q, ts) {
@@ -247,7 +265,7 @@ function rigDecode({ rigTexture, geometry }, rows) {
247
265
  }
248
266
  function createVATMesh(vat, instances, options = {}) {
249
267
  const time = options.time ?? uniform(0);
250
- const playback = createVATPlaybackTexture(instances);
268
+ const playback = createVATPlaybackTexture(instances, { maxTextureSize: options.maxTextureSize });
251
269
  for (const source of vat.materials) assertBakedNormal(vat, source);
252
270
  const materials = vat.materials.map((source) => source.clone());
253
271
  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-D4pkftSk.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 } from './chunk-BSAJL5PG.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 );
@@ -171,9 +180,9 @@ var VERTEX_PRELUDE = (
171
180
  `
172
181
  uniform highp sampler2D uVatPosTex;
173
182
 
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.
183
+ // One band of the position layer: the two rows this band sits between,
184
+ // mixed. The same function for the live band and the outgoing one, as
185
+ // vatBand is the same function for both pairs.
177
186
  vec3 vatBandSample( const in sampler2D tex, const in VatBand band ) {
178
187
  vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, band.row0 ), 0 ).xyz;
179
188
  vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, band.row1 ), 0 ).xyz;
@@ -203,6 +212,33 @@ var NORMAL_PRELUDE = (
203
212
  /* glsl */
204
213
  `
205
214
  uniform highp sampler2D uVatNrmTex;
215
+
216
+ vec2 vatOctSign( const in vec2 v ) {
217
+ return vec2( v.x >= 0.0 ? 1.0 : -1.0, v.y >= 0.0 ? 1.0 : -1.0 );
218
+ }
219
+
220
+ vec3 vatOctDecode( const in vec2 stored ) {
221
+ vec2 e = stored * 2.0 - 1.0;
222
+ float z = 1.0 - abs( e.x ) - abs( e.y );
223
+ vec2 xy = e - vatOctSign( e ) * max( -z, 0.0 );
224
+ return normalize( vec3( xy, z ) );
225
+ }
226
+
227
+ // One band of the normal layer \u2014 vatBandSample, over decoded normals.
228
+ vec3 vatBandSampleNormal( const in VatBand band ) {
229
+ vec3 s0 = vatOctDecode( texelFetch( uVatNrmTex, ivec2( gl_VertexID, band.row0 ), 0 ).xy );
230
+ vec3 s1 = vatOctDecode( texelFetch( uVatNrmTex, ivec2( gl_VertexID, band.row1 ), 0 ).xy );
231
+ return mix( s0, s1, band.blend );
232
+ }
233
+
234
+ // vatSample, for the one layer whose texel is not what it decodes to. The
235
+ // outgoing band is resolved and mixed unconditionally here too, for the
236
+ // reason spelled out on vatSample.
237
+ vec3 vatSampleNormal( const in int vatInstance ) {
238
+ VatRows rows = vatRows( vatInstance );
239
+ VatBand outgoing = vatOutgoingBand( vatInstance, rows.weight > 0.0 );
240
+ return mix( vatBandSampleNormal( rows.live ), vatBandSampleNormal( outgoing ), rows.weight );
241
+ }
206
242
  `
207
243
  );
208
244
  var RIG_PRELUDE = (
@@ -320,7 +356,7 @@ var vertexPosition = (id) => (
320
356
  var vertexNormal = (id) => (
321
357
  /* glsl */
322
358
  `
323
- vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
359
+ vec3 objectNormal = normalize( vatSampleNormal( ${INSTANCE_ID[id]} ) );
324
360
  #ifdef USE_TANGENT
325
361
  vec3 objectTangent = vec3( tangent.xyz );
326
362
  #endif
@@ -473,7 +509,7 @@ function createVATDepthMaterial(vat, uniforms, playback, options) {
473
509
  }
474
510
  function createVATMesh(vat, instances, options = {}) {
475
511
  const uniforms = options.time ? { uVatTime: options.time } : createVATUniforms();
476
- const playback = createVATPlaybackTexture(instances);
512
+ const playback = createVATPlaybackTexture(instances, { maxTextureSize: options.maxTextureSize });
477
513
  const patch = { hook: options.hook };
478
514
  const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback, patch));
479
515
  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.0.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",