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/tsl.d.ts CHANGED
@@ -1,44 +1,164 @@
1
+ import { BufferGeometry, InstancedMesh } from 'three';
1
2
  import { Node } from 'three/webgpu';
2
- import { V as VAT } from './types-DVtQvVjk.js';
3
- import 'three';
3
+ import { b as VATClock, V as VAT, d as VATInstance, c as VATCrowd } from './instance-playback-BrGBIKLe.js';
4
4
 
5
+ /**
6
+ * The real maximum texture dimension this renderer accepts, for
7
+ * `bakeVAT(..., { maxTextureSize })`. The baker is renderer-agnostic (it runs
8
+ * in Node, and in a Web Worker) so it cannot query this itself.
9
+ *
10
+ * Call this *after* `await renderer.init()` — the backend has no device before
11
+ * that. Handles both a WebGPU backend and the WebGL fallback backend a
12
+ * `WebGPURenderer` may silently switch to when WebGPU is unavailable.
13
+ */
14
+ declare function getMaxTextureSize(renderer: object): number;
5
15
  /** A fluent TSL float node (has `.add`, `.mul`, … via NodeExtensions). */
6
16
  type FloatNode = Node<'float'>;
7
17
  /** A fluent TSL vec3 node. */
8
18
  type Vec3Node = Node<'vec3'>;
19
+ /**
20
+ * A TSL float uniform: a node the graph reads, and a `{ value }` clock the
21
+ * caller sets per frame. Both halves matter — the node is what the decode
22
+ * samples against, the clock is what the render loop writes — which is why
23
+ * `createVATMesh` can hand the same object back as a {@link VATClock} and have
24
+ * it mean the same thing as the WebGL path's uniform.
25
+ */
26
+ type VATTimeUniform = FloatNode & VATClock;
9
27
  interface VATNodeOptions {
10
28
  /**
11
- * Elapsed-time uniform node (seconds). Create once with `uniform(0)` and set
29
+ * Elapsed-time uniform (seconds). Create once with `uniform(0)` and set
12
30
  * `.value` per frame. Defaults to a fresh `uniform(0)` you can read back.
13
31
  */
14
- time?: FloatNode;
15
- /** Which clip to play (index into `vat.clips`). Default `0`. */
32
+ time?: VATTimeUniform;
33
+ /**
34
+ * The geometry these nodes will render. When it carries the instance-playback
35
+ * attributes — write them with `addVATInstanceAttributes` from `three-vat`
36
+ * *before* calling this — each instance plays its own clip, at its own phase
37
+ * and rate. Without them, every instance plays `clipIndex`, phase-desynced by
38
+ * `desync`.
39
+ *
40
+ * Which decode the graph compiles is decided here, at build time: a TSL
41
+ * attribute that is missing from the geometry reads as a constant, so the
42
+ * fallback cannot be a shader-side branch.
43
+ */
44
+ geometry?: BufferGeometry;
45
+ /**
46
+ * The `InstancedMesh` these nodes will render, when there is one.
47
+ *
48
+ * Required for a crowd, and for one reason: three applies the instance matrix
49
+ * to `positionLocal` *before* it reads `positionNode`, so the decode has to
50
+ * displace in the geometry's own space and then re-apply the instancing
51
+ * itself. Without this the delta is added in instance space — unrotated and
52
+ * unscaled — and every instance deforms according to its own matrix.
53
+ *
54
+ * Omit it for a single, non-instanced mesh, where `positionLocal` is the
55
+ * geometry position and there is nothing to re-apply.
56
+ */
57
+ instancedMesh?: InstancedMesh;
58
+ /**
59
+ * Which clip to play (index into `vat.clips`). Ignored — along with
60
+ * `desync` — when `geometry` carries instance playback, which says all of this
61
+ * per instance. Default `0`.
62
+ */
16
63
  clipIndex?: number;
17
64
  /**
18
65
  * Max random per-instance time offset in seconds, hashed from `instanceIndex`.
19
- * `0` (default) plays every instance in lockstep.
66
+ * `0` (default) plays every instance in lockstep. Ignored when `geometry`
67
+ * carries instance playback.
20
68
  */
21
69
  desync?: number;
22
70
  }
23
71
  /** Position/normal nodes to assign onto a `MeshStandardNodeMaterial` (or similar). */
