three-vat 1.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,7 +1,58 @@
1
- export { addVATInstanceAttributes } from './chunk-SXTVASKG.js';
2
- import { FloatType, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, DataTexture, RGBAFormat, NearestFilter, BufferGeometry, BufferAttribute } from 'three';
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';
3
4
 
4
- var MAX_TEXTURE_SIZE = 16384;
5
+ var LOOP_MODES = /* @__PURE__ */ new Map([
6
+ [LoopRepeat, LoopMode.Repeat],
7
+ [LoopOnce, LoopMode.Once],
8
+ [LoopPingPong, LoopMode.PingPong]
9
+ ]);
10
+ function isAction(input) {
11
+ return typeof input.getClip === "function";
12
+ }
13
+ function resolveAnimation(input) {
14
+ if (!isAction(input)) return { clip: input, defaults: { ...LIBRARY_PLAYBACK_DEFAULTS } };
15
+ const clip = input.getClip();
16
+ const name = clip.name || "(unnamed)";
17
+ const cannotBlend = (field, value) => new Error(
18
+ `three-vat: action for clip "${name}" has ${field} ${value}; that blends several actions at once, which a single baked band cannot represent \u2014 bake the clips separately and crossfade between them (three-vat#30), or reset the action before baking`
19
+ );
20
+ if (input.weight !== 1) throw cannotBlend("weight", String(input.weight));
21
+ if (input.blendMode === AdditiveAnimationBlendMode) throw cannotBlend("blendMode", "additive");
22
+ if (input.timeScale < 0) {
23
+ throw new Error(
24
+ `three-vat: action for clip "${name}" has timeScale ${input.timeScale}; ${FORWARD_ONLY_REASON}.`
25
+ );
26
+ }
27
+ const loopMode = LOOP_MODES.get(input.loop);
28
+ if (loopMode === void 0) {
29
+ throw new Error(
30
+ `three-vat: action for clip "${name}" has an unrecognised loop mode ${input.loop}; expected THREE.LoopRepeat, LoopOnce or LoopPingPong`
31
+ );
32
+ }
33
+ return {
34
+ clip,
35
+ defaults: {
36
+ loopMode,
37
+ // three's `repetitions` defaults to Infinity and `LoopOnce` ignores it
38
+ // outright — so a one-shot is one play whatever the field says, and an
39
+ // endless count becomes the sentinel a Float32Array can carry.
40
+ repetitions: loopMode === LoopMode.Once ? 1 : Number.isFinite(input.repetitions) ? input.repetitions : INFINITE_REPETITIONS,
41
+ // Not read off the action (see the rule above), so a bake clamps whichever
42
+ // input it was handed. Uniform across loop modes on purpose: an instance
43
+ // may override a repeating clip's `loopMode` to `Once` and inherit this
44
+ // field, so there is no mode whose end mode is safely unreachable and
45
+ // could hold a different answer. Rewind is per instance:
46
+ // `{ ..., endMode: EndMode.Rewind }` (#43, ADR-0017).
47
+ endMode: LIBRARY_PLAYBACK_DEFAULTS.endMode,
48
+ speed: input.timeScale
49
+ }
50
+ };
51
+ }
52
+ function morphCountOf({ mesh, morphPos, morphNrm }) {
53
+ const influences = mesh.morphTargetInfluences;
54
+ return influences ? Math.min(influences.length, Math.max(morphPos?.length ?? 0, morphNrm?.length ?? 0)) : 0;
55
+ }
5
56
  function asAttribute(value, mesh, name) {
6
57
  if (!(value instanceof BufferAttribute)) {
7
58
  throw new Error(
@@ -44,6 +95,8 @@ function collectParts(root) {
44
95
  baseNrm: asAttribute(geometry.attributes.normal, mesh, "normal"),
45
96
  isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
46
97
  skeleton: skinned.skeleton,
98
+ pose: void 0,
99
+ // assigned below, once the distinct rigs are known
47
100
  skinIndex: geometry.attributes.skinIndex,
48
101
  skinWeight: geometry.attributes.skinWeight,
49
102
  morphPos: geometry.morphAttributes.position,
@@ -62,20 +115,20 @@ function collectParts(root) {
62
115
  function mergeGeometry(parts, restMatrices, total) {
63
116
  const position = new Float32Array(total * 3);
64
117
  const normal = new Float32Array(total * 3);
65
- const wantUV = parts.every((p) => !!p.mesh.geometry.attributes.uv);
66
- const wantColor = parts.every((p) => !!p.mesh.geometry.attributes.color);
67
- const uv = wantUV ? new Float32Array(total * 2) : null;
68
- const color = wantColor ? new Float32Array(total * 3) : null;
69
- const indices = [];
70
- const groups = [];
118
+ const want = optionalAttributes(parts);
119
+ const uv = want.uv ? new Float32Array(total * 2) : null;
120
+ const color = want.color ? new Float32Array(total * 3) : null;
121
+ const tangent = want.tangent ? new Float32Array(total * 4) : null;
71
122
  const _v = new Vector3();
72
123
  const _n = new Vector3();
124
+ const _t = new Vector3();
73
125
  parts.forEach((part, pi) => {
74
126
  const m = restMatrices[pi];
75
127
  const geometry = part.mesh.geometry;
76
128
  const start = part.vertexStart;
77
129
  const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
78
130
  const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
131
+ const srcTangent = tangent ? geometry.attributes.tangent : null;
79
132
  for (let v = 0; v < part.vertexCount; v++) {
80
133
  _v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
81
134
  _n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
@@ -95,9 +148,42 @@ function mergeGeometry(parts, restMatrices, total) {
95
148
  color[o3 + 1] = srcColor.getY(v);
96
149
  color[o3 + 2] = srcColor.getZ(v);
97
150
  }
151
+ if (tangent && srcTangent) {
152
+ _t.fromBufferAttribute(srcTangent, v).transformDirection(m);
153
+ const o4 = (start + v) * 4;
154
+ tangent[o4] = _t.x;
155
+ tangent[o4 + 1] = _t.y;
156
+ tangent[o4 + 2] = _t.z;
157
+ tangent[o4 + 3] = srcTangent.getW(v);
158
+ }
98
159
  }
160
+ });
161
+ const merged = new BufferGeometry();
162
+ merged.setAttribute("position", new BufferAttribute(position, 3));
163
+ merged.setAttribute("normal", new BufferAttribute(normal, 3));
164
+ if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
165
+ if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
166
+ if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
167
+ indexParts(merged, parts);
168
+ return merged;
169
+ }
170
+ function optionalAttributes(parts) {
171
+ return {
172
+ uv: parts.every((p) => !!p.mesh.geometry.attributes.uv),
173
+ color: parts.every((p) => !!p.mesh.geometry.attributes.color),
174
+ tangent: parts.every((p) => {
175
+ const t = p.mesh.geometry.attributes.tangent;
176
+ return t instanceof BufferAttribute && t.itemSize === 4;
177
+ })
178
+ };
179
+ }
180
+ function indexParts(merged, parts) {
181
+ const indices = [];
182
+ const groups = [];
183
+ for (const part of parts) {
184
+ const start = part.vertexStart;
99
185
  const groupStart = indices.length;
100
- const index = geometry.index;
186
+ const index = part.mesh.geometry.index;
101
187
  if (index) {
102
188
  for (let i = 0; i < index.count; i++) indices.push(start + index.getX(i));
103
189
  } else {
@@ -113,32 +199,34 @@ function mergeGeometry(parts, restMatrices, total) {
113
199
  materialIndex: part.materialIndex
114
200
  });
115
201
  }
116
- });
117
- const merged = new BufferGeometry();
118
- merged.setAttribute("position", new BufferAttribute(position, 3));
119
- merged.setAttribute("normal", new BufferAttribute(normal, 3));
120
- if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
121
- if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
202
+ }
122
203
  merged.setIndex(indices);
123
204
  for (const g of groups) merged.addGroup(g.start, g.count, g.materialIndex);
124
- return merged;
125
205
  }
126
- function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } = {}) {
206
+ function frameCountsFor(clips, fps, maxTextureSize) {
207
+ const frameCounts = clips.map((c) => Math.max(2, Math.round(c.duration * fps)));
208
+ const totalFrames = frameCounts.reduce((a, b) => a + b, 0);
209
+ if (totalFrames > maxTextureSize) {
210
+ throw new Error(
211
+ `three-vat: totalFrames ${totalFrames} exceeds maxTextureSize ${maxTextureSize}; lower fps or bake fewer clips`
212
+ );
213
+ }
214
+ return { frameCounts, totalFrames };
215
+ }
216
+ function bakeVAT(root, animations, options = {}) {
217
+ const { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE, bakeNormals = true, encoding = "delta" } = options;
218
+ const resolved = animations.map(resolveAnimation);
219
+ const clips = resolved.map((a) => a.clip);
127
220
  root.updateMatrixWorld(true);
128
221
  const parts = collectParts(root);
222
+ if (encoding === "rig") return bakeRig(root, parts, resolved, { fps, maxTextureSize });
129
223
  const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
130
224
  if (vertexCount > maxTextureSize) {
131
225
  throw new Error(
132
226
  `three-vat: vertexCount ${vertexCount} exceeds maxTextureSize ${maxTextureSize}; row wrapping is not implemented`
133
227
  );
134
228
  }
135
- const frameCounts = clips.map((c) => Math.max(2, Math.round(c.duration * fps)));
136
- const totalFrames = frameCounts.reduce((a, b) => a + b, 0);
137
- if (totalFrames > maxTextureSize) {
138
- throw new Error(
139
- `three-vat: totalFrames ${totalFrames} exceeds maxTextureSize ${maxTextureSize}; lower fps or bake fewer clips`
140
- );
141
- }
229
+ const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
142
230
  const rootInverse = root.matrixWorld.clone().invert();
143
231
  const restMatrices = parts.map(
144
232
  (p) => new Matrix4().multiplyMatrices(rootInverse, p.mesh.matrixWorld)
@@ -146,13 +234,13 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
146
234
  const geometry = mergeGeometry(parts, restMatrices, vertexCount);
147
235
  const mergedBase = geometry.attributes.position;
148
236
  const posData = new Float32Array(vertexCount * totalFrames * 4);
149
- const nrmData = new Float32Array(vertexCount * totalFrames * 4);
237
+ const nrmData = bakeNormals ? new Float32Array(vertexCount * totalFrames * 4) : null;
238
+ const bakeNormal = nrmData !== null;
150
239
  const mixer = new AnimationMixer(root);
151
240
  const bounds = new Box3();
152
241
  const clipTable = [];
153
242
  const _si = new Vector4();
154
243
  const _sw = new Vector4();
155
- const _bone = new Matrix4();
156
244
  const _acc = new Matrix4();
157
245
  const _skin = new Matrix4();
158
246
  const _partMatrix = new Matrix4();
@@ -162,7 +250,8 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
162
250
  const _mt = new Vector3();
163
251
  const _mb = new Vector3();
164
252
  const _mbn = new Vector3();
165
- const influencers = parts.filter((p) => p.isSkinned && p.skeleton).map(influencedBones);
253
+ const poses = attachPoseBuffers(parts);
254
+ const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
166
255
  let warnedNonUniformScale = false;
167
256
  let rowOffset = 0;
168
257
  clips.forEach((clip, ci) => {
@@ -174,22 +263,24 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
174
263
  mixer.setTime(f / frames * clip.duration);
175
264
  root.updateMatrixWorld(true);
176
265
  const row = rowOffset + f;
266
+ poseSkeletons(poses);
177
267
  if (!warnedNonUniformScale) {
178
- warnedNonUniformScale = warnOnNonUniformBoneScale(influencers, _bone);
268
+ warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
179
269
  }
180
270
  for (const part of parts) {
181
271
  _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
182
272
  const influences = part.mesh.morphTargetInfluences;
183
- const { morphPos, morphNrm, morphRelative, isSkinned, skeleton, skinIndex, skinWeight } = part;
184
- const morphCount = influences ? Math.min(influences.length, Math.max(morphPos?.length ?? 0, morphNrm?.length ?? 0)) : 0;
273
+ const { morphPos, morphNrm, morphRelative, isSkinned, skinIndex, skinWeight } = part;
274
+ const boneMatrices = part.pose?.matrices;
275
+ const morphCount = morphCountOf(part);
185
276
  for (let v = 0; v < part.vertexCount; v++) {
186
277
  const vi = part.vertexStart + v;
187
278
  _p.fromBufferAttribute(part.basePos, v);
188
- _n.fromBufferAttribute(part.baseNrm, v);
279
+ if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
189
280
  if (influences && morphCount > 0) {
190
281
  if (!morphRelative) {
191
282
  _mb.fromBufferAttribute(part.basePos, v);
192
- _mbn.fromBufferAttribute(part.baseNrm, v);
283
+ if (bakeNormal) _mbn.fromBufferAttribute(part.baseNrm, v);
193
284
  }
194
285
  for (let t = 0; t < morphCount; t++) {
195
286
  const w = influences[t];
@@ -200,7 +291,7 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
200
291
  if (!morphRelative) _mt.sub(_mb);
201
292
  _p.addScaledVector(_mt, w);
202
293
  }
203
- const targetNrm = morphNrm?.[t];
294
+ const targetNrm = bakeNormal ? morphNrm?.[t] : void 0;
204
295
  if (targetNrm) {
205
296
  _mt.fromBufferAttribute(targetNrm, v);
206
297
  if (!morphRelative) _mt.sub(_mbn);
@@ -208,26 +299,24 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
208
299
  }
209
300
  }
210
301
  }
211
- if (isSkinned && skeleton) {
302
+ if (isSkinned && boneMatrices) {
212
303
  const skinned = part.mesh;
213
304
  _si.fromBufferAttribute(skinIndex, v);
214
305
  _sw.fromBufferAttribute(skinWeight, v);
215
- _acc.elements.fill(0);
306
+ const ae = _acc.elements;
307
+ ae.fill(0);
216
308
  for (let i = 0; i < 4; i++) {
217
309
  const w = _sw.getComponent(i);
218
310
  if (w === 0) continue;
219
- const bi = _si.getComponent(i);
220
- _bone.multiplyMatrices(skeleton.bones[bi].matrixWorld, skeleton.boneInverses[bi]);
221
- const ae = _acc.elements;
222
- const be = _bone.elements;
223
- for (let e = 0; e < 16; e++) ae[e] += be[e] * w;
311
+ const b = _si.getComponent(i) * BONE_STRIDE;
312
+ for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
224
313
  }
225
314
  _skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
226
315
  _p.applyMatrix4(_skin);
227
- _n.transformDirection(_skin);
316
+ if (bakeNormal) _n.transformDirection(_skin);
228
317
  }
229
318
  _p.applyMatrix4(_partMatrix);
230
- _n.transformDirection(_partMatrix);
319
+ if (bakeNormal) _n.transformDirection(_partMatrix);
231
320
  bounds.expandByPoint(_p);
232
321
  _bp.fromBufferAttribute(mergedBase, vi);
233
322
  const o = (row * vertexCount + vi) * 4;
@@ -239,10 +328,12 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
239
328
  posData[o + 1] = dy;
240
329
  posData[o + 2] = dz;
241
330
  posData[o + 3] = 1;
242
- nrmData[o] = _n.x;
243
- nrmData[o + 1] = _n.y;
244
- nrmData[o + 2] = _n.z;
245
- nrmData[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;
336
+ }
246
337
  }
247
338
  }
248
339
  }
@@ -253,7 +344,10 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
253
344
  frames,
254
345
  fps: frames / clip.duration,
255
346
  duration: clip.duration,
256
- maxDelta: Math.sqrt(maxDeltaSq)
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
257
351
  });
258
352
  rowOffset += frames;
259
353
  });
