three-vat 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,8 +84,8 @@ Two things worth knowing the first time:
84
84
 
85
85
  **A skinned character?** The bake picks the rig encoding for it by itself: the
86
86
  posed rig instead of the posed vertices, for two orders of magnitude less
87
- texture, a bake in milliseconds, and no vertex ceiling for a phone's 4096 to
88
- refuse. Assets a rig cannot express fall back to vertices, and `vat.fallback`
87
+ texture, a bake in milliseconds, and a texture whose width is the rig's, not
88
+ the mesh's. Assets a rig cannot express fall back to vertices, and `vat.fallback`
89
89
  says why. [The rig encoding](./docs/usage.md#the-rig-encoding-encoding-rig).
90
90
 
91
91
  <details>
@@ -478,9 +478,17 @@ interface VATBase {
478
478
  clips: VATClip[];
479
479
  /** Union of every baked frame's bounds; use as the geometry bounding box. */
480
480
  bounds: Box3;
481
- /** Vertex count (texture width). */
481
+ /**
482
+ * Vertices in the merged geometry — the vertex encoding's texture width
483
+ * wherever they fit one row, which is every bake below the ceiling
484
+ * ({@link DeltaVAT.rowsPerFrame}).
485
+ */
482
486
  vertexCount: number;
483
- /** Total frame rows across all clips (texture height). */
487
+ /**
488
+ * Total frames across all clips — the texture's height in rows, times
489
+ * {@link DeltaVAT.rowsPerFrame} under the vertex encoding. The unit every
490
+ * `startFrame` and `frames` in the clip table counts in.
491
+ */
484
492
  totalFrames: number;
485
493
  }
486
494
  /**
@@ -515,6 +523,16 @@ interface DeltaVAT extends VATBase {
515
523
  * a VAT is refused rather than lit by its rest pose.
516
524
  */
517
525
  normalTexture: DataTexture | null;
526
+ /**
527
+ * Texture rows one frame takes (ADR-0030). `1` wherever the vertex count
528
+ * fits the bake's `maxTextureSize`, and then both layers are `vertexCount`
529
+ * wide and `totalFrames` tall, as every bake before this field was. Past it,
530
+ * a frame's vertices continue onto the next row: the fewest rows that hold
531
+ * them, `ceil(vertexCount / rowsPerFrame)` texels wide, and vertex `v` of
532
+ * frame `f` at column `v mod width`, row `f × rowsPerFrame + floor(v /
533
+ * width)`. Both decode paths read it; neither adds a line where it is `1`.
534
+ */
535
+ rowsPerFrame: number;
518
536
  /**
519
537
  * Why the default encoding fell back to this one (ADR-0029): the message of
520
538
  * the rig encoding's refusal, naming what the rig could not store and where
@@ -3,6 +3,20 @@ import { FloatType, DataTexture, RGBAFormat, RGFormat, UnsignedByteType, HalfFlo
3
3
  // src/vat-texture.ts
4
4
  var MAX_TEXTURE_SIZE = 16384;
5
5
  var HALF_FLOAT_MAX = 65504;
6
+ function vertexLayoutFor(vertexCount, totalFrames, maxTextureSize) {
7
+ const rowsPerFrame = Math.max(1, Math.ceil(vertexCount / maxTextureSize));
8
+ const width = vertexWidthOf({ vertexCount, rowsPerFrame });
9
+ const height = totalFrames * rowsPerFrame;
10
+ if (height > maxTextureSize) {
11
+ throw new Error(
12
+ `three-vat: totalFrames ${totalFrames} at ${rowsPerFrame} rows a frame is ${height} rows, which exceeds maxTextureSize ${maxTextureSize} \u2014 the vertex encoding spans each frame's ${vertexCount} vertices across ${rowsPerFrame} rows of ${width}; lower fps or bake fewer clips`
13
+ );
14
+ }
15
+ return { rowsPerFrame, width, height, frameStride: rowsPerFrame * width };
16
+ }
17
+ function vertexWidthOf({ vertexCount, rowsPerFrame }) {
18
+ return Math.ceil(vertexCount / rowsPerFrame);
19
+ }
6
20
  function sampledExactly(tex) {
7
21
  tex.minFilter = NearestFilter;
8
22
  tex.magFilter = NearestFilter;
@@ -246,4 +260,4 @@ var RIG_TEXELS = {
246
260
  };
247
261
  var RIG_TEXELS_PER_SLOT = 2;
248
262
 
249
- export { EndMode, FORWARD_ONLY_REASON, HALF_FLOAT_MAX, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_TEXTURE_SIZE, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance };
263
+ export { EndMode, FORWARD_ONLY_REASON, HALF_FLOAT_MAX, INFINITE_REPETITIONS, LIBRARY_PLAYBACK_DEFAULTS, LoopMode, MAX_TEXTURE_SIZE, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance, vertexLayoutFor, vertexWidthOf };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AnimationClip, AnimationAction, Object3D, DataTexture, FloatType, HalfFloatType } from 'three';
2
- import { D as DeltaVAT, R as RigVAT, V as VAT } from './carrier-D4pkftSk.js';
3
- export { E as EndMode, I as INFINITE_REPETITIONS, L as LoopMode, a as VATBase, b as VATCarrier, c as VATClip, d as VATClipDefaults, e as VATClock, f as VATCrowd, g as VATFrame, h as VATInstance, i as VATOutgoingFrame, j as VATPlaybackState, k as VATPlaybackTexture, l as VATPlaybackTextureOptions, m as createVATPlaybackTexture, n as endsAt, r as resolveVATFrame, s as setVATInstance } from './carrier-D4pkftSk.js';
2
+ import { D as DeltaVAT, R as RigVAT, V as VAT } from './carrier-VLSJLgjX.js';
3
+ export { E as EndMode, I as INFINITE_REPETITIONS, L as LoopMode, a as VATBase, b as VATCarrier, c as VATClip, d as VATClipDefaults, e as VATClock, f as VATCrowd, g as VATFrame, h as VATInstance, i as VATOutgoingFrame, j as VATPlaybackState, k as VATPlaybackTexture, l as VATPlaybackTextureOptions, m as createVATPlaybackTexture, n as endsAt, r as resolveVATFrame, s as setVATInstance } from './carrier-VLSJLgjX.js';
4
4
 
5
5
  /**
6
6
  * What {@link bakeVAT} takes for each animation: the clip itself, or an
@@ -15,10 +15,14 @@ interface BakeOptions {
15
15
  /** Sample rate in frames per second. Default `30`. */
16
16
  fps?: number;
17
17
  /**
18
- * Largest texture dimension the target GPU accepts. Both VAT axes are checked
19
- * against it: `vertexCount` (width) and `totalFrames` (height). Defaults to
18
+ * Largest texture dimension the target GPU accepts. Under the rig encoding
19
+ * both axes are checked against it: the rig's width and `totalFrames`. Under
20
+ * the vertex encoding a frame whose vertices outnumber it spans several rows
21
+ * instead of refusing (ADR-0030), so the height alone is checked: `totalFrames
22
+ * × rowsPerFrame`. Defaults to
20
23
  * {@link MAX_TEXTURE_SIZE}; pass the renderer's real limit to avoid baking a
21
- * VAT that allocates on your desktop and fails on a phone.
24
+ * VAT that allocates on your desktop and fails on a phone, and so that a
25
+ * frame spans rows only where this GPU needs it to.
22
26
  */
23
27
  maxTextureSize?: number;
24
28
  /**
@@ -220,7 +224,8 @@ interface Vec3Out {
220
224
  }
221
225
  /**
222
226
  * Write a normal into two bytes of a VAT normal texture's buffer, at `offset`
223
- * and `offset + 1` — `(row * vertexCount + vertex) * 2` for the texel of one
227
+ * and `offset + 1` — `(row * vertexCount + vertex) * 2` in a one-row bake,
228
+ * `deltaTexel` in `src/test-utils.ts` in any, for the texel of one
224
229
  * vertex at one frame.
225
230
  *
226
231
  * Takes a direction, not a unit vector: the first step is a division by the L1
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- import { LoopMode, MAX_TEXTURE_SIZE, HALF_FLOAT_MAX, makeVATNormalTexture, makeVATTexture, RIG_TEXELS_PER_SLOT, RIG_TEXELS, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS } from './chunk-BSAJL5PG.js';
2
- export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-BSAJL5PG.js';
3
- import { LoopRepeat, LoopOnce, LoopPingPong, DataUtils, Quaternion, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, HalfFloatType, BufferGeometry, BufferAttribute, AdditiveAnimationBlendMode, PropertyBinding, InterleavedBufferAttribute, FloatType, Material, InterleavedBuffer, SkinnedMesh, Mesh, Bone, Object3D, Skeleton, AnimationClip, Color, VectorKeyframeTrack, StringKeyframeTrack, QuaternionKeyframeTrack, NumberKeyframeTrack, ColorKeyframeTrack, BooleanKeyframeTrack, Float16BufferAttribute, Interpolant } from 'three';
1
+ import { LoopMode, MAX_TEXTURE_SIZE, vertexLayoutFor, makeVATNormalTexture, makeVATTexture, RIG_TEXELS_PER_SLOT, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS, HALF_FLOAT_MAX, RIG_TEXELS } from './chunk-OEKKEHRQ.js';
2
+ export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATNormalTexture, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-OEKKEHRQ.js';
3
+ import { LoopRepeat, LoopOnce, LoopPingPong, DataUtils, Quaternion, Matrix4, Box3, Vector4, Vector3, Sphere, HalfFloatType, BufferGeometry, BufferAttribute, AdditiveAnimationBlendMode, AnimationMixer, PropertyBinding, FloatType, Material, InterleavedBuffer, SkinnedMesh, Mesh, Bone, Object3D, Skeleton, AnimationClip, Color, InterleavedBufferAttribute, VectorKeyframeTrack, StringKeyframeTrack, QuaternionKeyframeTrack, NumberKeyframeTrack, ColorKeyframeTrack, BooleanKeyframeTrack, Float16BufferAttribute, Interpolant } from 'three';
4
4
 
5
5
  var IGNORED = ["uuid", "name", "color", "userData", "metadata"];
6
6
  function flatFacts(material) {
@@ -125,6 +125,32 @@ function morphCountOf({ mesh, morphPos, morphNrm }) {
125
125
  const influences = mesh.morphTargetInfluences;
126
126
  return influences ? Math.min(influences.length, Math.max(morphPos?.length ?? 0, morphNrm?.length ?? 0)) : 0;
127
127
  }
128
+ var _mt = /* @__PURE__ */ new Vector3();
129
+ var _mb = /* @__PURE__ */ new Vector3();
130
+ var _mbn = /* @__PURE__ */ new Vector3();
131
+ function morphVertex(part, v, weights, count, p, n) {
132
+ const { basePos, baseNrm, morphPos, morphNrm, morphRelative } = part;
133
+ if (!morphRelative) {
134
+ _mb.fromBufferAttribute(basePos, v);
135
+ if (n) _mbn.fromBufferAttribute(baseNrm, v);
136
+ }
137
+ for (let t = 0; t < count; t++) {
138
+ const w = weights[t];
139
+ if (w === 0) continue;
140
+ const targetPos = morphPos?.[t];
141
+ if (targetPos) {
142
+ _mt.fromBufferAttribute(targetPos, v);
143
+ if (!morphRelative) _mt.sub(_mb);
144
+ p.addScaledVector(_mt, w);
145
+ }
146
+ const targetNrm = n ? morphNrm?.[t] : void 0;
147
+ if (n && targetNrm) {
148
+ _mt.fromBufferAttribute(targetNrm, v);
149
+ if (!morphRelative) _mt.sub(_mbn);
150
+ n.addScaledVector(_mt, w);
151
+ }
152
+ }
153
+ }
128
154
  function asAttribute(value, mesh, name) {
129
155
  if (!(value instanceof BufferAttribute)) {
130
156
  throw new Error(
@@ -201,39 +227,48 @@ function collectParts(root, hooks) {
201
227
  function mergeGeometry(parts, restMatrices, total) {
202
228
  const position = new Float32Array(total * 3);
203
229
  const normal = new Float32Array(total * 3);
204
- const want = optionalAttributes(parts);
230
+ const _v = new Vector3();
231
+ const _n = new Vector3();
232
+ parts.forEach((part, pi) => {
233
+ const m = restMatrices[pi];
234
+ for (let v = 0; v < part.vertexCount; v++) {
235
+ _v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
236
+ _n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
237
+ _v.toArray(position, (part.vertexStart + v) * 3);
238
+ _n.toArray(normal, (part.vertexStart + v) * 3);
239
+ }
240
+ });
241
+ const merged = new BufferGeometry();
242
+ merged.setAttribute("position", new BufferAttribute(position, 3));
243
+ merged.setAttribute("normal", new BufferAttribute(normal, 3));
244
+ mergeSurface(merged, parts, total, restMatrices);
245
+ return merged;
246
+ }
247
+ function mergeSurface(merged, parts, total, restMatrices) {
248
+ const merging = parts.some((p) => p.tint);
249
+ const want = optionalAttributes(parts, merging);
205
250
  const uv = want.uv ? new Float32Array(total * 2) : null;
206
251
  const color = want.color ? new Float32Array(total * 3) : null;
207
- const merging = parts.some((p) => p.tint);
208
252
  const tangent = want.tangent ? new Float32Array(total * 4) : null;
209
- const _v = new Vector3();
210
- const _n = new Vector3();
211
253
  const _t = new Vector3();
212
254
  parts.forEach((part, pi) => {
213
- const m = restMatrices[pi];
214
255
  const geometry = part.mesh.geometry;
215
256
  const start = part.vertexStart;
216
257
  const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
217
- const srcColor = colorSource(part, !!color, merging);
258
+ const srcColor = color ? colorSource(part, merging) : null;
218
259
  const srcTangent = tangent ? geometry.attributes.tangent : null;
260
+ const m = restMatrices?.[pi];
219
261
  for (let v = 0; v < part.vertexCount; v++) {
220
- _v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
221
- _n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
222
- const o3 = (start + v) * 3;
223
- position[o3] = _v.x;
224
- position[o3 + 1] = _v.y;
225
- position[o3 + 2] = _v.z;
226
- normal[o3] = _n.x;
227
- normal[o3 + 1] = _n.y;
228
- normal[o3 + 2] = _n.z;
262
+ const vi = start + v;
229
263
  if (uv && srcUV) {
230
- uv[(start + v) * 2] = srcUV.getX(v);
231
- uv[(start + v) * 2 + 1] = srcUV.getY(v);
264
+ uv[vi * 2] = srcUV.getX(v);
265
+ uv[vi * 2 + 1] = srcUV.getY(v);
232
266
  }
233
- if (color) writeColor(color, o3, part, srcColor, v);
267
+ if (color) writeColor(color, vi * 3, part, srcColor, v);
234
268
  if (tangent && srcTangent) {
235
- _t.fromBufferAttribute(srcTangent, v).transformDirection(m);
236
- const o4 = (start + v) * 4;
269
+ _t.fromBufferAttribute(srcTangent, v);
270
+ if (m) _t.transformDirection(m);
271
+ const o4 = vi * 4;
237
272
  tangent[o4] = _t.x;
238
273
  tangent[o4 + 1] = _t.y;
239
274
  tangent[o4 + 2] = _t.z;
@@ -241,18 +276,14 @@ function mergeGeometry(parts, restMatrices, total) {
241
276
  }
242
277
  }
243
278
  });
244
- const merged = new BufferGeometry();
245
- merged.setAttribute("position", new BufferAttribute(position, 3));
246
- merged.setAttribute("normal", new BufferAttribute(normal, 3));
247
279
  if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
248
280
  if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
249
281
  if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
250
282
  indexParts(merged, parts);
251
- return merged;
252
283
  }
253
- function colorSource(part, wanted, merging) {
284
+ function colorSource(part, merging) {
254
285
  const source = part.mesh.geometry.attributes.color;
255
- if (!wanted || part.tint || !source) return null;
286
+ if (part.tint || !source) return null;
256
287
  if (merging && source instanceof InterleavedBufferAttribute) return source;
257
288
  return asAttribute(source, part.mesh, "color");
258
289
  }
@@ -269,13 +300,13 @@ function writeColor(out, o3, part, source, v) {
269
300
  out[o3] = out[o3 + 1] = out[o3 + 2] = 1;
270
301
  }
271
302
  }
272
- function optionalAttributes(parts) {
303
+ function optionalAttributes(parts, merging) {
273
304
  return {
274
305
  uv: parts.every((p) => !!p.mesh.geometry.attributes.uv),
275
306
  // A merge writes a colour for every part it touched, so the attribute is
276
307
  // there whenever one happened; a part it did not touch keeps its own, or
277
308
  // 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),
309
+ color: merging || parts.every((p) => !!p.mesh.geometry.attributes.color),
279
310
  tangent: parts.every((p) => {
280
311
  const t = p.mesh.geometry.attributes.tangent;
281
312
  return t instanceof BufferAttribute && t.itemSize === 4;
@@ -356,44 +387,13 @@ function bakeVATWith(root, animations, options, hooks) {
356
387
  function unprefixed(error) {
357
388
  return error.message.replace(/^three-vat: /, "");
358
389
  }
359
- function bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }) {
360
- const clips = resolved.map((a) => a.clip);
361
- const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
362
- if (vertexCount > maxTextureSize) {
363
- throw new Error(
364
- `three-vat: vertexCount ${vertexCount} exceeds maxTextureSize ${maxTextureSize}; row wrapping is not implemented`
365
- );
366
- }
367
- const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
368
- const rootInverse = root.matrixWorld.clone().invert();
369
- const restMatrices = parts.map(
370
- (p) => new Matrix4().multiplyMatrices(rootInverse, p.mesh.matrixWorld)
371
- );
372
- const geometry = mergeGeometry(parts, restMatrices, vertexCount);
373
- const mergedBase = geometry.attributes.position;
374
- const posData = new Uint16Array(vertexCount * totalFrames * 4);
375
- const nrmData = bakeNormals ? new Uint8Array(vertexCount * totalFrames * 2) : null;
376
- const bakeNormal = nrmData !== null;
390
+ function sampleClips(root, resolved, frameCounts, poses, writeFrame) {
377
391
  const mixer = new AnimationMixer(root);
378
- const bounds = new Box3();
379
392
  const clipTable = [];
380
- const _si = new Vector4();
381
- const _sw = new Vector4();
382
- const _acc = new Matrix4();
383
- const _skin = new Matrix4();
384
- const _partMatrix = new Matrix4();
385
- const _p = new Vector3();
386
- const _bp = new Vector3();
387
- const _n = new Vector3();
388
- const _mt = new Vector3();
389
- const _mb = new Vector3();
390
- const _mbn = new Vector3();
391
- const poses = attachPoseBuffers(parts);
392
- const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
393
- let warnedNonUniformScale = false;
394
393
  let rowOffset = 0;
395
394
  try {
396
- clips.forEach((clip, ci) => {
395
+ for (let ci = 0; ci < resolved.length; ci++) {
396
+ const { clip, defaults } = resolved[ci];
397
397
  const frames = frameCounts[ci];
398
398
  const action = mixer.clipAction(clip);
399
399
  action.play();
@@ -401,81 +401,8 @@ function bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals
401
401
  for (let f = 0; f < frames; f++) {
402
402
  mixer.setTime(f / frames * clip.duration);
403
403
  root.updateMatrixWorld(true);
404
- const row = rowOffset + f;
405
404
  poseSkeletons(poses);
406
- if (!warnedNonUniformScale) {
407
- warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
408
- }
409
- for (const part of parts) {
410
- _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
411
- const influences = part.mesh.morphTargetInfluences;
412
- const { morphPos, morphNrm, morphRelative, isSkinned, skinIndex, skinWeight } = part;
413
- const boneMatrices = part.pose?.matrices;
414
- const morphCount = morphCountOf(part);
415
- for (let v = 0; v < part.vertexCount; v++) {
416
- const vi = part.vertexStart + v;
417
- _p.fromBufferAttribute(part.basePos, v);
418
- if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
419
- if (influences && morphCount > 0) {
420
- if (!morphRelative) {
421
- _mb.fromBufferAttribute(part.basePos, v);
422
- if (bakeNormal) _mbn.fromBufferAttribute(part.baseNrm, v);
423
- }
424
- for (let t = 0; t < morphCount; t++) {
425
- const w = influences[t];
426
- if (w === 0) continue;
427
- const targetPos = morphPos?.[t];
428
- if (targetPos) {
429
- _mt.fromBufferAttribute(targetPos, v);
430
- if (!morphRelative) _mt.sub(_mb);
431
- _p.addScaledVector(_mt, w);
432
- }
433
- const targetNrm = bakeNormal ? morphNrm?.[t] : void 0;
434
- if (targetNrm) {
435
- _mt.fromBufferAttribute(targetNrm, v);
436
- if (!morphRelative) _mt.sub(_mbn);
437
- _n.addScaledVector(_mt, w);
438
- }
439
- }
440
- }
441
- if (isSkinned && boneMatrices) {
442
- const skinned = part.mesh;
443
- _si.fromBufferAttribute(skinIndex, v);
444
- _sw.fromBufferAttribute(skinWeight, v);
445
- const ae = _acc.elements;
446
- ae.fill(0);
447
- for (let i = 0; i < 4; i++) {
448
- const w = _sw.getComponent(i);
449
- if (w === 0) continue;
450
- const b = _si.getComponent(i) * BONE_STRIDE;
451
- for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
452
- }
453
- _skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
454
- _p.applyMatrix4(_skin);
455
- if (bakeNormal) _n.transformDirection(_skin);
456
- }
457
- _p.applyMatrix4(_partMatrix);
458
- if (bakeNormal) _n.transformDirection(_partMatrix);
459
- bounds.expandByPoint(_p);
460
- _bp.fromBufferAttribute(mergedBase, vi);
461
- const o = (row * vertexCount + vi) * 4;
462
- const dx = _p.x - _bp.x;
463
- const dy = _p.y - _bp.y;
464
- const dz = _p.z - _bp.z;
465
- maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
466
- const largest = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
467
- if (largest > HALF_FLOAT_MAX) {
468
- throw new Error(
469
- `three-vat: position delta component ${largest} (clip "${clip.name}", frame ${f}, vertex ${vi}) exceeds the half-float limit of ${HALF_FLOAT_MAX} the position texture stores; a unit smaller than the metre is the usual cause`
470
- );
471
- }
472
- posData[o] = toHalfFloat(dx);
473
- posData[o + 1] = toHalfFloat(dy);
474
- posData[o + 2] = toHalfFloat(dz);
475
- posData[o + 3] = HALF_ONE;
476
- if (bakeNormal) encodeOctahedral(_n.x, _n.y, _n.z, nrmData, (row * vertexCount + vi) * 2);
477
- }
478
- }
405
+ maxDeltaSq = Math.max(maxDeltaSq, writeFrame(clip, f, rowOffset + f));
479
406
  }
480
407
  action.stop();
481
408
  clipTable.push({
@@ -487,26 +414,113 @@ function bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals
487
414
  maxDelta: Math.sqrt(maxDeltaSq),
488
415
  // Declared once, here, rather than repeated at every instance that plays
489
416
  // this band. An instance overrides any of them, field by field.
490
- ...resolved[ci].defaults
417
+ ...defaults
491
418
  });
492
419
  rowOffset += frames;
493
- });
420
+ }
494
421
  } finally {
495
422
  mixer.stopAllAction();
496
423
  mixer.setTime(0);
497
424
  root.updateMatrixWorld(true);
498
425
  }
426
+ return clipTable;
427
+ }
428
+ function bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }) {
429
+ const clips = resolved.map((a) => a.clip);
430
+ const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
431
+ const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
432
+ const { rowsPerFrame, width, height, frameStride } = vertexLayoutFor(vertexCount, totalFrames, maxTextureSize);
433
+ const rootInverse = root.matrixWorld.clone().invert();
434
+ const restMatrices = parts.map(
435
+ (p) => new Matrix4().multiplyMatrices(rootInverse, p.mesh.matrixWorld)
436
+ );
437
+ const geometry = mergeGeometry(parts, restMatrices, vertexCount);
438
+ const mergedBase = geometry.attributes.position;
439
+ const posData = new Uint16Array(frameStride * totalFrames * 4);
440
+ const nrmData = bakeNormals ? new Uint8Array(frameStride * totalFrames * 2) : null;
441
+ const bakeNormal = nrmData !== null;
442
+ const bounds = new Box3();
443
+ const _si = new Vector4();
444
+ const _sw = new Vector4();
445
+ const _acc = new Matrix4();
446
+ const _skin = new Matrix4();
447
+ const _partMatrix = new Matrix4();
448
+ const _p = new Vector3();
449
+ const _bp = new Vector3();
450
+ const _n = new Vector3();
451
+ const poses = attachPoseBuffers(parts);
452
+ const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
453
+ let warnedNonUniformScale = false;
454
+ const writeFrame = (clip, f, row) => {
455
+ let maxDeltaSq = 0;
456
+ if (!warnedNonUniformScale) {
457
+ warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
458
+ }
459
+ for (const part of parts) {
460
+ _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
461
+ const influences = part.mesh.morphTargetInfluences;
462
+ const { isSkinned, skinIndex, skinWeight } = part;
463
+ const boneMatrices = part.pose?.matrices;
464
+ const morphCount = morphCountOf(part);
465
+ for (let v = 0; v < part.vertexCount; v++) {
466
+ const vi = part.vertexStart + v;
467
+ _p.fromBufferAttribute(part.basePos, v);
468
+ if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
469
+ if (influences && morphCount > 0) morphVertex(part, v, influences, morphCount, _p, bakeNormal ? _n : null);
470
+ if (isSkinned && boneMatrices) {
471
+ const skinned = part.mesh;
472
+ _si.fromBufferAttribute(skinIndex, v);
473
+ _sw.fromBufferAttribute(skinWeight, v);
474
+ const ae = _acc.elements;
475
+ for (let e = 0; e < 16; e++) ae[e] = 0;
476
+ for (let i = 0; i < 4; i++) {
477
+ const w = _sw.getComponent(i);
478
+ if (w === 0) continue;
479
+ const b = _si.getComponent(i) * BONE_STRIDE;
480
+ for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
481
+ }
482
+ _skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
483
+ _p.applyMatrix4(_skin);
484
+ if (bakeNormal) _n.transformDirection(_skin);
485
+ }
486
+ _p.applyMatrix4(_partMatrix);
487
+ if (bakeNormal) _n.transformDirection(_partMatrix);
488
+ bounds.expandByPoint(_p);
489
+ _bp.fromBufferAttribute(mergedBase, vi);
490
+ const texel = row * frameStride + vi;
491
+ const o = texel * 4;
492
+ const dx = _p.x - _bp.x;
493
+ const dy = _p.y - _bp.y;
494
+ const dz = _p.z - _bp.z;
495
+ maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
496
+ const largest = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
497
+ if (largest > HALF_FLOAT_MAX) {
498
+ throw new Error(
499
+ `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`
500
+ );
501
+ }
502
+ posData[o] = toHalfFloat(dx);
503
+ posData[o + 1] = toHalfFloat(dy);
504
+ posData[o + 2] = toHalfFloat(dz);
505
+ posData[o + 3] = HALF_ONE;
506
+ if (bakeNormal) encodeOctahedral(_n.x, _n.y, _n.z, nrmData, texel * 2);
507
+ }
508
+ }
509
+ return maxDeltaSq;
510
+ };
511
+ const clipTable = sampleClips(root, resolved, frameCounts, poses, writeFrame);
499
512
  geometry.boundingBox = bounds.clone();
500
513
  geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
501
514
  const materials = [];
502
515
  for (const part of parts) materials[part.materialIndex] = part.material;
503
516
  return {
504
- positionTexture: makeVATTexture(posData, vertexCount, totalFrames, HalfFloatType),
505
- normalTexture: nrmData ? makeVATNormalTexture(nrmData, vertexCount, totalFrames) : null,
517
+ positionTexture: makeVATTexture(posData, width, height, HalfFloatType),
518
+ normalTexture: nrmData ? makeVATNormalTexture(nrmData, width, height) : null,
506
519
  clips: clipTable,
507
520
  bounds,
508
521
  vertexCount,
509
522
  totalFrames,
523
+ rowsPerFrame,
510
524
  encoding: "delta",
511
525
  geometry,
512
526
  materials
@@ -713,34 +727,17 @@ ${clips.join("\n")}`;
713
727
  );
714
728
  }
715
729
  function foldMorphs(part, statics) {
716
- const { vertexCount, basePos, baseNrm, morphPos, morphNrm, morphRelative } = part;
730
+ const { vertexCount, basePos, baseNrm } = part;
717
731
  const position = new Float32Array(vertexCount * 3);
718
732
  const normal = new Float32Array(vertexCount * 3);
719
733
  const active = statics.some((w) => w !== 0);
720
734
  const _p = new Vector3();
721
735
  const _n = new Vector3();
722
- const _mt = new Vector3();
723
- const _mb = new Vector3();
724
736
  for (let v = 0; v < vertexCount; v++) {
725
737
  _p.fromBufferAttribute(basePos, v);
726
738
  _n.fromBufferAttribute(baseNrm, v);
727
739
  if (active) {
728
- for (let t = 0; t < statics.length; t++) {
729
- const w = statics[t];
730
- if (w === 0) continue;
731
- const targetPos = morphPos?.[t];
732
- if (targetPos) {
733
- _mt.fromBufferAttribute(targetPos, v);
734
- if (!morphRelative) _mt.sub(_mb.fromBufferAttribute(basePos, v));
735
- _p.addScaledVector(_mt, w);
736
- }
737
- const targetNrm = morphNrm?.[t];
738
- if (targetNrm) {
739
- _mt.fromBufferAttribute(targetNrm, v);
740
- if (!morphRelative) _mt.sub(_mb.fromBufferAttribute(baseNrm, v));
741
- _n.addScaledVector(_mt, w);
742
- }
743
- }
740
+ morphVertex(part, v, statics, statics.length, _p, _n);
744
741
  _n.normalize();
745
742
  }
746
743
  _p.toArray(position, v * 3);
@@ -838,128 +835,101 @@ function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
838
835
  const data = new Float32Array(width * totalFrames * 4);
839
836
  const slotMatrices = new Float64Array(slotCount * BONE_STRIDE);
840
837
  const previous = new Float64Array(slotCount * 4);
841
- const mixer = new AnimationMixer(root);
842
838
  const bounds = new Box3();
843
- const clipTable = [];
844
839
  let minX = Infinity;
845
840
  let minY = Infinity;
846
841
  let minZ = Infinity;
847
842
  let maxX = -Infinity;
848
843
  let maxY = -Infinity;
849
844
  let maxZ = -Infinity;
850
- let rowOffset = 0;
851
- try {
852
- clips.forEach((clip, ci) => {
853
- const frames = frameCounts[ci];
854
- const action = mixer.clipAction(clip);
855
- action.play();
856
- let maxDeltaSq = 0;
857
- for (let f = 0; f < frames; f++) {
858
- mixer.setTime(f / frames * clip.duration);
859
- root.updateMatrixWorld(true);
860
- const row = rowOffset + f;
861
- poseSkeletons(poses);
862
- for (const [part, placement] of placements) placementOf(part, rootInverse, placement);
863
- for (let slot = 0; slot < slotCount; slot++) {
864
- const { rig, parts: readers } = slots[slot];
865
- const lead = readers[0];
866
- const placement = placements.get(lead);
867
- for (let pi = 1; pi < readers.length; pi++) {
868
- const other = readers[pi];
869
- if (!matricesClose(placement, placements.get(other))) {
870
- throw new RigRefusal(
871
- `three-vat: parts ${partName(lead)} and ${partName(other)} read the same slots but move apart in clip "${clip.name}"; the rig encoding gives them one set of slots, which cannot place them differently, and ${VERTEX_ENCODING_STORES_IT}`
872
- );
873
- }
874
- }
875
- if (rig) {
876
- _slot.fromArray(rig.pose.matrices, rig.bone * BONE_STRIDE).multiply(rig.bindMatrix).premultiply(placement);
877
- } else {
878
- _slot.copy(placement);
879
- }
880
- slotMatrices.set(_slot.elements, slot * BONE_STRIDE);
881
- if (influenced[slot] && (hasNonUniformScale(slotMatrices, slot) || hasShear(slotMatrices, slot))) {
882
- throw nonUniformScale(slotLabel(slots[slot]), clip);
883
- }
884
- const p4 = slot * 4;
885
- const det = _slot.determinant();
886
- let scale;
887
- if (det === 0) {
888
- _t.setFromMatrixPosition(_slot);
889
- _q2.fromArray(previous, p4);
890
- if (_q2.lengthSq() === 0) _q2.identity();
891
- scale = 0;
892
- } else {
893
- if (det < 0) for (const e of MATRIX_BASIS) _slot.elements[e] = -_slot.elements[e];
894
- _slot.decompose(_t, _q2, _s);
895
- scale = det < 0 ? -_s.x : _s.x;
896
- }
897
- if (previous[p4] * _q2.x + previous[p4 + 1] * _q2.y + previous[p4 + 2] * _q2.z + previous[p4 + 3] * _q2.w < 0) {
898
- _q2.set(-_q2.x, -_q2.y, -_q2.z, -_q2.w);
899
- }
900
- previous[p4] = _q2.x;
901
- previous[p4 + 1] = _q2.y;
902
- previous[p4 + 2] = _q2.z;
903
- previous[p4 + 3] = _q2.w;
904
- const o = (row * width + slot * RIG_TEXELS_PER_SLOT) * 4;
905
- const rotation = o + RIG_TEXELS.rotation * 4;
906
- data[rotation] = _q2.x;
907
- data[rotation + 1] = _q2.y;
908
- data[rotation + 2] = _q2.z;
909
- data[rotation + 3] = _q2.w;
910
- const placementTexel = o + RIG_TEXELS.placement * 4;
911
- data[placementTexel] = _t.x;
912
- data[placementTexel + 1] = _t.y;
913
- data[placementTexel + 2] = _t.z;
914
- data[placementTexel + 3] = scale;
915
- }
916
- for (let v = 0; v < vertexCount; v++) {
917
- const o3 = v * 3;
918
- const o4 = v * 4;
919
- const x = localPosition[o3];
920
- const y = localPosition[o3 + 1];
921
- const z = localPosition[o3 + 2];
922
- let px = 0;
923
- let py = 0;
924
- let pz = 0;
925
- for (let i = 0; i < 4; i++) {
926
- const w = skinWeight[o4 + i];
927
- if (w === 0) continue;
928
- const m = skinIndex[o4 + i] * BONE_STRIDE;
929
- px += w * (slotMatrices[m] * x + slotMatrices[m + 4] * y + slotMatrices[m + 8] * z + slotMatrices[m + 12]);
930
- py += w * (slotMatrices[m + 1] * x + slotMatrices[m + 5] * y + slotMatrices[m + 9] * z + slotMatrices[m + 13]);
931
- pz += w * (slotMatrices[m + 2] * x + slotMatrices[m + 6] * y + slotMatrices[m + 10] * z + slotMatrices[m + 14]);
932
- }
933
- if (px < minX) minX = px;
934
- if (px > maxX) maxX = px;
935
- if (py < minY) minY = py;
936
- if (py > maxY) maxY = py;
937
- if (pz < minZ) minZ = pz;
938
- if (pz > maxZ) maxZ = pz;
939
- const dx = px - restRoot[o3];
940
- const dy = py - restRoot[o3 + 1];
941
- const dz = pz - restRoot[o3 + 2];
942
- const deltaSq = dx * dx + dy * dy + dz * dz;
943
- if (deltaSq > maxDeltaSq) maxDeltaSq = deltaSq;
845
+ const writeFrame = (clip, _f, row) => {
846
+ let maxDeltaSq = 0;
847
+ for (const [part, placement] of placements) placementOf(part, rootInverse, placement);
848
+ for (let slot = 0; slot < slotCount; slot++) {
849
+ const { rig, parts: readers } = slots[slot];
850
+ const lead = readers[0];
851
+ const placement = placements.get(lead);
852
+ for (let pi = 1; pi < readers.length; pi++) {
853
+ const other = readers[pi];
854
+ if (!matricesClose(placement, placements.get(other))) {
855
+ throw new RigRefusal(
856
+ `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}`
857
+ );
944
858
  }
945
859
  }
946
- action.stop();
947
- clipTable.push({
948
- name: clip.name,
949
- startFrame: rowOffset,
950
- frames,
951
- fps: frames / clip.duration,
952
- duration: clip.duration,
953
- maxDelta: Math.sqrt(maxDeltaSq),
954
- ...resolved[ci].defaults
955
- });
956
- rowOffset += frames;
957
- });
958
- } finally {
959
- mixer.stopAllAction();
960
- mixer.setTime(0);
961
- root.updateMatrixWorld(true);
962
- }
860
+ if (rig) {
861
+ _slot.fromArray(rig.pose.matrices, rig.bone * BONE_STRIDE).multiply(rig.bindMatrix).premultiply(placement);
862
+ } else {
863
+ _slot.copy(placement);
864
+ }
865
+ slotMatrices.set(_slot.elements, slot * BONE_STRIDE);
866
+ if (influenced[slot] && (hasNonUniformScale(slotMatrices, slot) || hasShear(slotMatrices, slot))) {
867
+ throw nonUniformScale(slotLabel(slots[slot]), clip);
868
+ }
869
+ const p4 = slot * 4;
870
+ const det = _slot.determinant();
871
+ let scale;
872
+ if (det === 0) {
873
+ _t.setFromMatrixPosition(_slot);
874
+ _q2.fromArray(previous, p4);
875
+ if (_q2.lengthSq() === 0) _q2.identity();
876
+ scale = 0;
877
+ } else {
878
+ if (det < 0) for (const e of MATRIX_BASIS) _slot.elements[e] = -_slot.elements[e];
879
+ _slot.decompose(_t, _q2, _s);
880
+ scale = det < 0 ? -_s.x : _s.x;
881
+ }
882
+ if (previous[p4] * _q2.x + previous[p4 + 1] * _q2.y + previous[p4 + 2] * _q2.z + previous[p4 + 3] * _q2.w < 0) {
883
+ _q2.set(-_q2.x, -_q2.y, -_q2.z, -_q2.w);
884
+ }
885
+ previous[p4] = _q2.x;
886
+ previous[p4 + 1] = _q2.y;
887
+ previous[p4 + 2] = _q2.z;
888
+ previous[p4 + 3] = _q2.w;
889
+ const o = (row * width + slot * RIG_TEXELS_PER_SLOT) * 4;
890
+ const rotation = o + RIG_TEXELS.rotation * 4;
891
+ data[rotation] = _q2.x;
892
+ data[rotation + 1] = _q2.y;
893
+ data[rotation + 2] = _q2.z;
894
+ data[rotation + 3] = _q2.w;
895
+ const placementTexel = o + RIG_TEXELS.placement * 4;
896
+ data[placementTexel] = _t.x;
897
+ data[placementTexel + 1] = _t.y;
898
+ data[placementTexel + 2] = _t.z;
899
+ data[placementTexel + 3] = scale;
900
+ }
901
+ for (let v = 0; v < vertexCount; v++) {
902
+ const o3 = v * 3;
903
+ const o4 = v * 4;
904
+ const x = localPosition[o3];
905
+ const y = localPosition[o3 + 1];
906
+ const z = localPosition[o3 + 2];
907
+ let px = 0;
908
+ let py = 0;
909
+ let pz = 0;
910
+ for (let i = 0; i < 4; i++) {
911
+ const w = skinWeight[o4 + i];
912
+ if (w === 0) continue;
913
+ const m = skinIndex[o4 + i] * BONE_STRIDE;
914
+ px += w * (slotMatrices[m] * x + slotMatrices[m + 4] * y + slotMatrices[m + 8] * z + slotMatrices[m + 12]);
915
+ py += w * (slotMatrices[m + 1] * x + slotMatrices[m + 5] * y + slotMatrices[m + 9] * z + slotMatrices[m + 13]);
916
+ pz += w * (slotMatrices[m + 2] * x + slotMatrices[m + 6] * y + slotMatrices[m + 10] * z + slotMatrices[m + 14]);
917
+ }
918
+ if (px < minX) minX = px;
919
+ if (px > maxX) maxX = px;
920
+ if (py < minY) minY = py;
921
+ if (py > maxY) maxY = py;
922
+ if (pz < minZ) minZ = pz;
923
+ if (pz > maxZ) maxZ = pz;
924
+ const dx = px - restRoot[o3];
925
+ const dy = py - restRoot[o3 + 1];
926
+ const dz = pz - restRoot[o3 + 2];
927
+ const deltaSq = dx * dx + dy * dy + dz * dz;
928
+ if (deltaSq > maxDeltaSq) maxDeltaSq = deltaSq;
929
+ }
930
+ return maxDeltaSq;
931
+ };
932
+ const clipTable = sampleClips(root, resolved, frameCounts, poses, writeFrame);
963
933
  bounds.min.set(minX, minY, minZ);
964
934
  bounds.max.set(maxX, maxY, maxZ);
965
935
  geometry.boundingBox = bounds.clone();
@@ -979,30 +949,18 @@ function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
979
949
  };
980
950
  }
981
951
  function mergeRigGeometry(rigParts, total) {
982
- const parts = rigParts.map((r) => r.part);
983
952
  const position = new Float32Array(total * 3);
984
953
  const normal = new Float32Array(total * 3);
985
954
  const slotIndex = new Uint16Array(total * 4);
986
955
  const slotWeight = new Float32Array(total * 4);
987
- const want = optionalAttributes(parts);
988
- const uv = want.uv ? new Float32Array(total * 2) : null;
989
- const color = want.color ? new Float32Array(total * 3) : null;
990
- const merging = parts.some((p) => p.tint);
991
- const tangent = want.tangent ? new Float32Array(total * 4) : null;
992
956
  for (const { part, slotOf, position: restPos, normal: restNrm } of rigParts) {
993
- const geometry = part.mesh.geometry;
994
957
  const start = part.vertexStart;
995
958
  const srcIndex = part.pose ? part.skinIndex : null;
996
959
  const srcWeight = part.pose ? part.skinWeight : null;
997
- const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
998
- const srcColor = colorSource(part, !!color, merging);
999
- const srcTangent = tangent ? geometry.attributes.tangent : null;
1000
960
  position.set(restPos, start * 3);
1001
961
  normal.set(restNrm, start * 3);
1002
962
  for (let v = 0; v < part.vertexCount; v++) {
1003
- const vi = start + v;
1004
- const o3 = vi * 3;
1005
- const o4 = vi * 4;
963
+ const o4 = (start + v) * 4;
1006
964
  if (srcIndex && srcWeight) {
1007
965
  for (let i = 0; i < 4; i++) {
1008
966
  slotIndex[o4 + i] = slotOf[srcIndex.getComponent(v, i)];
@@ -1012,17 +970,6 @@ function mergeRigGeometry(rigParts, total) {
1012
970
  slotIndex[o4] = slotOf[0];
1013
971
  slotWeight[o4] = 1;
1014
972
  }
1015
- if (uv && srcUV) {
1016
- uv[vi * 2] = srcUV.getX(v);
1017
- uv[vi * 2 + 1] = srcUV.getY(v);
1018
- }
1019
- if (color) writeColor(color, o3, part, srcColor, v);
1020
- if (tangent && srcTangent) {
1021
- tangent[o4] = srcTangent.getX(v);
1022
- tangent[o4 + 1] = srcTangent.getY(v);
1023
- tangent[o4 + 2] = srcTangent.getZ(v);
1024
- tangent[o4 + 3] = srcTangent.getW(v);
1025
- }
1026
973
  }
1027
974
  }
1028
975
  const merged = new BufferGeometry();
@@ -1030,10 +977,7 @@ function mergeRigGeometry(rigParts, total) {
1030
977
  merged.setAttribute("normal", new BufferAttribute(normal, 3));
1031
978
  merged.setAttribute("skinIndex", new BufferAttribute(slotIndex, 4));
1032
979
  merged.setAttribute("skinWeight", new BufferAttribute(slotWeight, 4));
1033
- if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
1034
- if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
1035
- if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
1036
- indexParts(merged, parts);
980
+ mergeSurface(merged, rigParts.map((r) => r.part), total, null);
1037
981
  return merged;
1038
982
  }
1039
983
  var REQUEST = "three-vat:bake";
@@ -1396,9 +1340,11 @@ function recordVAT(vat, stand) {
1396
1340
  encoding: vat.encoding,
1397
1341
  texels: transferable(texture.image.data, true, transfer),
1398
1342
  width: texture.image.width,
1343
+ height: texture.image.height,
1399
1344
  normals: normals ? transferable(normals, true, transfer) : null,
1400
1345
  slotCount: vat.encoding === "rig" ? vat.slotCount : 0,
1401
1346
  fallback: vat.encoding === "delta" ? vat.fallback : null,
1347
+ rowsPerFrame: vat.encoding === "delta" ? vat.rowsPerFrame : 1,
1402
1348
  geometry: recordGeometry(vat.geometry, true, transfer, /* @__PURE__ */ new Map(), []),
1403
1349
  materials: vat.materials.map((m) => stand.get(m)),
1404
1350
  clips: vat.clips,
@@ -1426,17 +1372,18 @@ function rebuildVAT(record, materials) {
1426
1372
  if (record.encoding === "rig") {
1427
1373
  const vat2 = {
1428
1374
  encoding: "rig",
1429
- rigTexture: makeVATTexture(record.texels, record.width, record.totalFrames, FloatType),
1375
+ rigTexture: makeVATTexture(record.texels, record.width, record.height, FloatType),
1430
1376
  slotCount: record.slotCount,
1431
1377
  ...base
1432
1378
  };
1433
1379
  return vat2;
1434
1380
  }
1435
1381
  const vat = {
1436
- positionTexture: makeVATTexture(record.texels, record.width, record.totalFrames, HalfFloatType),
1437
- normalTexture: record.normals ? makeVATNormalTexture(record.normals, record.width, record.totalFrames) : null,
1382
+ positionTexture: makeVATTexture(record.texels, record.width, record.height, HalfFloatType),
1383
+ normalTexture: record.normals ? makeVATNormalTexture(record.normals, record.width, record.height) : null,
1438
1384
  encoding: "delta",
1439
1385
  fallback: record.fallback,
1386
+ rowsPerFrame: record.rowsPerFrame,
1440
1387
  ...base
1441
1388
  };
1442
1389
  return vat;
package/dist/tsl.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { Node } from 'three/webgpu';
2
- import { e as VATClock, k as VATPlaybackTexture, b as VATCarrier, V as VAT, h as VATInstance, f as VATCrowd } from './carrier-D4pkftSk.js';
2
+ import { e as VATClock, k as VATPlaybackTexture, b as VATCarrier, V as VAT, h as VATInstance, f as VATCrowd } from './carrier-VLSJLgjX.js';
3
3
  import 'three';
4
4
 
5
5
  /**
package/dist/tsl.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isBatchedCarrier, assertVATCarrier, assertBakedNormal } from './chunk-I4STYOD5.js';
2
- import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS, RIG_TEXELS, RIG_TEXELS_PER_SLOT } from './chunk-BSAJL5PG.js';
2
+ import { INFINITE_REPETITIONS, LoopMode, EndMode, createVATPlaybackTexture, PACK_TEXELS, RIG_TEXELS, vertexWidthOf, RIG_TEXELS_PER_SLOT } from './chunk-OEKKEHRQ.js';
3
3
  import { InstancedMesh } from 'three';
4
4
  import { uniform, Fn, positionLocal, normalLocal, tangentLocal, positionGeometry, batch, instancedMesh, float, bool, int, vertexIndex, attribute, mat3, tangentGeometry, normalGeometry, vec4, batchIndirectIndex, instanceIndex, hash, mix, textureLoad, ivec2, dot, mat4, vec3 } from 'three/tsl';
5
5
 
@@ -176,17 +176,34 @@ function octDecode(stored) {
176
176
  const back = (component) => component.sub(component.greaterThanEqual(0).select(overshoot, overshoot.negate()));
177
177
  return vec3(back(ex), back(ey), z).normalize();
178
178
  }
179
- function vertexDecode({ positionTexture, normalTexture }, rows) {
179
+ function texelOf(vat, vertexRow) {
180
+ const { rowsPerFrame } = vat;
181
+ if (rowsPerFrame === 1) return { column: vertexRow, rowOf: (row) => row };
182
+ const width = int(vertexWidthOf(vat));
183
+ const line = vertexRow.div(width);
184
+ const spanned = /* @__PURE__ */ new Map();
185
+ return {
186
+ column: vertexRow.mod(width),
187
+ rowOf: (row) => {
188
+ let at = spanned.get(row);
189
+ if (!at) spanned.set(row, at = row.mul(int(rowsPerFrame)).add(line));
190
+ return at;
191
+ }
192
+ };
193
+ }
194
+ function vertexDecode(vat, rows) {
195
+ const { positionTexture, normalTexture } = vat;
180
196
  const vertexRow = int(vertexIndex);
197
+ const { column, rowOf } = texelOf(vat, vertexRow);
181
198
  const band = (tex, of) => {
182
- const s0 = textureLoad(tex, ivec2(vertexRow, of.row0)).xyz;
183
- const s1 = textureLoad(tex, ivec2(vertexRow, of.row1)).xyz;
199
+ const s0 = textureLoad(tex, ivec2(column, rowOf(of.row0))).xyz;
200
+ const s1 = textureLoad(tex, ivec2(column, rowOf(of.row1))).xyz;
184
201
  return mix(s0, s1, of.blend);
185
202
  };
186
203
  const sample = (tex) => mix(band(tex, rows.live), band(tex, rows.outgoing), rows.weight);
187
204
  const bandNormal = (tex, of) => {
188
- const s0 = octDecode(textureLoad(tex, ivec2(vertexRow, of.row0)).xy);
189
- const s1 = octDecode(textureLoad(tex, ivec2(vertexRow, of.row1)).xy);
205
+ const s0 = octDecode(textureLoad(tex, ivec2(column, rowOf(of.row0))).xy);
206
+ const s1 = octDecode(textureLoad(tex, ivec2(column, rowOf(of.row1))).xy);
190
207
  return mix(s0, s1, of.blend);
191
208
  };
192
209
  const sampleNormal = (tex) => mix(bandNormal(tex, rows.live), bandNormal(tex, rows.outgoing), rows.weight);
package/dist/webgl.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IUniform, MeshDepthMaterial, WebGLRenderer, Material } from 'three';
2
- import { b as VATCarrier, V as VAT, k as VATPlaybackTexture, h as VATInstance, f as VATCrowd } from './carrier-D4pkftSk.js';
2
+ import { b as VATCarrier, V as VAT, k as VATPlaybackTexture, h as VATInstance, f as VATCrowd } from './carrier-VLSJLgjX.js';
3
3
 
4
4
  /**
5
5
  * The real maximum texture dimension this GPU accepts, for
package/dist/webgl.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { assertBakedNormal, assertVATCarrier, isBatchedCarrier } from './chunk-I4STYOD5.js';
2
- import { PACK_TEXELS, EndMode, LoopMode, RIG_TEXELS_PER_SLOT, RIG_TEXELS, INFINITE_REPETITIONS, createVATPlaybackTexture } from './chunk-BSAJL5PG.js';
2
+ import { PACK_TEXELS, EndMode, LoopMode, RIG_TEXELS_PER_SLOT, RIG_TEXELS, INFINITE_REPETITIONS, createVATPlaybackTexture, vertexWidthOf } from './chunk-OEKKEHRQ.js';
3
3
  import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial, Material } from 'three';
4
4
 
5
5
  function getMaxTextureSize(renderer) {
@@ -175,7 +175,13 @@ var ROW_PRELUDE = (
175
175
  }
176
176
  `
177
177
  );
178
- var VERTEX_PRELUDE = (
178
+ function texelOf(vat) {
179
+ const { rowsPerFrame } = vat;
180
+ if (rowsPerFrame === 1) return (row) => `ivec2( gl_VertexID, ${row} )`;
181
+ const width = vertexWidthOf(vat);
182
+ return (row) => `ivec2( gl_VertexID % ${width}, ${row} * ${rowsPerFrame} + gl_VertexID / ${width} )`;
183
+ }
184
+ var vertexPrelude = (texel) => (
179
185
  /* glsl */
180
186
  `
181
187
  uniform highp sampler2D uVatPosTex;
@@ -184,8 +190,8 @@ var VERTEX_PRELUDE = (
184
190
  // mixed. The same function for the live band and the outgoing one, as
185
191
  // vatBand is the same function for both pairs.
186
192
  vec3 vatBandSample( const in sampler2D tex, const in VatBand band ) {
187
- vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, band.row0 ), 0 ).xyz;
188
- vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, band.row1 ), 0 ).xyz;
193
+ vec3 s0 = texelFetch( tex, ${texel("band.row0")}, 0 ).xyz;
194
+ vec3 s1 = texelFetch( tex, ${texel("band.row1")}, 0 ).xyz;
189
195
  return mix( s0, s1, band.blend );
190
196
  }
191
197
 
@@ -208,7 +214,7 @@ var VERTEX_PRELUDE = (
208
214
  }
209
215
  `
210
216
  );
211
- var NORMAL_PRELUDE = (
217
+ var normalPrelude = (texel) => (
212
218
  /* glsl */
213
219
  `
214
220
  uniform highp sampler2D uVatNrmTex;
@@ -226,8 +232,8 @@ var NORMAL_PRELUDE = (
226
232
 
227
233
  // One band of the normal layer \u2014 vatBandSample, over decoded normals.
228
234
  vec3 vatBandSampleNormal( const in VatBand band ) {
229
- vec3 s0 = vatOctDecode( texelFetch( uVatNrmTex, ivec2( gl_VertexID, band.row0 ), 0 ).xy );
230
- vec3 s1 = vatOctDecode( texelFetch( uVatNrmTex, ivec2( gl_VertexID, band.row1 ), 0 ).xy );
235
+ vec3 s0 = vatOctDecode( texelFetch( uVatNrmTex, ${texel("band.row0")}, 0 ).xy );
236
+ vec3 s1 = vatOctDecode( texelFetch( uVatNrmTex, ${texel("band.row1")}, 0 ).xy );
231
237
  return mix( s0, s1, band.blend );
232
238
  }
233
239
 
@@ -379,9 +385,12 @@ var rigNormal = (id) => (
379
385
  #endif
380
386
  `
381
387
  );
382
- function vertexDecode({ positionTexture, normalTexture }, id) {
388
+ function vertexDecode(vat, id) {
389
+ const { positionTexture, normalTexture, rowsPerFrame } = vat;
390
+ const texel = texelOf(vat);
391
+ const span = rowsPerFrame === 1 ? "" : `:rows${rowsPerFrame}x${vertexWidthOf(vat)}`;
383
392
  return {
384
- prelude: (normalTexture ? NORMAL_PRELUDE : "") + VERTEX_PRELUDE,
393
+ prelude: (normalTexture ? normalPrelude(texel) : "") + vertexPrelude(texel),
385
394
  bind(uniforms) {
386
395
  uniforms.uVatPosTex = { value: positionTexture };
387
396
  if (normalTexture) uniforms.uVatNrmTex = { value: normalTexture };
@@ -394,7 +403,7 @@ function vertexDecode({ positionTexture, normalTexture }, id) {
394
403
  normal: normalTexture ? vertexNormal(id) : null,
395
404
  // A normal-less VAT injects a different vertex shader off the same material
396
405
  // parameters, so the variant is in the key (see `patchVATMaterial`).
397
- key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}`,
406
+ key: `three-vat:${id}${normalTexture ? "" : ":no-normal"}${span}`,
398
407
  instanceIndex: INSTANCE_ID[id]
399
408
  };
400
409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-vat",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
4
4
  "description": "Bake glTF animation clips into vertex animation textures for zero-CPU instanced crowds in three.js.",
5
5
  "type": "module",
6
6
  "license": "MIT",