three-vat 2.1.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/webgl.js CHANGED
@@ -1,6 +1,6 @@
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-3PWAY6MD.js';
3
- import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
2
+ import { PACK_TEXELS, EndMode, LoopMode, RIG_TEXELS_PER_SLOT, RIG_TEXELS, INFINITE_REPETITIONS, createVATPlaybackTexture } from './chunk-BSAJL5PG.js';
3
+ import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial, Material } from 'three';
4
4
 
5
5
  function getMaxTextureSize(renderer) {
6
6
  return renderer.capabilities.maxTextureSize;
@@ -15,40 +15,39 @@ var ROW_PRELUDE = (
15
15
  uniform highp sampler2D uVatPlaybackTex;
16
16
  uniform float uVatTime;
17
17
 
18
- // Where an instance is reading: the two rows of its band it sits between and
19
- // the blend toward the second; and the pose-freeze fade's frozen row and
20
- // weight, a weight of zero being "not fading".
21
- struct VatRows {
18
+ // One band resolved: the two rows an instance sits between and the blend
19
+ // toward the second, plus the two facts those rows cannot be read back out
20
+ // of \u2014 whether the sampling wrapped past the band's last row into its first,
21
+ // and whether the repetitions have run out.
22
+ struct VatBand {
22
23
  int row0;
23
24
  int row1;
24
25
  float blend;
25
- int fadeRow;
26
- float fadeWeight;
26
+ bool wraps;
27
+ bool finished;
27
28
  };
28
29
 
29
- VatRows vatRows( const in int vatInstance ) {
30
- // The pack, fetched by this instance's *logical* index rather than read
31
- // off an attribute indexed by the drawn slot (ADR-0016). Three texels of
32
- // one row, in the order src/instance-playback.ts lays them out; the
33
- // arithmetic below is untouched by where they came from, because the pack
34
- // was already three vec4s.
35
- //
36
- // The index arrives as a parameter rather than being read here, because
37
- // where it comes from is the carrier's business and not the decode's:
38
- // gl_InstanceID on an InstancedMesh, getIndirectIndex( gl_DrawID ) on a
39
- // BatchedMesh \u2014 see INSTANCE_ID in src/webgl.ts. It also has to be a
40
- // parameter: getIndirectIndex is declared by batching_pars_vertex, which
41
- // three expands *after* this prelude, so naming it up here would not
42
- // compile.
43
- //
44
- // Fetched inside the function, so each injection point stays
45
- // self-contained (ADR-0006) \u2014 which costs a second set of fetches in the
46
- // normal decode. Every vertex of an instance reads the same three texels,
47
- // so the texture cache absorbs them; the 5% demo bench is what says so.
48
- vec4 vatClip = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.clip}, vatInstance ), 0 );
49
- vec4 vatPlayback = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.playback}, vatInstance ), 0 );
50
- vec4 vatFade = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.fade}, vatInstance ), 0 );
30
+ // Where an instance is reading: the band it is playing, and how much of the
31
+ // band it is leaving still shows \u2014 a weight of zero being "not
32
+ // transitioning".
33
+ //
34
+ // The outgoing band is *not* a field here. Each sampler resolves it for
35
+ // itself, because what it costs to resolve is not the same on the two
36
+ // encodings and #72 measured the difference: see vatOutgoingBand below.
37
+ struct VatRows {
38
+ VatBand live;
39
+ float weight;
40
+ };
51
41
 