24
72
  interface VATNodes {
73
+ /**
74
+ * Assign to `material.positionNode`. It carries the whole decode — the normal
75
+ * with it.
76
+ *
77
+ * There is deliberately no `normalNode`. A material's `normalNode` is built in
78
+ * the *fragment* stage (three reaches it from `normalView` through
79
+ * `builder.context.setupNormal()`) and is expected in **view** space, whereas a
80
+ * VAT's baked normals are per-vertex and in the geometry's own space. Handing
81
+ * an object-space normal to a fragment-stage node skipped both the instance
82
+ * matrix and the normal matrix, and took `vertexIndex` into the fragment stage
83
+ * with it — where `IndexNode` does not give you the vertex index at all, but
84
+ * quietly turns itself into a varying, so every fragment read a linearly
85
+ * *interpolated* index that addresses neither of the vertices it lies between.
86
+ *
87
+ * Writing `normalLocal` inside the vertex-stage decode instead is what the
88
+ * GLSL path does when it sets `objectNormal` in `beginnormal_vertex`: three
89
+ * then transforms it by the instance and normal matrices and interpolates the
90
+ * result, on both paths, for free.
91
+ */
25
92
  positionNode: Vec3Node;
26
- normalNode: Vec3Node;
27
93
  /** The time uniform in use — set `.value` each frame. */
28
- time: FloatNode;
94
+ time: VATTimeUniform;
29
95
  }
30
96
  /**
31
97
  * Build TSL decode nodes for a baked VAT, for the WebGPU/TSL renderer path.
32
- * Per-instance desync comes from `hash(instanceIndex)` — no instanced
33
- * attributes needed. Shadows work automatically because `positionNode` also
34
- * feeds the depth pass.
98
+ * Shadows work automatically because `positionNode` also feeds the depth pass.
35
99
  *
36
- * v1 limitation: a single clip per material (all instances share `clipIndex`,
37
- * only their phase is desynced). Per-instance clip variety is future work; use
38
- * the `three-vat/webgl` path for mixed-clip crowds today.
100
+ * Pass the `geometry` you are about to render and each instance plays the clip,
101
+ * phase and rate written into it by `addVATInstanceAttributes` — the same
102
+ * instance-playback contract the WebGL path reads (ADR-0009), so a mixed-clip
103
+ * crowd renders identically on either renderer. Without those attributes every
104
+ * instance plays `clipIndex`, desynced by a phase hashed from `instanceIndex`.
39
105
  *
40
- * NOTE: the TSL path is verified visually/manually in v1 (no automated GPU test).
106
+ * Coverage note: the node graph is tested structurally in CI (no GPU); that the
107
+ * two paths decode *identically* is a pixel-diff release gate.
41
108
  */
42
109
  declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
110
+ /**
111
+ * The decode's arithmetic: the position delta and the normal this instance reads
112
+ * at this moment, as nodes — before the vertex-stage writes that place them.
113
+ *
114
+ * @internal Split out and exported for the structural tests. A `Fn` body is
115
+ * opaque to graph traversal (its statements are not built until the shader is),
116
+ * and structural assertions are the only TSL coverage CI can run without a GPU —
117
+ * so the arithmetic that matters stays reachable as a graph. Not re-exported
118
+ * from `three-vat`; nothing outside this package should build against it.
119
+ */
120
+ declare function vatDecode(vat: VAT, options?: VATNodeOptions): {
121
+ position: Vec3Node;
122
+ normal: Vec3Node;
123
+ };
124
+ /** Options for {@link createVATMesh}. */
125
+ interface CreateVATMeshOptions {
126
+ /**
127
+ * The playback clock to drive this crowd from, in seconds. Pass one — from
128
+ * `uniform(0)` — to run several VAT meshes off a single time value, or to
129
+ * keep the node for wiring elsewhere in a graph, which the returned `time`
130
+ * gives back as a plain clock. Defaults to a fresh `uniform(0)`.
131
+ *
132
+ * The one place the two paths' signatures differ: `three-vat/webgl` takes a
133
+ * `THREE.IUniform` here. Both are `{ value }` clocks, and code that lets the
134
+ * call make its own is identical on either path.
135
+ */
136
+ time?: VATTimeUniform;
137
+ }
138
+ /**
139
+ * Turn a baked VAT and a list of instances into a crowd ready to render: an
140
+ * `InstancedMesh` whose geometry carries the instance-playback contract and
141
+ * whose materials decode the VAT on the vertex stage.
142
+ *
143
+ * ```ts
144
+ * const { mesh, time } = createVATMesh(vat, instances)
145
+ * mesh.castShadow = mesh.receiveShadow = true
146
+ * scene.add(mesh)
147
+ * // per frame:
148
+ * time.value = clock.elapsedTime
149
+ * ```
150
+ *
151
+ * The same call, the same signature and the same return as `three-vat/webgl`:
152
+ * a crowd moves between `WebGLRenderer` and `WebGPURenderer` by changing the
153
+ * import line and nothing else. The one asymmetry is absorbed here rather than
154
+ * passed on — this path attaches **no depth material**, because `positionNode`
155
+ * already feeds the depth pass, whereas the WebGL path must patch one by hand
156
+ * or cast bind-pose shadows.
157
+ *
158
+ * Instance matrices and `castShadow`/`receiveShadow` stay yours, as on the
159
+ * WebGL path: `mesh.setMatrixAt` then `mesh.computeBoundingSphere()`, or
160
+ * `frustumCulled = false` when the matrices change every frame.
161
+ */
162
+ declare function createVATMesh(vat: VAT, instances: VATInstance[], options?: CreateVATMeshOptions): VATCrowd;
43
163
 
