three-vat 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/tsl.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Node } from 'three/webgpu';
2
- import { 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, j as VATPlaybackTexture, b as VATCarrier, V as VAT, i as VATInstance, f as VATCrowd } from './carrier-BXaLAPRO.js';
3
3
  import 'three';
4
4
 
5
5
  /**
@@ -48,14 +48,16 @@ interface VATNodeOptions {
48
48
  *
49
49
  * Required for a crowd, and for two reasons. First: three applies the
50
50
  * 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).
51
+ * the decode has to pose in the geometry's own space and then re-apply that
52
+ * transform itself. Without this the vertex encoding adds its delta in
53
+ * instance space — unrotated and unscaled — and every instance deforms
54
+ * according to its own matrix; the rig encoding, which skins the rest pose
55
+ * outright, never applies the instance matrix at all and draws the whole
56
+ * crowd at the origin. Second: the carrier decides how this instance's
57
+ * *logical* index is spelled, which is the row of the playback texture the
58
+ * pack is read from — `instanceIndex` on an `InstancedMesh`,
59
+ * `batchIndirectIndex` on a `BatchedMesh`, whose drawn slot is a permutation
60
+ * that changes every frame (ADR-0016).
59
61
  *
60
62
  * One option rather than one per carrier, because those two answers have to
61
63
  * come from the same object: a decode that re-applied one mesh's transform
@@ -82,17 +84,19 @@ interface VATNodeOptions {
82
84
  interface VATNodes {
83
85
  /**
84
86
  * Assign to `material.positionNode`. It carries the whole decode — the normal
85
- * with it.
87
+ * with it, and the tangent under the rig encoding.
86
88
  *
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.
89
+ * There is deliberately no `normalNode`, under either encoding. A material's
90
+ * `normalNode` is built in the *fragment* stage (three reaches it from
91
+ * `normalView` through `builder.context.setupNormal()`) and is expected in
92
+ * **view** space, whereas a VAT's normals are per-vertex and in the geometry's
93
+ * own space — read from the normal texture, or skinned from the rig one.
94
+ * Handing an object-space normal to a fragment-stage node skipped both the
95
+ * instance matrix and the normal matrix, and took `vertexIndex` into the
96
+ * fragment stage with it — where `IndexNode` does not give you the vertex
97
+ * index at all, but quietly turns itself into a varying, so every fragment
98
+ * read a linearly *interpolated* index that addresses neither of the vertices
99
+ * it lies between.
96
100
  *
97
101
  * Writing `normalLocal` inside the vertex-stage decode instead is what the
98
102
  * GLSL path does when it sets `objectNormal` in `beginnormal_vertex`: three
@@ -118,9 +122,31 @@ interface VATNodes {
118
122
  */
119
123
  declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
120
124
  /**
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.
125
+ * What a decode hands the vertex stage, discriminated on the encoding it read
126
+ * (ADR-0018) — because the two encodings answer a different question. The
127
+ * vertex encoding reads where this vertex *moved to*: `position` is the delta
128
+ * to add to the rest position, `normal` the baked normal or `null` when the
129
+ * bake skipped it. The rig encoding *skins* the rest pose: `position` is the
130
+ * posed position itself, `normal` always exists because it comes out of the
131
+ * skin matrix, and so does `tangent` when the geometry carries one.
132
+ *
133
+ * @internal The return type of {@link vatDecode}, exported for the same
134
+ * structural tests and for nothing else.
135
+ */
136
+ type VATDecoded = {
137
+ encoding: 'delta';
138
+ position: Vec3Node;
139
+ normal: Vec3Node | null;
140
+ } | {
141
+ encoding: 'rig';
142
+ position: Vec3Node;
143
+ normal: Vec3Node;
144
+ tangent: Vec3Node | null;
145
+ };
146
+ /**
147
+ * The decode's arithmetic: what this instance reads at this moment, as nodes —
148
+ * before the vertex-stage writes that place it. See {@link VATDecoded} for what
149
+ * `position` means under each encoding.
124
150
  *
125
151
  * @internal Split out and exported for the structural tests. A `Fn` body is
126
152
  * opaque to graph traversal (its statements are not built until the shader is),
@@ -128,10 +154,7 @@ declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
128
154
  * so the arithmetic that matters stays reachable as a graph. Not re-exported
129
155
  * from `three-vat`; nothing outside this package should build against it.
130
156
  */
