three-vat 1.0.1 → 2.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/webgl.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { IUniform, MeshDepthMaterial, WebGLRenderer, Material } from 'three';
2
- import { d as VATInstance$1, e as addVATInstanceAttributes, V as VAT, c as VATCrowd } from './instance-playback-BrGBIKLe.js';
2
+ import { V as VAT, i as VATPlaybackTexture, a as VATCarrier, h as VATInstance, e as VATCrowd } from './carrier-BFCPmcQK.js';
3
3
 
4
4
  /**
5
5
  * The real maximum texture dimension this GPU accepts, for
@@ -16,34 +16,36 @@ interface VATUniforms {
16
16
  }
17
17
  /** Create the shared time uniform. Update `uVatTime.value` once per frame. */
18
18
  declare function createVATUniforms(time?: number): VATUniforms;
19
- /**
20
- * The instance-playback contract now lives in the core entry point, so both
21
- * decode paths can read it (ADR-0009).
22
- *
23
- * @deprecated Renamed to `addVATInstanceAttributes` and moved to `three-vat`.
24
- * Removed from `three-vat/webgl` in the next minor version — import it from
25
- * `three-vat` instead.
26
- */
27
- declare const addInstancedVATAttributes: typeof addVATInstanceAttributes;
28
- /**
29
- * @deprecated Moved to `three-vat`. Removed from `three-vat/webgl` in the next
30
- * minor version — import `VATInstance` from `three-vat` instead.
31
- */
32
- type VATInstance = VATInstance$1;
33
19
  /**
34
20
  * Patch any built-in material so its vertex stage samples the VAT instead of
35
21
  * skinning. Works on the render material and on `MeshDepthMaterial` (needed for
36
22
  * instanced shadows — see {@link createVATDepthMaterial}). Mutates and returns
37
23
  * the material.
24
+ *
25
+ * `playback` is the crowd's playback texture — the decode reads the pack out of
26
+ * it by this instance's logical index, so a material patched for one crowd
27
+ * renders that crowd's playback and no other's.
28
+ *
29
+ * `carrier` is the mesh this material will draw on, and it is needed for one
30
+ * reason: how the shader names that logical index. Omit it for an
31
+ * `InstancedMesh`, where the index is `gl_InstanceID`. Pass a `BatchedMesh` and
32
+ * the decode resolves the index through `getIndirectIndex( gl_DrawID )`
33
+ * instead, because that carrier culls and sorts per instance and its drawn slot
34
+ * is a permutation that changes every frame (ADR-0016). A batch a VAT cannot be
35
+ * decoded on is refused here rather than rendered wrong.
38
36
  */
39
- declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uniforms: VATUniforms): T;
37
+ declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, carrier?: VATCarrier): T;
40
38
  /**
41
- * Build the `customDepthMaterial` an `InstancedMesh` needs so a VAT crowd casts
39
+ * Build the `customDepthMaterial` a VAT crowd needs so it casts
42
40
  * correctly-deformed shadows instead of bind-pose shadows. Assign the result to
43
41
  * `mesh.customDepthMaterial` (and, for point lights, mirror with a patched
44
42
  * `MeshDistanceMaterial`).
43
+ *
44
+ * `carrier` means what it means in {@link patchVATMaterial}: omit it for an
45
+ * `InstancedMesh`, pass the `BatchedMesh` for a batched crowd, so the shadow
46
+ * pass resolves the same instance index the render pass does.
45
47
  */
46
- declare function createVATDepthMaterial(vat: VAT, uniforms: VATUniforms): MeshDepthMaterial;
48
+ declare function createVATDepthMaterial(vat: VAT, uniforms: VATUniforms, playback: VATPlaybackTexture, carrier?: VATCarrier): MeshDepthMaterial;
47
49
  /** Options for {@link createVATMesh}. */