42
+ // resolveVATFrame for one (clip texel, playback texel) pair, branch for
43
+ // branch: not started, finished, ping-pong, repeat \u2014 the resolver's own
44
+ // order, each case falling out into the shared phase-to-row arithmetic below
45
+ // rather than returning early, so all of them land on the same two rows.
46
+ //
47
+ // A function of the pair rather than of the instance, because the pair is
48
+ // what there are two of: a crossfading instance resolves its outgoing band
49
+ // by calling this a second time, not by transcribing it a second time.
50
+ VatBand vatBand( const in vec4 vatClip, const in vec4 vatPlayback ) {
52
51
  float frames = vatClip.y;
53
52
  float last = frames - 1.0;
54
53
  float duration = frames / vatClip.z;
@@ -63,65 +62,149 @@ var ROW_PRELUDE = (
63
62
  bool finished = started && repetitions != ${glslFloat(INFINITE_REPETITIONS)} && loops >= repetitions;
64
63
 
65
64
  float phase;
66
- bool wraps;
65
+ bool looping;
67
66
  if ( !started ) {
68
67
  phase = 0.0;
69
- wraps = false;
68
+ looping = false;
70
69
  } else if ( finished ) {
71
70
  // Held at an end pose, and in neither case sampling past it.
72
71
  phase = vatPlayback.w == ${glslFloat(EndMode.Clamp)} ? 1.0 : 0.0;
73
- wraps = false;
72
+ looping = false;
74
73
  } else if ( vatPlayback.y == ${glslFloat(LoopMode.PingPong)} ) {
75
- 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 );
76
76
  phase = m < 1.0 ? m : 2.0 - m;
77
- wraps = false; // a ping-pong bounces; it does not wrap
77
+ looping = false; // a ping-pong bounces; it does not wrap
78
78
  } else {
79
79
  phase = fract( loops );
80
- wraps = true; // and here the interpolation crossing back is correct
80
+ looping = true; // and here the interpolation crossing back is correct
81
81
  }
82
82
 
83
- 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 );
84
89
  float f0 = min( floor( f ), last );
85
- 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 );
94
+
95
+ VatBand band;
96
+ band.row0 = int( vatClip.x + f0 );
97
+ band.row1 = int( vatClip.x + f1 );
98
+ band.blend = f - f0;
99
+ band.wraps = wraps;
100
+ band.finished = finished;
101
+ return band;
102
+ }
103
+
104
+ VatRows vatRows( const in int vatInstance ) {
105
+ // The pack, fetched by this instance's *logical* index rather than read
106
+ // off an attribute indexed by the drawn slot (ADR-0016). Three texels of
107
+ // one row, in the order src/instance-playback.ts lays them out; the
108
+ // arithmetic above is untouched by where they came from, because the pack
109
+ // was already three vec4s.
110
+ //
111
+ // The index arrives as a parameter rather than being read here, because
112
+ // where it comes from is the carrier's business and not the decode's:
113
+ // gl_InstanceID on an InstancedMesh, getIndirectIndex( gl_DrawID ) on a
114
+ // BatchedMesh \u2014 see INSTANCE_ID in src/webgl.ts. It also has to be a
115
+ // parameter: getIndirectIndex is declared by batching_pars_vertex, which
116
+ // three expands *after* this prelude, so naming it up here would not
117
+ // compile.
118
+ //
119
+ // Fetched inside the function, so each injection point stays
120
+ // self-contained (ADR-0006) \u2014 which costs a second set of fetches in the
121
+ // normal decode. Every vertex of an instance reads the same texels of the
122
+ // same row, whichever branch it takes, so the texture cache absorbs them;
123
+ // the demo bench under ADR-0016's 5% bound is what said so for three of
124
+ // them, and #72's idle-crowd bench is what says so for all five.
125
+ //
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.
128
+ vec4 vatClip = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.clip}, vatInstance ), 0 );
129
+ vec4 vatPlayback = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.playback}, vatInstance ), 0 );
130
+ vec4 vatCrossfade = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.crossfade}, vatInstance ), 0 );
86
131
 
87
132
  VatRows rows;
88
- rows.row0 = int( vatClip.x + f0 );
89
- rows.row1 = int( vatClip.x + f1 );
90
- rows.blend = f - f0;
133
+ // The live band: the one clip this instance is playing, resolved from its
134
+ // own pair of texels.
135
+ rows.live = vatBand( vatClip, vatPlayback );
91
136
 