131
- declare function vatDecode(vat: VAT, options?: VATNodeOptions): {
132
- position: Vec3Node;
133
- normal: Vec3Node | null;
134
- };
157
+ declare function vatDecode(vat: VAT, options?: VATNodeOptions): VATDecoded;
135
158
  /** Options for {@link createVATMesh}. */
136
159
  interface CreateVATMeshOptions {
137
160
  /**
@@ -173,4 +196,4 @@ interface CreateVATMeshOptions {
173
196
  */
174
197
  declare function createVATMesh(vat: VAT, instances: VATInstance[], options?: CreateVATMeshOptions): VATCrowd;
175
198
 
176
- export { type CreateVATMeshOptions, type VATNodeOptions, type VATNodes, type VATTimeUniform, createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
199
+ export { type CreateVATMeshOptions, type VATDecoded, type VATNodeOptions, type VATNodes, type VATTimeUniform, createVATMesh, getMaxTextureSize, 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-3PWAY6MD.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;
@@ -65,10 +65,16 @@ function hashedPlayback(clip, desync, instance) {
65
65
  }
66
66
  function vatNodes(vat, options = {}) {
67
67
  const { time = uniform(0), carrier } = options;
68
- const { position, normal } = vatDecode(vat, options);
68
+ const decoded = vatDecode(vat, options);
69
69
  const decode = Fn(() => {
70
- positionLocal.assign((carrier ? positionGeometry : positionLocal).add(position));
71
- if (normal) normalLocal.assign(normal);
70
+ if (decoded.encoding === "rig") {
71
+ positionLocal.assign(decoded.position);
72
+ normalLocal.assign(decoded.normal);
73
+ if (decoded.tangent) tangentLocal.assign(decoded.tangent);
74
+ } else {
75
+ positionLocal.assign((carrier ? positionGeometry : positionLocal).add(decoded.position));
76
+ if (decoded.normal) normalLocal.assign(decoded.normal);
77
+ }
72
78
  if (carrier) {
73
79
  if (isBatchedCarrier(carrier)) batch(carrier);
74
80
  else instancedMesh(carrier);
@@ -82,7 +88,6 @@ function vatDecode(vat, options = {}) {
82
88
  if (carrier) assertVATCarrier(carrier, vat);
83
89
  const instance = instanceIdOf(carrier);
84
90
  const playback = playbackTexture ? texturePlayback(playbackTexture.texture, instance) : hashedPlayback(clipAt(vat, clipIndex), desync, instance);
85
- const vertexRow = int(vertexIndex);
86
91
  const frames = playback.frames;
87
92
  const last = frames.sub(1);
88
93
  const elapsed = time.sub(playback.startTime);
@@ -105,7 +110,7 @@ function vatDecode(vat, options = {}) {
105
110
  const f = phase.mul(wraps.select(frames, last));
106
111
  const f0 = f.floor().min(last);
107
112
  const f1 = wraps.select(f0.add(1).mod(frames), f0.add(1).min(last));
108
- const frameMix = f.sub(f0);
113
+ const blend = f.sub(f0);
109
114
  const bandRow = (offset) => int(offset).add(playback.startFrame);
110
115
  const row0 = bandRow(f0);
111
116
  const row1 = bandRow(f1);
@@ -114,21 +119,96 @@ function vatDecode(vat, options = {}) {
114
119
  float(1).sub(elapsed.div(fade.duration).clamp(0, 1)),
115
120
  float(0)
116
121
  );
117
- const frozenRow = int(fade.phase.mul(fade.frames).floor().min(fade.frames.sub(1)).max(0)).add(
122
+ const fadeRow = int(fade.phase.mul(fade.frames).floor().min(fade.frames.sub(1)).max(0)).add(
118
123
  int(fade.startFrame)
119
124
  );
125
+ const rows = { row0, row1, blend, fadeRow, fadeWeight };
126
+ switch (vat.encoding) {
127
+ case "delta":
128
+ return vertexDecode(vat, rows);
129
+ case "rig":
130
+ return rigDecode(vat, rows);
131
+ default: {
132
+ const unhandled = vat;
133
+ throw new Error(
134
+ `three-vat: vatDecode has no decode for encoding "${String(unhandled.encoding)}"`
135
+ );
136
+ }
137
+ }
138
+ }
139
+ function vertexDecode({ positionTexture, normalTexture }, rows) {
140
+ const vertexRow = int(vertexIndex);
120
141
  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);
142
+ const s0 = textureLoad(tex, ivec2(vertexRow, rows.row0)).xyz;
143
+ const s1 = textureLoad(tex, ivec2(vertexRow, rows.row1)).xyz;
144
+ const frozen = textureLoad(tex, ivec2(vertexRow, rows.fadeRow)).xyz;
145
+ return mix(mix(s0, s1, rows.blend), frozen, rows.fadeWeight);
125
146
  };
126
- const normalTexture = vat.normalTexture;
127
147
  return {
128
- position: sample(vat.positionTexture),
148
+ encoding: "delta",
149
+ position: sample(positionTexture),
129
150
  normal: normalTexture ? sample(normalTexture).normalize() : null
130
151
  };
131
152
  }
153
+ function compose(q, ts) {
154
+ const x = q.x;
155
+ const y = q.y;
156
+ const z = q.z;
157
+ const w = q.w;
158
+ const x2 = x.add(x);
159
+ const y2 = y.add(y);
160
+ const z2 = z.add(z);
161
+ const xx = x.mul(x2);
162
+ const xy = x.mul(y2);
163
+ const xz = x.mul(z2);
164
+ const yy = y.mul(y2);
165
+ const yz = y.mul(z2);
166
+ const zz = z.mul(z2);
167
+ const wx = w.mul(x2);
168
+ const wy = w.mul(y2);
169
+ const wz = w.mul(z2);
170
+ const s = ts.w;
171
+ const one = float(1);
172
+ return mat4(
173
+ vec4(one.sub(yy.add(zz)).mul(s), xy.add(wz).mul(s), xz.sub(wy).mul(s), 0),
174
+ vec4(xy.sub(wz).mul(s), one.sub(xx.add(zz)).mul(s), yz.add(wx).mul(s), 0),
175
+ vec4(xz.add(wy).mul(s), yz.sub(wx).mul(s), one.sub(xx.add(yy)).mul(s), 0),
176
+ vec4(ts.xyz, 1)
177
+ );
178
+ }
179
+ var hemisphereOf = (reference, q) => dot(reference, q).lessThan(0).select(q.negate(), q);
180
+ function rigDecode({ rigTexture, geometry }, rows) {
181
+ const skinIndex = attribute("skinIndex", "uvec4");
182
+ const skinWeight = attribute("skinWeight", "vec4");
183
+ const fetch = (column, row) => textureLoad(rigTexture, ivec2(column, row));
184
+ const slot = (index, weight) => {
185
+ const column = (texel) => int(index).mul(RIG_TEXELS_PER_SLOT).add(texel);
186
+ const rotation = column(RIG_TEXELS.rotation);
187
+ const placement = column(RIG_TEXELS.placement);
188
+ const q0 = fetch(rotation, rows.row0);
189
+ const ts0 = fetch(placement, rows.row0);
190
+ const q1 = hemisphereOf(q0, fetch(rotation, rows.row1));
191
+ const ts1 = fetch(placement, rows.row1);
192
+ const q = mix(q0, q1, rows.blend).normalize();
193
+ const ts = mix(ts0, ts1, rows.blend);
194
+ const qf = hemisphereOf(q, fetch(rotation, rows.fadeRow));
195
+ const tsf = fetch(placement, rows.fadeRow);
196
+ const qFaded = mix(q, qf, rows.fadeWeight).normalize();
197
+ const tsFaded = mix(ts, tsf, rows.fadeWeight);
198
+ return compose(qFaded, tsFaded).mul(weight);
199
+ };
200
+ 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));
201
+ const skin3 = mat3(skin);
202
+ return {
203
+ encoding: "rig",
204
+ position: skin.mul(vec4(positionGeometry, 1)).xyz,
205
+ normal: skin3.mul(normalGeometry).normalize(),
206
+ // Gated on the geometry rather than read and ignored: `tangentGeometry`
207
+ // is an attribute read, and three computes tangents onto a geometry that
208
+ // has none the moment a graph asks for them.
209
+ tangent: geometry.hasAttribute("tangent") ? skin3.mul(tangentGeometry.xyz).normalize() : null
210
+ };
211
+ }
132
212
  function createVATMesh(vat, instances, options = {}) {
133
213
  const time = options.time ?? uniform(0);
134
214
  const playback = createVATPlaybackTexture(instances);
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 { V as VAT, j as VATPlaybackTexture, b as VATCarrier, i as VATInstance, f as VATCrowd } from './carrier-BXaLAPRO.js';
3
3
 
4
4
  /**
5
5
  * The real maximum texture dimension this GPU accepts, for
package/dist/webgl.js CHANGED
@@ -1,5 +1,5 @@
1
- import { assertBakedNormal, assertVATCarrier, isBatchedCarrier } from './chunk-W2ZAFMPB.js';
2
- import { PACK_TEXELS, EndMode, LoopMode, INFINITE_REPETITIONS, createVATPlaybackTexture } from './chunk-2PGZ44TP.js';
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
3
  import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
4
4
 
5
5
  function getMaxTextureSize(renderer) {
@@ -9,13 +9,24 @@ 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 ) {
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 {
22
+ int row0;
23
+ int row1;
24
+ float blend;
25
+ int fadeRow;
26
+ float fadeWeight;
27
+ };
28
+
29
+ VatRows vatRows( const in int vatInstance ) {
19
30
  // The pack, fetched by this instance's *logical* index rather than read
20
31
  // off an attribute indexed by the drawn slot (ADR-0016). Three texels of
21
32
  // one row, in the order src/instance-playback.ts lays them out; the
@@ -72,19 +83,42 @@ var DECODE_PRELUDE = (
72
83
  float f = phase * ( wraps ? frames : last );
73
84
  float f0 = min( floor( f ), last );
74
85
  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 );
86
+
87
+ VatRows rows;
88
+ rows.row0 = int( vatClip.x + f0 );
89
+ rows.row1 = int( vatClip.x + f1 );
90
+ rows.blend = f - f0;
78
91
 
79
92
  // The pose-freeze fade, transcribed from the same resolver: one frozen row
80
93
  // of the clip this instance was playing when it changed, blended away over
81
94
  // vatFade.w. Wall clock, not clip time \u2014 the incoming clip's speed does
82
95
  // not stretch a fade. A duration of zero is what "not fading" is, and the
83
96
  // pack never writes one without a band to go with it.
97
+ rows.fadeRow = 0;
98
+ rows.fadeWeight = 0.0;
84
99
  if ( vatFade.w > 0.0 ) {
85
100
  float weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatFade.w, 0.0, 1.0 );
86
101
  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;
102
+ rows.fadeRow = int( vatFade.x + fromRow );
103
+ rows.fadeWeight = weight;
104
+ }
105
+ return rows;
106
+ }
107
+ `
108
+ );
109
+ var VERTEX_PRELUDE = (
110
+ /* glsl */
111
+ `
112
+ uniform highp sampler2D uVatPosTex;
113
+ vec3 vatSample( const in sampler2D tex, const in int vatInstance ) {
114
+ 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;
88
122
  sampled = mix( sampled, frozen, weight );
89
123
  }
90
124
  return sampled;
@@ -97,17 +131,88 @@ var NORMAL_PRELUDE = (
97
131
  uniform highp sampler2D uVatNrmTex;
98
132
  `
99
133
  );
134
+ var RIG_PRELUDE = (
135
+ /* glsl */
136
+ `
137
+ uniform highp sampler2D uVatRigTex;
138
+ attribute vec4 skinIndex;
139
+ attribute vec4 skinWeight;
140
+
141
+ // Matrix4.compose, component for component: a rotation, a translation and
142
+ // one scale back to the matrix the skinning wants \u2014 so both encodings feed
143
+ // the same linear blend, and a rig crowd deforms as its vertex bake does.
144
+ mat4 vatCompose( const in vec4 q, const in vec4 ts ) {
145
+ float x2 = q.x + q.x, y2 = q.y + q.y, z2 = q.z + q.z;
146
+ float xx = q.x * x2, xy = q.x * y2, xz = q.x * z2;
147
+ float yy = q.y * y2, yz = q.y * z2, zz = q.z * z2;
148
+ float wx = q.w * x2, wy = q.w * y2, wz = q.w * z2;
149
+ float s = ts.w;
150
+ return mat4(
151
+ vec4( ( 1.0 - ( yy + zz ) ) * s, ( xy + wz ) * s, ( xz - wy ) * s, 0.0 ),
152
+ vec4( ( xy - wz ) * s, ( 1.0 - ( xx + zz ) ) * s, ( yz + wx ) * s, 0.0 ),
153
+ vec4( ( xz + wy ) * s, ( yz - wx ) * s, ( 1.0 - ( xx + yy ) ) * s, 0.0 ),
154
+ vec4( ts.xyz, 1.0 )
155
+ );
156
+ }
157
+
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 );
167
+ // The bake keeps consecutive rows on one hemisphere, but a looping clip
168
+ // blends its band's last row into its first, and a bone that turned a full
169
+ // circle over the clip arrives there on the far side: one dot product per
170
+ // slot, or the blend passes through zero on the wrap frame.
171
+ if ( dot( q0, q1 ) < 0.0 ) q1 = -q1;
172
+ // A normalised lerp, not a slerp: at a bake's frame step the angular error
173
+ // against a true slerp is far below anything visible. It is still a
174
+ // *rotation* at every blend, which is what a componentwise matrix lerp is
175
+ // 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
182
+ // 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 );
186
+ }
187
+ return vatCompose( q, ts );
188
+ }
189
+
190
+ // Linear blend skinning: the weighted sum of slot matrices, which is the
191
+ // blend the bake did on the CPU for the bounds and three's own
192
+ // skinning_vertex does on the GPU. A zero weight skips its four fetches.
193
+ mat4 vatSkinMatrix( const in int vatInstance ) {
194
+ VatRows rows = vatRows( vatInstance );
195
+ mat4 skin = mat4( 0.0 );
196
+ for ( int i = 0; i < 4; i ++ ) {
197
+ float w = skinWeight[ i ];
198
+ if ( w == 0.0 ) continue;
199
+ skin += w * vatSlot( int( skinIndex[ i ] ), rows );
200
+ }
201
+ return skin;
202
+ }
203
+ `
204
+ );
100
205
  var INSTANCE_ID = {
101
206
  instance: "gl_InstanceID",
102
207
  batch: "int( getIndirectIndex( gl_DrawID ) )"
103
208
  };
104
- var decodePosition = (id) => (
209
+ var vertexPosition = (id) => (
105
210
  /* glsl */
106
211
  `
107
212
  vec3 transformed = position + vatSample( uVatPosTex, ${INSTANCE_ID[id]} );
108
213
  `
109
214
  );
110
- var decodeNormal = (id) => (
215
+ var vertexNormal = (id) => (
111
216
  /* glsl */
112
217
  `
113
218
  vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
@@ -116,24 +221,77 @@ var decodeNormal = (id) => (
116
221
  #endif
117
222
  `
118
223
  );
224
+ var rigPosition = (id) => (
225
+ /* glsl */
226
+ `
227
+ mat4 vatSkin = vatSkinMatrix( ${INSTANCE_ID[id]} );
228
+ vec3 transformed = ( vatSkin * vec4( position, 1.0 ) ).xyz;
229
+ `
230
+ );
231
+ var rigNormal = (id) => (
232
+ /* glsl */
233
+ `
234
+ mat4 vatSkinN = vatSkinMatrix( ${INSTANCE_ID[id]} );
235
+ vec3 objectNormal = normalize( mat3( vatSkinN ) * normal );
236
+ #ifdef USE_TANGENT
237
+ vec3 objectTangent = normalize( mat3( vatSkinN ) * tangent.xyz );
238
+ #endif
239
+ `
240
+ );
241
+ function vertexDecode({ positionTexture, normalTexture }, id) {
242
+ return {
243
+ prelude: (normalTexture ? NORMAL_PRELUDE : "") + VERTEX_PRELUDE,
244
+ bind(uniforms) {
245
+ uniforms.uVatPosTex = { value: positionTexture };
246
+ if (normalTexture) uniforms.uVatNrmTex = { value: normalTexture };
247
+ },
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
+ },
252
+ // A normal-less VAT injects a different vertex shader off the same material
253
+ // parameters, so the variant is in the key (see `patchVATMaterial`).
254
+ key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}`
255
+ };
256
+ }
257
+ function rigDecode({ rigTexture }, id) {
258
+ return {
259
+ prelude: RIG_PRELUDE,
260
+ bind(uniforms) {
261
+ uniforms.uVatRigTex = { value: rigTexture };
262
+ },
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}`
267
+ };
268
+ }
269
+ function decodeFor(vat, id) {
270
+ switch (vat.encoding) {
271
+ case "delta":
272
+ return vertexDecode(vat, id);
273
+ case "rig":
274
+ return rigDecode(vat, id);
275
+ default: {
276
+ const unhandled = vat;
277
+ throw new Error(
278
+ `three-vat: patchVATMaterial has no decode for encoding "${String(unhandled.encoding)}"`
279
+ );
280
+ }
281
+ }
282
+ }
119
283
  function patchVATMaterial(material, vat, uniforms, playback, carrier) {
120
284
  assertBakedNormal(vat, material);
121
285
  if (carrier) assertVATCarrier(carrier, vat);
122
- const normalTexture = vat.normalTexture;
123
286
  const id = isBatchedCarrier(carrier) ? "batch" : "instance";
287
+ const decode = decodeFor(vat, id);
124
288
  material.onBeforeCompile = (shader) => {
125
- shader.uniforms.uVatPosTex = { value: vat.positionTexture };
126
289
  shader.uniforms.uVatPlaybackTex = { value: playback.texture };
127
290
  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;
291
+ decode.bind(shader.uniforms);
292
+ shader.vertexShader = ROW_PRELUDE + decode.prelude + decode.inject(shader.vertexShader);
134
293
  };
135
- const key = `three-vat:${id}${normalTexture ? "" : ":no-normal"}`;
136
- material.customProgramCacheKey = () => key;
294
+ material.customProgramCacheKey = () => decode.key;
137
295
  guardCarrierMismatch(material, id);
138
296
  return material;
139
297
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-vat",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
4
4
  "description": "Bake glTF animation clips into vertex animation textures for zero-CPU instanced crowds in three.js.",
5
5
  "type": "module",
6
6
  "license": "MIT",