three-vat 4.0.0 → 4.2.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/index.js CHANGED
@@ -1,1040 +1,158 @@
1
- import { LoopMode, MAX_TEXTURE_SIZE, HALF_FLOAT_MAX, makeVATNormalTexture, makeVATTexture, RIG_TEXELS_PER_SLOT, RIG_TEXELS, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS } from './chunk-BSAJL5PG.js';
2
- export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-BSAJL5PG.js';
3
- import { LoopRepeat, LoopOnce, LoopPingPong, DataUtils, Quaternion, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, HalfFloatType, BufferGeometry, BufferAttribute, AdditiveAnimationBlendMode, PropertyBinding, InterleavedBufferAttribute, FloatType, Material, InterleavedBuffer, SkinnedMesh, Mesh, Bone, Object3D, Skeleton, AnimationClip, Color, VectorKeyframeTrack, StringKeyframeTrack, QuaternionKeyframeTrack, NumberKeyframeTrack, ColorKeyframeTrack, BooleanKeyframeTrack, Float16BufferAttribute, Interpolant } from 'three';
1
+ import { BAKED_FILE_EXTENSION, BAKED_FILE_VERSION, BAKED_LAYER_FORMATS, positionDigest, bakeVATWith, flatFacts, mergedFlatMaterial, keepRestSlots, restSlotsOf } from './chunk-P6AFXEBT.js';
2
+ export { bakeVAT, decodeOctahedral, encodeOctahedral } from './chunk-P6AFXEBT.js';
3
+ import { makeVATTexture, makeVATNormalTexture } from './chunk-4IKOXUSX.js';
4
+ export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance, turnVATInstance } from './chunk-4IKOXUSX.js';
5
+ import { Quaternion, Box3, Vector3, Sphere, BufferAttribute, BufferGeometry, FloatType, HalfFloatType, Material, InterleavedBuffer, SkinnedMesh, Mesh, Bone, Object3D, Skeleton, Matrix4, AnimationClip, AnimationMixer, VectorKeyframeTrack, StringKeyframeTrack, QuaternionKeyframeTrack, NumberKeyframeTrack, ColorKeyframeTrack, BooleanKeyframeTrack, InterleavedBufferAttribute, Float16BufferAttribute, PropertyBinding, Interpolant } from 'three';
6
+ import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
4
7
 
