three-vat 1.0.1 → 2.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/README.md +67 -4
- package/dist/carrier-BFCPmcQK.d.ts +472 -0
- package/dist/chunk-2PGZ44TP.js +202 -0
- package/dist/chunk-W2ZAFMPB.js +53 -0
- package/dist/index.d.ts +78 -14
- package/dist/index.js +137 -47
- package/dist/tsl.d.ts +43 -31
- package/dist/tsl.js +94 -38
- package/dist/webgl.d.ts +31 -26
- package/dist/webgl.js +135 -33
- package/package.json +20 -14
- package/dist/chunk-SXTVASKG.js +0 -40
- package/dist/instance-playback-BrGBIKLe.d.ts +0 -109
package/dist/index.js
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { LoopMode, MAX_TEXTURE_SIZE, makeVATTexture, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS } from './chunk-2PGZ44TP.js';
|
|
2
|
+
export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_FADE_DURATION, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-2PGZ44TP.js';
|
|
3
|
+
import { LoopRepeat, LoopOnce, LoopPingPong, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, AdditiveAnimationBlendMode, BufferAttribute, BufferGeometry } from 'three';
|
|
3
4
|
|
|
4
|
-
var
|
|
5
|
+
var LOOP_MODES = /* @__PURE__ */ new Map([
|
|
6
|
+
[LoopRepeat, LoopMode.Repeat],
|
|
7
|
+
[LoopOnce, LoopMode.Once],
|
|
8
|
+
[LoopPingPong, LoopMode.PingPong]
|
|
9
|
+
]);
|
|
10
|
+
function isAction(input) {
|
|
11
|
+
return typeof input.getClip === "function";
|
|
12
|
+
}
|
|
13
|
+
function resolveAnimation(input) {
|
|
14
|
+
if (!isAction(input)) return { clip: input, defaults: { ...LIBRARY_PLAYBACK_DEFAULTS } };
|
|
15
|
+
const clip = input.getClip();
|
|
16
|
+
const name = clip.name || "(unnamed)";
|
|
17
|
+
const cannotBlend = (field, value) => new Error(
|
|
18
|
+
`three-vat: action for clip "${name}" has ${field} ${value}; that blends several actions at once, which a single baked band cannot represent \u2014 bake the clips separately and crossfade between them (three-vat#30), or reset the action before baking`
|
|
19
|
+
);
|
|
20
|
+
if (input.weight !== 1) throw cannotBlend("weight", String(input.weight));
|
|
21
|
+
if (input.blendMode === AdditiveAnimationBlendMode) throw cannotBlend("blendMode", "additive");
|
|
22
|
+
if (input.timeScale < 0) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`three-vat: action for clip "${name}" has timeScale ${input.timeScale}; ${FORWARD_ONLY_REASON}.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const loopMode = LOOP_MODES.get(input.loop);
|
|
28
|
+
if (loopMode === void 0) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`three-vat: action for clip "${name}" has an unrecognised loop mode ${input.loop}; expected THREE.LoopRepeat, LoopOnce or LoopPingPong`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
clip,
|
|
35
|
+
defaults: {
|
|
36
|
+
loopMode,
|
|
37
|
+
// three's `repetitions` defaults to Infinity and `LoopOnce` ignores it
|
|
38
|
+
// outright — so a one-shot is one play whatever the field says, and an
|
|
39
|
+
// endless count becomes the sentinel a Float32Array can carry.
|
|
40
|
+
repetitions: loopMode === LoopMode.Once ? 1 : Number.isFinite(input.repetitions) ? input.repetitions : INFINITE_REPETITIONS,
|
|
41
|
+
// Not read off the action (see the rule above), so a bake clamps whichever
|
|
42
|
+
// input it was handed. Uniform across loop modes on purpose: an instance
|
|
43
|
+
// may override a repeating clip's `loopMode` to `Once` and inherit this
|
|
44
|
+
// field, so there is no mode whose end mode is safely unreachable and
|
|
45
|
+
// could hold a different answer. Rewind is per instance:
|
|
46
|
+
// `{ ..., endMode: EndMode.Rewind }` (#43, ADR-0017).
|
|
47
|
+
endMode: LIBRARY_PLAYBACK_DEFAULTS.endMode,
|
|
48
|
+
speed: input.timeScale
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
5
52
|
function asAttribute(value, mesh, name) {
|
|
6
53
|
if (!(value instanceof BufferAttribute)) {
|
|
7
54
|
throw new Error(
|
|
@@ -44,6 +91,8 @@ function collectParts(root) {
|
|
|
44
91
|
baseNrm: asAttribute(geometry.attributes.normal, mesh, "normal"),
|
|
45
92
|
isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
|
|
46
93
|
skeleton: skinned.skeleton,
|
|
94
|
+
pose: void 0,
|
|
95
|
+
// assigned below, once the distinct rigs are known
|
|
47
96
|
skinIndex: geometry.attributes.skinIndex,
|
|
48
97
|
skinWeight: geometry.attributes.skinWeight,
|
|
49
98
|
morphPos: geometry.morphAttributes.position,
|
|
@@ -64,18 +113,25 @@ function mergeGeometry(parts, restMatrices, total) {
|
|
|
64
113
|
const normal = new Float32Array(total * 3);
|
|
65
114
|
const wantUV = parts.every((p) => !!p.mesh.geometry.attributes.uv);
|
|
66
115
|
const wantColor = parts.every((p) => !!p.mesh.geometry.attributes.color);
|
|
116
|
+
const wantTangent = parts.every((p) => {
|
|
117
|
+
const t = p.mesh.geometry.attributes.tangent;
|
|
118
|
+
return t instanceof BufferAttribute && t.itemSize === 4;
|
|
119
|
+
});
|
|
67
120
|
const uv = wantUV ? new Float32Array(total * 2) : null;
|
|
68
121
|
const color = wantColor ? new Float32Array(total * 3) : null;
|
|
122
|
+
const tangent = wantTangent ? new Float32Array(total * 4) : null;
|
|
69
123
|
const indices = [];
|
|
70
124
|
const groups = [];
|
|
71
125
|
const _v = new Vector3();
|
|
72
126
|
const _n = new Vector3();
|
|
127
|
+
const _t = new Vector3();
|
|
73
128
|
parts.forEach((part, pi) => {
|
|
74
129
|
const m = restMatrices[pi];
|
|
75
130
|
const geometry = part.mesh.geometry;
|
|
76
131
|
const start = part.vertexStart;
|
|
77
132
|
const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
|
|
78
133
|
const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
|
|
134
|
+
const srcTangent = tangent ? geometry.attributes.tangent : null;
|
|
79
135
|
for (let v = 0; v < part.vertexCount; v++) {
|
|
80
136
|
_v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
|
|
81
137
|
_n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
|
|
@@ -95,6 +151,14 @@ function mergeGeometry(parts, restMatrices, total) {
|
|
|
95
151
|
color[o3 + 1] = srcColor.getY(v);
|
|
96
152
|
color[o3 + 2] = srcColor.getZ(v);
|
|
97
153
|
}
|
|
154
|
+
if (tangent && srcTangent) {
|
|
155
|
+
_t.fromBufferAttribute(srcTangent, v).transformDirection(m);
|
|
156
|
+
const o4 = (start + v) * 4;
|
|
157
|
+
tangent[o4] = _t.x;
|
|
158
|
+
tangent[o4 + 1] = _t.y;
|
|
159
|
+
tangent[o4 + 2] = _t.z;
|
|
160
|
+
tangent[o4 + 3] = srcTangent.getW(v);
|
|
161
|
+
}
|
|
98
162
|
}
|
|
99
163
|
const groupStart = indices.length;
|
|
100
164
|
const index = geometry.index;
|
|
@@ -119,11 +183,14 @@ function mergeGeometry(parts, restMatrices, total) {
|
|
|
119
183
|
merged.setAttribute("normal", new BufferAttribute(normal, 3));
|
|
120
184
|
if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
|
|
121
185
|
if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
|
|
186
|
+
if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
|
|
122
187
|
merged.setIndex(indices);
|
|
123
188
|
for (const g of groups) merged.addGroup(g.start, g.count, g.materialIndex);
|
|
124
189
|
return merged;
|
|
125
190
|
}
|
|
126
|
-
function bakeVAT(root,
|
|
191
|
+
function bakeVAT(root, animations, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE, bakeNormals = true } = {}) {
|
|
192
|
+
const resolved = animations.map(resolveAnimation);
|
|
193
|
+
const clips = resolved.map((a) => a.clip);
|
|
127
194
|
root.updateMatrixWorld(true);
|
|
128
195
|
const parts = collectParts(root);
|
|
129
196
|
const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
|
|
@@ -146,13 +213,13 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
146
213
|
const geometry = mergeGeometry(parts, restMatrices, vertexCount);
|
|
147
214
|
const mergedBase = geometry.attributes.position;
|
|
148
215
|
const posData = new Float32Array(vertexCount * totalFrames * 4);
|
|
149
|
-
const nrmData = new Float32Array(vertexCount * totalFrames * 4);
|
|
216
|
+
const nrmData = bakeNormals ? new Float32Array(vertexCount * totalFrames * 4) : null;
|
|
217
|
+
const bakeNormal = nrmData !== null;
|
|
150
218
|
const mixer = new AnimationMixer(root);
|
|
151
219
|
const bounds = new Box3();
|
|
152
220
|
const clipTable = [];
|
|
153
221
|
const _si = new Vector4();
|
|
154
222
|
const _sw = new Vector4();
|
|
155
|
-
const _bone = new Matrix4();
|
|
156
223
|
const _acc = new Matrix4();
|
|
157
224
|
const _skin = new Matrix4();
|
|
158
225
|
const _partMatrix = new Matrix4();
|
|
@@ -162,7 +229,8 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
162
229
|
const _mt = new Vector3();
|
|
163
230
|
const _mb = new Vector3();
|
|
164
231
|
const _mbn = new Vector3();
|
|
165
|
-
const
|
|
232
|
+
const poses = attachPoseBuffers(parts);
|
|
233
|
+
const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
|
|
166
234
|
let warnedNonUniformScale = false;
|
|
167
235
|
let rowOffset = 0;
|
|
168
236
|
clips.forEach((clip, ci) => {
|
|
@@ -174,22 +242,24 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
174
242
|
mixer.setTime(f / frames * clip.duration);
|
|
175
243
|
root.updateMatrixWorld(true);
|
|
176
244
|
const row = rowOffset + f;
|
|
245
|
+
poseSkeletons(poses);
|
|
177
246
|
if (!warnedNonUniformScale) {
|
|
178
|
-
warnedNonUniformScale = warnOnNonUniformBoneScale(influencers
|
|
247
|
+
warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
|
|
179
248
|
}
|
|
180
249
|
for (const part of parts) {
|
|
181
250
|
_partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
|
|
182
251
|
const influences = part.mesh.morphTargetInfluences;
|
|
183
|
-
const { morphPos, morphNrm, morphRelative, isSkinned,
|
|
252
|
+
const { morphPos, morphNrm, morphRelative, isSkinned, skinIndex, skinWeight } = part;
|
|
253
|
+
const boneMatrices = part.pose?.matrices;
|
|
184
254
|
const morphCount = influences ? Math.min(influences.length, Math.max(morphPos?.length ?? 0, morphNrm?.length ?? 0)) : 0;
|
|
185
255
|
for (let v = 0; v < part.vertexCount; v++) {
|
|
186
256
|
const vi = part.vertexStart + v;
|
|
187
257
|
_p.fromBufferAttribute(part.basePos, v);
|
|
188
|
-
_n.fromBufferAttribute(part.baseNrm, v);
|
|
258
|
+
if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
|
|
189
259
|
if (influences && morphCount > 0) {
|
|
190
260
|
if (!morphRelative) {
|
|
191
261
|
_mb.fromBufferAttribute(part.basePos, v);
|
|
192
|
-
_mbn.fromBufferAttribute(part.baseNrm, v);
|
|
262
|
+
if (bakeNormal) _mbn.fromBufferAttribute(part.baseNrm, v);
|
|
193
263
|
}
|
|
194
264
|
for (let t = 0; t < morphCount; t++) {
|
|
195
265
|
const w = influences[t];
|
|
@@ -200,7 +270,7 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
200
270
|
if (!morphRelative) _mt.sub(_mb);
|
|
201
271
|
_p.addScaledVector(_mt, w);
|
|
202
272
|
}
|
|
203
|
-
const targetNrm = morphNrm?.[t];
|
|
273
|
+
const targetNrm = bakeNormal ? morphNrm?.[t] : void 0;
|
|
204
274
|
if (targetNrm) {
|
|
205
275
|
_mt.fromBufferAttribute(targetNrm, v);
|
|
206
276
|
if (!morphRelative) _mt.sub(_mbn);
|
|
@@ -208,26 +278,24 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
208
278
|
}
|
|
209
279
|
}
|
|
210
280
|
}
|
|
211
|
-
if (isSkinned &&
|
|
281
|
+
if (isSkinned && boneMatrices) {
|
|
212
282
|
const skinned = part.mesh;
|
|
213
283
|
_si.fromBufferAttribute(skinIndex, v);
|
|
214
284
|
_sw.fromBufferAttribute(skinWeight, v);
|
|
215
|
-
_acc.elements
|
|
285
|
+
const ae = _acc.elements;
|
|
286
|
+
ae.fill(0);
|
|
216
287
|
for (let i = 0; i < 4; i++) {
|
|
217
288
|
const w = _sw.getComponent(i);
|
|
218
289
|
if (w === 0) continue;
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
const ae = _acc.elements;
|
|
222
|
-
const be = _bone.elements;
|
|
223
|
-
for (let e = 0; e < 16; e++) ae[e] += be[e] * w;
|
|
290
|
+
const b = _si.getComponent(i) * BONE_STRIDE;
|
|
291
|
+
for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
|
|
224
292
|
}
|
|
225
293
|
_skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
|
|
226
294
|
_p.applyMatrix4(_skin);
|
|
227
|
-
_n.transformDirection(_skin);
|
|
295
|
+
if (bakeNormal) _n.transformDirection(_skin);
|
|
228
296
|
}
|
|
229
297
|
_p.applyMatrix4(_partMatrix);
|
|
230
|
-
_n.transformDirection(_partMatrix);
|
|
298
|
+
if (bakeNormal) _n.transformDirection(_partMatrix);
|
|
231
299
|
bounds.expandByPoint(_p);
|
|
232
300
|
_bp.fromBufferAttribute(mergedBase, vi);
|
|
233
301
|
const o = (row * vertexCount + vi) * 4;
|
|
@@ -239,10 +307,12 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
239
307
|
posData[o + 1] = dy;
|
|
240
308
|
posData[o + 2] = dz;
|
|
241
309
|
posData[o + 3] = 1;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
310
|
+
if (bakeNormal) {
|
|
311
|
+
nrmData[o] = _n.x;
|
|
312
|
+
nrmData[o + 1] = _n.y;
|
|
313
|
+
nrmData[o + 2] = _n.z;
|
|
314
|
+
nrmData[o + 3] = 1;
|
|
315
|
+
}
|
|
246
316
|
}
|
|
247
317
|
}
|
|
248
318
|
}
|
|
@@ -253,7 +323,10 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
253
323
|
frames,
|
|
254
324
|
fps: frames / clip.duration,
|
|
255
325
|
duration: clip.duration,
|
|
256
|
-
maxDelta: Math.sqrt(maxDeltaSq)
|
|
326
|
+
maxDelta: Math.sqrt(maxDeltaSq),
|
|
327
|
+
// Declared once, here, rather than repeated at every instance that plays
|
|
328
|
+
// this band. An instance overrides any of them, field by field.
|
|
329
|
+
...resolved[ci].defaults
|
|
257
330
|
});
|
|
258
331
|
rowOffset += frames;
|
|
259
332
|
});
|
|
@@ -266,7 +339,7 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
266
339
|
for (const part of parts) materials[part.materialIndex] = part.material;
|
|
267
340
|
return {
|
|
268
341
|
positionTexture: makeVATTexture(posData, vertexCount, totalFrames),
|
|
269
|
-
normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames),
|
|
342
|
+
normalTexture: nrmData ? makeVATTexture(nrmData, vertexCount, totalFrames) : null,
|
|
270
343
|
clips: clipTable,
|
|
271
344
|
bounds,
|
|
272
345
|
vertexCount,
|
|
@@ -276,6 +349,32 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
276
349
|
materials
|
|
277
350
|
};
|
|
278
351
|
}
|
|
352
|
+
var BONE_STRIDE = 16;
|
|
353
|
+
var IDENTITY = /* @__PURE__ */ new Matrix4();
|
|
354
|
+
function attachPoseBuffers(parts) {
|
|
355
|
+
const byRig = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const part of parts) {
|
|
357
|
+
if (!part.isSkinned || !part.skeleton) continue;
|
|
358
|
+
let pose = byRig.get(part.skeleton);
|
|
359
|
+
if (!pose) {
|
|
360
|
+
const matrices = new Float64Array(part.skeleton.bones.length * BONE_STRIDE);
|
|
361
|
+
pose = { skeleton: part.skeleton, matrices };
|
|
362
|
+
byRig.set(part.skeleton, pose);
|
|
363
|
+
}
|
|
364
|
+
part.pose = pose;
|
|
365
|
+
}
|
|
366
|
+
return [...byRig.values()];
|
|
367
|
+
}
|
|
368
|
+
function poseSkeletons(poses) {
|
|
369
|
+
const scratch = new Matrix4();
|
|
370
|
+
for (const { skeleton, matrices } of poses) {
|
|
371
|
+
const { bones, boneInverses } = skeleton;
|
|
372
|
+
for (let b = 0; b < bones.length; b++) {
|
|
373
|
+
scratch.multiplyMatrices(bones[b]?.matrixWorld ?? IDENTITY, boneInverses[b]);
|
|
374
|
+
matrices.set(scratch.elements, b * BONE_STRIDE);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
279
378
|
function influencedBones(part) {
|
|
280
379
|
const used = /* @__PURE__ */ new Set();
|
|
281
380
|
const index = part.skinIndex;
|
|
@@ -285,37 +384,28 @@ function influencedBones(part) {
|
|
|
285
384
|
if (weight.getComponent(v, i) !== 0) used.add(index.getComponent(v, i));
|
|
286
385
|
}
|
|
287
386
|
}
|
|
288
|
-
return {
|
|
387
|
+
return { pose: part.pose, bones: [...used] };
|
|
289
388
|
}
|
|
290
389
|
var SCALE_UNIFORMITY_EPSILON = 1e-4;
|
|
291
|
-
function hasNonUniformScale(
|
|
292
|
-
const
|
|
293
|
-
const x =
|
|
294
|
-
const y =
|
|
295
|
-
const z =
|
|
390
|
+
function hasNonUniformScale(matrices, b) {
|
|
391
|
+
const o = b * BONE_STRIDE;
|
|
392
|
+
const x = matrices[o] ** 2 + matrices[o + 1] ** 2 + matrices[o + 2] ** 2;
|
|
393
|
+
const y = matrices[o + 4] ** 2 + matrices[o + 5] ** 2 + matrices[o + 6] ** 2;
|
|
394
|
+
const z = matrices[o + 8] ** 2 + matrices[o + 9] ** 2 + matrices[o + 10] ** 2;
|
|
296
395
|
const max = Math.max(x, y, z);
|
|
297
396
|
return max - Math.min(x, y, z) > SCALE_UNIFORMITY_EPSILON * max;
|
|
298
397
|
}
|
|
299
|
-
function warnOnNonUniformBoneScale(influencers
|
|
300
|
-
for (const {
|
|
398
|
+
function warnOnNonUniformBoneScale(influencers) {
|
|
399
|
+
for (const { pose, bones } of influencers) {
|
|
301
400
|
for (const b of bones) {
|
|
302
|
-
|
|
303
|
-
if (!hasNonUniformScale(scratch)) continue;
|
|
401
|
+
if (!hasNonUniformScale(pose.matrices, b)) continue;
|
|
304
402
|
console.warn(
|
|
305
|
-
`three-vat: bone "${skeleton.bones[b].name || "(unnamed)"}" animates with non-uniform scale; baked normals under it are approximate, because linear-blend skinning transforms a normal by the skin matrix rather than its inverse-transpose \u2014 the same shortcut three's own skinning shader takes. Positions are exact.`
|
|
403
|
+
`three-vat: bone "${pose.skeleton.bones[b].name || "(unnamed)"}" animates with non-uniform scale; baked normals under it are approximate, because linear-blend skinning transforms a normal by the skin matrix rather than its inverse-transpose \u2014 the same shortcut three's own skinning shader takes. Positions are exact.`
|
|
306
404
|
);
|
|
307
405
|
return true;
|
|
308
406
|
}
|
|
309
407
|
}
|
|
310
408
|
return false;
|
|
311
409
|
}
|
|
312
|
-
function makeVATTexture(data, width, height, type = FloatType) {
|
|
313
|
-
const tex = new DataTexture(data, width, height, RGBAFormat, type);
|
|
314
|
-
tex.minFilter = NearestFilter;
|
|
315
|
-
tex.magFilter = NearestFilter;
|
|
316
|
-
tex.generateMipmaps = false;
|
|
317
|
-
tex.needsUpdate = true;
|
|
318
|
-
return tex;
|
|
319
|
-
}
|
|
320
410
|
|
|
321
|
-
export {
|
|
411
|
+
export { bakeVAT };
|
package/dist/tsl.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BufferGeometry, InstancedMesh } from 'three';
|
|
2
1
|
import { Node } from 'three/webgpu';
|
|
3
|
-
import {
|
|
2
|
+
import { d as VATClock, i as VATPlaybackTexture, a as VATCarrier, V as VAT, h as VATInstance, e as VATCrowd } from './carrier-BFCPmcQK.js';
|
|
3
|
+
import 'three';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The real maximum texture dimension this renderer accepts, for
|
|
@@ -31,40 +31,50 @@ interface VATNodeOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
time?: VATTimeUniform;
|
|
33
33
|
/**
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* `desync`.
|
|
34
|
+
* The crowd's playback texture — build it with `createVATPlaybackTexture`
|
|
35
|
+
* from `three-vat` *before* calling this, or let `createVATMesh` do it — and
|
|
36
|
+
* each instance plays its own clip, at its own phase and rate, read from its
|
|
37
|
+
* own row — which row that is, is the carrier's answer (see `carrier`). Without
|
|
38
|
+
* it, every instance plays `clipIndex`, phase-desynced by `desync`.
|
|
39
39
|
*
|
|
40
|
-
* Which decode the graph compiles is decided here, at build time:
|
|
41
|
-
*
|
|
42
|
-
* fallback cannot be a shader-side branch.
|
|
40
|
+
* Which decode the graph compiles is decided here, at build time: the
|
|
41
|
+
* fallback is a different graph, not a shader-side branch.
|
|
43
42
|
*/
|
|
44
|
-
|
|
43
|
+
playback?: VATPlaybackTexture;
|
|
45
44
|
/**
|
|
46
|
-
* The
|
|
45
|
+
* The crowd's **carrier** — the mesh these nodes will render on, an
|
|
46
|
+
* `InstancedMesh` or a `BatchedMesh`. Named as the WebGL path's
|
|
47
|
+
* `patchVATMaterial` names it, because it is the one concept (CONTEXT.md).
|
|
47
48
|
*
|
|
48
|
-
* Required for a crowd, and for
|
|
49
|
-
* to `positionLocal` *before* it reads `positionNode`, so
|
|
50
|
-
* displace in the geometry's own space and then re-apply
|
|
51
|
-
* itself. Without this the delta is added in instance space —
|
|
52
|
-
* unscaled — and every instance deforms according to its own
|
|
49
|
+
* Required for a crowd, and for two reasons. First: three applies the
|
|
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).
|
|
59
|
+
*
|
|
60
|
+
* One option rather than one per carrier, because those two answers have to
|
|
61
|
+
* come from the same object: a decode that re-applied one mesh's transform
|
|
62
|
+
* while reading another's index would render a crowd nothing could explain.
|
|
53
63
|
*
|
|
54
64
|
* Omit it for a single, non-instanced mesh, where `positionLocal` is the
|
|
55
65
|
* geometry position and there is nothing to re-apply.
|
|
56
66
|
*/
|
|
57
|
-
|
|
67
|
+
carrier?: VATCarrier;
|
|
58
68
|
/**
|
|
59
69
|
* Which clip to play (index into `vat.clips`). Ignored — along with
|
|
60
|
-
* `desync` — when `
|
|
61
|
-
*
|
|
70
|
+
* `desync` — when a `playback` texture is given, which says all of this per
|
|
71
|
+
* instance. Default `0`.
|
|
62
72
|
*/
|
|
63
73
|
clipIndex?: number;
|
|
64
74
|
/**
|
|
65
|
-
* Max random per-instance time offset in seconds, hashed from
|
|
66
|
-
* `0` (default) plays every instance in lockstep. Ignored when
|
|
67
|
-
*
|
|
75
|
+
* Max random per-instance time offset in seconds, hashed from the instance's
|
|
76
|
+
* logical index. `0` (default) plays every instance in lockstep. Ignored when
|
|
77
|
+
* a `playback` texture is given.
|
|
68
78
|
*/
|
|
69
79
|
desync?: number;
|
|
70
80
|
}
|
|
@@ -97,11 +107,11 @@ interface VATNodes {
|
|
|
97
107
|
* Build TSL decode nodes for a baked VAT, for the WebGPU/TSL renderer path.
|
|
98
108
|
* Shadows work automatically because `positionNode` also feeds the depth pass.
|
|
99
109
|
*
|
|
100
|
-
* Pass the `
|
|
101
|
-
*
|
|
110
|
+
* Pass the crowd's `playback` texture and each instance plays the clip, phase
|
|
111
|
+
* and rate written into its row by `createVATPlaybackTexture` — the same
|
|
102
112
|
* instance-playback contract the WebGL path reads (ADR-0009), so a mixed-clip
|
|
103
|
-
* crowd renders identically on either renderer. Without
|
|
104
|
-
*
|
|
113
|
+
* crowd renders identically on either renderer. Without it every instance plays
|
|
114
|
+
* `clipIndex`, desynced by a phase hashed from the instance index.
|
|
105
115
|
*
|
|
106
116
|
* Coverage note: the node graph is tested structurally in CI (no GPU); that the
|
|
107
117
|
* two paths decode *identically* is a pixel-diff release gate.
|
|
@@ -110,6 +120,7 @@ declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
|
|
|
110
120
|
/**
|
|
111
121
|
* The decode's arithmetic: the position delta and the normal this instance reads
|
|
112
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.
|
|
113
124
|
*
|
|
114
125
|
* @internal Split out and exported for the structural tests. A `Fn` body is
|
|
115
126
|
* opaque to graph traversal (its statements are not built until the shader is),
|
|
@@ -119,7 +130,7 @@ declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
|
|
|
119
130
|
*/
|
|
120
131
|
declare function vatDecode(vat: VAT, options?: VATNodeOptions): {
|
|
121
132
|
position: Vec3Node;
|
|
122
|
-
normal: Vec3Node;
|
|
133
|
+
normal: Vec3Node | null;
|
|
123
134
|
};
|
|
124
135
|
/** Options for {@link createVATMesh}. */
|
|
125
136
|
interface CreateVATMeshOptions {
|
|
@@ -137,11 +148,12 @@ interface CreateVATMeshOptions {
|
|
|
137
148
|
}
|
|
138
149
|
/**
|
|
139
150
|
* Turn a baked VAT and a list of instances into a crowd ready to render: an
|
|
140
|
-
* `InstancedMesh`
|
|
141
|
-
*
|
|
151
|
+
* `InstancedMesh` rendering the bake's geometry, a playback texture carrying
|
|
152
|
+
* the instance-playback contract, and materials that decode the VAT on the
|
|
153
|
+
* vertex stage.
|
|
142
154
|
*
|
|
143
155
|
* ```ts
|
|
144
|
-
* const { mesh, time } = createVATMesh(vat, instances)
|
|
156
|
+
* const { mesh, time, playback } = createVATMesh(vat, instances)
|
|
145
157
|
* mesh.castShadow = mesh.receiveShadow = true
|
|
146
158
|
* scene.add(mesh)
|
|
147
159
|
* // per frame:
|
package/dist/tsl.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { isBatchedCarrier, assertVATCarrier, assertBakedNormal } from './chunk-W2ZAFMPB.js';
|
|
2
|
+
import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS } from './chunk-2PGZ44TP.js';
|
|
2
3
|
import { InstancedMesh } from 'three';
|
|
3
|
-
import { uniform, Fn, positionLocal, positionGeometry, normalLocal, instancedMesh, int, vertexIndex, textureLoad, ivec2, mix,
|
|
4
|
+
import { uniform, Fn, positionLocal, positionGeometry, normalLocal, batch, instancedMesh, int, vertexIndex, float, bool, textureLoad, ivec2, mix, batchIndirectIndex, instanceIndex, hash } from 'three/tsl';
|
|
4
5
|
|
|
5
6
|
function getMaxTextureSize(renderer) {
|
|
6
7
|
const backend = renderer.backend;
|
|
@@ -10,17 +11,30 @@ function getMaxTextureSize(renderer) {
|
|
|
10
11
|
if (gl) return gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
11
12
|
throw new Error("three-vat: renderer has no initialized backend \u2014 call `await renderer.init()` first");
|
|
12
13
|
}
|
|
13
|
-
var
|
|
14
|
-
function
|
|
15
|
-
const
|
|
14
|
+
var packTexel = (texture, field, instance) => textureLoad(texture, ivec2(int(field), instance));
|
|
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
20
|
return {
|
|
17
|
-
startFrame: int(
|
|
21
|
+
startFrame: int(clip.x),
|
|
18
22
|
frames,
|
|
19
|
-
duration: frames.div(
|
|
20
|
-
|
|
21
|
-
speed:
|
|
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
|
|
34
|
+
}
|
|
22
35
|
};
|
|
23
36
|
}
|
|
37
|
+
var instanceIdOf = (carrier) => isBatchedCarrier(carrier) ? int(batchIndirectIndex) : int(instanceIndex);
|
|
24
38
|
function clipAt(vat, clipIndex) {
|
|
25
39
|
const clip = vat.clips[clipIndex];
|
|
26
40
|
if (!clip) {
|
|
@@ -28,60 +42,102 @@ function clipAt(vat, clipIndex) {
|
|
|
28
42
|
}
|
|
29
43
|
return clip;
|
|
30
44
|
}
|
|
31
|
-
function hashedPlayback(clip, desync) {
|
|
45
|
+
function hashedPlayback(clip, desync, instance) {
|
|
32
46
|
return {
|
|
33
47
|
startFrame: int(clip.startFrame),
|
|
34
48
|
frames: float(clip.frames),
|
|
35
49
|
duration: float(clip.frames / clip.fps),
|
|
36
|
-
|
|
37
|
-
|
|
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) }
|
|
38
64
|
};
|
|
39
65
|
}
|
|
40
|
-
function checkPlaybackAttributes(geometry) {
|
|
41
|
-
const names = Object.values(PLAYBACK_ATTRIBUTES);
|
|
42
|
-
const missing = names.filter((name) => geometry.getAttribute(name) === void 0);
|
|
43
|
-
if (missing.length === 0) return true;
|
|
44
|
-
if (missing.length === names.length) return false;
|
|
45
|
-
throw new Error(
|
|
46
|
-
`three-vat: geometry carries only part of the instance-playback contract (missing ${missing.join(", ")}) \u2014 write all of it with \`addVATInstanceAttributes\` from \`three-vat\`, or pass no geometry for the hashed default`
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
66
|
function vatNodes(vat, options = {}) {
|
|
50
|
-
const { time = uniform(0),
|
|
67
|
+
const { time = uniform(0), carrier } = options;
|
|
51
68
|
const { position, normal } = vatDecode(vat, options);
|
|
52
69
|
const decode = Fn(() => {
|
|
53
|
-
positionLocal.assign((
|
|
54
|
-
normalLocal.assign(normal);
|
|
55
|
-
if (
|
|
70
|
+
positionLocal.assign((carrier ? positionGeometry : positionLocal).add(position));
|
|
71
|
+
if (normal) normalLocal.assign(normal);
|
|
72
|
+
if (carrier) {
|
|
73
|
+
if (isBatchedCarrier(carrier)) batch(carrier);
|
|
74
|
+
else instancedMesh(carrier);
|
|
75
|
+
}
|
|
56
76
|
return positionLocal;
|
|
57
77
|
}, "vec3");
|
|
58
78
|
return { positionNode: decode(), time };
|
|
59
79
|
}
|
|
60
80
|
function vatDecode(vat, options = {}) {
|
|
61
|
-
const { time = uniform(0),
|
|
62
|
-
|
|
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);
|
|
63
85
|
const vertexRow = int(vertexIndex);
|
|
86
|
+
const frames = playback.frames;
|
|
87
|
+
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);
|
|
91
|
+
const started = local.greaterThanEqual(0);
|
|
92
|
+
const finished = started.and(playback.repetitions.notEqual(INFINITE_REPETITIONS)).and(loops.greaterThanEqual(playback.repetitions));
|
|
93
|
+
const isPingPong = playback.loopMode.equal(LoopMode.PingPong);
|
|
94
|
+
const bounce = loops.mod(2);
|
|
95
|
+
const pingPongPhase = bounce.lessThan(1).select(bounce, float(2).sub(bounce));
|
|
96
|
+
const endPhase = playback.endMode.equal(EndMode.Clamp).select(float(1), float(0));
|
|
97
|
+
const phase = started.select(
|
|
98
|
+
finished.select(endPhase, isPingPong.select(pingPongPhase, loops.fract())),
|
|
99
|
+
float(0)
|
|
100
|
+
);
|
|
101
|
+
const wraps = started.select(
|
|
102
|
+
finished.select(bool(false), isPingPong.select(bool(false), bool(true))),
|
|
103
|
+
bool(false)
|
|
104
|
+
);
|
|
105
|
+
const f = phase.mul(wraps.select(frames, last));
|
|
106
|
+
const f0 = f.floor().min(last);
|
|
107
|
+
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)),
|
|
115
|
+
float(0)
|
|
116
|
+
);
|
|
117
|
+
const frozenRow = int(fade.phase.mul(fade.frames).floor().min(fade.frames.sub(1)).max(0)).add(
|
|
118
|
+
int(fade.startFrame)
|
|
119
|
+
);
|
|
64
120
|
const sample = (tex) => {
|
|
65
|
-
const
|
|
66
|
-
const
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
const s1 = textureLoad(tex, ivec2(vertexRow, f1.add(playback.startFrame))).xyz;
|
|
70
|
-
return mix(s0, s1, t.fract());
|
|
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);
|
|
71
125
|
};
|
|
126
|
+
const normalTexture = vat.normalTexture;
|
|
72
127
|
return {
|
|
73
128
|
position: sample(vat.positionTexture),
|
|
74
|
-
normal: sample(
|
|
129
|
+
normal: normalTexture ? sample(normalTexture).normalize() : null
|
|
75
130
|
};
|
|
76
131
|
}
|
|
77
132
|
function createVATMesh(vat, instances, options = {}) {
|
|
78
133
|
const time = options.time ?? uniform(0);
|
|
79
|
-
const
|
|
134
|
+
const playback = createVATPlaybackTexture(instances);
|
|
135
|
+
for (const source of vat.materials) assertBakedNormal(vat, source);
|
|
80
136
|
const materials = vat.materials.map((source) => source.clone());
|
|
81
|
-
const mesh = new InstancedMesh(geometry, materials, instances.length);
|
|
82
|
-
const { positionNode } = vatNodes(vat, { time,
|
|
137
|
+
const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
|
|
138
|
+
const { positionNode } = vatNodes(vat, { time, playback, carrier: mesh });
|
|
83
139
|
for (const material of materials) material.positionNode = positionNode;
|
|
84
|
-
return { mesh, time };
|
|
140
|
+
return { mesh, time, playback };
|
|
85
141
|
}
|
|
86
142
|
|
|
87
143
|
export { createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
|