@@ -266,7 +360,7 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
266
360
  for (const part of parts) materials[part.materialIndex] = part.material;
267
361
  return {
268
362
  positionTexture: makeVATTexture(posData, vertexCount, totalFrames),
269
- normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames),
363
+ normalTexture: nrmData ? makeVATTexture(nrmData, vertexCount, totalFrames) : null,
270
364
  clips: clipTable,
271
365
  bounds,
272
366
  vertexCount,
@@ -276,6 +370,32 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
276
370
  materials
277
371
  };
278
372
  }
373
+ var BONE_STRIDE = 16;
374
+ var IDENTITY = /* @__PURE__ */ new Matrix4();
375
+ function attachPoseBuffers(parts) {
376
+ const byRig = /* @__PURE__ */ new Map();
377
+ for (const part of parts) {
378
+ if (!part.isSkinned || !part.skeleton) continue;
379
+ let pose = byRig.get(part.skeleton);
380
+ if (!pose) {
381
+ const matrices = new Float64Array(part.skeleton.bones.length * BONE_STRIDE);
382
+ pose = { skeleton: part.skeleton, matrices };
383
+ byRig.set(part.skeleton, pose);
384
+ }
385
+ part.pose = pose;
386
+ }
387
+ return [...byRig.values()];
388
+ }
389
+ function poseSkeletons(poses) {
390
+ const scratch = new Matrix4();
391
+ for (const { skeleton, matrices } of poses) {
392
+ const { bones, boneInverses } = skeleton;
393
+ for (let b = 0; b < bones.length; b++) {
394
+ scratch.multiplyMatrices(bones[b]?.matrixWorld ?? IDENTITY, boneInverses[b]);
395
+ matrices.set(scratch.elements, b * BONE_STRIDE);
396
+ }
397
+ }
398
+ }
279
399
  function influencedBones(part) {
280
400
  const used = /* @__PURE__ */ new Set();
281
401
  const index = part.skinIndex;
@@ -285,37 +405,471 @@ function influencedBones(part) {
285
405
  if (weight.getComponent(v, i) !== 0) used.add(index.getComponent(v, i));
286
406
  }
287
407
  }
