three-vat 0.1.0 → 1.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,24 +1,152 @@
1
- import { FloatType, AnimationMixer, Box3, Vector4, Matrix4, Vector3, DataTexture, RGBAFormat, NearestFilter, DataUtils, HalfFloatType } from 'three';
1
+ export { addVATInstanceAttributes } from './chunk-SXTVASKG.js';
2
+ import { FloatType, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, DataTexture, RGBAFormat, NearestFilter, BufferGeometry, BufferAttribute } from 'three';
2
3
 
3
- // src/bake.ts
4
4
  var MAX_TEXTURE_SIZE = 16384;
5
- function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
6
- const geometry = skinnedMesh.geometry;
7
- const vertexCount = geometry.attributes.position.count;
8
- if (vertexCount > MAX_TEXTURE_SIZE) {
5
+ function asAttribute(value, mesh, name) {
6
+ if (!(value instanceof BufferAttribute)) {
9
7
  throw new Error(
10
- `three-vat: vertexCount ${vertexCount} exceeds MAX_TEXTURE_SIZE ${MAX_TEXTURE_SIZE}; row wrapping is not implemented`
8
+ `three-vat: mesh "${mesh.name || "(unnamed)"}" has an interleaved or unsupported "${name}" attribute; VAT bakes plain BufferAttributes`
9
+ );
10
+ }
11
+ return value;
12
+ }
13
+ function collectParts(root) {
14
+ const found = [];
15
+ root.traverse((o) => {
16
+ const mesh = o;
17
+ if (mesh.isMesh && mesh.geometry) found.push(mesh);
18
+ });
19
+ if (found.length === 0) {
20
+ throw new Error("three-vat: no Mesh found under root; nothing to bake");
21
+ }
22
+ const materials = [];
23
+ const parts = [];
24
+ for (const mesh of found) {
25
+ if (Array.isArray(mesh.material)) {
26
+ throw new Error(
27
+ `three-vat: mesh "${mesh.name || "(unnamed)"}" uses a material array; split it into one mesh per material before baking`
28
+ );
29
+ }
30
+ const geometry = mesh.geometry;
31
+ if (!geometry.attributes.normal) geometry.computeVertexNormals();
32
+ const material = mesh.material;
33
+ let materialIndex = materials.indexOf(material);
34
+ if (materialIndex === -1) materialIndex = materials.push(material) - 1;
35
+ const skinned = mesh;
36
+ parts.push({
37
+ mesh,
38
+ material,
39
+ materialIndex,
40
+ vertexStart: 0,
41
+ // assigned below, after material sorting
42
+ vertexCount: geometry.attributes.position.count,
43
+ basePos: asAttribute(geometry.attributes.position, mesh, "position"),
44
+ baseNrm: asAttribute(geometry.attributes.normal, mesh, "normal"),
45
+ isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
46
+ skeleton: skinned.skeleton,
47
+ skinIndex: geometry.attributes.skinIndex,
48
+ skinWeight: geometry.attributes.skinWeight,
49
+ morphPos: geometry.morphAttributes.position,
50
+ morphNrm: geometry.morphAttributes.normal,
51
+ morphRelative: geometry.morphTargetsRelative
52
+ });
53
+ }
54
+ parts.sort((a, b) => a.materialIndex - b.materialIndex);
55
+ let offset = 0;
56
+ for (const part of parts) {
57
+ part.vertexStart = offset;
58
+ offset += part.vertexCount;
59
+ }
60
+ return parts;
61
+ }
62
+ function mergeGeometry(parts, restMatrices, total) {
63
+ const position = new Float32Array(total * 3);
64
+ 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 = [];
71
+ const _v = new Vector3();
72
+ const _n = new Vector3();
73
+ parts.forEach((part, pi) => {
74
+ const m = restMatrices[pi];
75
+ const geometry = part.mesh.geometry;
76
+ const start = part.vertexStart;
77
+ const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
78
+ const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
79
+ for (let v = 0; v < part.vertexCount; v++) {
80
+ _v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
81
+ _n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
82
+ const o3 = (start + v) * 3;
83
+ position[o3] = _v.x;
84
+ position[o3 + 1] = _v.y;
85
+ position[o3 + 2] = _v.z;
86
+ normal[o3] = _n.x;
87
+ normal[o3 + 1] = _n.y;
88
+ normal[o3 + 2] = _n.z;
89
+ if (uv && srcUV) {
90
+ uv[(start + v) * 2] = srcUV.getX(v);
91
+ uv[(start + v) * 2 + 1] = srcUV.getY(v);
92
+ }
93
+ if (color && srcColor) {
94
+ color[o3] = srcColor.getX(v);
95
+ color[o3 + 1] = srcColor.getY(v);
96
+ color[o3 + 2] = srcColor.getZ(v);
97
+ }
98
+ }
99
+ const groupStart = indices.length;
100
+ const index = geometry.index;
101
+ if (index) {
102
+ for (let i = 0; i < index.count; i++) indices.push(start + index.getX(i));
103
+ } else {
104
+ for (let v = 0; v < part.vertexCount; v++) indices.push(start + v);
105
+ }
106
+ const last = groups[groups.length - 1];
107
+ if (last && last.materialIndex === part.materialIndex) {
108
+ last.count += indices.length - groupStart;
109
+ } else {
110
+ groups.push({
111
+ start: groupStart,
112
+ count: indices.length - groupStart,
113
+ materialIndex: part.materialIndex
114
+ });
115
+ }
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));
122
+ merged.setIndex(indices);
123
+ for (const g of groups) merged.addGroup(g.start, g.count, g.materialIndex);
124
+ return merged;
125
+ }
126
+ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } = {}) {
127
+ root.updateMatrixWorld(true);
128
+ const parts = collectParts(root);
129
+ const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
130
+ if (vertexCount > maxTextureSize) {
131
+ throw new Error(
132
+ `three-vat: vertexCount ${vertexCount} exceeds maxTextureSize ${maxTextureSize}; row wrapping is not implemented`
11
133
  );
12
134
  }
13
135
  const frameCounts = clips.map((c) => Math.max(2, Math.round(c.duration * fps)));
14
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
+ }
142
+ const rootInverse = root.matrixWorld.clone().invert();
143
+ const restMatrices = parts.map(
144
+ (p) => new Matrix4().multiplyMatrices(rootInverse, p.mesh.matrixWorld)
145
+ );
146
+ const geometry = mergeGeometry(parts, restMatrices, vertexCount);
147
+ const mergedBase = geometry.attributes.position;
15
148
  const posData = new Float32Array(vertexCount * totalFrames * 4);