92
- // The pose-freeze fade, transcribed from the same resolver: one frozen row
93
- // of the clip this instance was playing when it changed, blended away over
94
- // vatFade.w. Wall clock, not clip time \u2014 the incoming clip's speed does
95
- // not stretch a fade. A duration of zero is what "not fading" is, and the
96
- // pack never writes one without a band to go with it.
97
- rows.fadeRow = 0;
98
- rows.fadeWeight = 0.0;
99
- if ( vatFade.w > 0.0 ) {
100
- float weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatFade.w, 0.0, 1.0 );
101
- float fromRow = max( min( floor( vatFade.z * vatFade.y ), vatFade.y - 1.0 ), 0.0 );
102
- rows.fadeRow = int( vatFade.x + fromRow );
103
- rows.fadeWeight = weight;
137
+ // The crossfade's weight, transcribed from the resolver: wall clock, not
138
+ // clip time \u2014 the incoming clip's speed does not stretch a transition \u2014 and
139
+ // a duration of zero is what a cut is, which is what the pack writes when
140
+ // there is no band to blend away.
141
+ rows.weight = 0.0;
142
+ if ( vatCrossfade.x > 0.0 ) {
143
+ rows.weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatCrossfade.x, 0.0, 1.0 );
104
144
  }
145
+
105
146
  return rows;
106
147
  }
148
+
149
+ // The band the instance is leaving: two more texels and a second call of the
150
+ // very same resolver, so the clip it is leaving keeps playing \u2014 keeping its
151
+ // own speed and its own end policy \u2014 rather than standing still (ADR-0025).
152
+ //
153
+ // While the weight is zero the pair selected *is* the live pair, so the two
154
+ // fetches land on texels this vertex has already read and the band resolves
155
+ // to the one it is playing. That is what lets a caller resolve it without a
156
+ // branch, and blending a pose into itself is what a weight of zero means.
157
+ //
158
+ // Selecting the live pair is also why this needs no counterpart to the TSL
159
+ // path's max-of-one on the outgoing frames and fps (src/tsl.ts): that path
160
+ // resolves a band from the zeroes "not transitioning" is written as, which is
161
+ // a 0/0 duration and a NaN row. The pair selected here is always real.
162
+ //
163
+ // Whether the instance is transitioning is the parameter, not the weight, so
164
+ // the caller is the one that says so \u2014 and the two encodings say it from
165
+ // different places (#72): the vertex sampler calls this unconditionally,
166
+ // because guarding two texel fetches cost an idle crowd 10%; the rig sampler
167
+ // calls it unconditionally too but guards what it *does* with the band, where
168
+ // what a guard skips is sixteen dependent fetches per vertex.
169
+ VatBand vatOutgoingBand( const in int vatInstance, const in bool transitioning ) {
170
+ int clipX = transitioning ? ${PACK_TEXELS.outgoingClip} : ${PACK_TEXELS.clip};
171
+ int playbackX = transitioning ? ${PACK_TEXELS.outgoingPlayback} : ${PACK_TEXELS.playback};
172
+ vec4 vatOutClip = texelFetch( uVatPlaybackTex, ivec2( clipX, vatInstance ), 0 );
173
+ vec4 vatOutPlayback = texelFetch( uVatPlaybackTex, ivec2( playbackX, vatInstance ), 0 );
174
+ return vatBand( vatOutClip, vatOutPlayback );
175
+ }
107
176
  `
108
177
  );
109
178
  var VERTEX_PRELUDE = (
110
179
  /* glsl */
111
180
  `
112
181
  uniform highp sampler2D uVatPosTex;