288
- return { skeleton: part.skeleton, bones: [...used] };
408
+ return { pose: part.pose, bones: [...used] };
289
409
  }
290
410
  var SCALE_UNIFORMITY_EPSILON = 1e-4;
291
- function hasNonUniformScale(m) {
292
- const e = m.elements;
293
- const x = e[0] * e[0] + e[1] * e[1] + e[2] * e[2];
294
- const y = e[4] * e[4] + e[5] * e[5] + e[6] * e[6];
295
- const z = e[8] * e[8] + e[9] * e[9] + e[10] * e[10];
411
+ function hasNonUniformScale(matrices, b) {
412
+ const o = b * BONE_STRIDE;
413
+ return unevenSquares(
414
+ matrices[o] ** 2 + matrices[o + 1] ** 2 + matrices[o + 2] ** 2,
415
+ matrices[o + 4] ** 2 + matrices[o + 5] ** 2 + matrices[o + 6] ** 2,
416
+ matrices[o + 8] ** 2 + matrices[o + 9] ** 2 + matrices[o + 10] ** 2
417
+ );
418
+ }
419
+ function unevenSquares(x, y, z) {
296
420
  const max = Math.max(x, y, z);
297
421
  return max - Math.min(x, y, z) > SCALE_UNIFORMITY_EPSILON * max;
298
422
  }
299
- function warnOnNonUniformBoneScale(influencers, scratch) {
300
- for (const { skeleton, bones } of influencers) {
423
+ function warnOnNonUniformBoneScale(influencers) {
424
+ for (const { pose, bones } of influencers) {
301
425
  for (const b of bones) {
302
- scratch.multiplyMatrices(skeleton.bones[b].matrixWorld, skeleton.boneInverses[b]);
303
- if (!hasNonUniformScale(scratch)) continue;
426
+ if (!hasNonUniformScale(pose.matrices, b)) continue;
304
427
  console.warn(
305
- `three-vat: bone "${skeleton.bones[b].name || "(unnamed)"}" animates with non-uniform scale; baked normals under it are approximate, because linear-blend skinning transforms a normal by the skin matrix rather than its inverse-transpose \u2014 the same shortcut three's own skinning shader takes. Positions are exact.`
428
+ `three-vat: bone "${pose.skeleton.bones[b].name || "(unnamed)"}" animates with non-uniform scale; baked normals under it are approximate, because linear-blend skinning transforms a normal by the skin matrix rather than its inverse-transpose \u2014 the same shortcut three's own skinning shader takes. Positions are exact.`
306
429
  );
307
430
  return true;
308
431
  }
309
432
  }
310
433
  return false;
311
434
  }
312
- function makeVATTexture(data, width, height, type = FloatType) {
313
- const tex = new DataTexture(data, width, height, RGBAFormat, type);
314
- tex.minFilter = NearestFilter;
315
- tex.magFilter = NearestFilter;
316
- tex.generateMipmaps = false;
317
- tex.needsUpdate = true;
318
- return tex;
435
+ var USE_VERTEX_ENCODING = "bake this subtree with the vertex encoding (the default) instead";
436
+ function placementOf(part, rootInverse, out) {
437
+ out.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
438
+ if (part.pose) out.multiply(part.mesh.bindMatrixInverse);
439
+ return out;
440
+ }
441
+ var MATRIX_EPSILON = 1e-6;
442
+ function matricesClose(a, b) {
443
+ const ae = a.elements;
444
+ const be = b.elements;
445
+ for (let e = 0; e < 16; e++) if (Math.abs(ae[e] - be[e]) > MATRIX_EPSILON) return false;
446
+ return true;
447
+ }
448
+ function slotLabel(slot) {
449
+ const parts = slot.parts.map(partName).join(", ");
450
+ if (!slot.rig) return `rigid part ${parts}`;
451
+ const { pose, bone } = slot.rig;
452
+ return `bone "${pose.skeleton.bones[bone]?.name || `bone ${bone}`}" of ${parts}`;
453
+ }
454
+ function partName(part) {
455
+ return `"${part.mesh.name || "(unnamed)"}"`;
456
+ }
457
+ function slotReads(slot, bone, boneInverse, bindMatrix, placement) {
458
+ const { rig } = slot;
459
+ 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);
460
+ }
461
+ function layoutSlots(parts, rootInverse) {
462
+ const slots = [];
463
+ const slotMaps = /* @__PURE__ */ new Map();
464
+ for (const part of parts) {
465
+ const restPlacement = placementOf(part, rootInverse, new Matrix4());
466
+ if (part.pose) {
467
+ const { pose } = part;
468
+ const { bones, boneInverses } = pose.skeleton;
469
+ const bindMatrix = part.mesh.bindMatrix.clone();
470
+ const map = [];
471
+ for (let b = 0; b < bones.length; b++) {
472
+ const bone = bones[b];
473
+ const shared = bone ? slots.findIndex((s) => slotReads(s, bone, boneInverses[b], bindMatrix, restPlacement)) : -1;
474
+ if (shared >= 0) {
475
+ const slot = slots[shared];
476
+ if (!slot.parts.includes(part)) slot.parts.push(part);
477
+ map.push(shared);
478
+ } else {
479
+ map.push(slots.length);
480
+ slots.push({ rig: { pose, bone: b, bindMatrix }, parts: [part], restPlacement });
481
+ }
482
+ }
483
+ slotMaps.set(part, map);
484
+ } else {
485
+ slotMaps.set(part, [slots.length]);
486
+ slots.push({ rig: void 0, parts: [part], restPlacement });
487
+ }
488
+ }
489
+ return { slots, slotMaps };
490
+ }
491
+ function morphTracksOn(mesh, clip, root) {
492
+ const driven = /* @__PURE__ */ new Map();
493
+ for (const track of clip.tracks) {
494
+ const parsed = PropertyBinding.parseTrackName(track.name);
495
+ if (parsed.propertyName !== "morphTargetInfluences") continue;
496
+ if (PropertyBinding.findNode(root, parsed.nodeName) !== mesh) continue;
497
+ const size = track.getValueSize();
498
+ const keys = track.times.length;
499
+ const values = track.values;
500
+ const add = (t, read) => {
501
+ const list = driven.get(t) ?? [];
502
+ for (let k = 0; k < keys; k++) list.push(read(k));
503
+ driven.set(t, list);
504
+ };
505
+ if (parsed.propertyIndex === void 0) {
506
+ for (let t = 0; t < size; t++) add(t, (k) => values[k * size + t]);
507
+ } else {
508
+ const byIndex = Number(parsed.propertyIndex);
509
+ const t = Number.isInteger(byIndex) ? byIndex : mesh.morphTargetDictionary?.[parsed.propertyIndex];
510
+ if (t !== void 0) add(t, (k) => values[k]);
511
+ }
512
+ }
513
+ return driven;
514
+ }
515
+ function morphInfluences(part, clips, root) {
516
+ const { mesh } = part;
517
+ const influences = mesh.morphTargetInfluences;
518
+ const count = morphCountOf(part);
519
+ if (!influences || count === 0) return { statics: [], animated: [] };
520
+ const driven = clips.map((clip) => morphTracksOn(mesh, clip, root));
521
+ const statics = [];
522
+ const animated = [];
523
+ for (let t = 0; t < count; t++) {
524
+ const seen = /* @__PURE__ */ new Set();
525
+ const offending = [];
526
+ driven.forEach((tracks, ci) => {
527
+ const values = tracks.get(t);
528
+ if (!values) {
529
+ seen.add(influences[t]);
530
+ return;
531
+ }
532
+ offending.push(`"${clips[ci].name}"`);
533
+ for (const value of values) seen.add(value);
534
+ });
535
+ if (seen.size > 1) {
536
+ const name = Object.entries(mesh.morphTargetDictionary ?? {}).find(([, i]) => i === t)?.[0];
537
+ animated.push({ part, target: name ? `"${name}"` : String(t), clips: offending });
538
+ }
539
+ statics.push(seen.values().next().value);
540
+ }
541
+ return { statics, animated };
542
+ }
543
+ function refuseAnimatedMorphs(animated) {
544
+ const byDriver = /* @__PURE__ */ new Map();
545
+ for (const { part, target, clips } of animated) {
546
+ const key = `${target}
547
+ ${clips.join("\n")}`;
548
+ const entry = byDriver.get(key) ?? { target, clips, parts: [] };
549
+ if (!entry.parts.includes(part)) entry.parts.push(part);
550
+ byDriver.set(key, entry);
551
+ }
552
+ const offences = [...byDriver.values()].map(({ target, clips, parts }) => {
553
+ const one = clips.length === 1;
554
+ const its = parts.length === 1 ? "its" : "their";
555
+ 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
+ });
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}`
559
+ );
560
+ }
561
+ function foldMorphs(part, statics) {
562
+ const { vertexCount, basePos, baseNrm, morphPos, morphNrm, morphRelative } = part;
563
+ const position = new Float32Array(vertexCount * 3);
564
+ const normal = new Float32Array(vertexCount * 3);
565
+ const active = statics.some((w) => w !== 0);
566
+ const _p = new Vector3();
567
+ const _n = new Vector3();
568
+ const _mt = new Vector3();
569
+ const _mb = new Vector3();
570
+ for (let v = 0; v < vertexCount; v++) {
571
+ _p.fromBufferAttribute(basePos, v);
572
+ _n.fromBufferAttribute(baseNrm, v);
573
+ if (active) {
574
+ for (let t = 0; t < statics.length; t++) {
575
+ const w = statics[t];
576
+ if (w === 0) continue;
577
+ const targetPos = morphPos?.[t];
578
+ if (targetPos) {
579
+ _mt.fromBufferAttribute(targetPos, v);
580
+ if (!morphRelative) _mt.sub(_mb.fromBufferAttribute(basePos, v));
581
+ _p.addScaledVector(_mt, w);
582
+ }
583
+ const targetNrm = morphNrm?.[t];
584
+ if (targetNrm) {
585
+ _mt.fromBufferAttribute(targetNrm, v);
586
+ if (!morphRelative) _mt.sub(_mb.fromBufferAttribute(baseNrm, v));
587
+ _n.addScaledVector(_mt, w);
588
+ }
589
+ }
590
+ _n.normalize();
591
+ }
592
+ _p.toArray(position, v * 3);
593
+ _n.toArray(normal, v * 3);
594
+ }
595
+ return { position, normal };
596
+ }
597
+ function scaleSensitiveNodes(slots, root, influenced) {
598
+ const sensitive = /* @__PURE__ */ new Map();
599
+ slots.forEach((slot, i) => {
600
+ if (!influenced[i]) return;
601
+ if (slot.rig) {
602
+ const { bones } = slot.rig.pose.skeleton;
603
+ for (let node = bones[slot.rig.bone] ?? null; node && bones.includes(node); node = node.parent) {
604
+ sensitive.set(node, slot);
605
+ }
606
+ } else {
607
+ for (let node = slot.parts[0].mesh; node && node !== root; node = node.parent) {
608
+ sensitive.set(node, slot);
609
+ }
610
+ }
611
+ });
612
+ return sensitive;
613
+ }
614
+ function refuseAnimatedNonUniformScale(slots, clips, root, influenced) {
615
+ const sensitive = scaleSensitiveNodes(slots, root, influenced);
616
+ for (const clip of clips) {
617
+ for (const track of clip.tracks) {
618
+ const parsed = PropertyBinding.parseTrackName(track.name);
619
+ if (parsed.propertyName !== "scale" || track.getValueSize() !== 3) continue;
620
+ const node = PropertyBinding.findNode(root, parsed.nodeName);
621
+ const reached = node && sensitive.get(node);
622
+ if (!reached) continue;
623
+ const values = track.values;
624
+ for (let k = 0; k < track.times.length; k++) {
625
+ const o = k * 3;
626
+ if (unevenSquares(values[o] ** 2, values[o + 1] ** 2, values[o + 2] ** 2)) {
627
+ throw nonUniformScale(slotLabel(reached), clip);
628
+ }
629
+ }
630
+ }
631
+ }
632
+ }
633
+ 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.`
636
+ );
637
+ }
638
+ function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
639
+ const clips = resolved.map((a) => a.clip);
640
+ const rootInverse = root.matrixWorld.clone().invert();
641
+ const poses = attachPoseBuffers(parts);
642
+ const { slots, slotMaps } = layoutSlots(parts, rootInverse);
643
+ const slotCount = slots.length;
644
+ const morphs = parts.map((part) => morphInfluences(part, clips, root));
645
+ const animated = morphs.flatMap((m) => m.animated);
646
+ if (animated.length > 0) throw refuseAnimatedMorphs(animated);
647
+ const rigParts = parts.map((part, i) => ({
648
+ part,
649
+ slotOf: slotMaps.get(part),
650
+ ...foldMorphs(part, morphs[i].statics)
651
+ }));
652
+ const width = slotCount * RIG_TEXELS_PER_SLOT;
653
+ if (width > maxTextureSize) {
654
+ throw new Error(
655
+ `three-vat: rig texture width ${width} (${slotCount} slots \xD7 ${RIG_TEXELS_PER_SLOT} texels) exceeds maxTextureSize ${maxTextureSize}`
656
+ );
657
+ }
658
+ const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
659
+ const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
660
+ const geometry = mergeRigGeometry(rigParts, vertexCount);
661
+ const localPosition = geometry.attributes.position.array;
662
+ const skinIndex = geometry.attributes.skinIndex.array;
663
+ const skinWeight = geometry.attributes.skinWeight.array;
664
+ const influenced = new Uint8Array(slotCount);
665
+ for (let i = 0; i < skinWeight.length; i++) {
666
+ if (skinWeight[i] !== 0) influenced[skinIndex[i]] = 1;
667
+ }
668
+ refuseAnimatedNonUniformScale(slots, clips, root, influenced);
669
+ const _p = new Vector3();
670
+ const _slot = new Matrix4();
671
+ const _partMatrix = new Matrix4();
672
+ const placements = new Map(parts.map((part) => [part, new Matrix4()]));
673
+ const _q = new Quaternion();
674
+ const _t = new Vector3();
675
+ const _s = new Vector3();
676
+ const restRoot = new Float64Array(vertexCount * 3);
677
+ for (const part of parts) {
678
+ _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
679
+ for (let v = 0; v < part.vertexCount; v++) {
680
+ _p.fromBufferAttribute(part.basePos, v).applyMatrix4(_partMatrix);
681
+ _p.toArray(restRoot, (part.vertexStart + v) * 3);
682
+ }
683
+ }
684
+ const data = new Float32Array(width * totalFrames * 4);
685
+ const slotMatrices = new Float64Array(slotCount * BONE_STRIDE);
686
+ const previous = new Float64Array(slotCount * 4);
687
+ const mixer = new AnimationMixer(root);
688
+ const bounds = new Box3();
689
+ const clipTable = [];
690
+ let minX = Infinity;
691
+ let minY = Infinity;
692
+ let minZ = Infinity;
693
+ let maxX = -Infinity;
694
+ let maxY = -Infinity;
695
+ let maxZ = -Infinity;
696
+ 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
+ );
718
+ }
719
+ }
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]);
766
+ }
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
+ }
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
789
+ });
790
+ rowOffset += frames;
791
+ });
792
+ mixer.stopAllAction();
793
+ mixer.setTime(0);
794
+ root.updateMatrixWorld(true);
795
+ bounds.min.set(minX, minY, minZ);
796
+ bounds.max.set(maxX, maxY, maxZ);
797
+ geometry.boundingBox = bounds.clone();
798
+ geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
799
+ const materials = [];
800
+ for (const part of parts) materials[part.materialIndex] = part.material;
801
+ return {
802
+ encoding: "rig",
803
+ rigTexture: makeVATTexture(data, width, totalFrames),
804
+ slotCount,
805
+ clips: clipTable,
806
+ bounds,
807
+ vertexCount,
808
+ totalFrames,
809
+ geometry,
810
+ materials
811
+ };
812
+ }
813
+ function mergeRigGeometry(rigParts, total) {
814
+ const parts = rigParts.map((r) => r.part);
815
+ const position = new Float32Array(total * 3);
816
+ const normal = new Float32Array(total * 3);
817
+ const slotIndex = new Uint16Array(total * 4);
818
+ const slotWeight = new Float32Array(total * 4);
819
+ const want = optionalAttributes(parts);
820
+ const uv = want.uv ? new Float32Array(total * 2) : null;
821
+ const color = want.color ? new Float32Array(total * 3) : null;
822
+ const tangent = want.tangent ? new Float32Array(total * 4) : null;
823
+ for (const { part, slotOf, position: restPos, normal: restNrm } of rigParts) {
824
+ const geometry = part.mesh.geometry;
825
+ const start = part.vertexStart;
826
+ const srcIndex = part.pose ? part.skinIndex : null;
827
+ const srcWeight = part.pose ? part.skinWeight : null;
828
+ const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
829
+ const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
830
+ const srcTangent = tangent ? geometry.attributes.tangent : null;
831
+ position.set(restPos, start * 3);
832
+ normal.set(restNrm, start * 3);
833
+ for (let v = 0; v < part.vertexCount; v++) {
834
+ const vi = start + v;
835
+ const o3 = vi * 3;
836
+ const o4 = vi * 4;
837
+ if (srcIndex && srcWeight) {
838
+ for (let i = 0; i < 4; i++) {
839
+ slotIndex[o4 + i] = slotOf[srcIndex.getComponent(v, i)];
840
+ slotWeight[o4 + i] = srcWeight.getComponent(v, i);
841
+ }
842
+ } else {
843
+ slotIndex[o4] = slotOf[0];
844
+ slotWeight[o4] = 1;
845
+ }
846
+ if (uv && srcUV) {
847
+ uv[vi * 2] = srcUV.getX(v);
848
+ uv[vi * 2 + 1] = srcUV.getY(v);
849
+ }
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
+ }
855
+ if (tangent && srcTangent) {
856
+ tangent[o4] = srcTangent.getX(v);
857
+ tangent[o4 + 1] = srcTangent.getY(v);
858
+ tangent[o4 + 2] = srcTangent.getZ(v);
859
+ tangent[o4 + 3] = srcTangent.getW(v);
860
+ }
861
+ }
862
+ }
863
+ const merged = new BufferGeometry();
864
+ merged.setAttribute("position", new BufferAttribute(position, 3));
865
+ merged.setAttribute("normal", new BufferAttribute(normal, 3));
866
+ merged.setAttribute("skinIndex", new BufferAttribute(slotIndex, 4));
867
+ merged.setAttribute("skinWeight", new BufferAttribute(slotWeight, 4));
868
+ if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
869
+ if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
870
+ if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
871
+ indexParts(merged, parts);
872
+ return merged;
319
873
  }
320
874
 
321
- export { MAX_TEXTURE_SIZE, bakeVAT, makeVATTexture };
875
+ export { bakeVAT };