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/tsl.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Node } from 'three/webgpu';
2
- import { d as VATClock, i as VATPlaybackTexture, a as VATCarrier, V as VAT, h as VATInstance, e as VATCrowd } from './carrier-BFCPmcQK.js';
2
+ import { e as VATClock, k as VATPlaybackTexture, b as VATCarrier, V as VAT, h as VATInstance, f as VATCrowd } from './carrier-BxRuW47G.js';
3
3
  import 'three';
4
4
 
5
5
  /**
@@ -14,8 +14,12 @@ import 'three';
14
14
  declare function getMaxTextureSize(renderer: object): number;
15
15
  /** A fluent TSL float node (has `.add`, `.mul`, … via NodeExtensions). */
16
16
  type FloatNode = Node<'float'>;
17
+ /** A fluent TSL int node. */
18
+ type IntNode = Node<'int'>;
17
19
  /** A fluent TSL vec3 node. */
18
20
  type Vec3Node = Node<'vec3'>;
21
+ /** A fluent TSL bool node — a branch condition, and what `.select()` reads. */
22
+ type BoolNode = Node<'bool'>;
19
23
  /**
20
24
  * A TSL float uniform: a node the graph reads, and a `{ value }` clock the
21
25
  * caller sets per frame. Both halves matter — the node is what the decode
@@ -48,14 +52,16 @@ interface VATNodeOptions {
48
52
  *
49
53
  * Required for a crowd, and for two reasons. First: three applies the
50
54
  * carrier's transform to `positionLocal` *before* it reads `positionNode`, so
51
- * the decode has to displace in the geometry's own space and then re-apply
52
- * that transform itself. Without this the delta is added in instance space —
53
- * unrotated and unscaled — and every instance deforms according to its own
54
- * matrix. Second: the carrier decides how this instance's *logical* index is
55
- * spelled, which is the row of the playback texture the pack is read from —
56
- * `instanceIndex` on an `InstancedMesh`, `batchIndirectIndex` on a
57
- * `BatchedMesh`, whose drawn slot is a permutation that changes every frame
58
- * (ADR-0016).
55
+ * the decode has to pose in the geometry's own space and then re-apply that
56
+ * transform itself. Without this the vertex encoding adds its delta in
57
+ * instance space — unrotated and unscaled — and every instance deforms
58
+ * according to its own matrix; the rig encoding, which skins the rest pose
59
+ * outright, never applies the instance matrix at all and draws the whole
60
+ * crowd at the origin. Second: the carrier decides how this instance's
61
+ * *logical* index is spelled, which is the row of the playback texture the
62
+ * pack is read from — `instanceIndex` on an `InstancedMesh`,
63
+ * `batchIndirectIndex` on a `BatchedMesh`, whose drawn slot is a permutation
64
+ * that changes every frame (ADR-0016).
59
65
  *
60
66
  * One option rather than one per carrier, because those two answers have to
61
67
  * come from the same object: a decode that re-applied one mesh's transform
@@ -82,17 +88,19 @@ interface VATNodeOptions {
82
88
  interface VATNodes {
83
89
  /**
84
90
  * Assign to `material.positionNode`. It carries the whole decode — the normal
85
- * with it.
91
+ * with it, and the tangent under the rig encoding.
86
92
  *
87
- * There is deliberately no `normalNode`. A material's `normalNode` is built in
88
- * the *fragment* stage (three reaches it from `normalView` through
89
- * `builder.context.setupNormal()`) and is expected in **view** space, whereas a
90
- * VAT's baked normals are per-vertex and in the geometry's own space. Handing
91
- * an object-space normal to a fragment-stage node skipped both the instance
92
- * matrix and the normal matrix, and took `vertexIndex` into the fragment stage
93
- * with it — where `IndexNode` does not give you the vertex index at all, but
94
- * quietly turns itself into a varying, so every fragment read a linearly
95
- * *interpolated* index that addresses neither of the vertices it lies between.
93
+ * There is deliberately no `normalNode`, under either encoding. A material's
94
+ * `normalNode` is built in the *fragment* stage (three reaches it from
95
+ * `normalView` through `builder.context.setupNormal()`) and is expected in
96
+ * **view** space, whereas a VAT's normals are per-vertex and in the geometry's
97
+ * own space — read from the normal texture, or skinned from the rig one.
98
+ * Handing an object-space normal to a fragment-stage node skipped both the
99
+ * instance matrix and the normal matrix, and took `vertexIndex` into the
100
+ * fragment stage with it — where `IndexNode` does not give you the vertex
101
+ * index at all, but quietly turns itself into a varying, so every fragment
102
+ * read a linearly *interpolated* index that addresses neither of the vertices
103
+ * it lies between.
96
104
  *
97
105
  * Writing `normalLocal` inside the vertex-stage decode instead is what the
98
106
  * GLSL path does when it sets `objectNormal` in `beginnormal_vertex`: three
@@ -103,6 +111,28 @@ interface VATNodes {
103
111
  /** The time uniform in use — set `.value` each frame. */