48
50
  interface CreateVATMeshOptions {
49
51
  /**
@@ -56,12 +58,12 @@ interface CreateVATMeshOptions {
56
58
  }
57
59
  /**
58
60
  * Turn a baked VAT and a list of instances into a crowd ready to render: an
59
- * `InstancedMesh` whose geometry carries the instance-playback contract, whose
60
- * materials decode the VAT, and whose shadows are deformed rather than frozen
61
- * in the bind pose.
61
+ * `InstancedMesh` rendering the bake's geometry, a playback texture carrying
62
+ * the instance-playback contract, materials that decode the VAT, and shadows
63
+ * that are deformed rather than frozen in the bind pose.
62
64
  *
63
65
  * ```ts
64
- * const { mesh, time } = createVATMesh(vat, instances)
66
+ * const { mesh, time, playback } = createVATMesh(vat, instances)
65
67
  * mesh.castShadow = mesh.receiveShadow = true
66
68
  * scene.add(mesh)
67
69
  * // per frame:
@@ -78,11 +80,14 @@ interface CreateVATMeshOptions {
78
80
  * - **`castShadow` / `receiveShadow`**, which are scene decisions. The depth and
79
81
  * distance materials the shadow passes need are already attached either way.
80
82
  *
81
- * Everything here is the exported primitives — {@link addVATInstanceAttributes},
83
+ * Everything here is the exported primitives — `createVATPlaybackTexture`,
82
84
  * {@link patchVATMaterial}, {@link createVATDepthMaterial} — composed in the one
83
85
  * order that is correct. Reach for them directly only when rendering onto
84
- * something other than a plain `InstancedMesh`.
86
+ * something other than a plain `InstancedMesh` — a `BatchedMesh`, for three's
87
+ * own per-instance culling and sorting, being the other carrier this library
88
+ * supports (docs/usage.md); the returned `playback` is what
89
+ * {@link setVATInstance} writes into either way.
85
90
  */
86
- declare function createVATMesh(vat: VAT, instances: VATInstance$1[], options?: CreateVATMeshOptions): VATCrowd;
91
+ declare function createVATMesh(vat: VAT, instances: VATInstance[], options?: CreateVATMeshOptions): VATCrowd;
87
92
 
88
- export { type CreateVATMeshOptions, type VATInstance, type VATUniforms, addInstancedVATAttributes, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
93
+ export { type CreateVATMeshOptions, type VATUniforms, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
package/dist/webgl.js CHANGED
@@ -1,4 +1,5 @@
1
- import { addVATInstanceAttributes, createCrowdGeometry } from './chunk-SXTVASKG.js';
1
+ import { assertBakedNormal, assertVATCarrier, isBatchedCarrier } from './chunk-W2ZAFMPB.js';
2
+ import { PACK_TEXELS, EndMode, LoopMode, INFINITE_REPETITIONS, createVATPlaybackTexture } from './chunk-2PGZ44TP.js';
2
3
  import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
3
4
 
4
5
  function getMaxTextureSize(renderer) {
@@ -7,67 +8,168 @@ function getMaxTextureSize(renderer) {
7
8
  function createVATUniforms(time = 0) {
8
9
  return { uVatTime: { value: time } };
9
10
  }
10
- var addInstancedVATAttributes = addVATInstanceAttributes;
11
+ var glslFloat = (n) => n.toFixed(1);
11
12
  var DECODE_PRELUDE = (
12
13
  /* glsl */
13
14
  `
14
15
  uniform highp sampler2D uVatPosTex;
15
- uniform highp sampler2D uVatNrmTex;
16
+ uniform highp sampler2D uVatPlaybackTex;
16
17
  uniform float uVatTime;
17
- attribute float aClipStart;
18
- attribute float aClipFrames;
19
- attribute float aClipFps;
20
- attribute float aTimeOffset;
21
- attribute float aSpeed;
22
- vec3 vatSample( const in sampler2D tex ) {
23
- float duration = aClipFrames / aClipFps;
24
- float t = fract( ( uVatTime * aSpeed + aTimeOffset ) / duration ) * aClipFrames;
25
- int f0 = int( t );
26
- int f1 = int( mod( float( f0 + 1 ), aClipFrames ) );
27
- vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, f0 + int( aClipStart ) ), 0 ).xyz;
28
- vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, f1 + int( aClipStart ) ), 0 ).xyz;
29
- return mix( s0, s1, fract( t ) );
18
+ vec3 vatSample( const in sampler2D tex, const in int vatInstance ) {
19
+ // The pack, fetched by this instance's *logical* index rather than read
20
+ // off an attribute indexed by the drawn slot (ADR-0016). Three texels of
21
+ // one row, in the order src/instance-playback.ts lays them out; the
22
+ // arithmetic below is untouched by where they came from, because the pack
23
+ // was already three vec4s.
24
+ //
25
+ // The index arrives as a parameter rather than being read here, because
26
+ // where it comes from is the carrier's business and not the decode's:
27
+ // gl_InstanceID on an InstancedMesh, getIndirectIndex( gl_DrawID ) on a
28
+ // BatchedMesh \u2014 see INSTANCE_ID in src/webgl.ts. It also has to be a
29
+ // parameter: getIndirectIndex is declared by batching_pars_vertex, which
30
+ // three expands *after* this prelude, so naming it up here would not
31
+ // compile.
32
+ //
33
+ // Fetched inside the function, so each injection point stays
34
+ // self-contained (ADR-0006) \u2014 which costs a second set of fetches in the
35
+ // normal decode. Every vertex of an instance reads the same three texels,
36
+ // so the texture cache absorbs them; the 5% demo bench is what says so.
37
+ vec4 vatClip = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.clip}, vatInstance ), 0 );
38
+ vec4 vatPlayback = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.playback}, vatInstance ), 0 );
39
+ vec4 vatFade = texelFetch( uVatPlaybackTex, ivec2( ${PACK_TEXELS.fade}, vatInstance ), 0 );
40
+
41
+ float frames = vatClip.y;
42
+ float last = frames - 1.0;
43
+ float duration = frames / vatClip.z;
44
+ // Local time: how far into its own animation this instance is. A start time
45
+ // in the past is what desyncs a crowd; a start time in the future has not
46
+ // begun, which is not the same thing as having finished.
47
+ float local = ( uVatTime - vatPlayback.x ) * vatClip.w;
48
+ float loops = local / duration;
49
+ float repetitions = vatPlayback.z;
50
+
51
+ bool started = local >= 0.0;
52
+ bool finished = started && repetitions != ${glslFloat(INFINITE_REPETITIONS)} && loops >= repetitions;
53
+
54
+ float phase;
55
+ bool wraps;
56
+ if ( !started ) {
57
+ phase = 0.0;
58
+ wraps = false;
59
+ } else if ( finished ) {
60
+ // Held at an end pose, and in neither case sampling past it.
61
+ phase = vatPlayback.w == ${glslFloat(EndMode.Clamp)} ? 1.0 : 0.0;
62
+ wraps = false;
63
+ } else if ( vatPlayback.y == ${glslFloat(LoopMode.PingPong)} ) {
64
+ float m = mod( loops, 2.0 );
65
+ phase = m < 1.0 ? m : 2.0 - m;
66
+ wraps = false; // a ping-pong bounces; it does not wrap
67
+ } else {
68
+ phase = fract( loops );
69
+ wraps = true; // and here the interpolation crossing back is correct
70
+ }
71
+
72
+ float f = phase * ( wraps ? frames : last );
73
+ float f0 = min( floor( f ), last );
74
+ float f1 = wraps ? mod( f0 + 1.0, frames ) : min( f0 + 1.0, last );
75
+ vec3 s0 = texelFetch( tex, ivec2( gl_VertexID, int( vatClip.x + f0 ) ), 0 ).xyz;
76
+ vec3 s1 = texelFetch( tex, ivec2( gl_VertexID, int( vatClip.x + f1 ) ), 0 ).xyz;
77
+ vec3 sampled = mix( s0, s1, f - f0 );
78
+
79
+ // The pose-freeze fade, transcribed from the same resolver: one frozen row
80
+ // of the clip this instance was playing when it changed, blended away over
81
+ // vatFade.w. Wall clock, not clip time \u2014 the incoming clip's speed does
82
+ // not stretch a fade. A duration of zero is what "not fading" is, and the
83
+ // pack never writes one without a band to go with it.
84
+ if ( vatFade.w > 0.0 ) {
85
+ float weight = 1.0 - clamp( ( uVatTime - vatPlayback.x ) / vatFade.w, 0.0, 1.0 );
86
+ float fromRow = max( min( floor( vatFade.z * vatFade.y ), vatFade.y - 1.0 ), 0.0 );
87
+ vec3 frozen = texelFetch( tex, ivec2( gl_VertexID, int( vatFade.x + fromRow ) ), 0 ).xyz;
88
+ sampled = mix( sampled, frozen, weight );
89
+ }
90
+ return sampled;
30
91
  }
31
92
  `
32
93
  );
33
- var DECODE_POSITION = (
94
+ var NORMAL_PRELUDE = (
34
95
  /* glsl */
35
96
  `
36
- vec3 transformed = position + vatSample( uVatPosTex );
97
+ uniform highp sampler2D uVatNrmTex;
37
98
  `
38
99
  );
39
- var DECODE_NORMAL = (
100
+ var INSTANCE_ID = {
101
+ instance: "gl_InstanceID",
102
+ batch: "int( getIndirectIndex( gl_DrawID ) )"
103
+ };
104
+ var decodePosition = (id) => (
40
105
  /* glsl */
41
106
  `
42
- vec3 objectNormal = normalize( vatSample( uVatNrmTex ) );
107
+ vec3 transformed = position + vatSample( uVatPosTex, ${INSTANCE_ID[id]} );
108
+ `
109
+ );
110
+ var decodeNormal = (id) => (
111
+ /* glsl */
112
+ `
113
+ vec3 objectNormal = normalize( vatSample( uVatNrmTex, ${INSTANCE_ID[id]} ) );
43
114
  #ifdef USE_TANGENT
44
115
  vec3 objectTangent = vec3( tangent.xyz );
45
116
  #endif
46
117
  `
47
118
  );
48
- function patchVATMaterial(material, vat, uniforms) {
119
+ function patchVATMaterial(material, vat, uniforms, playback, carrier) {
120
+ assertBakedNormal(vat, material);
121
+ if (carrier) assertVATCarrier(carrier, vat);
122
+ const normalTexture = vat.normalTexture;
123
+ const id = isBatchedCarrier(carrier) ? "batch" : "instance";
49
124
  material.onBeforeCompile = (shader) => {
50
125
  shader.uniforms.uVatPosTex = { value: vat.positionTexture };
51
- shader.uniforms.uVatNrmTex = { value: vat.normalTexture };
126
+ shader.uniforms.uVatPlaybackTex = { value: playback.texture };
52
127
  shader.uniforms.uVatTime = uniforms.uVatTime;
53
- shader.vertexShader = DECODE_PRELUDE + shader.vertexShader.replace("#include <begin_vertex>", DECODE_POSITION).replace("#include <beginnormal_vertex>", DECODE_NORMAL);
128
+ let vertexShader = shader.vertexShader.replace("#include <begin_vertex>", decodePosition(id));
129
+ if (normalTexture) {
130
+ shader.uniforms.uVatNrmTex = { value: normalTexture };
131
+ vertexShader = vertexShader.replace("#include <beginnormal_vertex>", decodeNormal(id));
132
+ }
133
+ shader.vertexShader = (normalTexture ? NORMAL_PRELUDE : "") + DECODE_PRELUDE + vertexShader;
54
134
  };
55
- material.customProgramCacheKey = () => "three-vat";
135
+ const key = `three-vat:${id}${normalTexture ? "" : ":no-normal"}`;
136
+ material.customProgramCacheKey = () => key;
137
+ guardCarrierMismatch(material, id);
56
138
  return material;
57
139
  }
58
- function createVATDepthMaterial(vat, uniforms) {
140
+ function guardCarrierMismatch(material, id) {
141
+ const previous = material.onBeforeRender.bind(material);
142
+ let checked = false;
143
+ material.onBeforeRender = function(renderer, scene, camera, geometry, object, group) {
144
+ if (!checked) {
145
+ checked = true;
146
+ const drawnOn = isBatchedCarrier(object) ? "batch" : "instance";
147
+ if (drawnOn !== id) {
148
+ throw new Error(
149
+ `three-vat: this material was patched for ${CARRIER_NAME[id]} and is being drawn on ${CARRIER_NAME[drawnOn]}. Pass the carrier as \`patchVATMaterial\`'s fifth argument \u2014 the two spell the instance index differently, and a batch drawn with the instanced spelling plays instance 0\u2019s clip on every instance.`
150
+ );
151
+ }
152
+ }
153
+ previous(renderer, scene, camera, geometry, object, group);
154
+ };
155
+ }
156
+ var CARRIER_NAME = {
157
+ instance: "an InstancedMesh",
158
+ batch: "a BatchedMesh"
159
+ };
160
+ function createVATDepthMaterial(vat, uniforms, playback, carrier) {
59
161
  const depth = new MeshDepthMaterial({ depthPacking: RGBADepthPacking });
60
- patchVATMaterial(depth, vat, uniforms);
162
+ patchVATMaterial(depth, vat, uniforms, playback, carrier);
61
163
  return depth;
62
164
  }
63
165
  function createVATMesh(vat, instances, options = {}) {
64
166
  const uniforms = options.time ? { uVatTime: options.time } : createVATUniforms();
65
- const geometry = createCrowdGeometry(vat, instances);
66
- const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms));
67
- const mesh = new InstancedMesh(geometry, materials, instances.length);
68
- mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms);
69
- mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms);
70
- return { mesh, time: uniforms.uVatTime };
167
+ const playback = createVATPlaybackTexture(instances);
168
+ const materials = vat.materials.map((source) => patchVATMaterial(source.clone(), vat, uniforms, playback));
169
+ const mesh = new InstancedMesh(vat.geometry, materials, instances.length);
170
+ mesh.customDepthMaterial = createVATDepthMaterial(vat, uniforms, playback);
171
+ mesh.customDistanceMaterial = patchVATMaterial(new MeshDistanceMaterial(), vat, uniforms, playback);
172
+ return { mesh, time: uniforms.uVatTime, playback };
71
173
  }
72
174
 
73
- export { addInstancedVATAttributes, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
175
+ export { createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-vat",
3
- "version": "1.0.1",
3
+ "version": "2.0.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",
@@ -16,6 +16,7 @@
16
16
  "publishConfig": {
17
17
  "access": "public"
18
18
  },
19
+ "packageManager": "pnpm@10.19.0",
19
20
  "sideEffects": false,
20
21
  "files": [
21
22
  "dist"
@@ -34,6 +35,15 @@
34
35
  "import": "./dist/tsl.js"
35
36
  }
36
37
  },
38
+ "scripts": {
39
+ "dev": "pnpm --filter three-vat-example dev",
40
+ "test": "vitest run && pnpm --filter three-vat-example test",
41
+ "build": "tsup",
42
+ "typecheck": "tsc --noEmit && tsc --noEmit -p release/tsconfig.json && pnpm --filter three-vat-example typecheck",
43
+ "build:watch": "tsup --watch",
44
+ "test:watch": "vitest",
45
+ "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build"
46
+ },
37
47
  "keywords": [
38
48
  "three",
39
49
  "threejs",
@@ -48,28 +58,24 @@
48
58
  "tsl"
49
59
  ],
50
60
  "peerDependencies": {
51
- "three": ">=0.185.0"
61
+ "three": ">=0.186.0"
52
62
  },
53
63
  "devDependencies": {
54
- "@types/three": "^0.185.0",
64
+ "@types/three": "^0.186.0",
55
65
  "gifenc": "1.0.3",
56
66
  "playwright-core": "1.63.0",
57
67
  "pngjs": "7.0.0",
58
- "three": "^0.185.0",
68
+ "three": "^0.186.0",
59
69
  "tsup": "^8.3.0",
60
70
  "typescript": "^5.6.0",
61
71
  "vite": "^7.1.0",
62
72
  "vitest": "^2.1.0"
63
73
  },
64
74
  "engines": {
65
- "node": ">=18"
75
+ "node": ">=20"
66
76
  },
67
- "scripts": {
68
- "dev": "pnpm --filter three-vat-example dev",
69
- "test": "vitest run && pnpm --filter three-vat-example test",
70
- "build": "tsup",
71
- "typecheck": "tsc --noEmit && tsc --noEmit -p release/tsconfig.json && pnpm --filter three-vat-example typecheck",
72
- "build:watch": "tsup --watch",
73
- "test:watch": "vitest"
74
- }
75
- }
77
+ "workspaces": [
78
+ ".",
79
+ "examples"
80
+ ]
81
+ }
@@ -1,40 +0,0 @@
1
- import { InstancedBufferAttribute } from 'three';
2
-
3
- // src/instance-playback.ts
4
- var PLAYBACK_ATTRIBUTES = {
5
- clipStart: "aClipStart",
6
- clipFrames: "aClipFrames",
7
- clipFps: "aClipFps",
8
- timeOffset: "aTimeOffset",
9
- speed: "aSpeed"
10
- };
11
- function addVATInstanceAttributes(geometry, instances) {
12
- geometry.morphAttributes = {};
13
- geometry.morphTargetsRelative = false;
14
- const n = instances.length;
15
- const clipStart = new Float32Array(n);
16
- const clipFrames = new Float32Array(n);
17
- const clipFps = new Float32Array(n);
18
- const timeOffset = new Float32Array(n);
19
- const speed = new Float32Array(n);
20
- for (let i = 0; i < n; i++) {
21
- const inst = instances[i];
22
- clipStart[i] = inst.clip.startFrame;
23
- clipFrames[i] = inst.clip.frames;
24
- clipFps[i] = inst.clip.fps;
25
- timeOffset[i] = inst.timeOffset;
26
- speed[i] = inst.speed;
27
- }
28
- geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipStart, new InstancedBufferAttribute(clipStart, 1));
29
- geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipFrames, new InstancedBufferAttribute(clipFrames, 1));
30
- geometry.setAttribute(PLAYBACK_ATTRIBUTES.clipFps, new InstancedBufferAttribute(clipFps, 1));
31
- geometry.setAttribute(PLAYBACK_ATTRIBUTES.timeOffset, new InstancedBufferAttribute(timeOffset, 1));
32
- geometry.setAttribute(PLAYBACK_ATTRIBUTES.speed, new InstancedBufferAttribute(speed, 1));
33
- }
34
- function createCrowdGeometry(vat, instances) {
35
- const geometry = vat.geometry.clone();
36
- addVATInstanceAttributes(geometry, instances);
37
- return geometry;
38
- }
39
-
40
- export { PLAYBACK_ATTRIBUTES, addVATInstanceAttributes, createCrowdGeometry };
@@ -1,109 +0,0 @@
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 };