three-vat 2.1.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,79 @@
1
- import { LoopMode, MAX_TEXTURE_SIZE, makeVATTexture, RIG_TEXELS_PER_SLOT, RIG_TEXELS, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS } from './chunk-3PWAY6MD.js';
2
- export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_FADE_DURATION, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-3PWAY6MD.js';
3
- import { LoopRepeat, LoopOnce, LoopPingPong, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, Quaternion, BufferGeometry, BufferAttribute, AdditiveAnimationBlendMode, PropertyBinding } from 'three';
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';
4
4
 
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;
14
+ }
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
5
77
  var LOOP_MODES = /* @__PURE__ */ new Map([
6
78
  [LoopRepeat, LoopMode.Repeat],
7
79
  [LoopOnce, LoopMode.Once],
@@ -61,7 +133,14 @@ function asAttribute(value, mesh, name) {
61
133
  }
62
134
  return value;
63
135
  }
64
- function collectParts(root) {
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) {
65
144
  const found = [];
66
145
  root.traverse((o) => {
67
146
  const mesh = o;
@@ -70,6 +149,10 @@ function collectParts(root) {
70
149
  if (found.length === 0) {
71
150
  throw new Error("three-vat: no Mesh found under root; nothing to bake");
72
151
  }
152
+ const flat = hooks ? planFlatMerge(
153
+ found.flatMap((mesh) => Array.isArray(mesh.material) ? [] : [mesh.material]),
154
+ hooks
155
+ ) : null;
73
156
  const materials = [];
74
157
  const parts = [];
75
158
  for (const mesh of found) {
@@ -79,11 +162,13 @@ function collectParts(root) {
79
162
  );
80
163
  }
81
164
  const geometry = mesh.geometry;
82
- if (!geometry.attributes.normal) geometry.computeVertexNormals();
83
- const material = mesh.material;
165
+ const source = mesh.material;
166
+ const merged = flat?.targetOf(source);
167
+ const material = merged?.material ?? source;
84
168
  let materialIndex = materials.indexOf(material);
85
169
  if (materialIndex === -1) materialIndex = materials.push(material) - 1;
86
170
  const skinned = mesh;
171
+ const basePos = asAttribute(geometry.attributes.position, mesh, "position");
87
172
  parts.push({
88
173
  mesh,
89
174
  material,
@@ -91,8 +176,8 @@ function collectParts(root) {
91
176
  vertexStart: 0,
92
177
  // assigned below, after material sorting
93
178
  vertexCount: geometry.attributes.position.count,
94
- basePos: asAttribute(geometry.attributes.position, mesh, "position"),
95
- baseNrm: asAttribute(geometry.attributes.normal, mesh, "normal"),
179
+ basePos,
180
+ baseNrm: geometry.attributes.normal ? asAttribute(geometry.attributes.normal, mesh, "normal") : deriveNormals(basePos, geometry.index),
96
181
  isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
97
182
  skeleton: skinned.skeleton,
98
183
  pose: void 0,
@@ -101,7 +186,8 @@ function collectParts(root) {
101
186
  skinWeight: geometry.attributes.skinWeight,
102
187
  morphPos: geometry.morphAttributes.position,
103
188
  morphNrm: geometry.morphAttributes.normal,
104
- morphRelative: geometry.morphTargetsRelative
189
+ morphRelative: geometry.morphTargetsRelative,
190
+ tint: merged?.tint ?? null
105
191
  });
106
192
  }
107
193
  parts.sort((a, b) => a.materialIndex - b.materialIndex);
@@ -118,6 +204,7 @@ function mergeGeometry(parts, restMatrices, total) {
118
204
  const want = optionalAttributes(parts);
119
205
  const uv = want.uv ? new Float32Array(total * 2) : null;
120
206
  const color = want.color ? new Float32Array(total * 3) : null;
207
+ const merging = parts.some((p) => p.tint);
121
208
  const tangent = want.tangent ? new Float32Array(total * 4) : null;
122
209
  const _v = new Vector3();
123
210
  const _n = new Vector3();
@@ -127,7 +214,7 @@ function mergeGeometry(parts, restMatrices, total) {
127
214
  const geometry = part.mesh.geometry;
128
215
  const start = part.vertexStart;
129
216
  const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
130
- const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
217
+ const srcColor = colorSource(part, !!color, merging);
131
218
  const srcTangent = tangent ? geometry.attributes.tangent : null;
132
219
  for (let v = 0; v < part.vertexCount; v++) {
133
220
  _v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
@@ -143,11 +230,7 @@ function mergeGeometry(parts, restMatrices, total) {
143
230
  uv[(start + v) * 2] = srcUV.getX(v);
144
231
  uv[(start + v) * 2 + 1] = srcUV.getY(v);
145
232
  }
146
- if (color && srcColor) {
147
- color[o3] = srcColor.getX(v);
148
- color[o3 + 1] = srcColor.getY(v);
149
- color[o3 + 2] = srcColor.getZ(v);
150
- }
233
+ if (color) writeColor(color, o3, part, srcColor, v);
151
234
  if (tangent && srcTangent) {
152
235
  _t.fromBufferAttribute(srcTangent, v).transformDirection(m);
153
236
  const o4 = (start + v) * 4;
@@ -167,10 +250,32 @@ function mergeGeometry(parts, restMatrices, total) {
167
250
  indexParts(merged, parts);
168
251
  return merged;
169
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
+ }
170
272
  function optionalAttributes(parts) {
171
273
  return {
172
274
  uv: parts.every((p) => !!p.mesh.geometry.attributes.uv),
173
- color: parts.every((p) => !!p.mesh.geometry.attributes.color),
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),
174
279
  tangent: parts.every((p) => {
175
280
  const t = p.mesh.geometry.attributes.tangent;
176
281
  return t instanceof BufferAttribute && t.itemSize === 4;
@@ -214,12 +319,45 @@ function frameCountsFor(clips, fps, maxTextureSize) {
214
319
  return { frameCounts, totalFrames };
215
320
  }
216
321
  function bakeVAT(root, animations, options = {}) {
217
- const { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE, bakeNormals = true, encoding = "delta" } = 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;
218
332
  const resolved = animations.map(resolveAnimation);
219
- const clips = resolved.map((a) => a.clip);
220
333
  root.updateMatrixWorld(true);
221
- const parts = collectParts(root);
334
+ const parts = collectParts(root, mergeFlatMaterials ? hooks : null);
222
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;
340
+ try {
341
+ return bakeRig(root, parts, resolved, { fps, maxTextureSize });
342
+ } catch (error) {
343
+ if (!(error instanceof RigRefusal)) throw error;
344
+ refusal = error;
345
+ }
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);
223
361
  const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
224
362
  if (vertexCount > maxTextureSize) {
225
363
  throw new Error(
@@ -233,8 +371,8 @@ function bakeVAT(root, animations, options = {}) {
233
371
  );
234
372
  const geometry = mergeGeometry(parts, restMatrices, vertexCount);
235
373
  const mergedBase = geometry.attributes.position;
236
- const posData = new Float32Array(vertexCount * totalFrames * 4);
237
- const nrmData = bakeNormals ? new Float32Array(vertexCount * totalFrames * 4) : null;
374
+ const posData = new Uint16Array(vertexCount * totalFrames * 4);
375
+ const nrmData = bakeNormals ? new Uint8Array(vertexCount * totalFrames * 2) : null;
238
376
  const bakeNormal = nrmData !== null;
239
377
  const mixer = new AnimationMixer(root);
240
378
  const bounds = new Box3();
@@ -254,113 +392,117 @@ function bakeVAT(root, animations, options = {}) {
254
392
  const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
255
393
  let warnedNonUniformScale = false;
256
394
  let rowOffset = 0;
257
- clips.forEach((clip, ci) => {
258
- const frames = frameCounts[ci];
259
- const action = mixer.clipAction(clip);
260
- action.play();
261
- let maxDeltaSq = 0;
262
- for (let f = 0; f < frames; f++) {
263
- mixer.setTime(f / frames * clip.duration);
264
- root.updateMatrixWorld(true);
265
- const row = rowOffset + f;
266
- poseSkeletons(poses);
267
- if (!warnedNonUniformScale) {
268
- warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
269
- }
270
- for (const part of parts) {
271
- _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
272
- const influences = part.mesh.morphTargetInfluences;
273
- const { morphPos, morphNrm, morphRelative, isSkinned, skinIndex, skinWeight } = part;
274
- const boneMatrices = part.pose?.matrices;
275
- const morphCount = morphCountOf(part);
276
- for (let v = 0; v < part.vertexCount; v++) {
277
- const vi = part.vertexStart + v;
278
- _p.fromBufferAttribute(part.basePos, v);
279
- if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
280
- if (influences && morphCount > 0) {
281
- if (!morphRelative) {
282
- _mb.fromBufferAttribute(part.basePos, v);
283
- if (bakeNormal) _mbn.fromBufferAttribute(part.baseNrm, v);
284
- }
285
- for (let t = 0; t < morphCount; t++) {
286
- const w = influences[t];
287
- if (w === 0) continue;
288
- const targetPos = morphPos?.[t];
289
- if (targetPos) {
290
- _mt.fromBufferAttribute(targetPos, v);
291
- if (!morphRelative) _mt.sub(_mb);
292
- _p.addScaledVector(_mt, w);
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);
293
423
  }
294
- const targetNrm = bakeNormal ? morphNrm?.[t] : void 0;
295
- if (targetNrm) {
296
- _mt.fromBufferAttribute(targetNrm, v);
297
- if (!morphRelative) _mt.sub(_mbn);
298
- _n.addScaledVector(_mt, w);
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
+ }
299
439
  }
300
440
  }
301
- }
302
- if (isSkinned && boneMatrices) {
303
- const skinned = part.mesh;
304
- _si.fromBufferAttribute(skinIndex, v);
305
- _sw.fromBufferAttribute(skinWeight, v);
306
- const ae = _acc.elements;
307
- ae.fill(0);
308
- for (let i = 0; i < 4; i++) {
309
- const w = _sw.getComponent(i);
310
- if (w === 0) continue;
311
- const b = _si.getComponent(i) * BONE_STRIDE;
312
- for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
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);
313
456
  }
314
- _skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
315
- _p.applyMatrix4(_skin);
316
- if (bakeNormal) _n.transformDirection(_skin);
317
- }
318
- _p.applyMatrix4(_partMatrix);
319
- if (bakeNormal) _n.transformDirection(_partMatrix);
320
- bounds.expandByPoint(_p);
321
- _bp.fromBufferAttribute(mergedBase, vi);
322
- const o = (row * vertexCount + vi) * 4;
323
- const dx = _p.x - _bp.x;
324
- const dy = _p.y - _bp.y;
325
- const dz = _p.z - _bp.z;
326
- maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
327
- posData[o] = dx;
328
- posData[o + 1] = dy;
329
- posData[o + 2] = dz;
330
- posData[o + 3] = 1;
331
- if (bakeNormal) {
332
- nrmData[o] = _n.x;
333
- nrmData[o + 1] = _n.y;
334
- nrmData[o + 2] = _n.z;
335
- nrmData[o + 3] = 1;
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);
336
477
  }
337
478
  }
338
479
  }
339
- }
340
- action.stop();
341
- clipTable.push({
342
- name: clip.name,
343
- startFrame: rowOffset,
344
- frames,
345
- fps: frames / clip.duration,
346
- duration: clip.duration,
347
- maxDelta: Math.sqrt(maxDeltaSq),
348
- // Declared once, here, rather than repeated at every instance that plays
349
- // this band. An instance overrides any of them, field by field.
350
- ...resolved[ci].defaults
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;
351
493
  });
352
- rowOffset += frames;
353
- });
354
- mixer.stopAllAction();
355
- mixer.setTime(0);
356
- root.updateMatrixWorld(true);
494
+ } finally {
495
+ mixer.stopAllAction();
496
+ mixer.setTime(0);
497
+ root.updateMatrixWorld(true);
498
+ }
357
499
  geometry.boundingBox = bounds.clone();
358
500
  geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
359
501
  const materials = [];
360
502
  for (const part of parts) materials[part.materialIndex] = part.material;
361
503
  return {
362
- positionTexture: makeVATTexture(posData, vertexCount, totalFrames),
363
- normalTexture: nrmData ? makeVATTexture(nrmData, vertexCount, totalFrames) : null,
504
+ positionTexture: makeVATTexture(posData, vertexCount, totalFrames, HalfFloatType),
505
+ normalTexture: nrmData ? makeVATNormalTexture(nrmData, vertexCount, totalFrames) : null,
364
506
  clips: clipTable,
365
507
  bounds,
366
508
  vertexCount,
@@ -370,6 +512,8 @@ function bakeVAT(root, animations, options = {}) {
370
512
  materials
371
513
  };
372
514
  }
515
+ var toHalfFloat = DataUtils.toHalfFloat;
516
+ var HALF_ONE = /* @__PURE__ */ toHalfFloat(1);
373
517
  var BONE_STRIDE = 16;
374
518
  var IDENTITY = /* @__PURE__ */ new Matrix4();
375
519
  function attachPoseBuffers(parts) {
@@ -408,6 +552,7 @@ function influencedBones(part) {
408
552
  return { pose: part.pose, bones: [...used] };
409
553
  }
410
554
  var SCALE_UNIFORMITY_EPSILON = 1e-4;
555
+ var MATRIX_BASIS = [0, 1, 2, 4, 5, 6, 8, 9, 10];
411
556
  function hasNonUniformScale(matrices, b) {
412
557
  const o = b * BONE_STRIDE;
413
558
  return unevenSquares(
@@ -416,6 +561,13 @@ function hasNonUniformScale(matrices, b) {
416
561
  matrices[o + 8] ** 2 + matrices[o + 9] ** 2 + matrices[o + 10] ** 2
417
562
  );
418
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
+ }
419
571
  function unevenSquares(x, y, z) {
420
572
  const max = Math.max(x, y, z);
421
573
  return max - Math.min(x, y, z) > SCALE_UNIFORMITY_EPSILON * max;
@@ -432,7 +584,9 @@ function warnOnNonUniformBoneScale(influencers) {
432
584
  }
433
585
  return false;
434
586
  }
435
- var USE_VERTEX_ENCODING = "bake this subtree with the vertex encoding (the default) instead";
587
+ var VERTEX_ENCODING_STORES_IT = "the vertex encoding (`encoding: 'delta'`) stores what a rig cannot";
588
+ var RigRefusal = class extends Error {
589
+ };
436
590
  function placementOf(part, rootInverse, out) {
437
591
  out.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
438
592
  if (part.pose) out.multiply(part.mesh.bindMatrixInverse);
@@ -554,8 +708,8 @@ ${clips.join("\n")}`;
554
708
  const its = parts.length === 1 ? "its" : "their";
555
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`;
556
710
  });
557
- return new Error(
558
- `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 \u2014 ${USE_VERTEX_ENCODING}`
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}`
559
713
  );
560
714
  }
561
715
  function foldMorphs(part, statics) {
@@ -631,8 +785,8 @@ function refuseAnimatedNonUniformScale(slots, clips, root, influenced) {
631
785
  }
632
786
  }
633
787
  function nonUniformScale(label, clip) {
634
- return new Error(
635
- `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. ${USE_VERTEX_ENCODING}, or author it with a uniform scale.`
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`
636
790
  );
637
791
  }
638
792
  function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
@@ -651,7 +805,7 @@ function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
651
805
  }));
652
806
  const width = slotCount * RIG_TEXELS_PER_SLOT;
653
807
  if (width > maxTextureSize) {
654
- throw new Error(
808
+ throw new RigRefusal(
655
809
  `three-vat: rig texture width ${width} (${slotCount} slots \xD7 ${RIG_TEXELS_PER_SLOT} texels) exceeds maxTextureSize ${maxTextureSize}`
656
810
  );
657
811
  }
@@ -670,7 +824,7 @@ function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
670
824
  const _slot = new Matrix4();
671
825
  const _partMatrix = new Matrix4();
672
826
  const placements = new Map(parts.map((part) => [part, new Matrix4()]));
673
- const _q = new Quaternion();
827
+ const _q2 = new Quaternion();
674
828
  const _t = new Vector3();
675
829
  const _s = new Vector3();
676
830
  const restRoot = new Float64Array(vertexCount * 3);
@@ -694,104 +848,118 @@ function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
694
848
  let maxY = -Infinity;
695
849
  let maxZ = -Infinity;
696
850
  let rowOffset = 0;
697
- clips.forEach((clip, ci) => {
698
- const frames = frameCounts[ci];
699
- const action = mixer.clipAction(clip);
700
- action.play();
701
- let maxDeltaSq = 0;
702
- for (let f = 0; f < frames; f++) {
703
- mixer.setTime(f / frames * clip.duration);
704
- root.updateMatrixWorld(true);
705
- const row = rowOffset + f;
706
- poseSkeletons(poses);
707
- for (const [part, placement] of placements) placementOf(part, rootInverse, placement);
708
- for (let slot = 0; slot < slotCount; slot++) {
709
- const { rig, parts: readers } = slots[slot];
710
- const lead = readers[0];
711
- const placement = placements.get(lead);
712
- for (let pi = 1; pi < readers.length; pi++) {
713
- const other = readers[pi];
714
- if (!matricesClose(placement, placements.get(other))) {
715
- throw new Error(
716
- `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 \u2014 ${USE_VERTEX_ENCODING}`
717
- );
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
+ }
718
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;
719
915
  }
720
- if (rig) {
721
- _slot.fromArray(rig.pose.matrices, rig.bone * BONE_STRIDE).multiply(rig.bindMatrix).premultiply(placement);
722
- } else {
723
- _slot.copy(placement);
724
- }
725
- slotMatrices.set(_slot.elements, slot * BONE_STRIDE);
726
- if (influenced[slot] && hasNonUniformScale(slotMatrices, slot)) {
727
- throw nonUniformScale(slotLabel(slots[slot]), clip);
728
- }
729
- _slot.decompose(_t, _q, _s);
730
- const p4 = slot * 4;
731
- if (previous[p4] * _q.x + previous[p4 + 1] * _q.y + previous[p4 + 2] * _q.z + previous[p4 + 3] * _q.w < 0) {
732
- _q.set(-_q.x, -_q.y, -_q.z, -_q.w);
733
- }
734
- previous[p4] = _q.x;
735
- previous[p4 + 1] = _q.y;
736
- previous[p4 + 2] = _q.z;
737
- previous[p4 + 3] = _q.w;
738
- const o = (row * width + slot * RIG_TEXELS_PER_SLOT) * 4;
739
- const rotation = o + RIG_TEXELS.rotation * 4;
740
- data[rotation] = _q.x;
741
- data[rotation + 1] = _q.y;
742
- data[rotation + 2] = _q.z;
743
- data[rotation + 3] = _q.w;
744
- const placementTexel = o + RIG_TEXELS.placement * 4;
745
- data[placementTexel] = _t.x;
746
- data[placementTexel + 1] = _t.y;
747
- data[placementTexel + 2] = _t.z;
748
- data[placementTexel + 3] = _s.x;
749
- }
750
- for (let v = 0; v < vertexCount; v++) {
751
- const o3 = v * 3;
752
- const o4 = v * 4;
753
- const x = localPosition[o3];
754
- const y = localPosition[o3 + 1];
755
- const z = localPosition[o3 + 2];
756
- let px = 0;
757
- let py = 0;
758
- let pz = 0;
759
- for (let i = 0; i < 4; i++) {
760
- const w = skinWeight[o4 + i];
761
- if (w === 0) continue;
762
- const m = skinIndex[o4 + i] * BONE_STRIDE;
763
- px += w * (slotMatrices[m] * x + slotMatrices[m + 4] * y + slotMatrices[m + 8] * z + slotMatrices[m + 12]);
764
- py += w * (slotMatrices[m + 1] * x + slotMatrices[m + 5] * y + slotMatrices[m + 9] * z + slotMatrices[m + 13]);
765
- pz += w * (slotMatrices[m + 2] * x + slotMatrices[m + 6] * y + slotMatrices[m + 10] * z + slotMatrices[m + 14]);
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;
766
944
  }
767
- if (px < minX) minX = px;
768
- if (px > maxX) maxX = px;
769
- if (py < minY) minY = py;
770
- if (py > maxY) maxY = py;
771
- if (pz < minZ) minZ = pz;
772
- if (pz > maxZ) maxZ = pz;
773
- const dx = px - restRoot[o3];
774
- const dy = py - restRoot[o3 + 1];
775
- const dz = pz - restRoot[o3 + 2];
776
- const deltaSq = dx * dx + dy * dy + dz * dz;
777
- if (deltaSq > maxDeltaSq) maxDeltaSq = deltaSq;
778
945
  }
779
- }
780
- action.stop();
781
- clipTable.push({
782
- name: clip.name,
783
- startFrame: rowOffset,
784
- frames,
785
- fps: frames / clip.duration,
786
- duration: clip.duration,
787
- maxDelta: Math.sqrt(maxDeltaSq),
788
- ...resolved[ci].defaults
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;
789
957
  });
790
- rowOffset += frames;
791
- });
792
- mixer.stopAllAction();
793
- mixer.setTime(0);
794
- root.updateMatrixWorld(true);
958
+ } finally {
959
+ mixer.stopAllAction();
960
+ mixer.setTime(0);
961
+ root.updateMatrixWorld(true);
962
+ }
795
963
  bounds.min.set(minX, minY, minZ);
796
964
  bounds.max.set(maxX, maxY, maxZ);
797
965
  geometry.boundingBox = bounds.clone();
@@ -819,6 +987,7 @@ function mergeRigGeometry(rigParts, total) {
819
987
  const want = optionalAttributes(parts);
820
988
  const uv = want.uv ? new Float32Array(total * 2) : null;
821
989
  const color = want.color ? new Float32Array(total * 3) : null;
990
+ const merging = parts.some((p) => p.tint);
822
991
  const tangent = want.tangent ? new Float32Array(total * 4) : null;
823
992
  for (const { part, slotOf, position: restPos, normal: restNrm } of rigParts) {
824
993
  const geometry = part.mesh.geometry;
@@ -826,7 +995,7 @@ function mergeRigGeometry(rigParts, total) {
826
995
  const srcIndex = part.pose ? part.skinIndex : null;
827
996
  const srcWeight = part.pose ? part.skinWeight : null;
828
997
  const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
829
- const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
998
+ const srcColor = colorSource(part, !!color, merging);
830
999
  const srcTangent = tangent ? geometry.attributes.tangent : null;
831
1000
  position.set(restPos, start * 3);
832
1001
  normal.set(restNrm, start * 3);
@@ -847,11 +1016,7 @@ function mergeRigGeometry(rigParts, total) {
847
1016
  uv[vi * 2] = srcUV.getX(v);
848
1017
  uv[vi * 2 + 1] = srcUV.getY(v);
849
1018
  }
850
- if (color && srcColor) {
851
- color[o3] = srcColor.getX(v);
852
- color[o3 + 1] = srcColor.getY(v);
853
- color[o3 + 2] = srcColor.getZ(v);
854
- }
1019
+ if (color) writeColor(color, o3, part, srcColor, v);
855
1020
  if (tangent && srcTangent) {
856
1021
  tangent[o4] = srcTangent.getX(v);
857
1022
  tangent[o4 + 1] = srcTangent.getY(v);
@@ -871,5 +1036,466 @@ function mergeRigGeometry(rigParts, total) {
871
1036
  indexParts(merged, parts);
872
1037
  return merged;
873
1038
  }
1039
+ var REQUEST = "three-vat:bake";
1040
+ var RESPONSE = "three-vat:baked";
1041
+ function transferable(array, owned, transfer) {
1042
+ const whole = array.byteOffset === 0 && array.byteLength === array.buffer.byteLength;
1043
+ const out = owned && whole ? array : array.slice();
1044
+ transfer.add(out.buffer);
1045
+ return out;
1046
+ }
1047
+ function recordGeometry(geometry, owned, transfer, interleaved, interleavedRecords) {
1048
+ const attribute = (a, what) => {
1049
+ if (a instanceof InterleavedBufferAttribute) {
1050
+ let buffer = interleaved.get(a.data);
1051
+ if (buffer === void 0) {
1052
+ buffer = interleavedRecords.push({ array: transferable(a.data.array, owned, transfer), stride: a.data.stride }) - 1;
1053
+ interleaved.set(a.data, buffer);
1054
+ }
1055
+ return { kind: "interleaved", buffer, itemSize: a.itemSize, offset: a.offset, normalized: a.normalized, name: a.name };
1056
+ }
1057
+ if (a instanceof BufferAttribute) {
1058
+ return {
1059
+ kind: "plain",
1060
+ array: transferable(a.array, owned, transfer),
1061
+ itemSize: a.itemSize,
1062
+ normalized: a.normalized,
1063
+ name: a.name,
1064
+ ...a instanceof Float16BufferAttribute ? { half: true } : {}
1065
+ };
1066
+ }
1067
+ throw new Error(`three-vat: the "${what}" attribute is neither a BufferAttribute nor an interleaved one; a worker cannot carry it`);
1068
+ };
1069
+ const index = geometry.getIndex();
1070
+ return {
1071
+ attributes: Object.fromEntries(Object.entries(geometry.attributes).map(([name, a]) => [name, attribute(a, name)])),
1072
+ morphAttributes: Object.fromEntries(
1073
+ Object.entries(geometry.morphAttributes).map(([name, list]) => [name, list.map((a) => attribute(a, `morph ${name}`))])
1074
+ ),
1075
+ morphTargetsRelative: geometry.morphTargetsRelative,
1076
+ index: index ? attribute(index, "index") : null,
1077
+ groups: geometry.groups.map((g) => ({ ...g })),
1078
+ drawRange: { ...geometry.drawRange }
1079
+ };
1080
+ }
1081
+ function rebuildGeometry(record, interleaved) {
1082
+ const attribute = (a) => {
1083
+ const built = a.kind === "plain" ? a.half ? new Float16BufferAttribute(a.array, a.itemSize, a.normalized) : new BufferAttribute(a.array, a.itemSize, a.normalized) : new InterleavedBufferAttribute(interleaved[a.buffer], a.itemSize, a.offset, a.normalized);
1084
+ built.name = a.name;
1085
+ return built;
1086
+ };
1087
+ const geometry = new BufferGeometry();
1088
+ for (const [name, a] of Object.entries(record.attributes)) geometry.setAttribute(name, attribute(a));
1089
+ for (const [name, list] of Object.entries(record.morphAttributes)) {
1090
+ geometry.morphAttributes[name] = list.map(attribute);
1091
+ }
1092
+ geometry.morphTargetsRelative = record.morphTargetsRelative;
1093
+ if (record.index) geometry.setIndex(attribute(record.index));
1094
+ for (const g of record.groups) geometry.addGroup(g.start, g.count, g.materialIndex);
1095
+ geometry.setDrawRange(record.drawRange.start, record.drawRange.count);
1096
+ return geometry;
1097
+ }
1098
+ var TRACK_TYPES = {
1099
+ bool: BooleanKeyframeTrack,
1100
+ color: ColorKeyframeTrack,
1101
+ number: NumberKeyframeTrack,
1102
+ quaternion: QuaternionKeyframeTrack,
1103
+ string: StringKeyframeTrack,
1104
+ vector: VectorKeyframeTrack
1105
+ };
1106
+ var READ_PROPERTIES = /* @__PURE__ */ new Set(["position", "quaternion", "rotation", "scale", "morphTargetInfluences"]);
1107
+ function bakeReadsTrack(track, root, baked) {
1108
+ const { nodeName, objectName, propertyName } = PropertyBinding.parseTrackName(track.name);
1109
+ if (objectName && objectName !== "bones") return false;
1110
+ if (!READ_PROPERTIES.has(propertyName)) return false;
1111
+ if (propertyName !== "morphTargetInfluences" || objectName) return true;
1112
+ return baked.has(PropertyBinding.findNode(root, nodeName));
1113
+ }
1114
+ function recordTrack(track, clip, transfer) {
1115
+ const factory = track.createInterpolant;
1116
+ const interpolation = factory.isInterpolantFactoryMethodGLTFCubicSpline ? "gltf-cubic" : track.getInterpolation();
1117
+ if (interpolation === void 0) {
1118
+ throw new Error(
1119
+ `three-vat: track "${track.name}" of clip "${clip.name || "(unnamed)"}" uses a custom interpolant, which a worker cannot carry; bake this clip with bakeVAT on the main thread`
1120
+ );
1121
+ }
1122
+ if (!(track.ValueTypeName in TRACK_TYPES)) {
1123
+ throw new Error(`three-vat: track "${track.name}" is a "${track.ValueTypeName}" track, which a worker cannot rebuild`);
1124
+ }
1125
+ const values = ArrayBuffer.isView(track.values) ? transferable(track.values, false, transfer) : Array.from(track.values);
1126
+ const settings = track.settings;
1127
+ return {
1128
+ name: track.name,
1129
+ type: track.ValueTypeName,
1130
+ times: transferable(track.times, false, transfer),
1131
+ values,
1132
+ interpolation,
1133
+ ...settings?.inTangents && settings.outTangents ? { settings: { inTangents: settings.inTangents, outTangents: settings.outTangents } } : {}
1134
+ };
1135
+ }
1136
+ var CubicSplineInterpolant = class extends Interpolant {
1137
+ copySampleValue_(index) {
1138
+ const result = this.resultBuffer;
1139
+ const values = this.sampleValues;
1140
+ const size = this.valueSize;
1141
+ const offset = index * size * 3 + size;
1142
+ for (let i = 0; i !== size; i++) result[i] = values[offset + i];
1143
+ return result;
1144
+ }
1145
+ interpolate_(i1, t0, t, t1) {
1146
+ const result = this.resultBuffer;
1147
+ const values = this.sampleValues;
1148
+ const stride = this.valueSize;
1149
+ const stride2 = stride * 2;
1150
+ const stride3 = stride * 3;
1151
+ const td = t1 - t0;
1152
+ const p = (t - t0) / td;
1153
+ const pp = p * p;
1154
+ const ppp = pp * p;
1155
+ const offset1 = i1 * stride3;
1156
+ const offset0 = offset1 - stride3;
1157
+ const s2 = -2 * ppp + 3 * pp;
1158
+ const s3 = ppp - pp;
1159
+ const s0 = 1 - s2;
1160
+ const s1 = s3 - pp + p;
1161
+ for (let i = 0; i !== stride; i++) {
1162
+ const p0 = values[offset0 + i + stride];
1163
+ const m0 = values[offset0 + i + stride2] * td;
1164
+ const p1 = values[offset1 + i + stride];
1165
+ const m1 = values[offset1 + i] * td;
1166
+ result[i] = s0 * p0 + s1 * m0 + s2 * p1 + s3 * m1;
1167
+ }
1168
+ return result;
1169
+ }
1170
+ };
1171
+ var _q = new Quaternion();
1172
+ var CubicSplineQuaternionInterpolant = class extends CubicSplineInterpolant {
1173
+ interpolate_(i1, t0, t, t1) {
1174
+ const result = super.interpolate_(i1, t0, t, t1);
1175
+ _q.fromArray(result).normalize().toArray(result);
1176
+ return result;
1177
+ }
1178
+ };
1179
+ function rebuildTrack(record) {
1180
+ const Track = TRACK_TYPES[record.type];
1181
+ const track = new Track(record.name, record.times, record.values);
1182
+ if (record.interpolation === "gltf-cubic") {
1183
+ const quaternion = track instanceof QuaternionKeyframeTrack;
1184
+ const factory = function(result) {
1185
+ const Kind = quaternion ? CubicSplineQuaternionInterpolant : CubicSplineInterpolant;
1186
+ return new Kind(this.times, this.values, this.getValueSize() / 3, result);
1187
+ };
1188
+ factory.isInterpolantFactoryMethodGLTFCubicSpline = true;
1189
+ track.createInterpolant = factory;
1190
+ } else if (record.interpolation !== track.DefaultInterpolation) {
1191
+ track.setInterpolation(record.interpolation);
1192
+ }
1193
+ if (record.settings) Object.assign(track, { settings: record.settings });
1194
+ return track;
1195
+ }
1196
+ function isAction2(input) {
1197
+ return typeof input.getClip === "function";
1198
+ }
1199
+ function recordScene(root, animations, mergeFlat) {
1200
+ const transfer = /* @__PURE__ */ new Set();
1201
+ const nodes = [];
1202
+ const nodeIndex = /* @__PURE__ */ new Map();
1203
+ const geometries = /* @__PURE__ */ new Map();
1204
+ const geometryRecords = [];
1205
+ const interleaved = /* @__PURE__ */ new Map();
1206
+ const interleavedRecords = [];
1207
+ const materials = [];
1208
+ const skeletons = /* @__PURE__ */ new Map();
1209
+ const skinned = [];
1210
+ const baked = /* @__PURE__ */ new Set();
1211
+ const materialOf = (m) => {
1212
+ const i = materials.indexOf(m);
1213
+ return i === -1 ? materials.push(m) - 1 : i;
1214
+ };
1215
+ root.traverse((o) => {
1216
+ const mesh = o;
1217
+ const skin = o;
1218
+ const isBaked = mesh.isMesh && !!mesh.geometry;
1219
+ const record = {
1220
+ kind: isBaked ? skin.isSkinnedMesh ? "skinned" : "mesh" : o.isBone ? "bone" : "object",
1221
+ parent: o === root ? -1 : nodeIndex.get(o.parent),
1222
+ name: o.name,
1223
+ uuid: o.uuid,
1224
+ position: o.position.toArray(),
1225
+ quaternion: o.quaternion.toArray(),
1226
+ scale: o.scale.toArray(),
1227
+ rotationOrder: o.rotation.order,
1228
+ matrixAutoUpdate: o.matrixAutoUpdate,
1229
+ matrixWorldAutoUpdate: o.matrixWorldAutoUpdate,
1230
+ matrix: [...o.matrix.elements],
1231
+ matrixWorld: [...o.matrixWorld.elements]
1232
+ };
1233
+ if (o.pivot) record.pivot = o.pivot.toArray();
1234
+ nodeIndex.set(o, nodes.push(record) - 1);
1235
+ if (isBaked) {
1236
+ baked.add(o);
1237
+ let g = geometries.get(mesh.geometry);
1238
+ if (g === void 0) {
1239
+ g = geometryRecords.push(recordGeometry(mesh.geometry, false, transfer, interleaved, interleavedRecords)) - 1;
1240
+ geometries.set(mesh.geometry, g);
1241
+ }
1242
+ record.geometry = g;
1243
+ record.material = Array.isArray(mesh.material) ? mesh.material.map(materialOf) : materialOf(mesh.material);
1244
+ if (mesh.morphTargetInfluences) record.morphTargetInfluences = [...mesh.morphTargetInfluences];
1245
+ if (mesh.morphTargetDictionary) record.morphTargetDictionary = { ...mesh.morphTargetDictionary };
1246
+ if (skin.isSkinnedMesh) {
1247
+ record.bindMode = skin.bindMode;
1248
+ record.bindMatrix = [...skin.bindMatrix.elements];
1249
+ record.bindMatrixInverse = [...skin.bindMatrixInverse.elements];
1250
+ skinned.push([skin, record]);
1251
+ }
1252
+ }
1253
+ });
1254
+ const skeletonRecords = [];
1255
+ for (const [mesh, record] of skinned) {
1256
+ const skeleton = mesh.skeleton;
1257
+ if (!skeleton) continue;
1258
+ let s = skeletons.get(skeleton);
1259
+ if (s === void 0) {
1260
+ const bones = skeleton.bones.map((bone) => {
1261
+ if (!bone) return -1;
1262
+ const i = nodeIndex.get(bone);
1263
+ if (i === void 0) {
1264
+ throw new Error(
1265
+ `three-vat: bone "${bone.name || "(unnamed)"}" of mesh "${mesh.name || "(unnamed)"}" is outside the subtree being baked; bake from an ancestor of the whole rig`
1266
+ );
1267
+ }
1268
+ return i;
1269
+ });
1270
+ s = skeletonRecords.push({ bones, boneInverses: skeleton.boneInverses.map((m) => [...m.elements]) }) - 1;
1271
+ skeletons.set(skeleton, s);
1272
+ }
1273
+ record.skeleton = s;
1274
+ }
1275
+ const clips = [];
1276
+ const clipRecords = [];
1277
+ const clipOf = (clip) => {
1278
+ const i = clips.indexOf(clip);
1279
+ if (i !== -1) return i;
1280
+ clipRecords.push({
1281
+ name: clip.name,
1282
+ duration: clip.duration,
1283
+ blendMode: clip.blendMode,
1284
+ tracks: clip.tracks.filter((t) => bakeReadsTrack(t, root, baked)).map((t) => recordTrack(t, clip, transfer))
1285
+ });
1286
+ return clips.push(clip) - 1;
1287
+ };
1288
+ const inputs = animations.map(
1289
+ (input) => isAction2(input) ? {
1290
+ clip: clipOf(input.getClip()),
1291
+ action: {
1292
+ weight: input.weight,
1293
+ blendMode: input.blendMode,
1294
+ timeScale: input.timeScale,
1295
+ loop: input.loop,
1296
+ repetitions: input.repetitions,
1297
+ clampWhenFinished: input.clampWhenFinished
1298
+ }
1299
+ } : { clip: clipOf(input) }
1300
+ );
1301
+ return {
1302
+ scene: {
1303
+ parentWorld: root.parent ? [...root.parent.matrixWorld.elements] : null,
1304
+ nodes,
1305
+ geometries: geometryRecords,
1306
+ interleaved: interleavedRecords,
1307
+ skeletons: skeletonRecords,
1308
+ clips: clipRecords,
1309
+ inputs,
1310
+ materialCount: materials.length,
1311
+ flat: mergeFlat ? materials.map(flatFacts) : null
1312
+ },
1313
+ materials,
1314
+ transfer: [...transfer]
1315
+ };
1316
+ }
1317
+ function rebuildScene(scene) {
1318
+ const stand = /* @__PURE__ */ new Map();
1319
+ const standIns = Array.from({ length: scene.materialCount }, (_, i) => {
1320
+ const m = new Material();
1321
+ stand.set(m, i);
1322
+ return m;
1323
+ });
1324
+ const hooks = {
1325
+ facts: (m) => scene.flat?.[stand.get(m)] ?? null,
1326
+ merge: (members) => {
1327
+ const merged = new Material();
1328
+ stand.set(merged, members.map((m) => stand.get(m)));
1329
+ return merged;
1330
+ }
1331
+ };
1332
+ const interleaved = scene.interleaved.map(({ array, stride }) => new InterleavedBuffer(array, stride));
1333
+ const geometries = scene.geometries.map((g) => rebuildGeometry(g, interleaved));
1334
+ const nodes = scene.nodes.map((n) => {
1335
+ const material = n.material === void 0 ? void 0 : Array.isArray(n.material) ? n.material.map((i) => standIns[i]) : standIns[n.material];
1336
+ const geometry = n.geometry === void 0 ? void 0 : geometries[n.geometry];
1337
+ const o = n.kind === "skinned" ? new SkinnedMesh(geometry, material) : n.kind === "mesh" ? new Mesh(geometry, material) : n.kind === "bone" ? new Bone() : new Object3D();
1338
+ o.name = n.name;
1339
+ o.uuid = n.uuid;
1340
+ o.rotation.order = n.rotationOrder;
1341
+ o.position.fromArray(n.position);
1342
+ o.quaternion.fromArray(n.quaternion);
1343
+ o.scale.fromArray(n.scale);
1344
+ if (n.pivot) o.pivot = new Vector3().fromArray(n.pivot);
1345
+ o.matrixAutoUpdate = n.matrixAutoUpdate;
1346
+ o.matrixWorldAutoUpdate = n.matrixWorldAutoUpdate;
1347
+ o.matrix.fromArray(n.matrix);
1348
+ o.matrixWorld.fromArray(n.matrixWorld);
1349
+ const mesh = o;
1350
+ if (n.morphTargetInfluences) mesh.morphTargetInfluences = [...n.morphTargetInfluences];
1351
+ if (n.morphTargetDictionary) mesh.morphTargetDictionary = { ...n.morphTargetDictionary };
1352
+ return o;
1353
+ });
1354
+ scene.nodes.forEach((n, i) => {
1355
+ if (n.parent !== -1) nodes[n.parent].add(nodes[i]);
1356
+ });
1357
+ const skeletons = scene.skeletons.map(
1358
+ (s) => new Skeleton(
1359
+ s.bones.map((b) => b === -1 ? void 0 : nodes[b]),
1360
+ s.boneInverses.map((m) => new Matrix4().fromArray(m))
1361
+ )
1362
+ );
1363
+ scene.nodes.forEach((n, i) => {
1364
+ if (n.kind !== "skinned" || n.skeleton === void 0) return;
1365
+ const mesh = nodes[i];
1366
+ mesh.bindMode = n.bindMode;
1367
+ mesh.bind(skeletons[n.skeleton], new Matrix4().fromArray(n.bindMatrix));
1368
+ mesh.bindMatrixInverse.fromArray(n.bindMatrixInverse);
1369
+ });
1370
+ const root = nodes[0];
1371
+ if (scene.parentWorld) {
1372
+ const parent = new Object3D();
1373
+ parent.matrixWorld.fromArray(scene.parentWorld);
1374
+ parent.add(root);
1375
+ }
1376
+ const clips = scene.clips.map((c) => {
1377
+ return new AnimationClip(c.name, c.duration, c.tracks.map(rebuildTrack), c.blendMode);
1378
+ });
1379
+ const animations = scene.inputs.map(({ clip, action }) => {
1380
+ if (!action) return clips[clip];
1381
+ const a = new AnimationMixer(root).clipAction(clips[clip], void 0, action.blendMode);
1382
+ a.weight = action.weight;
1383
+ a.timeScale = action.timeScale;
1384
+ a.loop = action.loop;
1385
+ a.repetitions = action.repetitions;
1386
+ a.clampWhenFinished = action.clampWhenFinished;
1387
+ return a;
1388
+ });
1389
+ return { root, animations, stand, hooks };
1390
+ }
1391
+ function recordVAT(vat, stand) {
1392
+ const transfer = /* @__PURE__ */ new Set();
1393
+ const texture = vat.encoding === "delta" ? vat.positionTexture : vat.rigTexture;
1394
+ const normals = vat.encoding === "delta" && vat.normalTexture ? vat.normalTexture.image.data : null;
1395
+ const record = {
1396
+ encoding: vat.encoding,
1397
+ texels: transferable(texture.image.data, true, transfer),
1398
+ width: texture.image.width,
1399
+ normals: normals ? transferable(normals, true, transfer) : null,
1400
+ slotCount: vat.encoding === "rig" ? vat.slotCount : 0,
1401
+ fallback: vat.encoding === "delta" ? vat.fallback : null,
1402
+ geometry: recordGeometry(vat.geometry, true, transfer, /* @__PURE__ */ new Map(), []),
1403
+ materials: vat.materials.map((m) => stand.get(m)),
1404
+ clips: vat.clips,
1405
+ bounds: { min: vat.bounds.min.toArray(), max: vat.bounds.max.toArray() },
1406
+ vertexCount: vat.vertexCount,
1407
+ totalFrames: vat.totalFrames
1408
+ };
1409
+ return { vat: record, transfer: [...transfer] };
1410
+ }
1411
+ function rebuildVAT(record, materials) {
1412
+ const geometry = rebuildGeometry(record.geometry, []);
1413
+ const bounds = new Box3(new Vector3().fromArray(record.bounds.min), new Vector3().fromArray(record.bounds.max));
1414
+ geometry.boundingBox = bounds.clone();
1415
+ geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
1416
+ const base = {
1417
+ geometry,
1418
+ materials: record.materials.map(
1419
+ (i) => typeof i === "number" ? materials[i] : mergedFlatMaterial(i.map((j) => materials[j]))
1420
+ ),
1421
+ clips: record.clips,
1422
+ bounds,
1423
+ vertexCount: record.vertexCount,
1424
+ totalFrames: record.totalFrames
1425
+ };
1426
+ if (record.encoding === "rig") {
1427
+ const vat2 = {
1428
+ encoding: "rig",
1429
+ rigTexture: makeVATTexture(record.texels, record.width, record.totalFrames, FloatType),
1430
+ slotCount: record.slotCount,
1431
+ ...base
1432
+ };
1433
+ return vat2;
1434
+ }
1435
+ const vat = {
1436
+ positionTexture: makeVATTexture(record.texels, record.width, record.totalFrames, HalfFloatType),
1437
+ normalTexture: record.normals ? makeVATNormalTexture(record.normals, record.width, record.totalFrames) : null,
1438
+ encoding: "delta",
1439
+ fallback: record.fallback,
1440
+ ...base
1441
+ };
1442
+ return vat;
1443
+ }
1444
+ var nextId = 0;
1445
+ function bakeVATInWorker(worker, root, animations, options = {}) {
1446
+ return new Promise((resolve, reject) => {
1447
+ root.updateMatrixWorld(true);
1448
+ const { scene, materials, transfer } = recordScene(root, animations, !!options.mergeFlatMaterials);
1449
+ const id = nextId++;
1450
+ const done = () => {
1451
+ worker.removeEventListener("message", onMessage);
1452
+ worker.removeEventListener("error", onError);
1453
+ };
1454
+ const onMessage = (event) => {
1455
+ const data = event.data;
1456
+ if (data?.type !== RESPONSE || data.id !== id) return;
1457
+ done();
1458
+ if ("error" in data) reject(new Error(data.error));
1459
+ else resolve(rebuildVAT(data.vat, materials));
1460
+ };
1461
+ const onError = (event) => {
1462
+ done();
1463
+ const message = event.message;
1464
+ reject(
1465
+ new Error(
1466
+ `three-vat: the bake worker failed${message ? ` (${message})` : ""}; its module must import and call serveVATBakes()`
1467
+ )
1468
+ );
1469
+ };
1470
+ worker.addEventListener("message", onMessage);
1471
+ worker.addEventListener("error", onError);
1472
+ const request = { type: REQUEST, id, scene, options };
1473
+ try {
1474
+ worker.postMessage(request, transfer);
1475
+ } catch (error) {
1476
+ done();
1477
+ reject(error);
1478
+ }
1479
+ });
1480
+ }
1481
+ function serveVATBakes(scope = globalThis) {
1482
+ const onMessage = (event) => {
1483
+ const data = event.data;
1484
+ if (data?.type !== REQUEST) return;
1485
+ let response;
1486
+ let transfer = [];
1487
+ try {
1488
+ const { root, animations, stand, hooks } = rebuildScene(data.scene);
1489
+ const recorded = recordVAT(bakeVATWith(root, animations, data.options, hooks), stand);
1490
+ response = { type: RESPONSE, id: data.id, vat: recorded.vat };
1491
+ transfer = recorded.transfer;
1492
+ } catch (error) {
1493
+ response = { type: RESPONSE, id: data.id, error: error instanceof Error ? error.message : String(error) };
1494
+ }
1495
+ scope.postMessage(response, transfer);
1496
+ };
1497
+ scope.addEventListener("message", onMessage);
1498
+ return () => scope.removeEventListener("message", onMessage);
1499
+ }
874
1500
 
875
- export { bakeVAT };
1501
+ export { bakeVAT, bakeVATInWorker, decodeOctahedral, encodeOctahedral, serveVATBakes };