5
- var IGNORED = ["uuid", "name", "color", "userData", "metadata"];
6
- function flatFacts(material) {
7
- const m = material;
8
- if (!m.color?.isColor || m.vertexColors || m.isShadowMaterial) return null;
9
- const own = (key) => Object.prototype.hasOwnProperty.call(m, key);
10
- if (own("onBeforeCompile") || own("customProgramCacheKey")) return null;
11
- for (const value of Object.values(m)) {
12
- const v = value;
13
- if (v?.isTexture || v?.isNode) return null;
8
+ async function loadVAT(url, { loader = new GLTFLoader(), materials } = {}) {
9
+ if (!withPlugin.has(loader)) {
10
+ loader.register((parser) => new VATPlugin(parser));
11
+ withPlugin.add(loader);
14
12
  }
15
- const json = m.toJSON();
16
- for (const key of IGNORED) delete json[key];
17
- return { key: JSON.stringify(json), color: [m.color.r, m.color.g, m.color.b] };
18
- }
19
- function mergedFlatMaterial(members) {
20
- const merged = members[0].clone();
21
- merged.color.setRGB(1, 1, 1);
22
- merged.vertexColors = true;
23
- merged.name = members.map((m) => m.name).filter(Boolean).join(" + ");
24
- return merged;
25
- }
26
- var FLAT_MERGE = { facts: flatFacts, merge: mergedFlatMaterial };
27
- function planFlatMerge(materials, hooks) {
28
- const facts = /* @__PURE__ */ new Map();
29
- const groups = /* @__PURE__ */ new Map();
30
- for (const material of materials) {
31
- if (facts.has(material)) continue;
32
- const f = hooks.facts(material);
33
- if (!f) continue;
34
- facts.set(material, f);
35
- groups.set(f.key, [...groups.get(f.key) ?? [], material]);
36
- }
37
- const targets = /* @__PURE__ */ new Map();
38
- for (const members of groups.values()) {
39
- if (members.length < 2) continue;
40
- const merged = hooks.merge(members);
41
- for (const m of members) targets.set(m, { material: merged, tint: new Color().fromArray(facts.get(m).color) });
42
- }
43
- return { targetOf: (material) => targets.get(material) };
44
- }
45
-
46
- // src/octahedral.ts
47
- var signNotZero = (v) => v >= 0 ? 1 : -1;
48
- var toByte = (v) => Math.max(0, Math.min(255, Math.round((v * 0.5 + 0.5) * 255)));
49
- function encodeOctahedral(x, y, z, out, offset) {
50
- const l1 = Math.abs(x) + Math.abs(y) + Math.abs(z);
51
- if (l1 === 0) {
52
- out[offset] = toByte(0);
53
- out[offset + 1] = toByte(0);
54
- return;
55
- }
56
- const px = x / l1;
57
- const py = y / l1;
58
- const fold = z <= 0;
59
- out[offset] = toByte(fold ? (1 - Math.abs(py)) * signNotZero(px) : px);
60
- out[offset + 1] = toByte(fold ? (1 - Math.abs(px)) * signNotZero(py) : py);
61
- }
62
- function decodeOctahedral(u, v, out = { x: 0, y: 0, z: 0 }) {
63
- const ex = u / 255 * 2 - 1;
64
- const ey = v / 255 * 2 - 1;
65
- const z = 1 - Math.abs(ex) - Math.abs(ey);
66
- const t = Math.max(-z, 0);
67
- const x = ex - signNotZero(ex) * t;
68
- const y = ey - signNotZero(ey) * t;
69
- const length = Math.hypot(x, y, z) || 1;
70
- out.x = x / length;
71
- out.y = y / length;
72
- out.z = z / length;
73
- return out;
74
- }
75
-
76
- // src/bake.ts
77
- var LOOP_MODES = /* @__PURE__ */ new Map([
78
- [LoopRepeat, LoopMode.Repeat],
79
- [LoopOnce, LoopMode.Once],
80
- [LoopPingPong, LoopMode.PingPong]
81
- ]);
82
- function isAction(input) {
83
- return typeof input.getClip === "function";
84
- }
85
- function resolveAnimation(input) {
86
- if (!isAction(input)) return { clip: input, defaults: { ...LIBRARY_PLAYBACK_DEFAULTS } };
87
- const clip = input.getClip();
88
- const name = clip.name || "(unnamed)";
89
- const cannotBlend = (field, value) => new Error(
90
- `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`
91
- );
92
- if (input.weight !== 1) throw cannotBlend("weight", String(input.weight));
93
- if (input.blendMode === AdditiveAnimationBlendMode) throw cannotBlend("blendMode", "additive");
94
- if (input.timeScale < 0) {
95
- throw new Error(
96
- `three-vat: action for clip "${name}" has timeScale ${input.timeScale}; ${FORWARD_ONLY_REASON}.`
97
- );
98
- }
99
- const loopMode = LOOP_MODES.get(input.loop);
100
- if (loopMode === void 0) {
101
- throw new Error(
102
- `three-vat: action for clip "${name}" has an unrecognised loop mode ${input.loop}; expected THREE.LoopRepeat, LoopOnce or LoopPingPong`
103
- );
104
- }
105
- return {
106
- clip,
107
- defaults: {
108
- loopMode,
109
- // three's `repetitions` defaults to Infinity and `LoopOnce` ignores it
110
- // outright — so a one-shot is one play whatever the field says, and an
111
- // endless count becomes the sentinel a Float32Array can carry.
112
- repetitions: loopMode === LoopMode.Once ? 1 : Number.isFinite(input.repetitions) ? input.repetitions : INFINITE_REPETITIONS,
113
- // Not read off the action (see the rule above), so a bake clamps whichever
114
- // input it was handed. Uniform across loop modes on purpose: an instance
115
- // may override a repeating clip's `loopMode` to `Once` and inherit this
116
- // field, so there is no mode whose end mode is safely unreachable and
117
- // could hold a different answer. Rewind is per instance:
118
- // `{ ..., endMode: EndMode.Rewind }` (#43, ADR-0017).
119
- endMode: LIBRARY_PLAYBACK_DEFAULTS.endMode,
120
- speed: input.timeScale
121
- }
122
- };
123
- }
124
- function morphCountOf({ mesh, morphPos, morphNrm }) {
125
- const influences = mesh.morphTargetInfluences;
126
- return influences ? Math.min(influences.length, Math.max(morphPos?.length ?? 0, morphNrm?.length ?? 0)) : 0;
127
- }
128
- function asAttribute(value, mesh, name) {
129
- if (!(value instanceof BufferAttribute)) {
130
- throw new Error(
131
- `three-vat: mesh "${mesh.name || "(unnamed)"}" has an interleaved or unsupported "${name}" attribute; VAT bakes plain BufferAttributes`
132
- );
133
- }
134
- return value;
135
- }
136
- function deriveNormals(position, index) {
137
- const scratch = new BufferGeometry();
138
- scratch.setAttribute("position", position);
139
- scratch.setIndex(index);
140
- scratch.computeVertexNormals();
141
- return scratch.attributes.normal;
142
- }
143
- function collectParts(root, hooks) {
144
- const found = [];
145
- root.traverse((o) => {
146
- const mesh = o;
147
- if (mesh.isMesh && mesh.geometry) found.push(mesh);
148
- });
149
- if (found.length === 0) {
150
- throw new Error("three-vat: no Mesh found under root; nothing to bake");
151
- }
152
- const flat = hooks ? planFlatMerge(
153
- found.flatMap((mesh) => Array.isArray(mesh.material) ? [] : [mesh.material]),
154
- hooks
155
- ) : null;
156
- const materials = [];
157
- const parts = [];
158
- for (const mesh of found) {
159
- if (Array.isArray(mesh.material)) {
160
- throw new Error(
161
- `three-vat: mesh "${mesh.name || "(unnamed)"}" uses a material array; split it into one mesh per material before baking`
162
- );
163
- }
164
- const geometry = mesh.geometry;
165
- const source = mesh.material;
166
- const merged = flat?.targetOf(source);
167
- const material = merged?.material ?? source;
168
- let materialIndex = materials.indexOf(material);
169
- if (materialIndex === -1) materialIndex = materials.push(material) - 1;
170
- const skinned = mesh;
171
- const basePos = asAttribute(geometry.attributes.position, mesh, "position");
172
- parts.push({
173
- mesh,
174
- material,
175
- materialIndex,
176
- vertexStart: 0,
177
- // assigned below, after material sorting
178
- vertexCount: geometry.attributes.position.count,
179
- basePos,
180
- baseNrm: geometry.attributes.normal ? asAttribute(geometry.attributes.normal, mesh, "normal") : deriveNormals(basePos, geometry.index),
181
- isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
182
- skeleton: skinned.skeleton,
183
- pose: void 0,
184
- // assigned below, once the distinct rigs are known
185
- skinIndex: geometry.attributes.skinIndex,
186
- skinWeight: geometry.attributes.skinWeight,
187
- morphPos: geometry.morphAttributes.position,
188
- morphNrm: geometry.morphAttributes.normal,
189
- morphRelative: geometry.morphTargetsRelative,
190
- tint: merged?.tint ?? null
191
- });
192
- }
193
- parts.sort((a, b) => a.materialIndex - b.materialIndex);
194
- let offset = 0;
195
- for (const part of parts) {
196
- part.vertexStart = offset;
197
- offset += part.vertexCount;
198
- }
199
- return parts;
200
- }
201
- function mergeGeometry(parts, restMatrices, total) {
202
- const position = new Float32Array(total * 3);
203
- const normal = new Float32Array(total * 3);
204
- const want = optionalAttributes(parts);
205
- const uv = want.uv ? new Float32Array(total * 2) : null;
206
- const color = want.color ? new Float32Array(total * 3) : null;
207
- const merging = parts.some((p) => p.tint);
208
- const tangent = want.tangent ? new Float32Array(total * 4) : null;
209
- const _v = new Vector3();
210
- const _n = new Vector3();
211
- const _t = new Vector3();
212
- parts.forEach((part, pi) => {
213
- const m = restMatrices[pi];
214
- const geometry = part.mesh.geometry;
215
- const start = part.vertexStart;
216
- const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
217
- const srcColor = colorSource(part, !!color, merging);
218
- const srcTangent = tangent ? geometry.attributes.tangent : null;
219
- for (let v = 0; v < part.vertexCount; v++) {
220
- _v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
221
- _n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
222
- const o3 = (start + v) * 3;
223
- position[o3] = _v.x;
224
- position[o3 + 1] = _v.y;
225
- position[o3 + 2] = _v.z;
226
- normal[o3] = _n.x;
227
- normal[o3 + 1] = _n.y;
228
- normal[o3 + 2] = _n.z;
229
- if (uv && srcUV) {
230
- uv[(start + v) * 2] = srcUV.getX(v);
231
- uv[(start + v) * 2 + 1] = srcUV.getY(v);
232
- }
233
- if (color) writeColor(color, o3, part, srcColor, v);
234
- if (tangent && srcTangent) {
235
- _t.fromBufferAttribute(srcTangent, v).transformDirection(m);
236
- const o4 = (start + v) * 4;
237
- tangent[o4] = _t.x;
238
- tangent[o4 + 1] = _t.y;
239
- tangent[o4 + 2] = _t.z;
240
- tangent[o4 + 3] = srcTangent.getW(v);
241
- }
242
- }
243
- });
244
- const merged = new BufferGeometry();
245
- merged.setAttribute("position", new BufferAttribute(position, 3));
246
- merged.setAttribute("normal", new BufferAttribute(normal, 3));
247
- if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
248
- if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
249
- if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
250
- indexParts(merged, parts);
251
- return merged;
252
- }
253
- function colorSource(part, wanted, merging) {
254
- const source = part.mesh.geometry.attributes.color;
255
- if (!wanted || part.tint || !source) return null;
256
- if (merging && source instanceof InterleavedBufferAttribute) return source;
257
- return asAttribute(source, part.mesh, "color");
258
- }
259
- function writeColor(out, o3, part, source, v) {
260
- if (part.tint) {
261
- out[o3] = part.tint.r;
262
- out[o3 + 1] = part.tint.g;
263
- out[o3 + 2] = part.tint.b;
264
- } else if (source) {
265
- out[o3] = source.getX(v);
266
- out[o3 + 1] = source.getY(v);
267
- out[o3 + 2] = source.getZ(v);
268
- } else {
269
- out[o3] = out[o3 + 1] = out[o3 + 2] = 1;
270
- }
271
- }
272
- function optionalAttributes(parts) {
273
- return {
274
- uv: parts.every((p) => !!p.mesh.geometry.attributes.uv),
275
- // A merge writes a colour for every part it touched, so the attribute is
276
- // there whenever one happened; a part it did not touch keeps its own, or
277
- // white, which leaves an untinted material's shading as it was.
278
- color: parts.some((p) => p.tint) || parts.every((p) => !!p.mesh.geometry.attributes.color),
279
- tangent: parts.every((p) => {
280
- const t = p.mesh.geometry.attributes.tangent;
281
- return t instanceof BufferAttribute && t.itemSize === 4;
282
- })
283
- };
284
- }
285
- function indexParts(merged, parts) {
286
- const indices = [];
287
- const groups = [];
288
- for (const part of parts) {
289
- const start = part.vertexStart;
290
- const groupStart = indices.length;
291
- const index = part.mesh.geometry.index;
292
- if (index) {
293
- for (let i = 0; i < index.count; i++) indices.push(start + index.getX(i));
294
- } else {
295
- for (let v = 0; v < part.vertexCount; v++) indices.push(start + v);
296
- }
297
- const last = groups[groups.length - 1];
298
- if (last && last.materialIndex === part.materialIndex) {
299
- last.count += indices.length - groupStart;
300
- } else {
301
- groups.push({
302
- start: groupStart,
303
- count: indices.length - groupStart,
304
- materialIndex: part.materialIndex
305
- });
306
- }
307
- }
308
- merged.setIndex(indices);
309
- for (const g of groups) merged.addGroup(g.start, g.count, g.materialIndex);
310
- }
311
- function frameCountsFor(clips, fps, maxTextureSize) {
312
- const frameCounts = clips.map((c) => Math.max(2, Math.round(c.duration * fps)));
313
- const totalFrames = frameCounts.reduce((a, b) => a + b, 0);
314
- if (totalFrames > maxTextureSize) {
315
- throw new Error(
316
- `three-vat: totalFrames ${totalFrames} exceeds maxTextureSize ${maxTextureSize}; lower fps or bake fewer clips`
317
- );
318
- }
319
- return { frameCounts, totalFrames };
320
- }
321
- function bakeVAT(root, animations, options = {}) {
322
- return bakeVATWith(root, animations, options, FLAT_MERGE);
323
- }
324
- function bakeVATWith(root, animations, options, hooks) {
325
- const {
326
- fps = 30,
327
- maxTextureSize = MAX_TEXTURE_SIZE,
328
- bakeNormals = true,
329
- encoding = "auto",
330
- mergeFlatMaterials = false
331
- } = options;
332
- const resolved = animations.map(resolveAnimation);
333
- root.updateMatrixWorld(true);
334
- const parts = collectParts(root, mergeFlatMaterials ? hooks : null);
335
- if (encoding === "rig") return bakeRig(root, parts, resolved, { fps, maxTextureSize });
336
- if (encoding === "delta") {
337
- return { ...bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }), fallback: null };
338
- }
339
- let refusal;
13
+ let gltf;
340
14
  try {
341
- return bakeRig(root, parts, resolved, { fps, maxTextureSize });
15
+ gltf = await loader.loadAsync(url);
342
16
  } catch (error) {
343
- if (!(error instanceof RigRefusal)) throw error;
344
- refusal = error;
17
+ if (error instanceof Refusal) throw new Error(`three-vat: ${url} ${error.message}`);
18
+ throw error;
345
19
  }
346
- try {
347
- return { ...bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }), fallback: refusal.message };
348
- } catch (error) {
349
- if (!(error instanceof Error)) throw error;
350
- throw new Error(
351
- `three-vat: neither encoding can bake this subtree. The vertex encoding: ${unprefixed(error)}. The rig encoding, tried first: ${unprefixed(refusal)}`,
352
- { cause: refusal }
353
- );
354
- }
355
- }
356
- function unprefixed(error) {
357
- return error.message.replace(/^three-vat: /, "");
358
- }
359
- function bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }) {
360
- const clips = resolved.map((a) => a.clip);
361
- const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
362
- if (vertexCount > maxTextureSize) {
20
+ const vat = loaded.get(gltf);
21
+ if (vat === void 0) {
363
22
  throw new Error(
364
- `three-vat: vertexCount ${vertexCount} exceeds maxTextureSize ${maxTextureSize}; row wrapping is not implemented`
23
+ `three-vat: ${url} is not a baked file: it carries no ${BAKED_FILE_EXTENSION} extension. Write one with \`npx three-vat bake <input> --out <file>\``
365
24
  );
366
25
  }
367
- const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
368
- const rootInverse = root.matrixWorld.clone().invert();
369
- const restMatrices = parts.map(
370
- (p) => new Matrix4().multiplyMatrices(rootInverse, p.mesh.matrixWorld)
371
- );
372
- const geometry = mergeGeometry(parts, restMatrices, vertexCount);
373
- const mergedBase = geometry.attributes.position;
374
- const posData = new Uint16Array(vertexCount * totalFrames * 4);
375
- const nrmData = bakeNormals ? new Uint8Array(vertexCount * totalFrames * 2) : null;
376
- const bakeNormal = nrmData !== null;
377
- const mixer = new AnimationMixer(root);
378
- const bounds = new Box3();
379
- const clipTable = [];
380
- const _si = new Vector4();
381
- const _sw = new Vector4();
382
- const _acc = new Matrix4();
383
- const _skin = new Matrix4();
384
- const _partMatrix = new Matrix4();
385
- const _p = new Vector3();
386
- const _bp = new Vector3();
387
- const _n = new Vector3();
388
- const _mt = new Vector3();
389
- const _mb = new Vector3();
390
- const _mbn = new Vector3();
391
- const poses = attachPoseBuffers(parts);
392
- const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
393
- let warnedNonUniformScale = false;
394
- let rowOffset = 0;
395
- try {
396
- clips.forEach((clip, ci) => {
397
- const frames = frameCounts[ci];
398
- const action = mixer.clipAction(clip);
399
- action.play();
400
- let maxDeltaSq = 0;
401
- for (let f = 0; f < frames; f++) {
402
- mixer.setTime(f / frames * clip.duration);
403
- root.updateMatrixWorld(true);
404
- const row = rowOffset + f;
405
- poseSkeletons(poses);
406
- if (!warnedNonUniformScale) {
407
- warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
408
- }
409
- for (const part of parts) {
410
- _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
411
- const influences = part.mesh.morphTargetInfluences;
412
- const { morphPos, morphNrm, morphRelative, isSkinned, skinIndex, skinWeight } = part;
413
- const boneMatrices = part.pose?.matrices;
414
- const morphCount = morphCountOf(part);
415
- for (let v = 0; v < part.vertexCount; v++) {
416
- const vi = part.vertexStart + v;
417
- _p.fromBufferAttribute(part.basePos, v);
418
- if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
419
- if (influences && morphCount > 0) {
420
- if (!morphRelative) {
421
- _mb.fromBufferAttribute(part.basePos, v);
422
- if (bakeNormal) _mbn.fromBufferAttribute(part.baseNrm, v);
423
- }
424
- for (let t = 0; t < morphCount; t++) {
425
- const w = influences[t];
426
- if (w === 0) continue;
427
- const targetPos = morphPos?.[t];
428
- if (targetPos) {
429
- _mt.fromBufferAttribute(targetPos, v);
430
- if (!morphRelative) _mt.sub(_mb);
431
- _p.addScaledVector(_mt, w);
432
- }
433
- const targetNrm = bakeNormal ? morphNrm?.[t] : void 0;
434
- if (targetNrm) {
435
- _mt.fromBufferAttribute(targetNrm, v);
436
- if (!morphRelative) _mt.sub(_mbn);
437
- _n.addScaledVector(_mt, w);
438
- }
439
- }
440
- }
441
- if (isSkinned && boneMatrices) {
442
- const skinned = part.mesh;
443
- _si.fromBufferAttribute(skinIndex, v);
444
- _sw.fromBufferAttribute(skinWeight, v);
445
- const ae = _acc.elements;
446
- ae.fill(0);
447
- for (let i = 0; i < 4; i++) {
448
- const w = _sw.getComponent(i);
449
- if (w === 0) continue;
450
- const b = _si.getComponent(i) * BONE_STRIDE;
451
- for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
452
- }
453
- _skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
454
- _p.applyMatrix4(_skin);
455
- if (bakeNormal) _n.transformDirection(_skin);
456
- }
457
- _p.applyMatrix4(_partMatrix);
458
- if (bakeNormal) _n.transformDirection(_partMatrix);
459
- bounds.expandByPoint(_p);
460
- _bp.fromBufferAttribute(mergedBase, vi);
461
- const o = (row * vertexCount + vi) * 4;
462
- const dx = _p.x - _bp.x;
463
- const dy = _p.y - _bp.y;
464
- const dz = _p.z - _bp.z;
465
- maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
466
- const largest = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
467
- if (largest > HALF_FLOAT_MAX) {
468
- throw new Error(
469
- `three-vat: position delta component ${largest} (clip "${clip.name}", frame ${f}, vertex ${vi}) exceeds the half-float limit of ${HALF_FLOAT_MAX} the position texture stores; a unit smaller than the metre is the usual cause`
470
- );
471
- }
472
- posData[o] = toHalfFloat(dx);
473
- posData[o + 1] = toHalfFloat(dy);
474
- posData[o + 2] = toHalfFloat(dz);
475
- posData[o + 3] = HALF_ONE;
476
- if (bakeNormal) encodeOctahedral(_n.x, _n.y, _n.z, nrmData, (row * vertexCount + vi) * 2);
477
- }
478
- }
479
- }
480
- action.stop();
481
- clipTable.push({
482
- name: clip.name,
483
- startFrame: rowOffset,
484
- frames,
485
- fps: frames / clip.duration,
486
- duration: clip.duration,
487
- maxDelta: Math.sqrt(maxDeltaSq),
488
- // Declared once, here, rather than repeated at every instance that plays
489
- // this band. An instance overrides any of them, field by field.
490
- ...resolved[ci].defaults
491
- });
492
- rowOffset += frames;
493
- });
494
- } finally {
495
- mixer.stopAllAction();
496
- mixer.setTime(0);
497
- root.updateMatrixWorld(true);
498
- }
499
- geometry.boundingBox = bounds.clone();
500
- geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
501
- const materials = [];
502
- for (const part of parts) materials[part.materialIndex] = part.material;
503
- return {
504
- positionTexture: makeVATTexture(posData, vertexCount, totalFrames, HalfFloatType),
505
- normalTexture: nrmData ? makeVATNormalTexture(nrmData, vertexCount, totalFrames) : null,
506
- clips: clipTable,
507
- bounds,
508
- vertexCount,
509
- totalFrames,
510
- encoding: "delta",
511
- geometry,
512
- materials
513
- };
514
- }
515
- var toHalfFloat = DataUtils.toHalfFloat;
516
- var HALF_ONE = /* @__PURE__ */ toHalfFloat(1);
517
- var BONE_STRIDE = 16;
518
- var IDENTITY = /* @__PURE__ */ new Matrix4();
519
- function attachPoseBuffers(parts) {
520
- const byRig = /* @__PURE__ */ new Map();
521
- for (const part of parts) {
522
- if (!part.isSkinned || !part.skeleton) continue;
523
- let pose = byRig.get(part.skeleton);
524
- if (!pose) {
525
- const matrices = new Float64Array(part.skeleton.bones.length * BONE_STRIDE);
526
- pose = { skeleton: part.skeleton, matrices };
527
- byRig.set(part.skeleton, pose);
528
- }
529
- part.pose = pose;
530
- }
531
- return [...byRig.values()];
532
- }
533
- function poseSkeletons(poses) {
534
- const scratch = new Matrix4();
535
- for (const { skeleton, matrices } of poses) {
536
- const { bones, boneInverses } = skeleton;
537
- for (let b = 0; b < bones.length; b++) {
538
- scratch.multiplyMatrices(bones[b]?.matrixWorld ?? IDENTITY, boneInverses[b]);
539
- matrices.set(scratch.elements, b * BONE_STRIDE);
540
- }
541
- }
542
- }
543
- function influencedBones(part) {
544
- const used = /* @__PURE__ */ new Set();
545
- const index = part.skinIndex;
546
- const weight = part.skinWeight;
547
- for (let v = 0; v < part.vertexCount; v++) {
548
- for (let i = 0; i < 4; i++) {
549
- if (weight.getComponent(v, i) !== 0) used.add(index.getComponent(v, i));
550
- }
551
- }
552
- return { pose: part.pose, bones: [...used] };
553
- }
554
- var SCALE_UNIFORMITY_EPSILON = 1e-4;
555
- var MATRIX_BASIS = [0, 1, 2, 4, 5, 6, 8, 9, 10];
556
- function hasNonUniformScale(matrices, b) {
557
- const o = b * BONE_STRIDE;
558
- return unevenSquares(
559
- matrices[o] ** 2 + matrices[o + 1] ** 2 + matrices[o + 2] ** 2,
560
- matrices[o + 4] ** 2 + matrices[o + 5] ** 2 + matrices[o + 6] ** 2,
561
- matrices[o + 8] ** 2 + matrices[o + 9] ** 2 + matrices[o + 10] ** 2
562
- );
563
- }
564
- function hasShear(matrices, b) {
565
- const m = (i) => matrices[b * BONE_STRIDE + i];
566
- const lengthSq = (a) => m(a) ** 2 + m(a + 1) ** 2 + m(a + 2) ** 2;
567
- const dot = (a, c) => m(a) * m(c) + m(a + 1) * m(c + 1) + m(a + 2) * m(c + 2);
568
- const square = (a, c) => dot(a, c) ** 2 <= SCALE_UNIFORMITY_EPSILON ** 2 * lengthSq(a) * lengthSq(c);
569
- return !(square(0, 4) && square(0, 8) && square(4, 8));
570
- }
571
- function unevenSquares(x, y, z) {
572
- const max = Math.max(x, y, z);
573
- return max - Math.min(x, y, z) > SCALE_UNIFORMITY_EPSILON * max;
574
- }
575
- function warnOnNonUniformBoneScale(influencers) {
576
- for (const { pose, bones } of influencers) {
577
- for (const b of bones) {
578
- if (!hasNonUniformScale(pose.matrices, b)) continue;
579
- console.warn(
580
- `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.`
26
+ if (materials !== void 0) {
27
+ const plural = (n) => `${n} material${n === 1 ? "" : "s"}`;
28
+ if (materials.length !== vat.materials.length) {
29
+ throw new Error(
30
+ `three-vat: loadVAT's \`materials\` holds ${plural(materials.length)}, and ${url} has ${plural(vat.materials.length)}, one for each materialIndex; pass one for each, in that order`
581
31
  );
582
- return true;
583
32
  }
33
+ vat.materials = [...materials];
584
34
  }
585
- return false;
35
+ return vat;
586
36
  }
587
- var VERTEX_ENCODING_STORES_IT = "the vertex encoding (`encoding: 'delta'`) stores what a rig cannot";
588
- var RigRefusal = class extends Error {
37
+ var withPlugin = /* @__PURE__ */ new WeakSet();
38
+ var loaded = /* @__PURE__ */ new WeakMap();
39
+ var Refusal = class extends Error {
589
40
  };
590
- function placementOf(part, rootInverse, out) {
591
- out.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
592
- if (part.pose) out.multiply(part.mesh.bindMatrixInverse);
593
- return out;
594
- }
595
- var MATRIX_EPSILON = 1e-6;
596
- function matricesClose(a, b) {
597
- const ae = a.elements;
598
- const be = b.elements;
599
- for (let e = 0; e < 16; e++) if (Math.abs(ae[e] - be[e]) > MATRIX_EPSILON) return false;
600
- return true;
601
- }
602
- function slotLabel(slot) {
603
- const parts = slot.parts.map(partName).join(", ");
604
- if (!slot.rig) return `rigid part ${parts}`;
605
- const { pose, bone } = slot.rig;
606
- return `bone "${pose.skeleton.bones[bone]?.name || `bone ${bone}`}" of ${parts}`;
607
- }
608
- function partName(part) {
609
- return `"${part.mesh.name || "(unnamed)"}"`;
610
- }
611
- function slotReads(slot, bone, boneInverse, bindMatrix, placement) {
612
- const { rig } = slot;
613
- return rig !== void 0 && rig.pose.skeleton.bones[rig.bone] === bone && matricesClose(rig.pose.skeleton.boneInverses[rig.bone], boneInverse) && matricesClose(rig.bindMatrix, bindMatrix) && matricesClose(slot.restPlacement, placement);
614
- }
615
- function layoutSlots(parts, rootInverse) {
616
- const slots = [];
617
- const slotMaps = /* @__PURE__ */ new Map();
618
- for (const part of parts) {
619
- const restPlacement = placementOf(part, rootInverse, new Matrix4());
620
- if (part.pose) {
621
- const { pose } = part;
622
- const { bones, boneInverses } = pose.skeleton;
623
- const bindMatrix = part.mesh.bindMatrix.clone();
624
- const map = [];
625
- for (let b = 0; b < bones.length; b++) {
626
- const bone = bones[b];
627
- const shared = bone ? slots.findIndex((s) => slotReads(s, bone, boneInverses[b], bindMatrix, restPlacement)) : -1;
628
- if (shared >= 0) {
629
- const slot = slots[shared];
630
- if (!slot.parts.includes(part)) slot.parts.push(part);
631
- map.push(shared);
632
- } else {
633
- map.push(slots.length);
634
- slots.push({ rig: { pose, bone: b, bindMatrix }, parts: [part], restPlacement });
635
- }
636
- }
637
- slotMaps.set(part, map);
638
- } else {
639
- slotMaps.set(part, [slots.length]);
640
- slots.push({ rig: void 0, parts: [part], restPlacement });
41
+ var VATPlugin = class {
42
+ constructor(parser) {
43
+ this.parser = parser;
44
+ this.name = BAKED_FILE_EXTENSION;
45
+ }
46
+ get extension() {
47
+ return this.parser.json.extensions?.[BAKED_FILE_EXTENSION];
48
+ }
49
+ beforeRoot() {
50
+ const extension = this.extension;
51
+ if (extension !== void 0 && extension.version !== BAKED_FILE_VERSION) {
52
+ throw new Refusal(
53
+ `is a baked file of format version ${extension.version}, and this three-vat reads version ${BAKED_FILE_VERSION} only; bake it again with this version of \`three-vat bake\``
54
+ );
641
55
  }
642
- }
643
- return { slots, slotMaps };
644
- }
645
- function morphTracksOn(mesh, clip, root) {
646
- const driven = /* @__PURE__ */ new Map();
647
- for (const track of clip.tracks) {
648
- const parsed = PropertyBinding.parseTrackName(track.name);
649
- if (parsed.propertyName !== "morphTargetInfluences") continue;
650
- if (PropertyBinding.findNode(root, parsed.nodeName) !== mesh) continue;
651
- const size = track.getValueSize();
652
- const keys = track.times.length;
653
- const values = track.values;
654
- const add = (t, read) => {
655
- const list = driven.get(t) ?? [];
656
- for (let k = 0; k < keys; k++) list.push(read(k));
657
- driven.set(t, list);
56
+ return null;
57
+ }
58
+ async afterRoot(result) {
59
+ const extension = this.extension;
60
+ if (extension !== void 0) loaded.set(result, await this.build(extension));
61
+ }
62
+ async build(extension) {
63
+ const node = await this.parser.getDependency("mesh", extension.mesh);
64
+ const primitives = node.isMesh ? [node] : node.children;
65
+ const geometry = mergedGeometry(primitives, extension);
66
+ const materials = [];
67
+ extension.groups.forEach((group, i) => materials[group.materialIndex] = primitives[i].material);
68
+ const bounds = new Box3(new Vector3().fromArray(extension.bounds.min), new Vector3().fromArray(extension.bounds.max));
69
+ geometry.boundingBox = bounds.clone();
70
+ geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
71
+ const shared = {
72
+ geometry,
73
+ materials,
74
+ clips: extension.clips.map((clip) => ({ ...clip })),
75
+ bounds,
76
+ vertexCount: extension.vertexCount,
77
+ totalFrames: extension.totalFrames
658
78
  };
659
- if (parsed.propertyIndex === void 0) {
660
- for (let t = 0; t < size; t++) add(t, (k) => values[k * size + t]);
661
- } else {
662
- const byIndex = Number(parsed.propertyIndex);
663
- const t = Number.isInteger(byIndex) ? byIndex : mesh.morphTargetDictionary?.[parsed.propertyIndex];
664
- if (t !== void 0) add(t, (k) => values[k]);
79
+ if (extension.encoding === "rig") {
80
+ geometry.setAttribute("skinWeight", await this.writtenWeights(extension.mesh));
81
+ return {
82
+ encoding: "rig",
83
+ rigTexture: await this.texture(extension.layers.rig),
84
+ slotCount: extension.slotCount,
85
+ ...shared
86
+ };
665
87
  }
88
+ const { position, normal } = extension.layers;
89
+ return {
90
+ encoding: "delta",
91
+ positionTexture: await this.texture(position),
92
+ normalTexture: normal ? await this.texture(normal) : null,
93
+ rowsPerFrame: extension.rowsPerFrame,
94
+ fallback: extension.fallback,
95
+ ...shared
96
+ };
666
97
  }
667
- return driven;
668
- }
669
- function morphInfluences(part, clips, root) {
670
- const { mesh } = part;
671
- const influences = mesh.morphTargetInfluences;
672
- const count = morphCountOf(part);
673
- if (!influences || count === 0) return { statics: [], animated: [] };
674
- const driven = clips.map((clip) => morphTracksOn(mesh, clip, root));
675
- const statics = [];
676
- const animated = [];
677
- for (let t = 0; t < count; t++) {
678
- const seen = /* @__PURE__ */ new Set();
679
- const offending = [];
680
- driven.forEach((tracks, ci) => {
681
- const values = tracks.get(t);
682
- if (!values) {
683
- seen.add(influences[t]);
684
- return;
685
- }
686
- offending.push(`"${clips[ci].name}"`);
687
- for (const value of values) seen.add(value);
688
- });
689
- if (seen.size > 1) {
690
- const name = Object.entries(mesh.morphTargetDictionary ?? {}).find(([, i]) => i === t)?.[0];
691
- animated.push({ part, target: name ? `"${name}"` : String(t), clips: offending });
98
+ /**
99
+ * The skin weights of the mesh's primitives as the file holds them, four
100
+ * floats a vertex, read from the file rather than taken from the geometry:
101
+ * `GLTFLoader` normalizes a skinned primitive's weights in place, and the
102
+ * bake's are the source's, unrounded. That geometry is a view over the loaded buffer view,
103
+ * so the bytes are read from the buffer beneath it, which nothing writes to.
104
+ * The primitives share the attribute, so the first one's is read.
105
+ */
106
+ async writtenWeights(mesh) {
107
+ const json = this.parser.json;
108
+ const accessor = json.accessors[json.meshes[mesh].primitives[0].attributes["WEIGHTS_0"]];
109
+ const view = json.bufferViews[accessor.bufferView];
110
+ if (accessor.componentType !== 5126 || (view.byteStride ?? 16) !== 16) {
111
+ throw rewritten("its skin weights are no longer the floats the bake wrote");
692
112
  }
693
- statics.push(seen.values().next().value);
694
- }
695
- return { statics, animated };
696
- }
697
- function refuseAnimatedMorphs(animated) {
698
- const byDriver = /* @__PURE__ */ new Map();
699
- for (const { part, target, clips } of animated) {
700
- const key = `${target}
701
- ${clips.join("\n")}`;
702
- const entry = byDriver.get(key) ?? { target, clips, parts: [] };
703
- if (!entry.parts.includes(part)) entry.parts.push(part);
704
- byDriver.set(key, entry);
705
- }
706
- const offences = [...byDriver.values()].map(({ target, clips, parts }) => {
707
- const one = clips.length === 1;
708
- const its = parts.length === 1 ? "its" : "their";
709
- return `clip${one ? "" : "s"} ${clips.join(", ")} animate${one ? "s" : ""} morph target ${target} of part${parts.length === 1 ? "" : "s"} ${parts.map(partName).join(", ")}, ${its} vertices moving where no bone does`;
710
- });
711
- return new RigRefusal(
712
- `three-vat: the rig encoding cannot bake this subtree: ${offences.join("; ")}. A slot stores a rotation, a translation and one scale, not a per-vertex delta, and ${VERTEX_ENCODING_STORES_IT}`
713
- );
714
- }
715
- function foldMorphs(part, statics) {
716
- const { vertexCount, basePos, baseNrm, morphPos, morphNrm, morphRelative } = part;
717
- const position = new Float32Array(vertexCount * 3);
718
- const normal = new Float32Array(vertexCount * 3);
719
- const active = statics.some((w) => w !== 0);
720
- const _p = new Vector3();
721
- const _n = new Vector3();
722
- const _mt = new Vector3();
723
- const _mb = new Vector3();
724
- for (let v = 0; v < vertexCount; v++) {
725
- _p.fromBufferAttribute(basePos, v);
726
- _n.fromBufferAttribute(baseNrm, v);
727
- if (active) {
728
- for (let t = 0; t < statics.length; t++) {
729
- const w = statics[t];
730
- if (w === 0) continue;
731
- const targetPos = morphPos?.[t];
732
- if (targetPos) {
733
- _mt.fromBufferAttribute(targetPos, v);
734
- if (!morphRelative) _mt.sub(_mb.fromBufferAttribute(basePos, v));
735
- _p.addScaledVector(_mt, w);
736
- }
737
- const targetNrm = morphNrm?.[t];
738
- if (targetNrm) {
739
- _mt.fromBufferAttribute(targetNrm, v);
740
- if (!morphRelative) _mt.sub(_mb.fromBufferAttribute(baseNrm, v));
741
- _n.addScaledVector(_mt, w);
742
- }
743
- }
744
- _n.normalize();
113
+ const buffer = await this.parser.getDependency("buffer", view.buffer);
114
+ const start = (view.byteOffset ?? 0) + (accessor.byteOffset ?? 0);
115
+ return new BufferAttribute(new Float32Array(buffer.slice(start, start + accessor.count * 16)), 4);
116
+ }
117
+ /** A layer's texels, handed to the builder the bake used for that layer. */
118
+ async texture({ bufferView, width, height, format }) {
119
+ if (!Object.prototype.hasOwnProperty.call(BAKED_LAYER_FORMATS, format)) {
120
+ throw new Refusal(`holds a texture layer of unknown format "${format}"`);
745
121
  }
746
- _p.toArray(position, v * 3);
747
- _n.toArray(normal, v * 3);
122
+ const { bytesPerTexel, build } = BAKED_LAYER_FORMATS[format];
123
+ const buffer = await this.parser.getDependency("bufferView", bufferView);
124
+ return build(buffer.slice(0, width * height * bytesPerTexel), width, height);
748
125
  }
749
- return { position, normal };
750
- }
751
- function scaleSensitiveNodes(slots, root, influenced) {
752
- const sensitive = /* @__PURE__ */ new Map();
753
- slots.forEach((slot, i) => {
754
- if (!influenced[i]) return;
755
- if (slot.rig) {
756
- const { bones } = slot.rig.pose.skeleton;
757
- for (let node = bones[slot.rig.bone] ?? null; node && bones.includes(node); node = node.parent) {
758
- sensitive.set(node, slot);
759
- }
760
- } else {
761
- for (let node = slot.parts[0].mesh; node && node !== root; node = node.parent) {
762
- sensitive.set(node, slot);
763
- }
126
+ };
127
+ var rewritten = (what) => new Refusal(
128
+ `holds a geometry rewritten after the bake: ${what}. Its texels are addressed by the vertex order the bake wrote, so an optimizer that reorders, welds or simplifies vertices (a gltfpack or gltf-transform pass) is the likely cause; bake it again, and keep the baked file out of that step`
129
+ );
130
+ function mergedGeometry(primitives, extension) {
131
+ if (primitives.length !== extension.groups.length) {
132
+ throw rewritten(`${primitives.length} primitives where ${extension.groups.length} were written`);
133
+ }
134
+ const shared = primitives[0].geometry;
135
+ const position = shared.attributes.position;
136
+ if (position.count !== extension.vertexCount) {
137
+ throw rewritten(`${position.count} vertices where ${extension.vertexCount} were written`);
138
+ }
139
+ if (positionDigest(position) !== extension.digest) throw rewritten("its rest positions no longer match");
140
+ const geometry = new BufferGeometry();
141
+ for (const [name, attribute] of Object.entries(shared.attributes)) geometry.setAttribute(name, attribute);
142
+ const runs = primitives.map((mesh, i) => {
143
+ const index = mesh.geometry.index;
144
+ const group = extension.groups[i];
145
+ if (mesh.geometry.attributes.position !== position || index === null || index.count !== group.count) {
146
+ throw rewritten(`primitive ${i} no longer draws the run of vertices it was written with`);
764
147
  }
148
+ return index.array;
765
149
  });
766
- return sensitive;
767
- }
768
- function refuseAnimatedNonUniformScale(slots, clips, root, influenced) {
769
- const sensitive = scaleSensitiveNodes(slots, root, influenced);
770
- for (const clip of clips) {
771
- for (const track of clip.tracks) {
772
- const parsed = PropertyBinding.parseTrackName(track.name);
773
- if (parsed.propertyName !== "scale" || track.getValueSize() !== 3) continue;
774
- const node = PropertyBinding.findNode(root, parsed.nodeName);
775
- const reached = node && sensitive.get(node);
776
- if (!reached) continue;
777
- const values = track.values;
778
- for (let k = 0; k < track.times.length; k++) {
779
- const o = k * 3;
780
- if (unevenSquares(values[o] ** 2, values[o + 1] ** 2, values[o + 2] ** 2)) {
781
- throw nonUniformScale(slotLabel(reached), clip);
782
- }
783
- }
784
- }
785
- }
786
- }
787
- function nonUniformScale(label, clip) {
788
- return new RigRefusal(
789
- `three-vat: ${label} animates with non-uniform scale in clip "${clip.name}", which the rig encoding cannot store \u2014 a slot is a rotation, a translation and one scale, and ${VERTEX_ENCODING_STORES_IT}. Authored with a uniform scale, it bakes as a rig`
790
- );
791
- }
792
- function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
793
- const clips = resolved.map((a) => a.clip);
794
- const rootInverse = root.matrixWorld.clone().invert();
795
- const poses = attachPoseBuffers(parts);
796
- const { slots, slotMaps } = layoutSlots(parts, rootInverse);
797
- const slotCount = slots.length;
798
- const morphs = parts.map((part) => morphInfluences(part, clips, root));
799
- const animated = morphs.flatMap((m) => m.animated);
800
- if (animated.length > 0) throw refuseAnimatedMorphs(animated);
801
- const rigParts = parts.map((part, i) => ({
802
- part,
803
- slotOf: slotMaps.get(part),
804
- ...foldMorphs(part, morphs[i].statics)
805
- }));
806
- const width = slotCount * RIG_TEXELS_PER_SLOT;
807
- if (width > maxTextureSize) {
808
- throw new RigRefusal(
809
- `three-vat: rig texture width ${width} (${slotCount} slots \xD7 ${RIG_TEXELS_PER_SLOT} texels) exceeds maxTextureSize ${maxTextureSize}`
810
- );
811
- }
812
- const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
813
- const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
814
- const geometry = mergeRigGeometry(rigParts, vertexCount);
815
- const localPosition = geometry.attributes.position.array;
816
- const skinIndex = geometry.attributes.skinIndex.array;
817
- const skinWeight = geometry.attributes.skinWeight.array;
818
- const influenced = new Uint8Array(slotCount);
819
- for (let i = 0; i < skinWeight.length; i++) {
820
- if (skinWeight[i] !== 0) influenced[skinIndex[i]] = 1;
821
- }
822
- refuseAnimatedNonUniformScale(slots, clips, root, influenced);
823
- const _p = new Vector3();
824
- const _slot = new Matrix4();
825
- const _partMatrix = new Matrix4();
826
- const placements = new Map(parts.map((part) => [part, new Matrix4()]));
827
- const _q2 = new Quaternion();
828
- const _t = new Vector3();
829
- const _s = new Vector3();
830
- const restRoot = new Float64Array(vertexCount * 3);
831
- for (const part of parts) {
832
- _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
833
- for (let v = 0; v < part.vertexCount; v++) {
834
- _p.fromBufferAttribute(part.basePos, v).applyMatrix4(_partMatrix);
835
- _p.toArray(restRoot, (part.vertexStart + v) * 3);
836
- }
837
- }
838
- const data = new Float32Array(width * totalFrames * 4);
839
- const slotMatrices = new Float64Array(slotCount * BONE_STRIDE);
840
- const previous = new Float64Array(slotCount * 4);
841
- const mixer = new AnimationMixer(root);
842
- const bounds = new Box3();
843
- const clipTable = [];
844
- let minX = Infinity;
845
- let minY = Infinity;
846
- let minZ = Infinity;
847
- let maxX = -Infinity;
848
- let maxY = -Infinity;
849
- let maxZ = -Infinity;
850
- let rowOffset = 0;
851
- try {
852
- clips.forEach((clip, ci) => {
853
- const frames = frameCounts[ci];
854
- const action = mixer.clipAction(clip);
855
- action.play();
856
- let maxDeltaSq = 0;
857
- for (let f = 0; f < frames; f++) {
858
- mixer.setTime(f / frames * clip.duration);
859
- root.updateMatrixWorld(true);
860
- const row = rowOffset + f;
861
- poseSkeletons(poses);
862
- for (const [part, placement] of placements) placementOf(part, rootInverse, placement);
863
- for (let slot = 0; slot < slotCount; slot++) {
864
- const { rig, parts: readers } = slots[slot];
865
- const lead = readers[0];
866
- const placement = placements.get(lead);
867
- for (let pi = 1; pi < readers.length; pi++) {
868
- const other = readers[pi];
869
- if (!matricesClose(placement, placements.get(other))) {
870
- throw new RigRefusal(
871
- `three-vat: parts ${partName(lead)} and ${partName(other)} read the same slots but move apart in clip "${clip.name}"; the rig encoding gives them one set of slots, which cannot place them differently, and ${VERTEX_ENCODING_STORES_IT}`
872
- );
873
- }
874
- }
875
- if (rig) {
876
- _slot.fromArray(rig.pose.matrices, rig.bone * BONE_STRIDE).multiply(rig.bindMatrix).premultiply(placement);
877
- } else {
878
- _slot.copy(placement);
879
- }
880
- slotMatrices.set(_slot.elements, slot * BONE_STRIDE);
881
- if (influenced[slot] && (hasNonUniformScale(slotMatrices, slot) || hasShear(slotMatrices, slot))) {
882
- throw nonUniformScale(slotLabel(slots[slot]), clip);
883
- }
884
- const p4 = slot * 4;
885
- const det = _slot.determinant();
886
- let scale;
887
- if (det === 0) {
888
- _t.setFromMatrixPosition(_slot);
889
- _q2.fromArray(previous, p4);
890
- if (_q2.lengthSq() === 0) _q2.identity();
891
- scale = 0;
892
- } else {
893
- if (det < 0) for (const e of MATRIX_BASIS) _slot.elements[e] = -_slot.elements[e];
894
- _slot.decompose(_t, _q2, _s);
895
- scale = det < 0 ? -_s.x : _s.x;
896
- }
897
- if (previous[p4] * _q2.x + previous[p4 + 1] * _q2.y + previous[p4 + 2] * _q2.z + previous[p4 + 3] * _q2.w < 0) {
898
- _q2.set(-_q2.x, -_q2.y, -_q2.z, -_q2.w);
899
- }
900
- previous[p4] = _q2.x;
901
- previous[p4 + 1] = _q2.y;
902
- previous[p4 + 2] = _q2.z;
903
- previous[p4 + 3] = _q2.w;
904
- const o = (row * width + slot * RIG_TEXELS_PER_SLOT) * 4;
905
- const rotation = o + RIG_TEXELS.rotation * 4;
906
- data[rotation] = _q2.x;
907
- data[rotation + 1] = _q2.y;
908
- data[rotation + 2] = _q2.z;
909
- data[rotation + 3] = _q2.w;
910
- const placementTexel = o + RIG_TEXELS.placement * 4;
911
- data[placementTexel] = _t.x;
912
- data[placementTexel + 1] = _t.y;
913
- data[placementTexel + 2] = _t.z;
914
- data[placementTexel + 3] = scale;
915
- }
916
- for (let v = 0; v < vertexCount; v++) {
917
- const o3 = v * 3;
918
- const o4 = v * 4;
919
- const x = localPosition[o3];
920
- const y = localPosition[o3 + 1];
921
- const z = localPosition[o3 + 2];
922
- let px = 0;
923
- let py = 0;
924
- let pz = 0;
925
- for (let i = 0; i < 4; i++) {
926
- const w = skinWeight[o4 + i];
927
- if (w === 0) continue;
928
- const m = skinIndex[o4 + i] * BONE_STRIDE;
929
- px += w * (slotMatrices[m] * x + slotMatrices[m + 4] * y + slotMatrices[m + 8] * z + slotMatrices[m + 12]);
930
- py += w * (slotMatrices[m + 1] * x + slotMatrices[m + 5] * y + slotMatrices[m + 9] * z + slotMatrices[m + 13]);
931
- pz += w * (slotMatrices[m + 2] * x + slotMatrices[m + 6] * y + slotMatrices[m + 10] * z + slotMatrices[m + 14]);
932
- }
933
- if (px < minX) minX = px;
934
- if (px > maxX) maxX = px;
935
- if (py < minY) minY = py;
936
- if (py > maxY) maxY = py;
937
- if (pz < minZ) minZ = pz;
938
- if (pz > maxZ) maxZ = pz;
939
- const dx = px - restRoot[o3];
940
- const dy = py - restRoot[o3 + 1];
941
- const dz = pz - restRoot[o3 + 2];
942
- const deltaSq = dx * dx + dy * dy + dz * dz;
943
- if (deltaSq > maxDeltaSq) maxDeltaSq = deltaSq;
944
- }
945
- }
946
- action.stop();
947
- clipTable.push({
948
- name: clip.name,
949
- startFrame: rowOffset,
950
- frames,
951
- fps: frames / clip.duration,
952
- duration: clip.duration,
953
- maxDelta: Math.sqrt(maxDeltaSq),
954
- ...resolved[ci].defaults
955
- });
956
- rowOffset += frames;
957
- });
958
- } finally {
959
- mixer.stopAllAction();
960
- mixer.setTime(0);
961
- root.updateMatrixWorld(true);
962
- }
963
- bounds.min.set(minX, minY, minZ);
964
- bounds.max.set(maxX, maxY, maxZ);
965
- geometry.boundingBox = bounds.clone();
966
- geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
967
- const materials = [];
968
- for (const part of parts) materials[part.materialIndex] = part.material;
969
- return {
970
- encoding: "rig",
971
- rigTexture: makeVATTexture(data, width, totalFrames),
972
- slotCount,
973
- clips: clipTable,
974
- bounds,
975
- vertexCount,
976
- totalFrames,
977
- geometry,
978
- materials
979
- };
980
- }
981
- function mergeRigGeometry(rigParts, total) {
982
- const parts = rigParts.map((r) => r.part);
983
- const position = new Float32Array(total * 3);
984
- const normal = new Float32Array(total * 3);
985
- const slotIndex = new Uint16Array(total * 4);
986
- const slotWeight = new Float32Array(total * 4);
987
- const want = optionalAttributes(parts);
988
- const uv = want.uv ? new Float32Array(total * 2) : null;
989
- const color = want.color ? new Float32Array(total * 3) : null;
990
- const merging = parts.some((p) => p.tint);
991
- const tangent = want.tangent ? new Float32Array(total * 4) : null;
992
- for (const { part, slotOf, position: restPos, normal: restNrm } of rigParts) {
993
- const geometry = part.mesh.geometry;
994
- const start = part.vertexStart;
995
- const srcIndex = part.pose ? part.skinIndex : null;
996
- const srcWeight = part.pose ? part.skinWeight : null;
997
- const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
998
- const srcColor = colorSource(part, !!color, merging);
999
- const srcTangent = tangent ? geometry.attributes.tangent : null;
1000
- position.set(restPos, start * 3);
1001
- normal.set(restNrm, start * 3);
1002
- for (let v = 0; v < part.vertexCount; v++) {
1003
- const vi = start + v;
1004
- const o3 = vi * 3;
1005
- const o4 = vi * 4;
1006
- if (srcIndex && srcWeight) {
1007
- for (let i = 0; i < 4; i++) {
1008
- slotIndex[o4 + i] = slotOf[srcIndex.getComponent(v, i)];
1009
- slotWeight[o4 + i] = srcWeight.getComponent(v, i);
1010
- }
1011
- } else {
1012
- slotIndex[o4] = slotOf[0];
1013
- slotWeight[o4] = 1;
1014
- }
1015
- if (uv && srcUV) {
1016
- uv[vi * 2] = srcUV.getX(v);
1017
- uv[vi * 2 + 1] = srcUV.getY(v);
1018
- }
1019
- if (color) writeColor(color, o3, part, srcColor, v);
1020
- if (tangent && srcTangent) {
1021
- tangent[o4] = srcTangent.getX(v);
1022
- tangent[o4 + 1] = srcTangent.getY(v);
1023
- tangent[o4 + 2] = srcTangent.getZ(v);
1024
- tangent[o4 + 3] = srcTangent.getW(v);
1025
- }
1026
- }
1027
- }
1028
- const merged = new BufferGeometry();
1029
- merged.setAttribute("position", new BufferAttribute(position, 3));
1030
- merged.setAttribute("normal", new BufferAttribute(normal, 3));
1031
- merged.setAttribute("skinIndex", new BufferAttribute(slotIndex, 4));
1032
- merged.setAttribute("skinWeight", new BufferAttribute(slotWeight, 4));
1033
- if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
1034
- if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
1035
- if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
1036
- indexParts(merged, parts);
1037
- return merged;
150
+ const IndexArray = runs[0].constructor;
151
+ const indices = new IndexArray(runs.reduce((n, run) => n + run.length, 0));
152
+ extension.groups.forEach((group, i) => indices.set(runs[i], group.start));
153
+ geometry.setIndex(new BufferAttribute(indices, 1));
154
+ for (const { start, count, materialIndex } of extension.groups) geometry.addGroup(start, count, materialIndex);
155
+ return geometry;
1038
156
  }
1039
157
  var REQUEST = "three-vat:bake";
1040
158
  var RESPONSE = "three-vat:baked";
@@ -1193,7 +311,7 @@ function rebuildTrack(record) {
1193
311
  if (record.settings) Object.assign(track, { settings: record.settings });
1194
312
  return track;
1195
313
  }
1196
- function isAction2(input) {
314
+ function isAction(input) {
1197
315
  return typeof input.getClip === "function";
1198
316
  }
1199
317
  function recordScene(root, animations, mergeFlat) {
@@ -1286,7 +404,7 @@ function recordScene(root, animations, mergeFlat) {
1286
404
  return clips.push(clip) - 1;
1287
405
  };
1288
406
  const inputs = animations.map(
1289
- (input) => isAction2(input) ? {
407
+ (input) => isAction(input) ? {
1290
408
  clip: clipOf(input.getClip()),
1291
409
  action: {
1292
410
  weight: input.weight,
@@ -1396,9 +514,13 @@ function recordVAT(vat, stand) {
1396
514
  encoding: vat.encoding,
1397
515
  texels: transferable(texture.image.data, true, transfer),
1398
516
  width: texture.image.width,
517
+ height: texture.image.height,
1399
518
  normals: normals ? transferable(normals, true, transfer) : null,
1400
519
  slotCount: vat.encoding === "rig" ? vat.slotCount : 0,
520
+ // Transferred, so detached here: the worker's VAT is dropped once it is sent.
521
+ restSlots: vat.encoding === "rig" ? transferable(restSlotsOf(vat), true, transfer) : null,
1401
522
  fallback: vat.encoding === "delta" ? vat.fallback : null,
523
+ rowsPerFrame: vat.encoding === "delta" ? vat.rowsPerFrame : 1,
1402
524
  geometry: recordGeometry(vat.geometry, true, transfer, /* @__PURE__ */ new Map(), []),
1403
525
  materials: vat.materials.map((m) => stand.get(m)),
1404
526
  clips: vat.clips,
@@ -1426,17 +548,19 @@ function rebuildVAT(record, materials) {
1426
548
  if (record.encoding === "rig") {
1427
549
  const vat2 = {
1428
550
  encoding: "rig",
1429
- rigTexture: makeVATTexture(record.texels, record.width, record.totalFrames, FloatType),
551
+ rigTexture: makeVATTexture(record.texels, record.width, record.height, FloatType),
1430
552
  slotCount: record.slotCount,
1431
553
  ...base
1432
554
  };
555
+ keepRestSlots(vat2, record.restSlots);
1433
556
  return vat2;
1434
557
  }
1435
558
  const vat = {
1436
- positionTexture: makeVATTexture(record.texels, record.width, record.totalFrames, HalfFloatType),
1437
- normalTexture: record.normals ? makeVATNormalTexture(record.normals, record.width, record.totalFrames) : null,
559
+ positionTexture: makeVATTexture(record.texels, record.width, record.height, HalfFloatType),
560
+ normalTexture: record.normals ? makeVATNormalTexture(record.normals, record.width, record.height) : null,
1438
561
  encoding: "delta",
1439
562
  fallback: record.fallback,
563
+ rowsPerFrame: record.rowsPerFrame,
1440
564
  ...base
1441
565
  };
1442
566
  return vat;
@@ -1498,4 +622,4 @@ function serveVATBakes(scope = globalThis) {
1498
622
  return () => scope.removeEventListener("message", onMessage);
1499
623
  }
1500
624
 
1501
- export { bakeVAT, bakeVATInWorker, decodeOctahedral, encodeOctahedral, serveVATBakes };
625
+ export { bakeVATInWorker, loadVAT, serveVATBakes };