104
112
  time: VATTimeUniform;
105
113
  }
114
+ /** The clip texel: which band an instance plays, how long it is, and how fast. */
115
+ interface ClipTexel {
116
+ /** First texture row of the clip's band. */
117
+ startFrame: IntNode;
118
+ /** Rows in the band. */
119
+ frames: FloatNode;
120
+ /** Seconds the band spans. */
121
+ duration: FloatNode;
122
+ /** Rate multiplier. */
123
+ speed: FloatNode;
124
+ }
125
+ /** The playback texel: when this animation began, and how it repeats. */
126
+ interface PlaybackTexel {
127
+ /** Absolute clock time this animation began. In the past, for a desynced crowd. */
128
+ startTime: FloatNode;
129
+ /** {@link LoopMode}, as the number the pack carries. */
130
+ loopMode: FloatNode;
131
+ /** Repeat count, or {@link INFINITE_REPETITIONS}. */
132
+ repetitions: FloatNode;
133
+ /** {@link EndMode}, as the number the pack carries. */
134
+ endMode: FloatNode;
135
+ }
106
136
  /**
107
137
  * Build TSL decode nodes for a baked VAT, for the WebGPU/TSL renderer path.
108
138
  * Shadows work automatically because `positionNode` also feeds the depth pass.
@@ -118,9 +148,62 @@ interface VATNodes {
118
148
  */
119
149
  declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
120
150
  /**
121
- * The decode's arithmetic: the position delta and the normal this instance reads
122
- * at this moment, as nodes — before the vertex-stage writes that place them.
123
- * `normal` is `null` when the VAT was baked without a normal texture.
151
+ * What a decode hands the vertex stage, discriminated on the encoding it read
152
+ * (ADR-0018) — because the two encodings answer a different question. The
153
+ * vertex encoding reads where this vertex *moved to*: `position` is the delta
154
+ * to add to the rest position, `normal` the baked normal or `null` when the
155
+ * bake skipped it. The rig encoding *skins* the rest pose: `position` is the
156
+ * posed position itself, `normal` always exists because it comes out of the
157
+ * skin matrix, and so does `tangent` when the geometry carries one.
158
+ *
159
+ * @internal The return type of {@link vatDecode}, exported for the same
160
+ * structural tests and for nothing else.
161
+ */
162
+ type VATDecoded = {
163
+ encoding: 'delta';
164
+ position: Vec3Node;
165
+ normal: Vec3Node | null;
166
+ } | {
167
+ encoding: 'rig';
168
+ position: Vec3Node;
169
+ normal: Vec3Node;
170
+ tangent: Vec3Node | null;
171
+ };
172
+ /**
173
+ * One band resolved — the TSL spelling of the GLSL decode's `VatBand` struct:
174
+ * the two rows and the blend between them, plus the two facts those rows cannot
175
+ * be read back out of.
176
+ *
177
+ * @internal The return type of {@link resolveBand}, exported for the structural
178
+ * tests and for nothing else.
179
+ */
180
+ interface Band {
181
+ row0: IntNode;
182
+ row1: IntNode;
183
+ blend: FloatNode;
184
+ /** Whether the sampling crossed the band's last row back into its first. */
185
+ wraps: BoolNode;
186
+ /** Whether the repetitions have run out and the instance is holding an end pose. */
187
+ finished: BoolNode;
188
+ }
189
+ /**
190
+ * `resolveVATFrame` (src/instance-playback.ts) as a node graph, for one (clip
191
+ * texel, playback texel) pair — branch for branch with the GLSL decode's
192
+ * `vatBand`. The semantics live there; this transcribes them, and the mode
193
+ * constants come from that module rather than being retyped as literals.
194
+ *
195
+ * A function of the pair rather than of the instance, because the pair is what
196
+ * there are two of: a crossfading instance resolves its outgoing band by
197
+ * calling this a second time, not by transcribing it a second time.
198
+ *
199
+ * @internal Exported for the structural tests, and for nothing else. Not
200
+ * re-exported from `three-vat`.
201
+ */
202
+ declare function resolveBand(clip: ClipTexel, playback: PlaybackTexel, time: FloatNode): Band;
203
+ /**
204
+ * The decode's arithmetic: what this instance reads at this moment, as nodes —
205
+ * before the vertex-stage writes that place it. See {@link VATDecoded} for what
206
+ * `position` means under each encoding.
124
207
  *
125
208
  * @internal Split out and exported for the structural tests. A `Fn` body is
126
209
  * opaque to graph traversal (its statements are not built until the shader is),
@@ -128,10 +211,7 @@ declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
128
211
  * so the arithmetic that matters stays reachable as a graph. Not re-exported
129
212
  * from `three-vat`; nothing outside this package should build against it.
130
213
  */