182
+
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.
186
+ vec3 vatBandSample( const in sampler2D tex, const in VatBand band ) {
187
+ vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, band.row0 ), 0 ).xyz;
188
+ vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, band.row1 ), 0 ).xyz;
189
+ return mix( s0, s1, band.blend );
190
+ }
191
+
113
192
  vec3 vatSample( const in sampler2D tex, const in int vatInstance ) {
114
193
  VatRows rows = vatRows( vatInstance );
115
- vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, rows.row0 ), 0 ).xyz;
116
- vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, rows.row1 ), 0 ).xyz;
117
- vec3 sampled = mix( s0, s1, rows.blend );
118
- // The frozen row, blended in by the weight the rows resolved.
119
- float weight = rows.fadeWeight;
120
- if ( weight > 0.0 ) {
121
- vec3 frozen = texelFetch( tex, ivec2( gl_VertexID, rows.fadeRow ), 0 ).xyz;
122
- sampled = mix( sampled, frozen, weight );
123
- }
124
- return sampled;
194
+ // The outgoing band \u2014 still playing, two rows of its own \u2014 mixed in by the
195
+ // weight the rows resolved, and mixed in *unconditionally*: at a weight of
196
+ // zero the band resolved is the live one, so this blends a pose into
197
+ // itself. The normal layer is renormalised by the caller after the mix, as
198
+ // it is for a single band.
199
+ //
200
+ // No branch, and that is measured rather than reasoned (#72): here the
201
+ // outgoing band is two more fetches of one layer, and guarding them cost an
202
+ // idle crowd 10% where paying them costs nothing measurable \u2014 0.283 ms
203
+ // against the 0.282 ms it cost before the crossfade existed. A branch is
204
+ // not free because it is not taken: the compiler still holds registers for
205
+ // the side it skips, and that is what an idle crowd was paying for.
206
+ VatBand outgoing = vatOutgoingBand( vatInstance, rows.weight > 0.0 );
207
+ return mix( vatBandSample( tex, rows.live ), vatBandSample( tex, outgoing ), rows.weight );
125
208
  }
126
209
  `
127
210
  );
@@ -129,6 +212,33 @@ var NORMAL_PRELUDE = (
129
212
  /* glsl */
130
213
  `