16
149
  const nrmData = new Float32Array(vertexCount * totalFrames * 4);
17
- const basePos = geometry.attributes.position;
18
- const baseNrm = geometry.attributes.normal;
19
- const skinIndex = geometry.attributes.skinIndex;
20
- const skinWeight = geometry.attributes.skinWeight;
21
- const skeleton = skinnedMesh.skeleton;
22
150
  const mixer = new AnimationMixer(root);
23
151
  const bounds = new Box3();
24
152
  const clipTable = [];
@@ -27,9 +155,15 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
27
155
  const _bone = new Matrix4();
28
156
  const _acc = new Matrix4();
29
157
  const _skin = new Matrix4();
158
+ const _partMatrix = new Matrix4();
30
159
  const _p = new Vector3();
31
160
  const _bp = new Vector3();
32
161
  const _n = new Vector3();
162
+ const _mt = new Vector3();
163
+ const _mb = new Vector3();
164
+ const _mbn = new Vector3();
165
+ const influencers = parts.filter((p) => p.isSkinned && p.skeleton).map(influencedBones);
166
+ let warnedNonUniformScale = false;
33
167
  let rowOffset = 0;
34
168
  clips.forEach((clip, ci) => {
35
169
  const frames = frameCounts[ci];
@@ -40,37 +174,76 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
40
174
  mixer.setTime(f / frames * clip.duration);
41
175
  root.updateMatrixWorld(true);
42
176
  const row = rowOffset + f;
43
- for (let v = 0; v < vertexCount; v++) {
44
- _si.fromBufferAttribute(skinIndex, v);
45
- _sw.fromBufferAttribute(skinWeight, v);
46
- _acc.elements.fill(0);
47
- for (let i = 0; i < 4; i++) {
48
- const w = _sw.getComponent(i);
49
- if (w === 0) continue;
50
- const bi = _si.getComponent(i);
51
- _bone.multiplyMatrices(skeleton.bones[bi].matrixWorld, skeleton.boneInverses[bi]);
52
- const ae = _acc.elements;
53
- const be = _bone.elements;
54
- for (let e = 0; e < 16; e++) ae[e] += be[e] * w;
177
+ if (!warnedNonUniformScale) {
178
+ warnedNonUniformScale = warnOnNonUniformBoneScale(influencers, _bone);
179
+ }
180
+ for (const part of parts) {
181
+ _partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
182
+ 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;
185
+ for (let v = 0; v < part.vertexCount; v++) {
186
+ const vi = part.vertexStart + v;
187
+ _p.fromBufferAttribute(part.basePos, v);
188
+ _n.fromBufferAttribute(part.baseNrm, v);
189
+ if (influences && morphCount > 0) {
190
+ if (!morphRelative) {
191
+ _mb.fromBufferAttribute(part.basePos, v);
192
+ _mbn.fromBufferAttribute(part.baseNrm, v);
193
+ }
194
+ for (let t = 0; t < morphCount; t++) {
195
+ const w = influences[t];
196
+ if (w === 0) continue;
197
+ const targetPos = morphPos?.[t];
198
+ if (targetPos) {
199
+ _mt.fromBufferAttribute(targetPos, v);
200
+ if (!morphRelative) _mt.sub(_mb);
201
+ _p.addScaledVector(_mt, w);
202
+ }
203
+ const targetNrm = morphNrm?.[t];
204
+ if (targetNrm) {
205
+ _mt.fromBufferAttribute(targetNrm, v);
206
+ if (!morphRelative) _mt.sub(_mbn);
207
+ _n.addScaledVector(_mt, w);
208
+ }
209
+ }
210
+ }
211
+ if (isSkinned && skeleton) {
212
+ const skinned = part.mesh;
213
+ _si.fromBufferAttribute(skinIndex, v);
214
+ _sw.fromBufferAttribute(skinWeight, v);
215
+ _acc.elements.fill(0);
216
+ for (let i = 0; i < 4; i++) {
217
+ const w = _sw.getComponent(i);
218
+ 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;
224
+ }
225
+ _skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
226
+ _p.applyMatrix4(_skin);
227
+ _n.transformDirection(_skin);
228
+ }
229
+ _p.applyMatrix4(_partMatrix);
230
+ _n.transformDirection(_partMatrix);
231
+ bounds.expandByPoint(_p);
232
+ _bp.fromBufferAttribute(mergedBase, vi);
233
+ const o = (row * vertexCount + vi) * 4;
234
+ const dx = _p.x - _bp.x;
235
+ const dy = _p.y - _bp.y;
236
+ const dz = _p.z - _bp.z;
237
+ maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
238
+ posData[o] = dx;
239
+ posData[o + 1] = dy;
240
+ posData[o + 2] = dz;
241
+ 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;
55
246
  }
56
- _skin.multiplyMatrices(_acc, skinnedMesh.bindMatrix).premultiply(skinnedMesh.bindMatrixInverse);
57
- _bp.fromBufferAttribute(basePos, v);
58
- _p.copy(_bp).applyMatrix4(_skin);
59
- bounds.expandByPoint(_p);
60
- _n.fromBufferAttribute(baseNrm, v).transformDirection(_skin);
61
- const o = (row * vertexCount + v) * 4;
62
- const dx = _p.x - _bp.x;
63
- const dy = _p.y - _bp.y;
64
- const dz = _p.z - _bp.z;
65
- maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
66
- posData[o] = dx;
67
- posData[o + 1] = dy;
68
- posData[o + 2] = dz;
69
- posData[o + 3] = 1;
70
- nrmData[o] = _n.x;
71
- nrmData[o + 1] = _n.y;
72
- nrmData[o + 2] = _n.z;
73
- nrmData[o + 3] = 1;
74
247
  }
75
248
  }
76
249
  action.stop();
@@ -87,6 +260,10 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
87
260
  mixer.stopAllAction();
88
261
  mixer.setTime(0);
89
262
  root.updateMatrixWorld(true);
263
+ geometry.boundingBox = bounds.clone();
264
+ geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
265
+ const materials = [];
266
+ for (const part of parts) materials[part.materialIndex] = part.material;
90
267
  return {
91
268
  positionTexture: makeVATTexture(posData, vertexCount, totalFrames),
92
269
  normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames),
@@ -94,9 +271,44 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
94
271
  bounds,
95
272
  vertexCount,
96
273
  totalFrames,
97
- encoding: "delta"
274
+ encoding: "delta",
275
+ geometry,
276
+ materials
98
277
  };
99
278
  }
279
+ function influencedBones(part) {
280
+ const used = /* @__PURE__ */ new Set();
281
+ const index = part.skinIndex;
282
+ const weight = part.skinWeight;
283
+ for (let v = 0; v < part.vertexCount; v++) {
284
+ for (let i = 0; i < 4; i++) {
285
+ if (weight.getComponent(v, i) !== 0) used.add(index.getComponent(v, i));
286
+ }
287
+ }
288
+ return { skeleton: part.skeleton, bones: [...used] };
289
+ }
290
+ 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];
296
+ const max = Math.max(x, y, z);
297
+ return max - Math.min(x, y, z) > SCALE_UNIFORMITY_EPSILON * max;
298
+ }
299
+ function warnOnNonUniformBoneScale(influencers, scratch) {
300
+ for (const { skeleton, bones } of influencers) {
301
+ for (const b of bones) {
302
+ scratch.multiplyMatrices(skeleton.bones[b].matrixWorld, skeleton.boneInverses[b]);
303
+ if (!hasNonUniformScale(scratch)) continue;
304
+ 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.`
306
+ );
307
+ return true;
308
+ }
309
+ }
310
+ return false;
311
+ }
100
312
  function makeVATTexture(data, width, height, type = FloatType) {
101
313
  const tex = new DataTexture(data, width, height, RGBAFormat, type);
102
314
  tex.minFilter = NearestFilter;
@@ -105,50 +317,5 @@ function makeVATTexture(data, width, height, type = FloatType) {
105
317
  tex.needsUpdate = true;
106
318
  return tex;
107
319
  }
108
- function serializeVAT(vat, { precision = "float16" } = {}) {
109
- const pos = vat.positionTexture.image.data;
110
- const nrm = vat.normalTexture.image.data;
111
- const manifest = {
112
- version: 1,
113
- vertexCount: vat.vertexCount,
114
- totalFrames: vat.totalFrames,
115
- encoding: vat.encoding,
116
- precision,
117
- clips: vat.clips,
118
- bounds: {
119
- min: [vat.bounds.min.x, vat.bounds.min.y, vat.bounds.min.z],
120
- max: [vat.bounds.max.x, vat.bounds.max.y, vat.bounds.max.z]
121
- }
122
- };
123
- return { manifest, position: encode(pos, precision), normal: encode(nrm, precision) };
124
- }
125
- function encode(src, precision) {
126
- if (precision === "float32") {
127
- return src.slice().buffer;
128
- }
129
- const half = new Uint16Array(src.length);
130
- for (let i = 0; i < src.length; i++) half[i] = DataUtils.toHalfFloat(src[i]);
131
- return half.buffer;
132
- }
133
- function loadVAT(serialized) {
134
- const { manifest, position, normal } = serialized;
135
- const { vertexCount, totalFrames, precision } = manifest;
136
- const type = precision === "float32" ? FloatType : HalfFloatType;
137
- const posData = precision === "float32" ? new Float32Array(position) : new Uint16Array(position);
138
- const nrmData = precision === "float32" ? new Float32Array(normal) : new Uint16Array(normal);
139
- const bounds = new Box3(
140
- new Vector3().fromArray(manifest.bounds.min),
141
- new Vector3().fromArray(manifest.bounds.max)
142
- );
143
- return {
144
- positionTexture: makeVATTexture(posData, vertexCount, totalFrames, type),
145
- normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames, type),
146
- clips: manifest.clips,
147
- bounds,
148
- vertexCount,
149
- totalFrames,
150
- encoding: manifest.encoding
151
- };
152
- }
153
320
 
154
- export { MAX_TEXTURE_SIZE, bakeVAT, loadVAT, makeVATTexture, serializeVAT };
321
+ export { MAX_TEXTURE_SIZE, bakeVAT, makeVATTexture };
@@ -0,0 +1,109 @@
1
+ import { DataTexture, BufferGeometry, Material, Box3, InstancedMesh } from 'three';
2
+
3
+ /** One baked animation range within a VAT's stacked frame rows. */
4
+ interface VATClip {
5
+ /** Clip name, taken from the source `AnimationClip`. */
6
+ name: string;
7
+ /** First frame row (y) of this clip in the texture. */
8
+ startFrame: number;
9
+ /** Number of frame rows baked for this clip. */
10
+ frames: number;
11
+ /** Effective frames-per-second of the bake (`frames / duration`). */
12
+ fps: number;
13
+ /** Source clip duration in seconds. */
14
+ duration: number;
15
+ /**
16
+ * Largest per-vertex position-delta magnitude (metres) across the clip.
17
+ * Near-zero means the clip baked as a frozen pose — the diagnostic for a
18
+ * mis-targeted or genuinely static clip.
19
+ */
20
+ maxDelta: number;
21
+ }
22
+ /**
23
+ * A baked Vertex Animation Texture: the position/normal `DataTexture`s, the
24
+ * geometry they are indexed by, and the clip table and bounds needed to decode
25
+ * and render them. Produced exactly one way — {@link bakeVAT}, at runtime, from
26
+ * a loaded glTF (ADR-0010).
27
+ *
28
+ * The merged vertex ordering is the baker's own invention and the textures are
29
+ * indexed by it (`x = gl_VertexID`), so the caller cannot bring its own
30
+ * geometry — it must render the one baked here. `materials` is ordered to match
31
+ * `geometry.groups[].materialIndex`, giving one draw call per material.
32
+ */
33
+ interface VAT {
34
+ /** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
35
+ positionTexture: DataTexture;
36
+ /** RGBA float texture of per-vertex absolute normals (`x = vertex`, `y = frame`). */
37
+ normalTexture: DataTexture;
38
+ /** Merged, root-space rest-pose geometry. Its `position` is the delta reference. */
39
+ geometry: BufferGeometry;
40
+ /** Source materials, indexed by `geometry.groups[].materialIndex`. */
41
+ materials: Material[];
42
+ /** Clip table: name → `{ startFrame, frames, fps, ... }`. */
43
+ clips: VATClip[];
44
+ /** Union of every baked frame's bounds; use as the geometry bounding box. */
45
+ bounds: Box3;
46
+ /** Vertex count (texture width). */
47
+ vertexCount: number;
48
+ /** Total frame rows across all clips (texture height). */
49
+ totalFrames: number;
50
+ /** Position encoding. Only `'delta'` in v1. */
51
+ encoding: 'delta';
52
+ }
53
+ /**
54
+ * The shared playback clock: one `{ value }` in seconds, read by every material
55
+ * of every VAT mesh driven by it. Set it once per frame. Deliberately the
56
+ * narrowest shape both decode paths satisfy — a WebGL `IUniform<number>` and a
57
+ * TSL uniform node are both one of these — so `createVATMesh` returns the same
58
+ * thing on either renderer.
59
+ */
60
+ interface VATClock {
61
+ value: number;
62
+ }
63
+ /**
64
+ * A **crowd** ready to render: the mesh to add to the scene, and the clock to
65
+ * advance. What `createVATMesh` returns on either decode path, so moving a
66
+ * crowd between renderers is an import change and nothing else. Named for what
67
+ * it is rather than for its `mesh` field — the clock is half of it.
68
+ */
69
+ interface VATCrowd {
70
+ /** Add to the scene. Its instance matrices are yours to write. */
71
+ mesh: InstancedMesh;
72
+ /** The shared playback clock — set `.value` once per frame. */
73
+ time: VATClock;
74
+ }
75
+
76
+ /** Per-instance playback state consumed by both decode paths. */
77
+ interface VATInstance {
78
+ clip: Pick<VAT['clips'][number], 'startFrame' | 'frames' | 'fps'>;
79
+ /** Phase offset in seconds — desyncs the crowd. */
80
+ timeOffset: number;
81
+ /** Playback rate multiplier. */
82
+ speed: number;
83
+ }
84
+ /**
85
+ * Attach the instance-playback attributes to an instanced geometry. Call once
86
+ * before rendering, on the geometry you hand to the `InstancedMesh`.
87
+ *
88
+ * The attribute names and layout below are the shared contract, spelled once in
89
+ * {@link PLAYBACK_ATTRIBUTES}. Both decode paths read exactly these five —
90
+ * `DECODE_PRELUDE` in `src/webgl.ts` as GLSL attributes, `vatNodes` in
91
+ * `src/tsl.ts` as TSL attribute nodes, when it is handed this geometry.
92
+ *
93
+ * | Attribute | Type | Source |
94
+ * | ------------- | ------------- | --------------------- |
95
+ * | `aClipStart` | `float` (x 1) | `instance.clip.startFrame` — first texture row of the clip's frame band |
96
+ * | `aClipFrames` | `float` (x 1) | `instance.clip.frames` — rows in the band |
97
+ * | `aClipFps` | `float` (x 1) | `instance.clip.fps` — with `frames`, the clip's duration |
98
+ * | `aTimeOffset` | `float` (x 1) | `instance.timeOffset` — phase, in seconds |
99
+ * | `aSpeed` | `float` (x 1) | `instance.speed` — rate multiplier |
100
+ *
101
+ * Every entry is a one-component `InstancedBufferAttribute` of `Float32Array`,
102
+ * one element per instance, in instance order. Adding a field — crossfade's
103
+ * reserved second clip index being the known case (ADR-0007) — means adding it
104
+ * here, in the table above, and in each decode path's own attribute
105
+ * declarations.
106
+ */
107
+ declare function addVATInstanceAttributes(geometry: BufferGeometry, instances: VATInstance[]): void;
108
+
109
+ export { type VAT as V, type VATClip as a, type VATClock as b, type VATCrowd as c, type VATInstance as d, addVATInstanceAttributes as e };