131
- declare function vatDecode(vat: VAT, options?: VATNodeOptions): {
132
- position: Vec3Node;
133
- normal: Vec3Node | null;
134
- };
214
+ declare function vatDecode(vat: VAT, options?: VATNodeOptions): VATDecoded;
135
215
  /** Options for {@link createVATMesh}. */
136
216
  interface CreateVATMeshOptions {
137
217
  /**
@@ -173,4 +253,4 @@ interface CreateVATMeshOptions {
173
253
  */
174
254
  declare function createVATMesh(vat: VAT, instances: VATInstance[], options?: CreateVATMeshOptions): VATCrowd;
175
255
 
176
- export { type CreateVATMeshOptions, type VATNodeOptions, type VATNodes, type VATTimeUniform, createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
256
+ export { type Band, type CreateVATMeshOptions, type VATDecoded, type VATNodeOptions, type VATNodes, type VATTimeUniform, createVATMesh, getMaxTextureSize, resolveBand, vatDecode, vatNodes };
package/dist/tsl.js CHANGED
@@ -1,7 +1,7 @@
1
- import { isBatchedCarrier, assertVATCarrier, assertBakedNormal } from './chunk-W2ZAFMPB.js';
2
- import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS } from './chunk-2PGZ44TP.js';
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';
3
3
  import { InstancedMesh } from 'three';
4
- import { uniform, Fn, positionLocal, positionGeometry, normalLocal, batch, instancedMesh, int, vertexIndex, float, bool, textureLoad, ivec2, mix, batchIndirectIndex, instanceIndex, hash } 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 } from 'three/tsl';
5
5
 