131
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
+ }
132
242
  `
133
243
  );
134
244
  var RIG_PRELUDE = (
@@ -155,34 +265,58 @@ var RIG_PRELUDE = (
155
265
  );
156
266
  }
157
267
 
158
- // One slot's matrix between the two rows the instance sits between \u2014 and
159
- // through the fade, blended before it is composed.
160
- mat4 vatSlot( const in int slot, const in VatRows rows ) {
161
- int rotation = slot * ${RIG_TEXELS_PER_SLOT} + ${RIG_TEXELS.rotation};
162
- int placement = slot * ${RIG_TEXELS_PER_SLOT} + ${RIG_TEXELS.placement};
163
- vec4 q0 = texelFetch( uVatRigTex, ivec2( rotation, rows.row0 ), 0 );
164
- vec4 ts0 = texelFetch( uVatRigTex, ivec2( placement, rows.row0 ), 0 );
165
- vec4 q1 = texelFetch( uVatRigTex, ivec2( rotation, rows.row1 ), 0 );
166
- vec4 ts1 = texelFetch( uVatRigTex, ivec2( placement, rows.row1 ), 0 );
268
+ // One slot of the posed rig, at one band: a rotation and a placement, each
269
+ // between the two rows that band sits between. The same function for the live
270
+ // band and the outgoing one, as vatBand is the same function for both pairs.
271
+ struct VatPose {
272
+ vec4 q;
273
+ vec4 ts;
274
+ };
275
+
276
+ VatPose vatSlotPose( const in int rotation, const in int placement, const in VatBand band ) {
277
+ vec4 q0 = texelFetch( uVatRigTex, ivec2( rotation, band.row0 ), 0 );
278
+ vec4 ts0 = texelFetch( uVatRigTex, ivec2( placement, band.row0 ), 0 );
279
+ vec4 q1 = texelFetch( uVatRigTex, ivec2( rotation, band.row1 ), 0 );
280
+ vec4 ts1 = texelFetch( uVatRigTex, ivec2( placement, band.row1 ), 0 );
167
281
  // The bake keeps consecutive rows on one hemisphere, but a looping clip
168
282
  // blends its band's last row into its first, and a bone that turned a full
169
283
  // circle over the clip arrives there on the far side: one dot product per
170
284
  // slot, or the blend passes through zero on the wrap frame.
171
285
  if ( dot( q0, q1 ) < 0.0 ) q1 = -q1;
286
+ VatPose pose;
172
287
  // A normalised lerp, not a slerp: at a bake's frame step the angular error
173
288
  // against a true slerp is far below anything visible. It is still a
174
289
  // *rotation* at every blend, which is what a componentwise matrix lerp is
175
290
  // not \u2014 that one shortens a limb as it turns (ADR-0018).
176
- vec4 q = normalize( mix( q0, q1, rows.blend ) );
177
- vec4 ts = mix( ts0, ts1, rows.blend );
178
- if ( rows.fadeWeight > 0.0 ) {
179
- vec4 qf = texelFetch( uVatRigTex, ivec2( rotation, rows.fadeRow ), 0 );
180
- vec4 tsf = texelFetch( uVatRigTex, ivec2( placement, rows.fadeRow ), 0 );
181
- // The frozen row is any row of the bake, not this row's neighbour, so
291
+ pose.q = normalize( mix( q0, q1, band.blend ) );
292
+ pose.ts = mix( ts0, ts1, band.blend );
293
+ return pose;
294
+ }
295
+
296
+ // One slot's matrix: its pose in the band the instance is playing and, while
297
+ // it is transitioning, its pose in the band it is leaving \u2014 blended per slot
298
+ // before the matrix is composed, so the crowd skins from one rig rather than
299
+ // from the average of two matrices.
300
+ //
301
+ // The guard stays here, where the vertex sampler dropped its own. What it
302
+ // skips is four dependent fetches of the rig texture per slot, sixteen per
303
+ // vertex, against the two of one layer the vertex encoding skips \u2014 and #72
304
+ // measured it worth keeping: this encoding did not get slower when the
305
+ // crossfade landed, and the vertex encoding did.
306
+ mat4 vatSlot( const in int slot, const in VatRows rows, const in VatBand outgoing ) {
307
+ int rotation = slot * ${RIG_TEXELS_PER_SLOT} + ${RIG_TEXELS.rotation};
308
+ int placement = slot * ${RIG_TEXELS_PER_SLOT} + ${RIG_TEXELS.placement};
309
+ VatPose pose = vatSlotPose( rotation, placement, rows.live );
310
+ vec4 q = pose.q;
311
+ vec4 ts = pose.ts;
312
+ if ( rows.weight > 0.0 ) {
313
+ VatPose leaving = vatSlotPose( rotation, placement, outgoing );
314
+ vec4 qo = leaving.q;
315
+ // The outgoing band is any row of the bake, not this row's neighbour, so
182
316
  // the same check.
183
- if ( dot( q, qf ) < 0.0 ) qf = -qf;
184
- q = normalize( mix( q, qf, rows.fadeWeight ) );
185
- ts = mix( ts, tsf, rows.fadeWeight );
317
+ if ( dot( q, qo ) < 0.0 ) qo = -qo;
318
+ q = normalize( mix( q, qo, rows.weight ) );
319
+ ts = mix( ts, leaving.ts, rows.weight );
186
320
  }
187
321
  return vatCompose( q, ts );
188
322
  }
@@ -190,13 +324,20 @@ var RIG_PRELUDE = (
190
324
  // Linear blend skinning: the weighted sum of slot matrices, which is the
191
325
  // blend the bake did on the CPU for the bounds and three's own
192
326
  // skinning_vertex does on the GPU. A zero weight skips its four fetches.
327
+ //
328
+ // The band being left is resolved once for the vertex rather than once per
329
+ // slot, and unconditionally \u2014 two pack texels this encoding's old guard
330
+ // skipped, against the sixteen rig fetches per vertex the guard inside
331
+ // vatSlot still skips. At a weight of zero those two land on texels already
332
+ // read, and every slot below skips the pose.
193
333
  mat4 vatSkinMatrix( const in int vatInstance ) {
194
334
  VatRows rows = vatRows( vatInstance );
335
+ VatBand outgoing = vatOutgoingBand( vatInstance, rows.weight > 0.0 );
195
336
  mat4 skin = mat4( 0.0 );
196
337
  for ( int i = 0; i < 4; i ++ ) {
197
338
  float w = skinWeight[ i ];
198
339
  if ( w == 0.0 ) continue;
199
- skin += w * vatSlot( int( skinIndex[ i ] ), rows );
340
+ skin += w * vatSlot( int( skinIndex[ i ] ), rows, outgoing );
200
341
  }
201
342
  return skin;
202
343
  }
@@ -215,7 +356,7 @@ var vertexPosition = (id) => (
215
356
  var vertexNormal = (id) => (
216
357
  /* glsl */
217
358
  `