44
- export { type VATNodeOptions, type VATNodes, vatNodes };
164
+ export { type CreateVATMeshOptions, type VATNodeOptions, type VATNodes, type VATTimeUniform, createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
package/dist/tsl.js CHANGED
@@ -1,30 +1,87 @@
1
- import { uniform, float, int, hash, instanceIndex, vertexIndex, positionLocal, textureLoad, ivec2, mix } from 'three/tsl';
1
+ import { createCrowdGeometry, PLAYBACK_ATTRIBUTES } from './chunk-SXTVASKG.js';
2
+ import { InstancedMesh } from 'three';
3
+ import { uniform, Fn, positionLocal, positionGeometry, normalLocal, instancedMesh, int, vertexIndex, textureLoad, ivec2, mix, float, hash, instanceIndex, attribute } from 'three/tsl';
2
4
 
3
- // src/tsl.ts
4
- function vatNodes(vat, options = {}) {
5
- const { time = uniform(0), clipIndex = 0, desync = 0 } = options;
5
+ function getMaxTextureSize(renderer) {
6
+ const backend = renderer.backend;
7
+ const device = backend?.["device"];
8
+ if (device?.limits?.maxTextureDimension2D) return device.limits.maxTextureDimension2D;
9
+ const gl = backend?.["gl"];
10
+ if (gl) return gl.getParameter(gl.MAX_TEXTURE_SIZE);
11
+ throw new Error("three-vat: renderer has no initialized backend \u2014 call `await renderer.init()` first");
12
+ }
13
+ var floatAttribute = (name) => attribute(name, "float");
14
+ function attributePlayback() {
15
+ const frames = floatAttribute(PLAYBACK_ATTRIBUTES.clipFrames);
16
+ return {
17
+ startFrame: int(floatAttribute(PLAYBACK_ATTRIBUTES.clipStart)),
18
+ frames,
19
+ duration: frames.div(floatAttribute(PLAYBACK_ATTRIBUTES.clipFps)),
20
+ timeOffset: floatAttribute(PLAYBACK_ATTRIBUTES.timeOffset),
21
+ speed: floatAttribute(PLAYBACK_ATTRIBUTES.speed)
22
+ };
23
+ }
24
+ function clipAt(vat, clipIndex) {
6
25
  const clip = vat.clips[clipIndex];
7
26
  if (!clip) {
8
27
  throw new Error(`three-vat: clipIndex ${clipIndex} out of range (${vat.clips.length} clips)`);
9
28
  }
10
- const frames = float(clip.frames);
11
- const startFrame = int(clip.startFrame);
12
- const duration = float(clip.frames / clip.fps);
13
- const offset = hash(instanceIndex).mul(desync);
29
+ return clip;
30
+ }
31
+ function hashedPlayback(clip, desync) {
32
+ return {
33
+ startFrame: int(clip.startFrame),
34
+ frames: float(clip.frames),
35
+ duration: float(clip.frames / clip.fps),
36
+ timeOffset: hash(instanceIndex).mul(desync),
37
+ speed: float(1)
38
+ };
39
+ }
40
+ function checkPlaybackAttributes(geometry) {
41
+ const names = Object.values(PLAYBACK_ATTRIBUTES);
42
+ const missing = names.filter((name) => geometry.getAttribute(name) === void 0);
43
+ if (missing.length === 0) return true;
44
+ if (missing.length === names.length) return false;
45
+ throw new Error(
46
+ `three-vat: geometry carries only part of the instance-playback contract (missing ${missing.join(", ")}) \u2014 write all of it with \`addVATInstanceAttributes\` from \`three-vat\`, or pass no geometry for the hashed default`
47
+ );
48
+ }
49
+ function vatNodes(vat, options = {}) {
50
+ const { time = uniform(0), instancedMesh: instanced } = options;
51
+ const { position, normal } = vatDecode(vat, options);
52
+ const decode = Fn(() => {
53
+ positionLocal.assign((instanced ? positionGeometry : positionLocal).add(position));
54
+ normalLocal.assign(normal);
55
+ if (instanced) instancedMesh(instanced);
56
+ return positionLocal;
57
+ }, "vec3");
58
+ return { positionNode: decode(), time };
59
+ }
60
+ function vatDecode(vat, options = {}) {
61
+ const { time = uniform(0), geometry, clipIndex = 0, desync = 0 } = options;
62
+ const playback = geometry && checkPlaybackAttributes(geometry) ? attributePlayback() : hashedPlayback(clipAt(vat, clipIndex), desync);
14
63
  const vertexRow = int(vertexIndex);
15
64
  const sample = (tex) => {
16
- const t = time.add(offset).div(duration).fract().mul(frames);
65
+ const t = time.mul(playback.speed).add(playback.timeOffset).div(playback.duration).fract().mul(playback.frames);
17
66
  const f0 = int(t);
18
- const f1 = int(f0.add(1).toFloat().mod(frames));
19
- const s0 = textureLoad(tex, ivec2(vertexRow, f0.add(startFrame))).xyz;
20
- const s1 = textureLoad(tex, ivec2(vertexRow, f1.add(startFrame))).xyz;
67
+ const f1 = int(f0.add(1).toFloat().mod(playback.frames));
68
+ const s0 = textureLoad(tex, ivec2(vertexRow, f0.add(playback.startFrame))).xyz;
69
+ const s1 = textureLoad(tex, ivec2(vertexRow, f1.add(playback.startFrame))).xyz;
21
70
  return mix(s0, s1, t.fract());
22
71
  };
23
72
  return {
24
- positionNode: positionLocal.add(sample(vat.positionTexture)),
25
- normalNode: sample(vat.normalTexture).normalize(),
26
- time
73
+ position: sample(vat.positionTexture),
74
+ normal: sample(vat.normalTexture).normalize()
27
75
  };
28
76
  }
77
+ function createVATMesh(vat, instances, options = {}) {
78
+ const time = options.time ?? uniform(0);
79
+ const geometry = createCrowdGeometry(vat, instances);
80
+ const materials = vat.materials.map((source) => source.clone());
81
+ const mesh = new InstancedMesh(geometry, materials, instances.length);
82
+ const { positionNode } = vatNodes(vat, { time, geometry, instancedMesh: mesh });
83
+ for (const material of materials) material.positionNode = positionNode;
84
+ return { mesh, time };
85
+ }
29
86
 
30
- export { vatNodes };
87
+ export { createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
package/dist/webgl.d.ts CHANGED
@@ -1,25 +1,35 @@
1
- import { IUniform, BufferGeometry, MeshDepthMaterial, Material } from 'three';
2
- import { V as VAT } from './types-DVtQvVjk.js';
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';
3
3
 
4
+ /**
5
+ * The real maximum texture dimension this GPU accepts, for
6
+ * `bakeVAT(..., { maxTextureSize })`. The baker cannot query this itself — it
7
+ * is renderer-agnostic so it can run in Node or a Web Worker — so read it
8
+ * here and hand it over. Desktop typically reports 16384, but mobile GPUs
9
+ * commonly report 4096 or 8192, which is exactly the case a hardcoded default
10
+ * bakes straight past.
11
+ */
12
+ declare function getMaxTextureSize(renderer: WebGLRenderer): number;
4
13
  /** The shared uniform driving every VAT-patched material's playback clock. */
5
14
  interface VATUniforms {
6
15
  uVatTime: IUniform<number>;
7
16
  }
8
17
  /** Create the shared time uniform. Update `uVatTime.value` once per frame. */
9
18
  declare function createVATUniforms(time?: number): VATUniforms;
10
- /** Per-instance playback state consumed by the patched shader. */
11
- interface VATInstance {
12
- clip: Pick<VAT['clips'][number], 'startFrame' | 'frames' | 'fps'>;
13
- /** Phase offset in seconds — desyncs the crowd. */
14
- timeOffset: number;
15
- /** Playback rate multiplier. */
16
- speed: number;
17
- }
18
19
  /**
19
- * Attach the per-instance attributes the WebGL decode reads: clip band, fps,
20
- * time offset, and speed. Call on the instanced geometry before rendering.
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.
21
31
  */
22
- declare function addInstancedVATAttributes(geometry: BufferGeometry, instances: VATInstance[]): void;
32
+ type VATInstance = VATInstance$1;
23
33
  /**
24
34
  * Patch any built-in material so its vertex stage samples the VAT instead of
25
35
  * skinning. Works on the render material and on `MeshDepthMaterial` (needed for
@@ -34,5 +44,45 @@ declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uni
34
44
  * `MeshDistanceMaterial`).
35
45
  */
36
46
  declare function createVATDepthMaterial(vat: VAT, uniforms: VATUniforms): MeshDepthMaterial;
47
+ /** Options for {@link createVATMesh}. */
48
+ interface CreateVATMeshOptions {
49
+ /**
50
+ * The playback clock to drive this crowd from, in seconds. Pass one — from
51
+ * {@link createVATUniforms} or any `{ value }` — to run several VAT meshes off
52
+ * a single time value. Defaults to a fresh clock at `0`, returned to you as
53
+ * `time`. The TSL path's `vatNodes` takes its clock the same way.
54
+ */
55
+ time?: IUniform<number>;
56
+ }
57
+ /**
58
+ * 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.
62
+ *
63
+ * ```ts
64
+ * const { mesh, time } = createVATMesh(vat, instances)
65
+ * mesh.castShadow = mesh.receiveShadow = true
66
+ * scene.add(mesh)
67
+ * // per frame:
68
+ * time.value = clock.elapsedTime
69
+ * ```
70
+ *
71
+ * Two things stay yours, because only you can know them:
72
+ *
73
+ * - **Instance matrices.** Write them with `mesh.setMatrixAt`, then
74
+ * `mesh.instanceMatrix.needsUpdate = true`. An `InstancedMesh` caches the
75
+ * bounding sphere it culls against, so call `mesh.computeBoundingSphere()`
76
+ * after placing the crowd, or set `mesh.frustumCulled = false` when the
77
+ * matrices change every frame.
78
+ * - **`castShadow` / `receiveShadow`**, which are scene decisions. The depth and
79
+ * distance materials the shadow passes need are already attached either way.
80
+ *
81
+ * Everything here is the exported primitives — {@link addVATInstanceAttributes},
82
+ * {@link patchVATMaterial}, {@link createVATDepthMaterial} — composed in the one
83
+ * order that is correct. Reach for them directly only when rendering onto
84
+ * something other than a plain `InstancedMesh`.
85
+ */
86
+ declare function createVATMesh(vat: VAT, instances: VATInstance$1[], options?: CreateVATMeshOptions): VATCrowd;
37
87
 
38
- export { type VATInstance, type VATUniforms, addInstancedVATAttributes, createVATDepthMaterial, createVATUniforms, patchVATMaterial };
88
+ export { type CreateVATMeshOptions, type VATInstance, type VATUniforms, addInstancedVATAttributes, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
package/dist/webgl.js CHANGED
@@ -1,30 +1,13 @@
1
- import { InstancedBufferAttribute, MeshDepthMaterial, RGBADepthPacking } from 'three';
1
+ import { addVATInstanceAttributes, createCrowdGeometry } from './chunk-SXTVASKG.js';
2
+ import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
2
3
 
3
- // src/webgl.ts
4
+ function getMaxTextureSize(renderer) {
5
+ return renderer.capabilities.maxTextureSize;
6
+ }
4
7
  function createVATUniforms(time = 0) {
5
8
  return { uVatTime: { value: time } };
6
9
  }
7
- function addInstancedVATAttributes(geometry, instances) {
8
- const n = instances.length;
9
- const clipStart = new Float32Array(n);
10
- const clipFrames = new Float32Array(n);
11
- const clipFps = new Float32Array(n);
12
- const timeOffset = new Float32Array(n);
13
- const speed = new Float32Array(n);
14
- for (let i = 0; i < n; i++) {
15
- const inst = instances[i];
16
- clipStart[i] = inst.clip.startFrame;
17
- clipFrames[i] = inst.clip.frames;
18
- clipFps[i] = inst.clip.fps;
19
- timeOffset[i] = inst.timeOffset;
20
- speed[i] = inst.speed;
21
- }
22
- geometry.setAttribute("aClipStart", new InstancedBufferAttribute(clipStart, 1));
23
- geometry.setAttribute("aClipFrames", new InstancedBufferAttribute(clipFrames, 1));
24
- geometry.setAttribute("aClipFps", new InstancedBufferAttribute(clipFps, 1));
25
- geometry.setAttribute("aTimeOffset", new InstancedBufferAttribute(timeOffset, 1));
26
- geometry.setAttribute("aSpeed", new InstancedBufferAttribute(speed, 1));
27
- }
10
+ var addInstancedVATAttributes = addVATInstanceAttributes;
28
11
  var DECODE_PRELUDE = (
29
12
  /* glsl */
30
13
  `
@@ -77,5 +60,14 @@ function createVATDepthMaterial(vat, uniforms) {
77
60
  patchVATMaterial(depth, vat, uniforms);
78
61
  return depth;
79
62
  }
63
+ function createVATMesh(vat, instances, options = {}) {
64
+ 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 };
71
+ }
80
72
 
81
- export { addInstancedVATAttributes, createVATDepthMaterial, createVATUniforms, patchVATMaterial };
73
+ export { addInstancedVATAttributes, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "three-vat",
3
- "version": "0.1.0",
3
+ "version": "1.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",
@@ -34,15 +34,6 @@
34
34
  "import": "./dist/tsl.js"
35
35
  }
36
36
  },
37
- "scripts": {
38
- "build": "tsup",
39
- "dev": "tsup --watch",
40
- "test": "vitest run",
41
- "test:watch": "vitest",
42
- "typecheck": "tsc --noEmit",
43
- "prepublishOnly": "pnpm typecheck && pnpm test && pnpm build",
44
- "example": "pnpm --dir examples dev"
45
- },
46
37
  "keywords": [
47
38
  "three",
48
39
  "threejs",
@@ -68,5 +59,20 @@
68
59
  },
69
60
  "engines": {
70
61
  "node": ">=18"
62
+ },
63
+ "scripts": {
64
+ "build": "tsup",
65
+ "dev": "tsup --watch",
66
+ "test": "vitest run",
67
+ "test:watch": "vitest",
68
+ "fetch:test-assets": "node scripts/fetch-test-assets.mjs",
69
+ "typecheck": "tsc --noEmit",
70
+ "release": "pnpm publish ${NPM_OTP:+--otp=\"$NPM_OTP\"} && pnpm verify:published",
71
+ "verify:published": "node scripts/verify-published.mjs",
72
+ "example": "pnpm --filter three-vat-example dev",
73
+ "build:examples": "pnpm --filter three-vat-example build",
74
+ "test:examples": "pnpm --filter three-vat-example test",
75
+ "parity": "pnpm --filter three-vat-example parity",
76
+ "typecheck:examples": "pnpm --filter three-vat-example typecheck"
71
77
  }
72
- }
78
+ }
@@ -1,45 +0,0 @@
1
- import { DataTexture, Box3 } 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 plus the
24
- * clip table and bounds needed to decode and render them. Produced by
25
- * {@link bakeVAT} (runtime) or `loadVAT` (offline); the two paths yield
26
- * identical objects.
27
- */
28
- interface VAT {
29
- /** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
30
- positionTexture: DataTexture;
31
- /** RGBA float texture of per-vertex absolute normals (`x = vertex`, `y = frame`). */
32
- normalTexture: DataTexture;
33
- /** Clip table: name → `{ startFrame, frames, fps, ... }`. */
34
- clips: VATClip[];
35
- /** Union of every baked frame's bounds; use as the geometry bounding box. */
36
- bounds: Box3;
37
- /** Vertex count (texture width). */
38
- vertexCount: number;
39
- /** Total frame rows across all clips (texture height). */
40
- totalFrames: number;
41
- /** Position encoding. Only `'delta'` in v1. */
42
- encoding: 'delta';
43
- }
44
-
45
- export type { VAT as V, VATClip as a };