6
6
  function getMaxTextureSize(renderer) {
7
7
  const backend = renderer.backend;
@@ -13,27 +13,37 @@ function getMaxTextureSize(renderer) {
13
13
  }
14
14
  var packTexel = (texture, field, instance) => textureLoad(texture, ivec2(int(field), instance));
15
15
  function texturePlayback(texture, instance) {
16
- const clip = packTexel(texture, PACK_TEXELS.clip, instance);
17
- const playback = packTexel(texture, PACK_TEXELS.playback, instance);
18
- const fade = packTexel(texture, PACK_TEXELS.fade, instance);
19
- const frames = clip.y;
16
+ const crossfade = packTexel(texture, PACK_TEXELS.crossfade, instance);
20
17
  return {
21
- startFrame: int(clip.x),
22
- frames,
23
- duration: frames.div(clip.z),
24
- startTime: playback.x,
25
- speed: clip.w,
26
- loopMode: playback.y,
27
- repetitions: playback.z,
28
- endMode: playback.w,
29
- fade: {
30
- startFrame: fade.x,
31
- frames: fade.y,
32
- phase: fade.z,
33
- duration: fade.w
18
+ live: liveBand(
19
+ packTexel(texture, PACK_TEXELS.clip, instance),
20
+ packTexel(texture, PACK_TEXELS.playback, instance)
21
+ ),
22
+ crossfadeDuration: crossfade.x,
23
+ outgoing: outgoingBand(
24
+ packTexel(texture, PACK_TEXELS.outgoingClip, instance),
25
+ packTexel(texture, PACK_TEXELS.outgoingPlayback, instance)
26
+ )
27
+ };
28
+ }
29
+ function bandTexels(clip, playback, frames, fps) {
30
+ return {
31
+ clip: {
32
+ startFrame: int(clip.x),
33
+ frames,
34
+ duration: frames.div(fps),
35
+ speed: clip.w
36
+ },
37
+ playback: {
38
+ startTime: playback.x,
39
+ loopMode: playback.y,
40
+ repetitions: playback.z,
41
+ endMode: playback.w
34
42
  }
35
43
  };
36
44
  }
45
+ var liveBand = (clip, playback) => bandTexels(clip, playback, clip.y, clip.z);
46
+ var outgoingBand = (clip, playback) => bandTexels(clip, playback, clip.y.max(1), clip.z.max(1));
37
47
  var instanceIdOf = (carrier) => isBatchedCarrier(carrier) ? int(batchIndirectIndex) : int(instanceIndex);
38
48
  function clipAt(vat, clipIndex) {
39
49
  const clip = vat.clips[clipIndex];
@@ -43,32 +53,40 @@ function clipAt(vat, clipIndex) {
43
53
  return clip;
44
54
  }
45
55
  function hashedPlayback(clip, desync, instance) {
46
- return {
47
- startFrame: int(clip.startFrame),
48
- frames: float(clip.frames),
49
- duration: float(clip.frames / clip.fps),
50
- // Negated, because desync is now a start time in the *past*: an instance
51
- // that began `desync` seconds ago is that far into its clip already.
52
- startTime: hash(instance).mul(-desync),
53
- // Everything but the phase comes from the clip's own baked defaults, so a
54
- // clip baked "once, clamped, at 2x" plays that way here too. A second set
55
- // of defaults living in this path would be a crowd that animates
56
- // differently depending on whether anyone wrote the attributes.
57
- speed: float(clip.speed),
58
- loopMode: float(clip.loopMode),
59
- repetitions: float(clip.repetitions),
60
- endMode: float(clip.endMode),
61
- // Nothing to fade out of: this path is the zero-config default, where an
62
- // instance has never been written and so has no animation it left behind.
63
- fade: { startFrame: float(0), frames: float(0), phase: float(0), duration: float(0) }
56
+ const live = {
57
+ clip: {
58
+ startFrame: int(clip.startFrame),
59
+ frames: float(clip.frames),
60
+ duration: float(clip.frames / clip.fps),
61
+ // Everything but the phase comes from the clip's own baked defaults, so a
62
+ // clip baked "once, clamped, at 2x" plays that way here too. A second set
63
+ // of defaults living in this path would be a crowd that animates
64
+ // differently depending on whether anyone wrote the attributes.
65
+ speed: float(clip.speed)
66
+ },
67
+ playback: {
68
+ // Negated, because desync is now a start time in the *past*: an instance
69
+ // that began `desync` seconds ago is that far into its clip already.
70
+ startTime: hash(instance).mul(-desync),
71
+ loopMode: float(clip.loopMode),
72
+ repetitions: float(clip.repetitions),
73
+ endMode: float(clip.endMode)
74
+ }
64
75
  };
76
+ return { live, crossfadeDuration: float(0), outgoing: live };
65
77
  }
66
78
  function vatNodes(vat, options = {}) {
67
79
  const { time = uniform(0), carrier } = options;
68
- const { position, normal } = vatDecode(vat, options);
80
+ const decoded = vatDecode(vat, options);
69
81
  const decode = Fn(() => {
70
- positionLocal.assign((carrier ? positionGeometry : positionLocal).add(position));
71
- if (normal) normalLocal.assign(normal);
82
+ if (decoded.encoding === "rig") {
83
+ positionLocal.assign(decoded.position);
84
+ normalLocal.assign(decoded.normal);
85
+ if (decoded.tangent) tangentLocal.assign(decoded.tangent);
86
+ } else {
87
+ positionLocal.assign((carrier ? positionGeometry : positionLocal).add(decoded.position));
88
+ if (decoded.normal) normalLocal.assign(decoded.normal);
89
+ }
72
90
  if (carrier) {
73
91
  if (isBatchedCarrier(carrier)) batch(carrier);
74
92
  else instancedMesh(carrier);
@@ -77,17 +95,11 @@ function vatNodes(vat, options = {}) {
77
95
  }, "vec3");
78
96
  return { positionNode: decode(), time };
79
97
  }
80
- function vatDecode(vat, options = {}) {
81
- const { time = uniform(0), playback: playbackTexture, carrier, clipIndex = 0, desync = 0 } = options;
82
- if (carrier) assertVATCarrier(carrier, vat);
83
- const instance = instanceIdOf(carrier);
84
- const playback = playbackTexture ? texturePlayback(playbackTexture.texture, instance) : hashedPlayback(clipAt(vat, clipIndex), desync, instance);
85
- const vertexRow = int(vertexIndex);
86
- const frames = playback.frames;
98
+ function resolveBand(clip, playback, time) {
99
+ const frames = clip.frames;
87
100
  const last = frames.sub(1);
88
- const elapsed = time.sub(playback.startTime);
89
- const local = elapsed.mul(playback.speed);
90
- const loops = local.div(playback.duration);
101
+ const local = time.sub(playback.startTime).mul(clip.speed);
102
+ const loops = local.div(clip.duration);
91
103
  const started = local.greaterThanEqual(0);
92
104
  const finished = started.and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(loops.greaterThanEqual(playback.repetitions));
93
105
  const isPingPong = playback.loopMode.equal(LoopMode.PingPong);
@@ -105,30 +117,134 @@ function vatDecode(vat, options = {}) {
105
117
  const f = phase.mul(wraps.select(frames, last));
106
118
  const f0 = f.floor().min(last);
107
119
  const f1 = wraps.select(f0.add(1).mod(frames), f0.add(1).min(last));
108
- const frameMix = f.sub(f0);
109
- const bandRow = (offset) => int(offset).add(playback.startFrame);
110
- const row0 = bandRow(f0);
111
- const row1 = bandRow(f1);
112
- const fade = playback.fade;
113
- const fadeWeight = fade.duration.greaterThan(0).select(
114
- float(1).sub(elapsed.div(fade.duration).clamp(0, 1)),
120
+ const bandRow = (offset) => int(offset).add(clip.startFrame);
121
+ return {
122
+ row0: bandRow(f0),
123
+ row1: bandRow(f1),
124
+ blend: f.sub(f0),
125
+ wraps,
126
+ finished
127
+ };
128
+ }
129
+ function vatDecode(vat, options = {}) {
130
+ const { time = uniform(0), playback: playbackTexture, carrier, clipIndex = 0, desync = 0 } = options;
131
+ if (carrier) assertVATCarrier(carrier, vat);
132
+ const instance = instanceIdOf(carrier);
133
+ const playback = playbackTexture ? texturePlayback(playbackTexture.texture, instance) : hashedPlayback(clipAt(vat, clipIndex), desync, instance);
134
+ const live = resolveBand(playback.live.clip, playback.live.playback, time);
135
+ const elapsed = time.sub(playback.live.playback.startTime);
136
+ const duration = playback.crossfadeDuration;
137
+ const weight = duration.greaterThan(0).select(
138
+ float(1).sub(elapsed.div(duration).clamp(0, 1)),
115
139
  float(0)
116
140
  );
117
- const frozenRow = int(fade.phase.mul(fade.frames).floor().min(fade.frames.sub(1)).max(0)).add(
118
- int(fade.startFrame)
119
- );
120
- const sample = (tex) => {
121
- const s0 = textureLoad(tex, ivec2(vertexRow, row0)).xyz;
122
- const s1 = textureLoad(tex, ivec2(vertexRow, row1)).xyz;
123
- const frozen = textureLoad(tex, ivec2(vertexRow, frozenRow)).xyz;
124
- return mix(mix(s0, s1, frameMix), frozen, fadeWeight);
141
+ const resolved = resolveBand(playback.outgoing.clip, playback.outgoing.playback, time);
142
+ const blending = weight.greaterThan(0);
143
+ const outgoing = {
144
+ row0: blending.select(resolved.row0, live.row0),
145
+ row1: blending.select(resolved.row1, live.row1),
146
+ blend: blending.select(resolved.blend, live.blend),
147
+ // Not selected, and deliberately: a sampler reads a band's rows and its
148
+ // blend and nothing else, so these two are carried because the band
149
+ // resolver answers them and not because anything downstream asks.
150
+ wraps: resolved.wraps,
151
+ finished: resolved.finished
152
+ };
153
+ const rows = { live, outgoing, weight };
154
+ switch (vat.encoding) {
155
+ case "delta":
156
+ return vertexDecode(vat, rows);
157
+ case "rig":
158
+ return rigDecode(vat, rows);
159
+ default: {
160
+ const unhandled = vat;
161
+ throw new Error(
162
+ `three-vat: vatDecode has no decode for encoding "${String(unhandled.encoding)}"`
163
+ );
164
+ }
165
+ }
166
+ }
167
+ function vertexDecode({ positionTexture, normalTexture }, rows) {
168
+ const vertexRow = int(vertexIndex);
169
+ const band = (tex, of) => {
170
+ const s0 = textureLoad(tex, ivec2(vertexRow, of.row0)).xyz;
171
+ const s1 = textureLoad(tex, ivec2(vertexRow, of.row1)).xyz;
172
+ return mix(s0, s1, of.blend);
125
173
  };
126
- const normalTexture = vat.normalTexture;
174
+ const sample = (tex) => mix(band(tex, rows.live), band(tex, rows.outgoing), rows.weight);
127
175
  return {
128
- position: sample(vat.positionTexture),
176
+ encoding: "delta",
177
+ position: sample(positionTexture),
129
178
  normal: normalTexture ? sample(normalTexture).normalize() : null
130
179
  };
131
180
  }
181
+ function compose(q, ts) {
182
+ const x = q.x;
183
+ const y = q.y;
184
+ const z = q.z;
185
+ const w = q.w;
186
+ const x2 = x.add(x);
187
+ const y2 = y.add(y);
188
+ const z2 = z.add(z);
189
+ const xx = x.mul(x2);
190
+ const xy = x.mul(y2);
191
+ const xz = x.mul(z2);
192
+ const yy = y.mul(y2);
193
+ const yz = y.mul(z2);
194
+ const zz = z.mul(z2);
195
+ const wx = w.mul(x2);
196
+ const wy = w.mul(y2);
197
+ const wz = w.mul(z2);
198
+ const s = ts.w;
199
+ const one = float(1);
200
+ return mat4(
201
+ vec4(one.sub(yy.add(zz)).mul(s), xy.add(wz).mul(s), xz.sub(wy).mul(s), 0),
202
+ vec4(xy.sub(wz).mul(s), one.sub(xx.add(zz)).mul(s), yz.add(wx).mul(s), 0),
203
+ vec4(xz.add(wy).mul(s), yz.sub(wx).mul(s), one.sub(xx.add(yy)).mul(s), 0),
204
+ vec4(ts.xyz, 1)
205
+ );
206
+ }
207
+ var hemisphereOf = (reference, q) => dot(reference, q).lessThan(0).select(q.negate(), q);
208
+ function rigDecode({ rigTexture, geometry }, rows) {
209
+ const skinIndex = attribute("skinIndex", "uvec4");
210
+ const skinWeight = attribute("skinWeight", "vec4");
211
+ const fetch = (column, row) => textureLoad(rigTexture, ivec2(column, row));
212
+ const pose = (rotation, placement, of) => {
213
+ const q0 = fetch(rotation, of.row0);
214
+ const ts0 = fetch(placement, of.row0);
215
+ const q1 = hemisphereOf(q0, fetch(rotation, of.row1));
216
+ const ts1 = fetch(placement, of.row1);
217
+ return {
218
+ // A normalised lerp, not a slerp: at a bake's frame step the angular error
219
+ // against a true slerp is far below anything visible. It is still a
220
+ // *rotation* at every blend, which is what a componentwise matrix lerp is
221
+ // not — that one shortens a limb as it turns (ADR-0018).
222
+ q: mix(q0, q1, of.blend).normalize(),
223
+ ts: mix(ts0, ts1, of.blend)
224
+ };
225
+ };
226
+ const slot = (index, weight) => {
227
+ const column = (texel) => int(index).mul(RIG_TEXELS_PER_SLOT).add(texel);
228
+ const rotation = column(RIG_TEXELS.rotation);
229
+ const placement = column(RIG_TEXELS.placement);
230
+ const live = pose(rotation, placement, rows.live);
231
+ const outgoing = pose(rotation, placement, rows.outgoing);
232
+ const q = mix(live.q, hemisphereOf(live.q, outgoing.q), rows.weight).normalize();
233
+ const ts = mix(live.ts, outgoing.ts, rows.weight);
234
+ return compose(q, ts).mul(weight);
235
+ };
236
+ const skin = slot(skinIndex.x, skinWeight.x).add(slot(skinIndex.y, skinWeight.y)).add(slot(skinIndex.z, skinWeight.z)).add(slot(skinIndex.w, skinWeight.w));
237
+ const skin3 = mat3(skin);
238
+ return {
239
+ encoding: "rig",
240
+ position: skin.mul(vec4(positionGeometry, 1)).xyz,
241
+ normal: skin3.mul(normalGeometry).normalize(),
242
+ // Gated on the geometry rather than read and ignored: `tangentGeometry`
243
+ // is an attribute read, and three computes tangents onto a geometry that
244
+ // has none the moment a graph asks for them.
245
+ tangent: geometry.hasAttribute("tangent") ? skin3.mul(tangentGeometry.xyz).normalize() : null
246
+ };
247
+ }
132
248
  function createVATMesh(vat, instances, options = {}) {
133
249
  const time = options.time ?? uniform(0);
134
250
  const playback = createVATPlaybackTexture(instances);
@@ -140,4 +256,4 @@ function createVATMesh(vat, instances, options = {}) {
140
256
  return { mesh, time, playback };
141
257
  }
142
258
 
143
- export { createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
259
+ export { createVATMesh, getMaxTextureSize, resolveBand, vatDecode, vatNodes };
package/dist/webgl.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IUniform, MeshDepthMaterial, WebGLRenderer, Material } from 'three';
2
- import { V as VAT, i as VATPlaybackTexture, a as VATCarrier, h as VATInstance, e as VATCrowd } from './carrier-BFCPmcQK.js';
2
+ import { b as VATCarrier, V as VAT, k as VATPlaybackTexture, h as VATInstance, f as VATCrowd } from './carrier-BxRuW47G.js';
3
3
 
4
4
  /**
5
5
  * The real maximum texture dimension this GPU accepts, for
@@ -16,6 +16,73 @@ interface VATUniforms {
16
16
  }
17
17
  /** Create the shared time uniform. Update `uVatTime.value` once per frame. */
18
18
  declare function createVATUniforms(time?: number): VATUniforms;
19
+ /**
20
+ * The caller's own GLSL, run after the decode has posed the vertex (ADR-0021):
21
+ * a wind sway, a twist toward a target, a per-instance squash — deformation
22
+ * that is the scene's and never the library's. The WebGL path's answer to what
23
+ * `positionNode` already gives the TSL path, which needs none of this.
24
+ *
25
+ * It has **two** injection points because three expands `beginnormal_vertex`
26
+ * *before* `begin_vertex` and derives `transformedNormal` between them: a chunk
27
+ * that only moves the position cannot repair a normal that was already taken,
28
+ * and the crowd would shade as though it had never moved. One point is a hook
29
+ * that looks right in the viewport and is wrong in the light.
30
+ *
31
+ * ```ts
32
+ * const hook = {
33
+ * key: 'twist',
34
+ * uniforms: { uTarget: { value: new Vector3() } },
35
+ * prelude: `
36
+ * uniform vec3 uTarget;
37
+ * vec3 twistY( vec3 p, float a ) {
38
+ * float s = sin( a ), c = cos( a );
39
+ * return vec3( c * p.x + s * p.z, p.y, -s * p.x + c * p.z );
40
+ * }`,
41
+ * position: 'transformed = twistY( transformed, vatTwistAngle( vatInstanceIndex ) );',
42
+ * normal: 'objectNormal = twistY( objectNormal, vatTwistAngle( vatInstanceIndex ) );',
43
+ * }
44
+ * const { mesh } = createVATMesh(vat, instances, { hook })
45
+ * ```
46
+ */
47
+ interface VATPostDecodeHook {
48
+ /**
49
+ * What tells this hook's compiled program from another's. Required, and
50
+ * folded into the library's own key rather than replacing it: without it two
51
+ * crowds whose materials are identical in every parameter three looks at
52
+ * share one program, and one of them renders the other's GLSL (ADR-0006).
53
+ */
54
+ key: string;
55
+ /**
56
+ * GLSL emitted ahead of three's shader — helper functions, the `uniform`
57
+ * declarations {@link uniforms} binds. Not an injection point, so ADR-0006
58
+ * does not govern it: this is where a helper the two chunks share is declared
59
+ * once.
60
+ */
61
+ prelude?: string;
62
+ /**
63
+ * The chunk run where three takes the position, with `transformed` in object
64
+ * space and already posed by the decode. Assign to it.
65
+ */
66
+ position?: string;
67
+ /**
68
+ * The chunk run where three takes the normal, with `objectNormal` already
69
+ * posed. Assign to it — otherwise a deformed crowd shades undeformed.
70
+ */
71
+ normal?: string;
72
+ /** Uniforms bound beside the library's own, so the chunks can be driven by your game state. */
73
+ uniforms?: Record<string, IUniform>;
74
+ }
75
+ /**
76
+ * What {@link patchVATMaterial} and {@link createVATDepthMaterial} take in
77
+ * place of a bare carrier — so a batched crowd with a hook passes one object
78
+ * rather than a positional carrier plus something else.
79
+ */
80
+ interface VATPatchOptions {
81
+ /** The mesh this material will draw on. Means exactly what the bare argument means. */
82
+ carrier?: VATCarrier;
83
+ /** The caller's own GLSL, after the decode. */
84
+ hook?: VATPostDecodeHook;
85
+ }
19
86
  /**
20
87
  * Patch any built-in material so its vertex stage samples the VAT instead of
21
88
  * skinning. Works on the render material and on `MeshDepthMaterial` (needed for
@@ -33,19 +100,26 @@ declare function createVATUniforms(time?: number): VATUniforms;
33
100
  * instead, because that carrier culls and sorts per instance and its drawn slot
34
101
  * is a permutation that changes every frame (ADR-0016). A batch a VAT cannot be
35
102
  * decoded on is refused here rather than rendered wrong.
103
+ *
104
+ * That fifth argument also takes a {@link VATPatchOptions} object, which is how
105
+ * a {@link VATPostDecodeHook} is passed — the caller's own GLSL after the
106
+ * decode, and the carrier beside it in one object.
36
107
  */
37
- declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, carrier?: VATCarrier): T;
108
+ declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, options?: VATCarrier | VATPatchOptions): T;
38
109
  /**
39
110
  * Build the `customDepthMaterial` a VAT crowd needs so it casts
40
111
  * correctly-deformed shadows instead of bind-pose shadows. Assign the result to
41
112
  * `mesh.customDepthMaterial` (and, for point lights, mirror with a patched
42
113
  * `MeshDistanceMaterial`).
43
114
  *
44
- * `carrier` means what it means in {@link patchVATMaterial}: omit it for an
45
- * `InstancedMesh`, pass the `BatchedMesh` for a batched crowd, so the shadow
46
- * pass resolves the same instance index the render pass does.
115
+ * The fourth argument means what it means in {@link patchVATMaterial}: omit it
116
+ * for an `InstancedMesh`, pass the `BatchedMesh` for a batched crowd, so the
117
+ * shadow pass resolves the same instance index the render pass does — or pass
118
+ * the options object, so a hand-wired crowd's shadow deforms with the hook its
119
+ * render material carries. A deformed crowd casting an undeformed shadow is
120
+ * exactly the class of mistake this library exists to take off the caller.
47
121
  */
48
- declare function createVATDepthMaterial(vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, carrier?: VATCarrier): MeshDepthMaterial;
122
+ declare function createVATDepthMaterial(vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, options?: VATCarrier | VATPatchOptions): MeshDepthMaterial;
49
123
  /** Options for {@link createVATMesh}. */
50
124
  interface CreateVATMeshOptions {
51
125
  /**
@@ -55,6 +129,14 @@ interface CreateVATMeshOptions {
55
129
  * `time`. The TSL path's `vatNodes` takes its clock the same way.
56
130
  */
57
131
  time?: IUniform<number>;
132
+ /**
133
+ * Your own GLSL after the decode (ADR-0021), threaded to every material this
134
+ * crowd draws with — the render materials, the depth material and the
135
+ * distance material. Threaded here rather than applied by hand afterwards
136
+ * because omitting one of the three is the bug: a twisted crowd casting an
137
+ * untwisted shadow.
138
+ */
139
+ hook?: VATPostDecodeHook;
58
140
  }
59
141
  /**
60
142
  * Turn a baked VAT and a list of instances into a crowd ready to render: an
@@ -90,4 +172,4 @@ interface CreateVATMeshOptions {
90
172
  */
91
173
  declare function createVATMesh(vat: VAT, instances: VATInstance[], options?: CreateVATMeshOptions): VATCrowd;
92
174
 
93
- export { type CreateVATMeshOptions, type VATUniforms, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
175
+ export { type CreateVATMeshOptions, type VATPatchOptions, type VATPostDecodeHook, type VATUniforms, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };