three-vat 1.0.1 → 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/README.md +72 -4
- package/dist/carrier-BXaLAPRO.d.ts +515 -0
- package/dist/chunk-3PWAY6MD.js +211 -0
- package/dist/chunk-I4STYOD5.js +53 -0
- package/dist/index.d.ts +113 -14
- package/dist/index.js +622 -68
- package/dist/tsl.d.ts +82 -47
- package/dist/tsl.js +176 -40
- package/dist/webgl.d.ts +31 -26
- package/dist/webgl.js +295 -35
- package/package.json +20 -14
- package/dist/chunk-SXTVASKG.js +0 -40
- package/dist/instance-playback-BrGBIKLe.d.ts +0 -109
package/dist/webgl.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
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';
|
|
2
3
|
import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
|
|
3
4
|
|
|
4
5
|
function getMaxTextureSize(renderer) {
|
|
@@ -7,67 +8,326 @@ function getMaxTextureSize(renderer) {
|
|
|
7
8
|
function createVATUniforms(time = 0) {
|
|
8
9
|
return { uVatTime: { value: time } };
|
|
9
10
|
}
|
|
10
|
-
var
|
|
11
|
-
var
|
|
11
|
+
var glslFloat = (n) => n.toFixed(1);
|
|
12
|
+
var ROW_PRELUDE = (
|
|
13
|
+
/* glsl */
|
|
14
|
+
`
|
|
15
|
+
uniform highp sampler2D uVatPlaybackTex;
|
|
16
|
+
uniform float uVatTime;
|
|
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 ) {
|
|
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 );
|
|
51
|
+
|
|
52
|
+
float frames = vatClip.y;
|
|
53
|
+
float last = frames - 1.0;
|
|
54
|
+
float duration = frames / vatClip.z;
|
|
55
|
+
// Local time: how far into its own animation this instance is. A start time
|
|
56
|
+
// in the past is what desyncs a crowd; a start time in the future has not
|
|
57
|
+
// begun, which is not the same thing as having finished.
|
|
58
|
+
float local = ( uVatTime - vatPlayback.x ) * vatClip.w;
|
|
59
|
+
float loops = local / duration;
|
|
60
|
+
float repetitions = vatPlayback.z;
|
|
61
|
+
|
|
62
|
+
bool started = local >= 0.0;
|
|
63
|
+
bool finished = started && repetitions != ${glslFloat(INFINITE_REPETITIONS)} && loops >= repetitions;
|
|
64
|
+
|
|
65
|
+
float phase;
|
|
66
|
+
bool wraps;
|
|
67
|
+
if ( !started ) {
|
|
68
|
+
phase = 0.0;
|
|
69
|
+
wraps = false;
|
|
70
|
+
} else if ( finished ) {
|
|
71
|
+
// Held at an end pose, and in neither case sampling past it.
|
|
72
|
+
phase = vatPlayback.w == ${glslFloat(EndMode.Clamp)} ? 1.0 : 0.0;
|
|
73
|
+
wraps = false;
|
|
74
|
+
} else if ( vatPlayback.y == ${glslFloat(LoopMode.PingPong)} ) {
|
|
75
|
+
float m = mod( loops, 2.0 );
|
|
76
|
+
phase = m < 1.0 ? m : 2.0 - m;
|
|
77
|
+
wraps = false; // a ping-pong bounces; it does not wrap
|
|
78
|
+
} else {
|
|
79
|
+
phase = fract( loops );
|
|
80
|
+
wraps = true; // and here the interpolation crossing back is correct
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
float f = phase * ( wraps ? frames : last );
|
|
84
|
+
float f0 = min( floor( f ), last );
|
|
85
|
+
float f1 = wraps ? mod( f0 + 1.0, frames ) : min( f0 + 1.0, last );
|
|
86
|
+
|
|
87
|
+
VatRows rows;
|
|
88
|
+
rows.row0 = int( vatClip.x + f0 );
|
|
89
|
+
rows.row1 = int( vatClip.x + f1 );
|
|
90
|
+
rows.blend = f - f0;
|
|
91
|
+
|
|
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;
|
|
104
|
+
}
|
|
105
|
+
return rows;
|
|
106
|
+
}
|
|
107
|
+
`
|
|
108
|
+
);
|
|
109
|
+
var VERTEX_PRELUDE = (
|
|
12
110
|
/* glsl */
|
|
13
111
|
`
|
|
14
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;
|
|
122
|
+
sampled = mix( sampled, frozen, weight );
|
|
123
|
+
}
|
|
124
|
+
return sampled;
|
|
125
|
+
}
|
|
126
|
+
`
|
|
127
|
+
);
|
|
128
|
+
var NORMAL_PRELUDE = (
|
|
129
|
+
/* glsl */
|
|
130
|
+
`
|
|
15
131
|
uniform highp sampler2D uVatNrmTex;
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
132
|
+
`
|
|
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;
|
|
30
202
|
}
|
|
31
203
|
`
|
|
32
204
|
);
|
|
33
|
-
var
|
|
205
|
+
var INSTANCE_ID = {
|
|
206
|
+
instance: "gl_InstanceID",
|
|
207
|
+
batch: "int( getIndirectIndex( gl_DrawID ) )"
|
|
208
|
+
};
|
|
209
|
+
var vertexPosition = (id) => (
|
|
34
210
|
/* glsl */
|
|
35
211
|
`
|
|
36
|
-
vec3 transformed = position + vatSample( uVatPosTex );
|
|
212
|
+
vec3 transformed = position + vatSample( uVatPosTex, ${INSTANCE_ID[id]} );
|
|
37
213
|
`
|
|
38
214
|
);
|
|
39
|
-
var
|
|
215
|
+
var vertexNormal = (id) => (
|
|
40
216
|
/* glsl */
|
|
41
217
|
`
|
|
42
|
-
vec3 objectNormal = normalize( vatSample( uVatNrmTex ) );
|
|
218
|
+
vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
|
|
43
219
|
#ifdef USE_TANGENT
|
|
44
220
|
vec3 objectTangent = vec3( tangent.xyz );
|
|
45
221
|
#endif
|
|
46
222
|
`
|
|
47
223
|
);
|
|
48
|
-
|
|
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
|
+
}
|
|
283
|
+
function patchVATMaterial(material, vat, uniforms, playback, carrier) {
|
|
284
|
+
assertBakedNormal(vat, material);
|
|
285
|
+
if (carrier) assertVATCarrier(carrier, vat);
|
|
286
|
+
const id = isBatchedCarrier(carrier) ? "batch" : "instance";
|
|
287
|
+
const decode = decodeFor(vat, id);
|
|
49
288
|
material.onBeforeCompile = (shader) => {
|
|
50
|
-
shader.uniforms.
|
|
51
|
-
shader.uniforms.uVatNrmTex = { value: vat.normalTexture };
|
|
289
|
+
shader.uniforms.uVatPlaybackTex = { value: playback.texture };
|
|
52
290
|
shader.uniforms.uVatTime = uniforms.uVatTime;
|
|
53
|
-
|
|
291
|
+
decode.bind(shader.uniforms);
|
|
292
|
+
shader.vertexShader = ROW_PRELUDE + decode.prelude + decode.inject(shader.vertexShader);
|
|
54
293
|
};
|
|
55
|
-
material.customProgramCacheKey = () =>
|
|
294
|
+
material.customProgramCacheKey = () => decode.key;
|
|
295
|
+
guardCarrierMismatch(material, id);
|
|
56
296
|
return material;
|
|
57
297
|
}
|
|
58
|
-
function
|
|
298
|
+
function guardCarrierMismatch(material, id) {
|
|
299
|
+
const previous = material.onBeforeRender.bind(material);
|
|
300
|
+
let checked = false;
|
|
301
|
+
material.onBeforeRender = function(renderer, scene, camera, geometry, object, group) {
|
|
302
|
+
if (!checked) {
|
|
303
|
+
checked = true;
|
|
304
|
+
const drawnOn = isBatchedCarrier(object) ? "batch" : "instance";
|
|
305
|
+
if (drawnOn !== id) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`three-vat: this material was patched for ${CARRIER_NAME[id]} and is being drawn on ${CARRIER_NAME[drawnOn]}. Pass the carrier as \`patchVATMaterial\`'s fifth argument \u2014 the two spell the instance index differently, and a batch drawn with the instanced spelling plays instance 0\u2019s clip on every instance.`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
previous(renderer, scene, camera, geometry, object, group);
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
var CARRIER_NAME = {
|
|
315
|
+
instance: "an InstancedMesh",
|
|
316
|
+
batch: "a BatchedMesh"
|
|
317
|
+
};
|
|
318
|
+
function createVATDepthMaterial(vat, uniforms, playback, carrier) {
|
|
59
319
|
const depth = new MeshDepthMaterial({ depthPacking: RGBADepthPacking });
|
|
60
|
-
patchVATMaterial(depth, vat, uniforms);
|
|
320
|
+
patchVATMaterial(depth, vat, uniforms, playback, carrier);
|
|
61
321
|
return depth;
|
|
62
322
|
}
|
|
63
323
|
function createVATMesh(vat, instances, options = {}) {
|
|
64
324
|
const uniforms = options.time ? { uVatTime: options.time } : createVATUniforms();
|
|
65
|
-
const
|
|
66
|
-
const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms));
|
|
67
|
-
const mesh = new InstancedMesh(geometry, materials, instances.length);
|
|
68
|
-
mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms);
|
|
69
|
-
mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms);
|
|
70
|
-
return { mesh, time: uniforms.uVatTime };
|
|
325
|
+
const playback = createVATPlaybackTexture(instances);
|
|
326
|
+
const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback));
|
|
327
|
+
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);
|
|
330
|
+
return { mesh, time: uniforms.uVatTime, playback };
|
|
71
331
|
}
|
|
72
332
|
|
|
73
|
-
export {
|
|
333
|
+
export { createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "three-vat",
|
|
3
|
-
"version": "1.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",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
18
18
|
},
|
|
19
|
+
"packageManager": "pnpm@10.19.0",
|
|
19
20
|
"sideEffects": false,
|
|
20
21
|
"files": [
|
|
21
22
|
"dist"
|
|
@@ -34,6 +35,15 @@
|
|
|
34
35
|
"import": "./dist/tsl.js"
|
|
35
36
|
}
|
|
36
37
|
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"dev": "pnpm --filter three-vat-example dev",
|
|
40
|
+
"test": "vitest run && pnpm --filter three-vat-example test",
|
|
41
|
+
"build": "tsup",
|
|
42
|
+
"typecheck": "tsc --noEmit && tsc --noEmit -p release/tsconfig.json && pnpm --filter three-vat-example typecheck",
|
|
43
|
+
"build:watch": "tsup --watch",
|
|
44
|
+
"test:watch": "vitest",
|
|
45
|
+
"prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
|
|
46
|
+
},
|
|
37
47
|
"keywords": [
|
|
38
48
|
"three",
|
|
39
49
|
"threejs",
|
|
@@ -48,28 +58,24 @@
|
|
|
48
58
|
"tsl"
|
|
49
59
|
],
|
|
50
60
|
"peerDependencies": {
|
|
51
|
-
"three": ">=0.
|
|
61
|
+
"three": ">=0.186.0"
|
|
52
62
|
},
|
|
53
63
|
"devDependencies": {
|
|
54
|
-
"@types/three": "^0.
|
|
64
|
+
"@types/three": "^0.186.0",
|
|
55
65
|
"gifenc": "1.0.3",
|
|
56
66
|
"playwright-core": "1.63.0",
|
|
57
67
|
"pngjs": "7.0.0",
|
|
58
|
-
"three": "^0.
|
|
68
|
+
"three": "^0.186.0",
|
|
59
69
|
"tsup": "^8.3.0",
|
|
60
70
|
"typescript": "^5.6.0",
|
|
61
71
|
"vite": "^7.1.0",
|
|
62
72
|
"vitest": "^2.1.0"
|
|
63
73
|
},
|
|
64
74
|
"engines": {
|
|
65
|
-
"node": ">=
|
|
75
|
+
"node": ">=20"
|
|
66
76
|
},
|
|
67
|
-
"
|
|
68
|
-
"
|
|
69
|
-
"
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
"build:watch": "tsup --watch",
|
|
73
|
-
"test:watch": "vitest"
|
|
74
|
-
}
|
|
75
|
-
}
|
|
77
|
+
"workspaces": [
|
|
78
|
+
".",
|
|
79
|
+
"examples"
|
|
80
|
+
]
|
|
81
|
+
}
|
package/dist/chunk-SXTVASKG.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { InstancedBufferAttribute } from 'three';
|
|
2
|
-
|
|
3
|
-
// src/instance-playback.ts
|
|
4
|
-
var PLAYBACK_ATTRIBUTES = {
|
|
5
|
-
clipStart: "aClipStart",
|
|
6
|
-
clipFrames: "aClipFrames",
|
|
7
|
-
clipFps: "aClipFps",
|
|
8
|
-
timeOffset: "aTimeOffset",
|
|
9
|
-
speed: "aSpeed"
|
|
10
|
-
};
|
|
11
|
-
function addVATInstanceAttributes(geometry, instances) {
|
|
12
|
-
geometry.morphAttributes = {};
|
|
13
|
-
geometry.morphTargetsRelative = false;
|
|
14
|
-
const n = instances.length;
|
|
15
|
-
const clipStart = new Float32Array(n);
|
|
16
|
-
const clipFrames = new Float32Array(n);
|
|
17
|
-
const clipFps = new Float32Array(n);
|
|
18
|
-
const timeOffset = new Float32Array(n);
|
|
19
|
-
const speed = new Float32Array(n);
|
|
20
|
-
for (let i = 0; i < n; i++) {
|
|
21
|
-
const inst = instances[i];
|
|
22
|
-
clipStart[i] = inst.clip.startFrame;
|
|
23
|
-
clipFrames[i] = inst.clip.frames;
|
|
24
|
-
clipFps[i] = inst.clip.fps;
|
|
25
|
-
timeOffset[i] = inst.timeOffset;
|
|
26
|
-
speed[i] = inst.speed;
|
|
27
|
-
}
|
|
28
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipStart, new InstancedBufferAttribute(clipStart, 1));
|
|
29
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipFrames, new InstancedBufferAttribute(clipFrames, 1));
|
|
30
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipFps, new InstancedBufferAttribute(clipFps, 1));
|
|
31
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.timeOffset, new InstancedBufferAttribute(timeOffset, 1));
|
|
32
|
-
geometry.setAttribute(PLAYBACK_ATTRIBUTES.speed, new InstancedBufferAttribute(speed, 1));
|
|
33
|
-
}
|
|
34
|
-
function createCrowdGeometry(vat, instances) {
|
|
35
|
-
const geometry = vat.geometry.clone();
|
|
36
|
-
addVATInstanceAttributes(geometry, instances);
|
|
37
|
-
return geometry;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export { PLAYBACK_ATTRIBUTES, addVATInstanceAttributes, createCrowdGeometry };
|
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import { DataTexture, BufferGeometry, Material, Box3, InstancedMesh } from 'three';
|
|
2
|
-
|
|
3
|
-
/** One baked animation range within a VAT's stacked frame rows. */
|
|
4
|
-
interface VATClip {
|
|
5
|
-
/** Clip name, taken from the source `AnimationClip`. */
|
|
6
|
-
name: string;
|
|
7
|
-
/** First frame row (y) of this clip in the texture. */
|
|
8
|
-
startFrame: number;
|
|
9
|
-
/** Number of frame rows baked for this clip. */
|
|
10
|
-
frames: number;
|
|
11
|
-
/** Effective frames-per-second of the bake (`frames / duration`). */
|
|
12
|
-
fps: number;
|
|
13
|
-
/** Source clip duration in seconds. */
|
|
14
|
-
duration: number;
|
|
15
|
-
/**
|
|
16
|
-
* Largest per-vertex position-delta magnitude (metres) across the clip.
|
|
17
|
-
* Near-zero means the clip baked as a frozen pose — the diagnostic for a
|
|
18
|
-
* mis-targeted or genuinely static clip.
|
|
19
|
-
*/
|
|
20
|
-
maxDelta: number;
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* A baked Vertex Animation Texture: the position/normal `DataTexture`s, the
|
|
24
|
-
* geometry they are indexed by, and the clip table and bounds needed to decode
|
|
25
|
-
* and render them. Produced exactly one way — {@link bakeVAT}, at runtime, from
|
|
26
|
-
* a loaded glTF (ADR-0010).
|
|
27
|
-
*
|
|
28
|
-
* The merged vertex ordering is the baker's own invention and the textures are
|
|
29
|
-
* indexed by it (`x = gl_VertexID`), so the caller cannot bring its own
|
|
30
|
-
* geometry — it must render the one baked here. `materials` is ordered to match
|
|
31
|
-
* `geometry.groups[].materialIndex`, giving one draw call per material.
|
|
32
|
-
*/
|
|
33
|
-
interface VAT {
|
|
34
|
-
/** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
|
|
35
|
-
positionTexture: DataTexture;
|
|
36
|
-
/** RGBA float texture of per-vertex absolute normals (`x = vertex`, `y = frame`). */
|
|
37
|
-
normalTexture: DataTexture;
|
|
38
|
-
/** Merged, root-space rest-pose geometry. Its `position` is the delta reference. */
|
|
39
|
-
geometry: BufferGeometry;
|
|
40
|
-
/** Source materials, indexed by `geometry.groups[].materialIndex`. */
|
|
41
|
-
materials: Material[];
|
|
42
|
-
/** Clip table: name → `{ startFrame, frames, fps, ... }`. */
|
|
43
|
-
clips: VATClip[];
|
|
44
|
-
/** Union of every baked frame's bounds; use as the geometry bounding box. */
|
|
45
|
-
bounds: Box3;
|
|
46
|
-
/** Vertex count (texture width). */
|
|
47
|
-
vertexCount: number;
|
|
48
|
-
/** Total frame rows across all clips (texture height). */
|
|
49
|
-
totalFrames: number;
|
|
50
|
-
/** Position encoding. Only `'delta'` in v1. */
|
|
51
|
-
encoding: 'delta';
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* The shared playback clock: one `{ value }` in seconds, read by every material
|
|
55
|
-
* of every VAT mesh driven by it. Set it once per frame. Deliberately the
|
|
56
|
-
* narrowest shape both decode paths satisfy — a WebGL `IUniform<number>` and a
|
|
57
|
-
* TSL uniform node are both one of these — so `createVATMesh` returns the same
|
|
58
|
-
* thing on either renderer.
|
|
59
|
-
*/
|
|
60
|
-
interface VATClock {
|
|
61
|
-
value: number;
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* A **crowd** ready to render: the mesh to add to the scene, and the clock to
|
|
65
|
-
* advance. What `createVATMesh` returns on either decode path, so moving a
|
|
66
|
-
* crowd between renderers is an import change and nothing else. Named for what
|
|
67
|
-
* it is rather than for its `mesh` field — the clock is half of it.
|
|
68
|
-
*/
|
|
69
|
-
interface VATCrowd {
|
|
70
|
-
/** Add to the scene. Its instance matrices are yours to write. */
|
|
71
|
-
mesh: InstancedMesh;
|
|
72
|
-
/** The shared playback clock — set `.value` once per frame. */
|
|
73
|
-
time: VATClock;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/** Per-instance playback state consumed by both decode paths. */
|
|
77
|
-
interface VATInstance {
|
|
78
|
-
clip: Pick<VAT['clips'][number], 'startFrame' | 'frames' | 'fps'>;
|
|
79
|
-
/** Phase offset in seconds — desyncs the crowd. */
|
|
80
|
-
timeOffset: number;
|
|
81
|
-
/** Playback rate multiplier. */
|
|
82
|
-
speed: number;
|
|
83
|
-
}
|
|
84
|
-
/**
|
|
85
|
-
* Attach the instance-playback attributes to an instanced geometry. Call once
|
|
86
|
-
* before rendering, on the geometry you hand to the `InstancedMesh`.
|
|
87
|
-
*
|
|
88
|
-
* The attribute names and layout below are the shared contract, spelled once in
|
|
89
|
-
* {@link PLAYBACK_ATTRIBUTES}. Both decode paths read exactly these five —
|
|
90
|
-
* `DECODE_PRELUDE` in `src/webgl.ts` as GLSL attributes, `vatNodes` in
|
|
91
|
-
* `src/tsl.ts` as TSL attribute nodes, when it is handed this geometry.
|
|
92
|
-
*
|
|
93
|
-
* | Attribute | Type | Source |
|
|
94
|
-
* | ------------- | ------------- | --------------------- |
|
|
95
|
-
* | `aClipStart` | `float` (x 1) | `instance.clip.startFrame` — first texture row of the clip's frame band |
|
|
96
|
-
* | `aClipFrames` | `float` (x 1) | `instance.clip.frames` — rows in the band |
|
|
97
|
-
* | `aClipFps` | `float` (x 1) | `instance.clip.fps` — with `frames`, the clip's duration |
|
|
98
|
-
* | `aTimeOffset` | `float` (x 1) | `instance.timeOffset` — phase, in seconds |
|
|
99
|
-
* | `aSpeed` | `float` (x 1) | `instance.speed` — rate multiplier |
|
|
100
|
-
*
|
|
101
|
-
* Every entry is a one-component `InstancedBufferAttribute` of `Float32Array`,
|
|
102
|
-
* one element per instance, in instance order. Adding a field — crossfade's
|
|
103
|
-
* reserved second clip index being the known case (ADR-0007) — means adding it
|
|
104
|
-
* here, in the table above, and in each decode path's own attribute
|
|
105
|
-
* declarations.
|
|
106
|
-
*/
|
|
107
|
-
declare function addVATInstanceAttributes(geometry: BufferGeometry, instances: VATInstance[]): void;
|
|
108
|
-
|
|
109
|
-
export { type VAT as V, type VATClip as a, type VATClock as b, type VATCrowd as c, type VATInstance as d, addVATInstanceAttributes as e };
|