218
- vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
359
+ vec3 objectNormal = normalize( vatSampleNormal( ${INSTANCE_ID[id]} ) );
219
360
  #ifdef USE_TANGENT
220
361
  vec3 objectTangent = vec3( tangent.xyz );
221
362
  #endif
@@ -245,13 +386,16 @@ function vertexDecode({ positionTexture, normalTexture }, id) {
245
386
  uniforms.uVatPosTex = { value: positionTexture };
246
387
  if (normalTexture) uniforms.uVatNrmTex = { value: normalTexture };
247
388
  },
248
- inject(vertexShader) {
249
- const positioned = vertexShader.replace("#include <begin_vertex>", vertexPosition(id));
250
- return normalTexture ? positioned.replace("#include <beginnormal_vertex>", vertexNormal(id)) : positioned;
251
- },
389
+ position: vertexPosition(id),
390
+ // No normal texture, no normal decode, and no uniform bound for one: the
391
+ // material either does not read a normal or derives it from the deformed
392
+ // position itself (`flatShading`), so three's own `beginnormal_vertex` is
393
+ // left exactly where it is.
394
+ normal: normalTexture ? vertexNormal(id) : null,
252
395
  // A normal-less VAT injects a different vertex shader off the same material
253
396
  // parameters, so the variant is in the key (see `patchVATMaterial`).
254
- key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}`
397
+ key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}`,
398
+ instanceIndex: INSTANCE_ID[id]
255
399
  };
256
400
  }
257
401
  function rigDecode({ rigTexture }, id) {
@@ -260,12 +404,44 @@ function rigDecode({ rigTexture }, id) {
260
404
  bind(uniforms) {
261
405
  uniforms.uVatRigTex = { value: rigTexture };
262
406
  },
263
- inject(vertexShader) {
264
- return vertexShader.replace("#include <begin_vertex>", rigPosition(id)).replace("#include <beginnormal_vertex>", rigNormal(id));
265
- },
266
- key: `three-vat:rig:${id}`
407
+ position: rigPosition(id),
408
+ normal: rigNormal(id),
409
+ key: `three-vat:rig:${id}`,
410
+ instanceIndex: INSTANCE_ID[id]
267
411
  };
268
412
  }
413
+ function patchOptionsOf(fifth) {
414
+ if (!fifth) return {};
415
+ return fifth.isObject3D ? { carrier: fifth } : fifth;
416
+ }
417
+ function assertHook(hook) {
418
+ if (!hook.key.trim()) {
419
+ throw new Error(
420
+ "three-vat: a post-decode hook needs a non-empty `key`. It is folded into the library\u2019s own program cache key, and two hooks that fold in nothing share a compiled program \u2014 so one crowd renders the other\u2019s GLSL."
421
+ );
422
+ }
423
+ if (!hook.position && !hook.normal) {
424
+ throw new Error(
425
+ "three-vat: a post-decode hook with neither `position` nor `normal` injects nothing. Give it the chunk you meant \u2014 `position` deforms the posed vertex, `normal` repairs the normal that was taken before it, and a deformation wants both."
426
+ );
427
+ }
428
+ }
429
+ var hookChunk = (chunk, instanceIndex) => chunk ? (
430
+ /* glsl */
431
+ `
432
+ {
433
+ int vatInstanceIndex = ${instanceIndex};
434
+ ${chunk}
435
+ }
436
+ `
437
+ ) : "";
438
+ function injectVAT(vertexShader, decode, hook) {
439
+ const position = decode.position + hookChunk(hook?.position, decode.instanceIndex);
440
+ const normal = (decode.normal ?? "#include <beginnormal_vertex>") + hookChunk(hook?.normal, decode.instanceIndex);
441
+ return vertexShader.replace("#include <begin_vertex>", () => position).replace("#include <beginnormal_vertex>", () => normal);
442
+ }
443
+ var VAT_PATCHES = /* @__PURE__ */ new WeakSet();
444
+ var ours = (onBeforeCompile) => onBeforeCompile === Material.prototype.onBeforeCompile || VAT_PATCHES.has(onBeforeCompile);
269
445
  function decodeFor(vat, id) {
270
446
  switch (vat.encoding) {
271
447
  case "delta":
@@ -280,18 +456,29 @@ function decodeFor(vat, id) {
280
456
  }
281
457
  }
282
458
  }
