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.
package/dist/webgl.js CHANGED
@@ -1,6 +1,6 @@
1
- import { assertBakedNormal, assertVATCarrier, isBatchedCarrier } from './chunk-W2ZAFMPB.js';
2
- import { PACK_TEXELS, EndMode, LoopMode, INFINITE_REPETITIONS, createVATPlaybackTexture } from './chunk-2PGZ44TP.js';
3
- import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
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';
3
+ import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial, Material } from 'three';
4
4
 
5
5
  function getMaxTextureSize(renderer) {
6
6
  return renderer.capabilities.maxTextureSize;
@@ -9,35 +9,45 @@ function createVATUniforms(time = 0) {
9
9
  return { uVatTime: { value: time } };
10
10
  }
11
11
  var glslFloat = (n) => n.toFixed(1);
12
- var DECODE_PRELUDE = (
12
+ var ROW_PRELUDE = (
13
13
  /* glsl */
14
14
  `
15
- uniform highp sampler2D uVatPosTex;
16
15
  uniform highp sampler2D uVatPlaybackTex;
17
16
  uniform float uVatTime;
18
- vec3 vatSample( const in sampler2D tex, const in int vatInstance ) {
19
- // The pack, fetched by this instance's *logical* index rather than read
20
- // off an attribute indexed by the drawn slot (ADR-0016). Three texels of
21
- // one row, in the order src/instance-playback.ts lays them out; the
22
- // arithmetic below is untouched by where they came from, because the pack
23
- // was already three vec4s.
24
- //
25
- // The index arrives as a parameter rather than being read here, because
26
- // where it comes from is the carrier's business and not the decode's:
27
- // gl_InstanceID on an InstancedMesh, getIndirectIndex( gl_DrawID ) on a
28
- // BatchedMesh \u2014 see INSTANCE_ID in src/webgl.ts. It also has to be a
29
- // parameter: getIndirectIndex is declared by batching_pars_vertex, which
30
- // three expands *after* this prelude, so naming it up here would not
31
- // compile.
32
- //
33
- // Fetched inside the function, so each injection point stays
34
- // self-contained (ADR-0006) \u2014 which costs a second set of fetches in the
35
- // normal decode. Every vertex of an instance reads the same three texels,
36
- // so the texture cache absorbs them; the 5% demo bench is what says so.
37
- vec4 vatClip = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.clip}, vatInstance ), 0 );
38
- vec4 vatPlayback = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.playback}, vatInstance ), 0 );
39
- vec4 vatFade = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.fade}, vatInstance ), 0 );
40
17
 
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 {
23
+ int row0;
24
+ int row1;
25
+ float blend;
26
+ bool wraps;
27
+ bool finished;
28
+ };
29
+
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
+ };
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 ) {
41
51
  float frames = vatClip.y;
42
52
  float last = frames - 1.0;
43
53
  float duration = frames / vatClip.z;
@@ -72,22 +82,120 @@ var DECODE_PRELUDE = (
72
82
  float f = phase * ( wraps ? frames : last );
73
83
  float f0 = min( floor( f ), last );
74
84
  float f1 = wraps ? mod( f0 + 1.0, frames ) : min( f0 + 1.0, last );
75
- vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, int( vatClip.x + f0 ) ), 0 ).xyz;
76
- vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, int( vatClip.x + f1 ) ), 0 ).xyz;
77
- vec3 sampled = mix( s0, s1, f - f0 );
78
-
79
- // The pose-freeze fade, transcribed from the same resolver: one frozen row
80
- // of the clip this instance was playing when it changed, blended away over
81
- // vatFade.w. Wall clock, not clip time \u2014 the incoming clip's speed does
82
- // not stretch a fade. A duration of zero is what "not fading" is, and the
83
- // pack never writes one without a band to go with it.
84
- if ( vatFade.w > 0.0 ) {
85
- float weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatFade.w, 0.0, 1.0 );
86
- float fromRow = max( min( floor( vatFade.z * vatFade.y ), vatFade.y - 1.0 ), 0.0 );
87
- vec3 frozen = texelFetch( tex, ivec2( gl_VertexID, int( vatFade.x + fromRow ) ), 0 ).xyz;
88
- sampled = mix( sampled, frozen, weight );
85
+
86
+ VatBand band;
87
+ band.row0 = int( vatClip.x + f0 );
88
+ band.row1 = int( vatClip.x + f1 );
89
+ band.blend = f - f0;
90
+ band.wraps = wraps;
91
+ band.finished = finished;
92
+ return band;
93
+ }
94
+
95
+ VatRows vatRows( const in int vatInstance ) {
96
+ // The pack, fetched by this instance's *logical* index rather than read
97
+ // off an attribute indexed by the drawn slot (ADR-0016). Three texels of
98
+ // one row, in the order src/instance-playback.ts lays them out; the
99
+ // arithmetic above is untouched by where they came from, because the pack
100
+ // was already three vec4s.
101
+ //
102
+ // The index arrives as a parameter rather than being read here, because
103
+ // where it comes from is the carrier's business and not the decode's:
104
+ // gl_InstanceID on an InstancedMesh, getIndirectIndex( gl_DrawID ) on a
105
+ // BatchedMesh \u2014 see INSTANCE_ID in src/webgl.ts. It also has to be a
106
+ // parameter: getIndirectIndex is declared by batching_pars_vertex, which
107
+ // three expands *after* this prelude, so naming it up here would not
108
+ // compile.
109
+ //
110
+ // Fetched inside the function, so each injection point stays
111
+ // self-contained (ADR-0006) \u2014 which costs a second set of fetches in the
112
+ // normal decode. Every vertex of an instance reads the same texels of the
113
+ // same row, whichever branch it takes, so the texture cache absorbs them;
114
+ // 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.
116
+ //
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.
119
+ vec4 vatClip = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.clip}, vatInstance ), 0 );
120
+ vec4 vatPlayback = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.playback}, vatInstance ), 0 );
121
+ vec4 vatCrossfade = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.crossfade}, vatInstance ), 0 );
122
+
123
+ VatRows rows;
124
+ // The live band: the one clip this instance is playing, resolved from its
125
+ // own pair of texels.
126
+ rows.live = vatBand( vatClip, vatPlayback );
127
+
128
+ // The crossfade's weight, transcribed from the resolver: wall clock, not
129
+ // clip time \u2014 the incoming clip's speed does not stretch a transition \u2014 and
130
+ // a duration of zero is what a cut is, which is what the pack writes when
131
+ // there is no band to blend away.
132
+ rows.weight = 0.0;
133
+ if ( vatCrossfade.x > 0.0 ) {
134
+ rows.weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatCrossfade.x, 0.0, 1.0 );
89
135
  }
90
- return sampled;
136
+
137
+ return rows;
138
+ }
139
+
140
+ // The band the instance is leaving: two more texels and a second call of the
141
+ // very same resolver, so the clip it is leaving keeps playing \u2014 keeping its
142
+ // own speed and its own end policy \u2014 rather than standing still (ADR-0025).
143
+ //
144
+ // While the weight is zero the pair selected *is* the live pair, so the two
145
+ // fetches land on texels this vertex has already read and the band resolves
146
+ // to the one it is playing. That is what lets a caller resolve it without a
147
+ // branch, and blending a pose into itself is what a weight of zero means.
148
+ //
149
+ // Selecting the live pair is also why this needs no counterpart to the TSL
150
+ // path's max-of-one on the outgoing frames and fps (src/tsl.ts): that path
151
+ // resolves a band from the zeroes "not transitioning" is written as, which is
152
+ // a 0/0 duration and a NaN row. The pair selected here is always real.
153
+ //
154
+ // Whether the instance is transitioning is the parameter, not the weight, so
155
+ // the caller is the one that says so \u2014 and the two encodings say it from
156
+ // different places (#72): the vertex sampler calls this unconditionally,
157
+ // because guarding two texel fetches cost an idle crowd 10%; the rig sampler
158
+ // calls it unconditionally too but guards what it *does* with the band, where
159
+ // what a guard skips is sixteen dependent fetches per vertex.
160
+ VatBand vatOutgoingBand( const in int vatInstance, const in bool transitioning ) {
161
+ int clipX = transitioning ? ${PACK_TEXELS.outgoingClip} : ${PACK_TEXELS.clip};
162
+ int playbackX = transitioning ? ${PACK_TEXELS.outgoingPlayback} : ${PACK_TEXELS.playback};
163
+ vec4 vatOutClip = texelFetch( uVatPlaybackTex, ivec2( clipX, vatInstance ), 0 );
164
+ vec4 vatOutPlayback = texelFetch( uVatPlaybackTex, ivec2( playbackX, vatInstance ), 0 );
165
+ return vatBand( vatOutClip, vatOutPlayback );
166
+ }
167
+ `
168
+ );
169
+ var VERTEX_PRELUDE = (
170
+ /* glsl */
171
+ `
172
+ uniform highp sampler2D uVatPosTex;
173
+
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.
177
+ 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;
180
+ return mix( s0, s1, band.blend );
181
+ }
182
+
183
+ vec3 vatSample( const in sampler2D tex, const in int vatInstance ) {
184
+ VatRows rows = vatRows( vatInstance );
185
+ // The outgoing band \u2014 still playing, two rows of its own \u2014 mixed in by the
186
+ // weight the rows resolved, and mixed in *unconditionally*: at a weight of
187
+ // zero the band resolved is the live one, so this blends a pose into
188
+ // itself. The normal layer is renormalised by the caller after the mix, as
189
+ // it is for a single band.
190
+ //
191
+ // No branch, and that is measured rather than reasoned (#72): here the
192
+ // outgoing band is two more fetches of one layer, and guarding them cost an
193
+ // idle crowd 10% where paying them costs nothing measurable \u2014 0.283 ms
194
+ // against the 0.282 ms it cost before the crossfade existed. A branch is
195
+ // not free because it is not taken: the compiler still holds registers for
196
+ // the side it skips, and that is what an idle crowd was paying for.
197
+ VatBand outgoing = vatOutgoingBand( vatInstance, rows.weight > 0.0 );
198
+ return mix( vatBandSample( tex, rows.live ), vatBandSample( tex, outgoing ), rows.weight );
91
199
  }
92
200
  `
93
201
  );
@@ -97,17 +205,119 @@ var NORMAL_PRELUDE = (
97
205
  uniform highp sampler2D uVatNrmTex;
98
206
  `
99
207
  );
208
+ var RIG_PRELUDE = (
209
+ /* glsl */
210
+ `
211
+ uniform highp sampler2D uVatRigTex;
212
+ attribute vec4 skinIndex;
213
+ attribute vec4 skinWeight;
214
+
215
+ // Matrix4.compose, component for component: a rotation, a translation and
216
+ // one scale back to the matrix the skinning wants \u2014 so both encodings feed
217
+ // the same linear blend, and a rig crowd deforms as its vertex bake does.
218
+ mat4 vatCompose( const in vec4 q, const in vec4 ts ) {
219
+ float x2 = q.x + q.x, y2 = q.y + q.y, z2 = q.z + q.z;
220
+ float xx = q.x * x2, xy = q.x * y2, xz = q.x * z2;
221
+ float yy = q.y * y2, yz = q.y * z2, zz = q.z * z2;
222
+ float wx = q.w * x2, wy = q.w * y2, wz = q.w * z2;
223
+ float s = ts.w;
224
+ return mat4(
225
+ vec4( ( 1.0 - ( yy + zz ) ) * s, ( xy + wz ) * s, ( xz - wy ) * s, 0.0 ),
226
+ vec4( ( xy - wz ) * s, ( 1.0 - ( xx + zz ) ) * s, ( yz + wx ) * s, 0.0 ),
227
+ vec4( ( xz + wy ) * s, ( yz - wx ) * s, ( 1.0 - ( xx + yy ) ) * s, 0.0 ),
228
+ vec4( ts.xyz, 1.0 )
229
+ );
230
+ }
231
+
232
+ // One slot of the posed rig, at one band: a rotation and a placement, each
233
+ // between the two rows that band sits between. The same function for the live
234
+ // band and the outgoing one, as vatBand is the same function for both pairs.
235
+ struct VatPose {
236
+ vec4 q;
237
+ vec4 ts;
238
+ };
239
+
240
+ VatPose vatSlotPose( const in int rotation, const in int placement, const in VatBand band ) {
241
+ vec4 q0 = texelFetch( uVatRigTex, ivec2( rotation, band.row0 ), 0 );
242
+ vec4 ts0 = texelFetch( uVatRigTex, ivec2( placement, band.row0 ), 0 );
243
+ vec4 q1 = texelFetch( uVatRigTex, ivec2( rotation, band.row1 ), 0 );
244
+ vec4 ts1 = texelFetch( uVatRigTex, ivec2( placement, band.row1 ), 0 );
245
+ // The bake keeps consecutive rows on one hemisphere, but a looping clip
246
+ // blends its band's last row into its first, and a bone that turned a full
247
+ // circle over the clip arrives there on the far side: one dot product per
248
+ // slot, or the blend passes through zero on the wrap frame.
249
+ if ( dot( q0, q1 ) < 0.0 ) q1 = -q1;
250
+ VatPose pose;
251
+ // A normalised lerp, not a slerp: at a bake's frame step the angular error
252
+ // against a true slerp is far below anything visible. It is still a
253
+ // *rotation* at every blend, which is what a componentwise matrix lerp is
254
+ // not \u2014 that one shortens a limb as it turns (ADR-0018).
255
+ pose.q = normalize( mix( q0, q1, band.blend ) );
256
+ pose.ts = mix( ts0, ts1, band.blend );
257
+ return pose;
258
+ }
259
+
260
+ // One slot's matrix: its pose in the band the instance is playing and, while
261
+ // it is transitioning, its pose in the band it is leaving \u2014 blended per slot
262
+ // before the matrix is composed, so the crowd skins from one rig rather than
263
+ // from the average of two matrices.
264
+ //
265
+ // The guard stays here, where the vertex sampler dropped its own. What it
266
+ // skips is four dependent fetches of the rig texture per slot, sixteen per
267
+ // vertex, against the two of one layer the vertex encoding skips \u2014 and #72
268
+ // measured it worth keeping: this encoding did not get slower when the
269
+ // crossfade landed, and the vertex encoding did.
270
+ mat4 vatSlot( const in int slot, const in VatRows rows, const in VatBand outgoing ) {
271
+ int rotation = slot * ${RIG_TEXELS_PER_SLOT} + ${RIG_TEXELS.rotation};
272
+ int placement = slot * ${RIG_TEXELS_PER_SLOT} + ${RIG_TEXELS.placement};
273
+ VatPose pose = vatSlotPose( rotation, placement, rows.live );
274
+ vec4 q = pose.q;
275
+ vec4 ts = pose.ts;
276
+ if ( rows.weight > 0.0 ) {
277
+ VatPose leaving = vatSlotPose( rotation, placement, outgoing );
278
+ vec4 qo = leaving.q;
279
+ // The outgoing band is any row of the bake, not this row's neighbour, so
280
+ // the same check.
281
+ if ( dot( q, qo ) < 0.0 ) qo = -qo;
282
+ q = normalize( mix( q, qo, rows.weight ) );
283
+ ts = mix( ts, leaving.ts, rows.weight );
284
+ }
285
+ return vatCompose( q, ts );
286
+ }
287
+
288
+ // Linear blend skinning: the weighted sum of slot matrices, which is the
289
+ // blend the bake did on the CPU for the bounds and three's own
290
+ // skinning_vertex does on the GPU. A zero weight skips its four fetches.
291
+ //
292
+ // The band being left is resolved once for the vertex rather than once per
293
+ // slot, and unconditionally \u2014 two pack texels this encoding's old guard
294
+ // skipped, against the sixteen rig fetches per vertex the guard inside
295
+ // vatSlot still skips. At a weight of zero those two land on texels already
296
+ // read, and every slot below skips the pose.
297
+ mat4 vatSkinMatrix( const in int vatInstance ) {
298
+ VatRows rows = vatRows( vatInstance );
299
+ VatBand outgoing = vatOutgoingBand( vatInstance, rows.weight > 0.0 );
300
+ mat4 skin = mat4( 0.0 );
301
+ for ( int i = 0; i < 4; i ++ ) {
302
+ float w = skinWeight[ i ];
303
+ if ( w == 0.0 ) continue;
304
+ skin += w * vatSlot( int( skinIndex[ i ] ), rows, outgoing );
305
+ }
306
+ return skin;
307
+ }
308
+ `
309
+ );
100
310
  var INSTANCE_ID = {
101
311
  instance: "gl_InstanceID",
102
312
  batch: "int( getIndirectIndex( gl_DrawID ) )"
103
313
  };
104
- var decodePosition = (id) => (
314
+ var vertexPosition = (id) => (
105
315
  /* glsl */
106
316
  `
107
317
  vec3 transformed = position + vatSample( uVatPosTex, ${INSTANCE_ID[id]} );
108
318
  `
109
319
  );
110
- var decodeNormal = (id) => (
320
+ var vertexNormal = (id) => (
111
321
  /* glsl */
112
322
  `
113
323
  vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
@@ -116,23 +326,122 @@ var decodeNormal = (id) => (
116
326
  #endif
117
327
  `
118
328
  );
119
- function patchVATMaterial(material, vat, uniforms, playback, carrier) {
329
+ var rigPosition = (id) => (
330
+ /* glsl */
331
+ `
332
+ mat4 vatSkin = vatSkinMatrix( ${INSTANCE_ID[id]} );
333
+ vec3 transformed = ( vatSkin * vec4( position, 1.0 ) ).xyz;
334
+ `
335
+ );
336
+ var rigNormal = (id) => (
337
+ /* glsl */
338
+ `
339
+ mat4 vatSkinN = vatSkinMatrix( ${INSTANCE_ID[id]} );
340
+ vec3 objectNormal = normalize( mat3( vatSkinN ) * normal );
341
+ #ifdef USE_TANGENT
342
+ vec3 objectTangent = normalize( mat3( vatSkinN ) * tangent.xyz );
343
+ #endif
344
+ `
345
+ );
346
+ function vertexDecode({ positionTexture, normalTexture }, id) {
347
+ return {
348
+ prelude: (normalTexture ? NORMAL_PRELUDE : "") + VERTEX_PRELUDE,
349
+ bind(uniforms) {
350
+ uniforms.uVatPosTex = { value: positionTexture };
351
+ if (normalTexture) uniforms.uVatNrmTex = { value: normalTexture };
352
+ },
353
+ position: vertexPosition(id),
354
+ // No normal texture, no normal decode, and no uniform bound for one: the
355
+ // material either does not read a normal or derives it from the deformed
356
+ // position itself (`flatShading`), so three's own `beginnormal_vertex` is
357
+ // left exactly where it is.
358
+ normal: normalTexture ? vertexNormal(id) : null,
359
+ // A normal-less VAT injects a different vertex shader off the same material
360
+ // parameters, so the variant is in the key (see `patchVATMaterial`).
361
+ key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}`,
362
+ instanceIndex: INSTANCE_ID[id]
363
+ };
364
+ }
365
+ function rigDecode({ rigTexture }, id) {
366
+ return {
367
+ prelude: RIG_PRELUDE,
368
+ bind(uniforms) {
369
+ uniforms.uVatRigTex = { value: rigTexture };
370
+ },
371
+ position: rigPosition(id),
372
+ normal: rigNormal(id),
373
+ key: `three-vat:rig:${id}`,
374
+ instanceIndex: INSTANCE_ID[id]
375
+ };
376
+ }
377
+ function patchOptionsOf(fifth) {
378
+ if (!fifth) return {};
379
+ return fifth.isObject3D ? { carrier: fifth } : fifth;
380
+ }
381
+ function assertHook(hook) {
382
+ if (!hook.key.trim()) {
383
+ throw new Error(
384
+ "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."
385
+ );
386
+ }
387
+ if (!hook.position && !hook.normal) {
388
+ throw new Error(
389
+ "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."
390
+ );
391
+ }
392
+ }
393
+ var hookChunk = (chunk, instanceIndex) => chunk ? (
394
+ /* glsl */
395
+ `
396
+ {
397
+ int vatInstanceIndex = ${instanceIndex};
398
+ ${chunk}
399
+ }
400
+ `
401
+ ) : "";
402
+ function injectVAT(vertexShader, decode, hook) {
403
+ const position = decode.position + hookChunk(hook?.position, decode.instanceIndex);
404
+ const normal = (decode.normal ?? "#include <beginnormal_vertex>") + hookChunk(hook?.normal, decode.instanceIndex);
405
+ return vertexShader.replace("#include <begin_vertex>", () => position).replace("#include <beginnormal_vertex>", () => normal);
406
+ }
407
+ var VAT_PATCHES = /* @__PURE__ */ new WeakSet();
408
+ var ours = (onBeforeCompile) => onBeforeCompile === Material.prototype.onBeforeCompile || VAT_PATCHES.has(onBeforeCompile);
409
+ function decodeFor(vat, id) {
410
+ switch (vat.encoding) {
411
+ case "delta":
412
+ return vertexDecode(vat, id);
413
+ case "rig":
414
+ return rigDecode(vat, id);
415
+ default: {
416
+ const unhandled = vat;
417
+ throw new Error(
418
+ `three-vat: patchVATMaterial has no decode for encoding "${String(unhandled.encoding)}"`
419
+ );
420
+ }
421
+ }
422
+ }
423
+ function patchVATMaterial(material, vat, uniforms, playback, options) {
424
+ const { carrier, hook } = patchOptionsOf(options);
120
425
  assertBakedNormal(vat, material);
121
426
  if (carrier) assertVATCarrier(carrier, vat);
122
- const normalTexture = vat.normalTexture;
427
+ if (hook) assertHook(hook);
123
428
  const id = isBatchedCarrier(carrier) ? "batch" : "instance";
124
- material.onBeforeCompile = (shader) => {
125
- shader.uniforms.uVatPosTex = { value: vat.positionTexture };
429
+ const decode = decodeFor(vat, id);
430
+ const previous = ours(material.onBeforeCompile) ? null : material.onBeforeCompile.bind(material);
431
+ const patch = (shader, renderer) => {
432
+ previous?.(shader, renderer);
433
+ if (hook?.uniforms) Object.assign(shader.uniforms, hook.uniforms);
126
434
  shader.uniforms.uVatPlaybackTex = { value: playback.texture };
127
435
  shader.uniforms.uVatTime = uniforms.uVatTime;
128
- let vertexShader = shader.vertexShader.replace("#include <begin_vertex>", decodePosition(id));
129
- if (normalTexture) {
130
- shader.uniforms.uVatNrmTex = { value: normalTexture };
131
- vertexShader = vertexShader.replace("#include <beginnormal_vertex>", decodeNormal(id));
132
- }
133
- shader.vertexShader = (normalTexture ? NORMAL_PRELUDE : "") + DECODE_PRELUDE + vertexShader;
436
+ decode.bind(shader.uniforms);
437
+ shader.vertexShader = ROW_PRELUDE + decode.prelude + // Ahead of three's shader, where a declaration is safe from a dead block.
438
+ (hook?.prelude ? `
439
+ ${hook.prelude}
440
+ ` : "") + injectVAT(shader.vertexShader, decode, hook);
134
441
  };
135
- const key = `three-vat:${id}${normalTexture ? "" : ":no-normal"}`;
442
+ material.onBeforeCompile = patch;
443
+ VAT_PATCHES.add(patch);
444
+ const key = hook ? `${decode.key}+${hook.key}` : decode.key;
136
445
  material.customProgramCacheKey = () => key;
137
446
  guardCarrierMismatch(material, id);
138
447
  return material;
@@ -157,18 +466,19 @@ var CARRIER_NAME = {
157
466
  instance: "an InstancedMesh",
158
467
  batch: "a BatchedMesh"
159
468
  };
160
- function createVATDepthMaterial(vat, uniforms, playback, carrier) {
469
+ function createVATDepthMaterial(vat, uniforms, playback, options) {
161
470
  const depth = new MeshDepthMaterial({ depthPacking: RGBADepthPacking });
162
- patchVATMaterial(depth, vat, uniforms, playback, carrier);
471
+ patchVATMaterial(depth, vat, uniforms, playback, options);
163
472
  return depth;
164
473
  }
165
474
  function createVATMesh(vat, instances, options = {}) {
166
475
  const uniforms = options.time ? { uVatTime: options.time } : createVATUniforms();
167
476
  const playback = createVATPlaybackTexture(instances);
168
- const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback));
477
+ const patch = { hook: options.hook };
478
+ const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback, patch));
169
479
  const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
170
- mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms, playback);
171
- mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms, playback);
480
+ mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms, playback, patch);
481
+ mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms, playback, patch);
172
482
  return { mesh, time: uniforms.uVatTime, playback };
173
483
  }
174
484
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-vat",
3
- "version": "2.0.0",
3
+ "version": "3.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",