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/README.md +302 -29
- package/dist/chunk-SXTVASKG.js +40 -0
- package/dist/index.d.ts +41 -51
- package/dist/index.js +256 -89
- package/dist/instance-playback-BrGBIKLe.d.ts +109 -0
- package/dist/tsl.d.ts +136 -16
- package/dist/tsl.js +73 -16
- package/dist/webgl.d.ts +64 -14
- package/dist/webgl.js +16 -24
- package/package.json +17 -11
- package/dist/types-DVtQvVjk.d.ts +0 -45
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 './
|
|
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
|
|
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?:
|
|
15
|
-
/**
|
|
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:
|
|
94
|
+
time: VATTimeUniform;
|
|
29
95
|
}
|
|
30
96
|
/**
|
|
31
97
|
* Build TSL decode nodes for a baked VAT, for the WebGPU/TSL renderer path.
|
|
32
|
-
*
|
|
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
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* the
|
|
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
|
-
*
|
|
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 {
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
const
|
|
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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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(
|
|
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
|
-
|
|
25
|
-
|
|
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,
|
|
2
|
-
import { V as VAT } from './
|
|
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
|
-
*
|
|
20
|
-
*
|
|
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
|
-
|
|
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 {
|
|
1
|
+
import { addVATInstanceAttributes, createCrowdGeometry } from './chunk-SXTVASKG.js';
|
|
2
|
+
import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
|
|
2
3
|
|
|
3
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
+
}
|
package/dist/types-DVtQvVjk.d.ts
DELETED
|
@@ -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 };
|