283
- function patchVATMaterial(material, vat, uniforms, playback, carrier) {
459
+ function patchVATMaterial(material, vat, uniforms, playback, options) {
460
+ const { carrier, hook } = patchOptionsOf(options);
284
461
  assertBakedNormal(vat, material);
285
462
  if (carrier) assertVATCarrier(carrier, vat);
463
+ if (hook) assertHook(hook);
286
464
  const id = isBatchedCarrier(carrier) ? "batch" : "instance";
287
465
  const decode = decodeFor(vat, id);
288
- material.onBeforeCompile = (shader) => {
466
+ const previous = ours(material.onBeforeCompile) ? null : material.onBeforeCompile.bind(material);
467
+ const patch = (shader, renderer) => {
468
+ previous?.(shader, renderer);
469
+ if (hook?.uniforms) Object.assign(shader.uniforms, hook.uniforms);
289
470
  shader.uniforms.uVatPlaybackTex = { value: playback.texture };
290
471
  shader.uniforms.uVatTime = uniforms.uVatTime;
291
472
  decode.bind(shader.uniforms);
292
- shader.vertexShader = ROW_PRELUDE + decode.prelude + decode.inject(shader.vertexShader);
473
+ shader.vertexShader = ROW_PRELUDE + decode.prelude + // Ahead of three's shader, where a declaration is safe from a dead block.
474
+ (hook?.prelude ? `
475
+ ${hook.prelude}
476
+ ` : "") + injectVAT(shader.vertexShader, decode, hook);
293
477
  };
294
- material.customProgramCacheKey = () => decode.key;
478
+ material.onBeforeCompile = patch;
479
+ VAT_PATCHES.add(patch);
480
+ const key = hook ? `${decode.key}+${hook.key}` : decode.key;
481
+ material.customProgramCacheKey = () => key;
295
482
  guardCarrierMismatch(material, id);
296
483
  return material;
297
484
  }
@@ -315,18 +502,19 @@ var CARRIER_NAME = {
315
502
  instance: "an InstancedMesh",
316
503
  batch: "a BatchedMesh"
317
504
  };
318
- function createVATDepthMaterial(vat, uniforms, playback, carrier) {
505
+ function createVATDepthMaterial(vat, uniforms, playback, options) {
319
506
  const depth = new MeshDepthMaterial({ depthPacking: RGBADepthPacking });
320
- patchVATMaterial(depth, vat, uniforms, playback, carrier);
507
+ patchVATMaterial(depth, vat, uniforms, playback, options);
321
508
  return depth;
322
509
  }
323
510
  function createVATMesh(vat, instances, options = {}) {
324
511
  const uniforms = options.time ? { uVatTime: options.time } : createVATUniforms();
325
- const playback = createVATPlaybackTexture(instances);
326
- const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback));
512
+ const playback = createVATPlaybackTexture(instances, { maxTextureSize: options.maxTextureSize });
513
+ const patch = { hook: options.hook };
514
+ const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback, patch));
327
515
  const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
328
- mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms, playback);
329
- mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms, playback);
516
+ mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms, playback, patch);
517
+ mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms, playback, patch);
330
518
  return { mesh, time: uniforms.uVatTime, playback };
331
519
  }
332
520
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-vat",
3
- "version": "2.1.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",