three-vat 0.3.0 → 1.0.1
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 +115 -78
- package/dist/chunk-SXTVASKG.js +40 -0
- package/dist/index.d.ts +12 -47
- package/dist/index.js +63 -55
- package/dist/instance-playback-BrGBIKLe.d.ts +109 -0
- package/dist/tsl.d.ts +126 -16
- package/dist/tsl.js +65 -16
- package/dist/webgl.d.ts +55 -14
- package/dist/webgl.js +13 -26
- package/package.json +10 -8
- package/dist/types-wVmIj2tC.d.ts +0 -63
package/README.md
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
# three-vat
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
[](./LICENSE)
|
|
3
|
+

|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
**[Open the live demo →](https://mikefernandez-pro.github.io/three-vat/)** and drag the count from 1 robot to 340. The draw calls do not move.
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
Bake a glTF `AnimationClip` into GPU textures and animate **hundreds or thousands of instanced characters with zero per-frame CPU** — one draw call per material, no `SkinnedMesh` per character. Works on `WebGLRenderer` and `WebGPURenderer`, from the same baked VAT.
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
[](https://github.com/MikeFernandez-Pro/three-vat/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/three-vat)
|
|
11
|
+
[](./LICENSE)
|
|
11
12
|
|
|
12
13
|
## Install
|
|
13
14
|
|
|
@@ -17,111 +18,147 @@ npm install three-vat three
|
|
|
17
18
|
|
|
18
19
|
`three` (>= 0.185) is a peer dependency.
|
|
19
20
|
|
|
20
|
-
##
|
|
21
|
+
## One crowd, start to finish
|
|
21
22
|
|
|
22
23
|
```ts
|
|
24
|
+
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'
|
|
23
25
|
import { bakeVAT } from 'three-vat'
|
|
26
|
+
import { createVATMesh, getMaxTextureSize } from 'three-vat/webgl'
|
|
27
|
+
// WebGPURenderer? `from 'three-vat/tsl'` — that import is the only line that changes.
|
|
28
|
+
|
|
29
|
+
// The parts you already know — renderer, scene, camera, lights, clock — made
|
|
30
|
+
// however you usually make them. Nothing on this line is VAT-specific.
|
|
31
|
+
const { renderer, scene, camera, clock } = setUpYourScene()
|
|
32
|
+
|
|
33
|
+
const gltf = await new GLTFLoader().loadAsync('/robot.glb')
|
|
34
|
+
|
|
35
|
+
// Bake once, at load. Pass the subtree root — `gltf.scene` — and not a mesh
|
|
36
|
+
// inside it: the bake unit is the whole subtree, merged and recorded in root
|
|
37
|
+
// space. This same call takes
|
|
38
|
+
// - a skinned character (Mixamo, Sketchfab, `Soldier.glb`)
|
|
39
|
+
// - a morph-target mesh
|
|
40
|
+
// - a hierarchy of rigid, node-animated parts (three's `RobotExpressive`)
|
|
41
|
+
// - any mix of those in one subtree
|
|
42
|
+
// because a VAT records where a vertex ended up and never how it got there.
|
|
43
|
+
// Nothing to classify, and no variant of this call to go looking for.
|
|
44
|
+
const vat = bakeVAT(gltf.scene, gltf.animations, {
|
|
45
|
+
fps: 30,
|
|
46
|
+
maxTextureSize: getMaxTextureSize(renderer), // this GPU's real ceiling
|
|
47
|
+
})
|
|
24
48
|
|
|
25
|
-
//
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
The bake unit is the **whole subtree**, merged into one vertex set and recorded in root space ([ADR-0008](./docs/adr/0008-a-vat-bakes-a-posed-subtree-not-a-skinnedmesh.md)). A VAT only records *where a vertex ended up*, never how it got there, so one call handles a single `SkinnedMesh`, a morph-target mesh, a hierarchy of rigid node-animated parts (three.js `RobotExpressive`), or any mix — with no classification by the caller.
|
|
32
|
-
|
|
33
|
-
Two consequences worth knowing up front:
|
|
34
|
-
|
|
35
|
-
- **Render `vat.geometry`, not your source mesh.** The merged vertex ordering is the baker's, and the textures are indexed by it.
|
|
36
|
-
- **Materials are never merged.** `vat.materials` lines up with `vat.geometry.groups`, giving one draw call per material. VAT collapses *instance* count, not *material* count — a 500-robot crowd with 3 materials is 3 draw calls, not 1 and not 500.
|
|
49
|
+
// One entry per character: which clip it plays, its phase, its rate.
|
|
50
|
+
const instances = Array.from({ length: 500 }, (_, i) => ({
|
|
51
|
+
clip: vat.clips[i % vat.clips.length],
|
|
52
|
+
timeOffset: Math.random() * 2, // desync, so the crowd is not in lockstep
|
|
53
|
+
speed: 0.9 + Math.random() * 0.2,
|
|
54
|
+
}))
|
|
37
55
|
|
|
38
|
-
The
|
|
56
|
+
// The crowd: one InstancedMesh, its geometry and materials already decoding the
|
|
57
|
+
// VAT, plus the clock that drives every instance.
|
|
58
|
+
const { mesh, time } = createVATMesh(vat, instances)
|
|
59
|
+
mesh.castShadow = mesh.receiveShadow = true
|
|
60
|
+
scene.add(mesh)
|
|
39
61
|
|
|
40
|
-
|
|
41
|
-
|
|
62
|
+
// Where each character stands is yours — the library never guesses a layout.
|
|
63
|
+
for (let i = 0; i < instances.length; i++) mesh.setMatrixAt(i, matrixFor(i))
|
|
64
|
+
mesh.instanceMatrix.needsUpdate = true
|
|
65
|
+
mesh.computeBoundingSphere() // or frustumCulled = false, if matrices move every frame
|
|
42
66
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
67
|
+
renderer.setAnimationLoop(() => {
|
|
68
|
+
time.value = clock.getElapsedTime() // the whole per-frame cost of the animation
|
|
69
|
+
renderer.render(scene, camera)
|
|
46
70
|
})
|
|
47
71
|
```
|
|
48
72
|
|
|
49
|
-
|
|
73
|
+
Two things worth knowing the first time:
|
|
50
74
|
|
|
51
|
-
|
|
52
|
-
|
|
75
|
+
- **Render `vat.geometry`, not your source mesh.** The merged vertex ordering is
|
|
76
|
+
the baker's, and the textures are indexed by it. `createVATMesh` does this for
|
|
77
|
+
you; by hand, clone that geometry and no other.
|
|
78
|
+
- **Materials are never merged.** A 500-robot crowd with 3 materials is 3 draw
|
|
79
|
+
calls — not 1, and not 500. VAT collapses instance count, not material count.
|
|
53
80
|
|
|
54
|
-
|
|
81
|
+
<details>
|
|
82
|
+
<summary><b>Does it work with my model?</b></summary>
|
|
55
83
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const geometry = vat.geometry.clone()
|
|
59
|
-
addInstancedVATAttributes(geometry, instances) // instances: { clip, timeOffset, speed }[]
|
|
84
|
+
If `GLTFLoader` loads it and it has an `AnimationClip`, yes — the four shapes
|
|
85
|
+
listed in the snippet above, and any mix of them, through that one call.
|
|
60
86
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
return material
|
|
66
|
-
})
|
|
87
|
+
The bake unit is the **subtree**, not the mesh
|
|
88
|
+
([ADR-0008](./docs/adr/0008-a-vat-bakes-a-posed-subtree-not-a-skinnedmesh.md)),
|
|
89
|
+
which is where the one real mistake lives: pass `gltf.scene`, or the node you
|
|
90
|
+
want animated, never a `SkinnedMesh` you fished out of it.
|
|
67
91
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
92
|
+
Positions bake exactly under any rig; normals match what three's own skinning
|
|
93
|
+
shader draws, which is approximate under non-uniform bone scale — `bakeVAT`
|
|
94
|
+
warns once and names the bone. A rest-pose track such as Mixamo's `TPose` bakes
|
|
95
|
+
to a frozen band and reports it as a near-zero `clip.maxDelta`: filter those out
|
|
96
|
+
of `gltf.animations` rather than spending texture rows on them.
|
|
71
97
|
|
|
72
|
-
|
|
73
|
-
uniforms.uVatTime.value = clock.elapsedTime
|
|
74
|
-
```
|
|
98
|
+
</details>
|
|
75
99
|
|
|
76
|
-
|
|
100
|
+
<details>
|
|
101
|
+
<summary><b>Going bigger: texture limits, bake cost, Web Workers</b></summary>
|
|
77
102
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
103
|
+
The VAT is one `vertexCount` × `totalFrames` texture pair, so both axes hit the
|
|
104
|
+
GPU's texture ceiling. Always pass `maxTextureSize: getMaxTextureSize(renderer)`
|
|
105
|
+
as above: the default is a desktop-shaped guess, and mobile is often 4096.
|
|
81
106
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
material.normalNode = normalNode
|
|
107
|
+
The bake is CPU work done once at load — about 100 ms for the demo's robot, and
|
|
108
|
+
seconds for a 20k-vertex skinned character with many clips. It never touches the
|
|
109
|
+
renderer, so it moves into a Web Worker as-is.
|
|
86
110
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
111
|
+
**[docs/usage.md](./docs/usage.md)** has the measured bake-cost table, the
|
|
112
|
+
worker recipe, the draw-call arithmetic, and the primitives underneath
|
|
113
|
+
`createVATMesh` for when you are not rendering onto a plain `InstancedMesh`.
|
|
90
114
|
|
|
91
|
-
|
|
115
|
+
</details>
|
|
92
116
|
|
|
93
|
-
|
|
117
|
+
<details>
|
|
118
|
+
<summary><b>What it does not do</b></summary>
|
|
94
119
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
No clip crossfade (instances cut between clips), no LOD, no baking CLI or file
|
|
121
|
+
format, no React/drei binding, glTF input only. Each is a decision rather than a
|
|
122
|
+
gap, and each is written up with its reasoning in
|
|
123
|
+
**[docs/usage.md](./docs/usage.md#what-10-does-not-do)**, alongside the
|
|
124
|
+
trade-offs against `SkinnedMesh` and bone-texture instancing.
|
|
98
125
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
interchangeable with a freshly-baked one, whatever earlier releases claimed.
|
|
126
|
+
Both decode paths — GLSL on `WebGLRenderer`, TSL on `WebGPURenderer` — read one
|
|
127
|
+
shared instance-playback contract and export the same `createVATMesh`, so
|
|
128
|
+
nothing documented here is true on one renderer and false on the other. That the
|
|
129
|
+
two decode *pixel-identically* is a manual gate before every release
|
|
130
|
+
([docs/releasing.md](./docs/releasing.md)).
|
|
105
131
|
|
|
106
|
-
|
|
107
|
-
time hurts on load, run `bakeVAT` in a Web Worker and transfer the texel buffers back.
|
|
132
|
+
</details>
|
|
108
133
|
|
|
109
|
-
|
|
134
|
+
<details>
|
|
135
|
+
<summary><b>Development</b></summary>
|
|
110
136
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
137
|
+
```bash
|
|
138
|
+
pnpm i
|
|
139
|
+
pnpm run dev # opens the demo — this and the line above are the whole setup
|
|
140
|
+
```
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
Three more verbs, and that is the table:
|
|
116
143
|
|
|
117
144
|
```bash
|
|
118
|
-
pnpm
|
|
119
|
-
pnpm
|
|
120
|
-
pnpm
|
|
121
|
-
pnpm build
|
|
122
|
-
pnpm example # runs the robot-crowd demo in examples/ (model bundled)
|
|
145
|
+
pnpm test # every suite: the baker core, the release suite, the demo's own
|
|
146
|
+
pnpm typecheck # all three tsconfigs: library, release suite, demo
|
|
147
|
+
pnpm build # the published library (`pnpm run build:watch` to watch)
|
|
123
148
|
```
|
|
124
149
|
|
|
150
|
+
`examples/` is the demo — one page per renderer, self-contained on purpose
|
|
151
|
+
([ADR-0011](./docs/adr/0011-one-example-per-renderer-duplicated-on-purpose.md)),
|
|
152
|
+
deployed from `main` on every push. Release steps are `node` invocations rather
|
|
153
|
+
than table entries, the parity gate among them and required
|
|
154
|
+
([docs/releasing.md](./docs/releasing.md)). The suite is green on a fresh clone
|
|
155
|
+
with no network: real-asset tests skip when their asset is missing, so fetch it
|
|
156
|
+
before touching the baker ([docs/test-assets.md](./docs/test-assets.md)).
|
|
157
|
+
|
|
158
|
+
</details>
|
|
159
|
+
|
|
160
|
+
Deeper: [docs/](./docs) · [CHANGELOG.md](./CHANGELOG.md) · [live WebGPU demo](https://mikefernandez-pro.github.io/three-vat/webgpu_crowd.html)
|
|
161
|
+
|
|
125
162
|
## License
|
|
126
163
|
|
|
127
164
|
MIT
|
|
@@ -0,0 +1,40 @@
|
|
|
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 };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Object3D, AnimationClip, TypedArray, TextureDataType, DataTexture } from 'three';
|
|
2
|
-
import {
|
|
2
|
+
import { V as VAT } from './instance-playback-BrGBIKLe.js';
|
|
3
|
+
export { a as VATClip, b as VATClock, c as VATCrowd, d as VATInstance, e as addVATInstanceAttributes } from './instance-playback-BrGBIKLe.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Conservative fallback texture-dimension cap, used when the caller does not
|
|
@@ -33,10 +34,18 @@ interface BakeOptions {
|
|
|
33
34
|
* how it got there.
|
|
34
35
|
*
|
|
35
36
|
* Positions are stored as deltas from the merged rest pose; normals absolute.
|
|
37
|
+
* Positions are exact for skinning, morph targets, node animation and any mix
|
|
38
|
+
* of them. A normal follows the same stages its vertex does — morph targets
|
|
39
|
+
* (`morphAttributes.normal`, where the asset carries them), then the skin
|
|
40
|
+
* matrix, then the part matrix. Normals reproduce what three's own skinning
|
|
41
|
+
* shader renders: linear blend skinning transforms a normal by the skin matrix
|
|
42
|
+
* rather than its inverse-transpose, which is exact for rigid and
|
|
43
|
+
* uniformly-scaled bones and an approximation for anything else. Non-uniform bone scale is where that
|
|
44
|
+
* approximation becomes visible, so the bake warns once, naming the bone.
|
|
36
45
|
* Renderer-agnostic — touches no WebGL/WebGPU context — so it runs identically
|
|
37
46
|
* at runtime, in a Web Worker, and in Node.
|
|
38
47
|
*/
|
|
39
|
-
declare function bakeVAT(root: Object3D, clips: AnimationClip[], { fps, maxTextureSize }?: BakeOptions):
|
|
48
|
+
declare function bakeVAT(root: Object3D, clips: AnimationClip[], { fps, maxTextureSize }?: BakeOptions): VAT;
|
|
40
49
|
/**
|
|
41
50
|
* Build a VAT `DataTexture` with the fixed sampling flags every path relies on:
|
|
42
51
|
* RGBA, nearest filtering, no mipmaps. Frame interpolation is done manually in
|
|
@@ -44,48 +53,4 @@ declare function bakeVAT(root: Object3D, clips: AnimationClip[], { fps, maxTextu
|
|
|
44
53
|
*/
|
|
45
54
|
declare function makeVATTexture(data: TypedArray, width: number, height: number, type?: TextureDataType): DataTexture;
|
|
46
55
|
|
|
47
|
-
type
|
|
48
|
-
/**
|
|
49
|
-
* The versioned descriptor of an offline-baked VAT. The manifest *is* the
|
|
50
|
-
* format — bump `version` on any breaking layout change.
|
|
51
|
-
*/
|
|
52
|
-
interface VATManifest {
|
|
53
|
-
version: 1;
|
|
54
|
-
vertexCount: number;
|
|
55
|
-
totalFrames: number;
|
|
56
|
-
encoding: 'delta';
|
|
57
|
-
precision: VATPrecision;
|
|
58
|
-
clips: VATClip[];
|
|
59
|
-
bounds: {
|
|
60
|
-
min: [number, number, number];
|
|
61
|
-
max: [number, number, number];
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
/** A serialized VAT: manifest plus the two raw texel buffers. */
|
|
65
|
-
interface SerializedVAT {
|
|
66
|
-
manifest: VATManifest;
|
|
67
|
-
/** Interleaved RGBA position deltas, `float16` or `float32` per `manifest.precision`. */
|
|
68
|
-
position: ArrayBuffer;
|
|
69
|
-
/** Interleaved RGBA absolute normals, same precision. */
|
|
70
|
-
normal: ArrayBuffer;
|
|
71
|
-
}
|
|
72
|
-
interface SerializeOptions {
|
|
73
|
-
/** On-disk texel precision. Default `'float16'` (half the bytes, uploads directly). */
|
|
74
|
-
precision?: VATPrecision;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* Serialize a baked VAT to raw texel buffers + a versioned manifest. This is
|
|
78
|
-
* the on-disk format; the runtime object is always reconstructed with
|
|
79
|
-
* {@link loadVAT}.
|
|
80
|
-
*/
|
|
81
|
-
/** @deprecated Removed in 1.0 — see ADR-0010. Bake at runtime instead. */
|
|
82
|
-
declare function serializeVAT(vat: VAT, { precision }?: SerializeOptions): SerializedVAT;
|
|
83
|
-
/**
|
|
84
|
-
* Reconstruct a runtime VAT from serialized buffers. `float16` uploads as
|
|
85
|
-
* `HalfFloatType`; `float32` as `FloatType`. The result is interchangeable with
|
|
86
|
-
* a {@link bakeVAT} result.
|
|
87
|
-
*/
|
|
88
|
-
/** @deprecated Removed in 1.0 — see ADR-0010. Bake at runtime instead. */
|
|
89
|
-
declare function loadVAT(serialized: SerializedVAT): VAT;
|
|
90
|
-
|
|
91
|
-
export { type BakeOptions, BakedVAT, MAX_TEXTURE_SIZE, type SerializeOptions, type SerializedVAT, VAT, VATClip, type VATManifest, type VATPrecision, bakeVAT, loadVAT, makeVATTexture, serializeVAT };
|
|
56
|
+
export { type BakeOptions, MAX_TEXTURE_SIZE, VAT, bakeVAT, makeVATTexture };
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
|
|
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
5
|
function asAttribute(value, mesh, name) {
|
|
6
6
|
if (!(value instanceof BufferAttribute)) {
|
|
@@ -47,6 +47,7 @@ function collectParts(root) {
|
|
|
47
47
|
skinIndex: geometry.attributes.skinIndex,
|
|
48
48
|
skinWeight: geometry.attributes.skinWeight,
|
|
49
49
|
morphPos: geometry.morphAttributes.position,
|
|
50
|
+
morphNrm: geometry.morphAttributes.normal,
|
|
50
51
|
morphRelative: geometry.morphTargetsRelative
|
|
51
52
|
});
|
|
52
53
|
}
|
|
@@ -160,6 +161,9 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
160
161
|
const _n = new Vector3();
|
|
161
162
|
const _mt = new Vector3();
|
|
162
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;
|
|
163
167
|
let rowOffset = 0;
|
|
164
168
|
clips.forEach((clip, ci) => {
|
|
165
169
|
const frames = frameCounts[ci];
|
|
@@ -170,22 +174,38 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
170
174
|
mixer.setTime(f / frames * clip.duration);
|
|
171
175
|
root.updateMatrixWorld(true);
|
|
172
176
|
const row = rowOffset + f;
|
|
177
|
+
if (!warnedNonUniformScale) {
|
|
178
|
+
warnedNonUniformScale = warnOnNonUniformBoneScale(influencers, _bone);
|
|
179
|
+
}
|
|
173
180
|
for (const part of parts) {
|
|
174
181
|
_partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
|
|
175
182
|
const influences = part.mesh.morphTargetInfluences;
|
|
176
|
-
const { morphPos, morphRelative, isSkinned, skeleton, skinIndex, skinWeight } = part;
|
|
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;
|
|
177
185
|
for (let v = 0; v < part.vertexCount; v++) {
|
|
178
186
|
const vi = part.vertexStart + v;
|
|
179
187
|
_p.fromBufferAttribute(part.basePos, v);
|
|
180
188
|
_n.fromBufferAttribute(part.baseNrm, v);
|
|
181
|
-
if (
|
|
182
|
-
if (!morphRelative)
|
|
183
|
-
|
|
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++) {
|
|
184
195
|
const w = influences[t];
|
|
185
196
|
if (w === 0) continue;
|
|
186
|
-
|
|
187
|
-
if (
|
|
188
|
-
|
|
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
|
+
}
|
|
189
209
|
}
|
|
190
210
|
}
|
|
191
211
|
if (isSkinned && skeleton) {
|
|
@@ -256,6 +276,39 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
256
276
|
materials
|
|
257
277
|
};
|
|
258
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
|
+
}
|
|
259
312
|
function makeVATTexture(data, width, height, type = FloatType) {
|
|
260
313
|
const tex = new DataTexture(data, width, height, RGBAFormat, type);
|
|
261
314
|
tex.minFilter = NearestFilter;
|
|
@@ -264,50 +317,5 @@ function makeVATTexture(data, width, height, type = FloatType) {
|
|
|
264
317
|
tex.needsUpdate = true;
|
|
265
318
|
return tex;
|
|
266
319
|
}
|
|
267
|
-
function serializeVAT(vat, { precision = "float16" } = {}) {
|
|
268
|
-
const pos = vat.positionTexture.image.data;
|
|
269
|
-
const nrm = vat.normalTexture.image.data;
|
|
270
|
-
const manifest = {
|
|
271
|
-
version: 1,
|
|
272
|
-
vertexCount: vat.vertexCount,
|
|
273
|
-
totalFrames: vat.totalFrames,
|
|
274
|
-
encoding: vat.encoding,
|
|
275
|
-
precision,
|
|
276
|
-
clips: vat.clips,
|
|
277
|
-
bounds: {
|
|
278
|
-
min: [vat.bounds.min.x, vat.bounds.min.y, vat.bounds.min.z],
|
|
279
|
-
max: [vat.bounds.max.x, vat.bounds.max.y, vat.bounds.max.z]
|
|
280
|
-
}
|
|
281
|
-
};
|
|
282
|
-
return { manifest, position: encode(pos, precision), normal: encode(nrm, precision) };
|
|
283
|
-
}
|
|
284
|
-
function encode(src, precision) {
|
|
285
|
-
if (precision === "float32") {
|
|
286
|
-
return src.slice().buffer;
|
|
287
|
-
}
|
|
288
|
-
const half = new Uint16Array(src.length);
|
|
289
|
-
for (let i = 0; i < src.length; i++) half[i] = DataUtils.toHalfFloat(src[i]);
|
|
290
|
-
return half.buffer;
|
|
291
|
-
}
|
|
292
|
-
function loadVAT(serialized) {
|
|
293
|
-
const { manifest, position, normal } = serialized;
|
|
294
|
-
const { vertexCount, totalFrames, precision } = manifest;
|
|
295
|
-
const type = precision === "float32" ? FloatType : HalfFloatType;
|
|
296
|
-
const posData = precision === "float32" ? new Float32Array(position) : new Uint16Array(position);
|
|
297
|
-
const nrmData = precision === "float32" ? new Float32Array(normal) : new Uint16Array(normal);
|
|
298
|
-
const bounds = new Box3(
|
|
299
|
-
new Vector3().fromArray(manifest.bounds.min),
|
|
300
|
-
new Vector3().fromArray(manifest.bounds.max)
|
|
301
|
-
);
|
|
302
|
-
return {
|
|
303
|
-
positionTexture: makeVATTexture(posData, vertexCount, totalFrames, type),
|
|
304
|
-
normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames, type),
|
|
305
|
-
clips: manifest.clips,
|
|
306
|
-
bounds,
|
|
307
|
-
vertexCount,
|
|
308
|
-
totalFrames,
|
|
309
|
-
encoding: manifest.encoding
|
|
310
|
-
};
|
|
311
|
-
}
|
|
312
320
|
|
|
313
|
-
export { MAX_TEXTURE_SIZE, bakeVAT,
|
|
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 };
|
package/dist/tsl.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { BufferGeometry, InstancedMesh } from 'three';
|
|
1
2
|
import { Node } from 'three/webgpu';
|
|
2
|
-
import {
|
|
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
5
|
/**
|
|
6
6
|
* The real maximum texture dimension this renderer accepts, for
|
|
@@ -16,39 +16,149 @@ declare function getMaxTextureSize(renderer: object): number;
|
|
|
16
16
|
type FloatNode = Node<'float'>;
|
|
17
17
|
/** A fluent TSL vec3 node. */
|
|
18
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;
|
|
19
27
|
interface VATNodeOptions {
|
|
20
28
|
/**
|
|
21
|
-
* Elapsed-time uniform
|
|
29
|
+
* Elapsed-time uniform (seconds). Create once with `uniform(0)` and set
|
|
22
30
|
* `.value` per frame. Defaults to a fresh `uniform(0)` you can read back.
|
|
23
31
|
*/
|
|
24
|
-
time?:
|
|
25
|
-
/**
|
|
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
|
+
*/
|
|
26
63
|
clipIndex?: number;
|
|
27
64
|
/**
|
|
28
65
|
* Max random per-instance time offset in seconds, hashed from `instanceIndex`.
|
|
29
|
-
* `0` (default) plays every instance in lockstep.
|
|
66
|
+
* `0` (default) plays every instance in lockstep. Ignored when `geometry`
|
|
67
|
+
* carries instance playback.
|
|
30
68
|
*/
|
|
31
69
|
desync?: number;
|
|
32
70
|
}
|
|
33
71
|
/** Position/normal nodes to assign onto a `MeshStandardNodeMaterial` (or similar). */
|
|
34
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
|
+
*/
|
|
35
92
|
positionNode: Vec3Node;
|
|
36
|
-
normalNode: Vec3Node;
|
|
37
93
|
/** The time uniform in use — set `.value` each frame. */
|
|
38
|
-
time:
|
|
94
|
+
time: VATTimeUniform;
|
|
39
95
|
}
|
|
40
96
|
/**
|
|
41
97
|
* Build TSL decode nodes for a baked VAT, for the WebGPU/TSL renderer path.
|
|
42
|
-
*
|
|
43
|
-
* attributes needed. Shadows work automatically because `positionNode` also
|
|
44
|
-
* feeds the depth pass.
|
|
98
|
+
* Shadows work automatically because `positionNode` also feeds the depth pass.
|
|
45
99
|
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
* 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`.
|
|
49
105
|
*
|
|
50
|
-
*
|
|
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.
|
|
51
108
|
*/
|
|
52
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;
|
|
53
163
|
|
|
54
|
-
export { type VATNodeOptions, type VATNodes, getMaxTextureSize, vatNodes };
|
|
164
|
+
export { type CreateVATMeshOptions, type VATNodeOptions, type VATNodes, type VATTimeUniform, createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
|
package/dist/tsl.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
-
// src/tsl.ts
|
|
4
5
|
function getMaxTextureSize(renderer) {
|
|
5
6
|
const backend = renderer.backend;
|
|
6
7
|
const device = backend?.["device"];
|
|
@@ -9,30 +10,78 @@ function getMaxTextureSize(renderer) {
|
|
|
9
10
|
if (gl) return gl.getParameter(gl.MAX_TEXTURE_SIZE);
|
|
10
11
|
throw new Error("three-vat: renderer has no initialized backend \u2014 call `await renderer.init()` first");
|
|
11
12
|
}
|
|
12
|
-
|
|
13
|
-
|
|
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) {
|
|
14
25
|
const clip = vat.clips[clipIndex];
|
|
15
26
|
if (!clip) {
|
|
16
27
|
throw new Error(`three-vat: clipIndex ${clipIndex} out of range (${vat.clips.length} clips)`);
|
|
17
28
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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);
|
|
22
63
|
const vertexRow = int(vertexIndex);
|
|
23
64
|
const sample = (tex) => {
|
|
24
|
-
const t = time.add(
|
|
65
|
+
const t = time.mul(playback.speed).add(playback.timeOffset).div(playback.duration).fract().mul(playback.frames);
|
|
25
66
|
const f0 = int(t);
|
|
26
|
-
const f1 = int(f0.add(1).toFloat().mod(frames));
|
|
27
|
-
const s0 = textureLoad(tex, ivec2(vertexRow, f0.add(startFrame))).xyz;
|
|
28
|
-
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;
|
|
29
70
|
return mix(s0, s1, t.fract());
|
|
30
71
|
};
|
|
31
72
|
return {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
time
|
|
73
|
+
position: sample(vat.positionTexture),
|
|
74
|
+
normal: sample(vat.normalTexture).normalize()
|
|
35
75
|
};
|
|
36
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
|
+
}
|
|
37
86
|
|
|
38
|
-
export { getMaxTextureSize, vatNodes };
|
|
87
|
+
export { createVATMesh, getMaxTextureSize, vatDecode, vatNodes };
|
package/dist/webgl.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { IUniform,
|
|
2
|
-
import {
|
|
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
4
|
/**
|
|
5
5
|
* The real maximum texture dimension this GPU accepts, for
|
|
@@ -16,19 +16,20 @@ 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
|
-
/** Per-instance playback state consumed by the patched shader. */
|
|
20
|
-
interface VATInstance {
|
|
21
|
-
clip: Pick<VAT['clips'][number], 'startFrame' | 'frames' | 'fps'>;
|
|
22
|
-
/** Phase offset in seconds — desyncs the crowd. */
|
|
23
|
-
timeOffset: number;
|
|
24
|
-
/** Playback rate multiplier. */
|
|
25
|
-
speed: number;
|
|
26
|
-
}
|
|
27
19
|
/**
|
|
28
|
-
*
|
|
29
|
-
*
|
|
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.
|
|
30
31
|
*/
|
|
31
|
-
|
|
32
|
+
type VATInstance = VATInstance$1;
|
|
32
33
|
/**
|
|
33
34
|
* Patch any built-in material so its vertex stage samples the VAT instead of
|
|
34
35
|
* skinning. Works on the render material and on `MeshDepthMaterial` (needed for
|
|
@@ -43,5 +44,45 @@ declare function patchVATMaterial<T extends Material>(material: T, vat: VAT, uni
|
|
|
43
44
|
* `MeshDistanceMaterial`).
|
|
44
45
|
*/
|
|
45
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;
|
|
46
87
|
|
|
47
|
-
export { type VATInstance, type VATUniforms, addInstancedVATAttributes, createVATDepthMaterial, createVATUniforms, getMaxTextureSize, patchVATMaterial };
|
|
88
|
+
export { type CreateVATMeshOptions, type VATInstance, type VATUniforms, addInstancedVATAttributes, createVATDepthMaterial, createVATMesh, createVATUniforms, getMaxTextureSize, patchVATMaterial };
|
package/dist/webgl.js
CHANGED
|
@@ -1,35 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { addVATInstanceAttributes, createCrowdGeometry } from './chunk-SXTVASKG.js';
|
|
2
|
+
import { MeshDepthMaterial, RGBADepthPacking, InstancedMesh, MeshDistanceMaterial } from 'three';
|
|
2
3
|
|
|
3
|
-
// src/webgl.ts
|
|
4
4
|
function getMaxTextureSize(renderer) {
|
|
5
5
|
return renderer.capabilities.maxTextureSize;
|
|
6
6
|
}
|
|
7
7
|
function createVATUniforms(time = 0) {
|
|
8
8
|
return { uVatTime: { value: time } };
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
geometry.morphAttributes = {};
|
|
12
|
-
geometry.morphTargetsRelative = false;
|
|
13
|
-
const n = instances.length;
|
|
14
|
-
const clipStart = new Float32Array(n);
|
|
15
|
-
const clipFrames = new Float32Array(n);
|
|
16
|
-
const clipFps = new Float32Array(n);
|
|
17
|
-
const timeOffset = new Float32Array(n);
|
|
18
|
-
const speed = new Float32Array(n);
|
|
19
|
-
for (let i = 0; i < n; i++) {
|
|
20
|
-
const inst = instances[i];
|
|
21
|
-
clipStart[i] = inst.clip.startFrame;
|
|
22
|
-
clipFrames[i] = inst.clip.frames;
|
|
23
|
-
clipFps[i] = inst.clip.fps;
|
|
24
|
-
timeOffset[i] = inst.timeOffset;
|
|
25
|
-
speed[i] = inst.speed;
|
|
26
|
-
}
|
|
27
|
-
geometry.setAttribute("aClipStart", new InstancedBufferAttribute(clipStart, 1));
|
|
28
|
-
geometry.setAttribute("aClipFrames", new InstancedBufferAttribute(clipFrames, 1));
|
|
29
|
-
geometry.setAttribute("aClipFps", new InstancedBufferAttribute(clipFps, 1));
|
|
30
|
-
geometry.setAttribute("aTimeOffset", new InstancedBufferAttribute(timeOffset, 1));
|
|
31
|
-
geometry.setAttribute("aSpeed", new InstancedBufferAttribute(speed, 1));
|
|
32
|
-
}
|
|
10
|
+
var addInstancedVATAttributes = addVATInstanceAttributes;
|
|
33
11
|
var DECODE_PRELUDE = (
|
|
34
12
|
/* glsl */
|
|
35
13
|
`
|
|
@@ -82,5 +60,14 @@ function createVATDepthMaterial(vat, uniforms) {
|
|
|
82
60
|
patchVATMaterial(depth, vat, uniforms);
|
|
83
61
|
return depth;
|
|
84
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
|
+
}
|
|
85
72
|
|
|
86
|
-
export { addInstancedVATAttributes, createVATDepthMaterial, createVATUniforms, getMaxTextureSize, 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.
|
|
3
|
+
"version": "1.0.1",
|
|
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",
|
|
@@ -52,22 +52,24 @@
|
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/three": "^0.185.0",
|
|
55
|
+
"gifenc": "1.0.3",
|
|
56
|
+
"playwright-core": "1.63.0",
|
|
57
|
+
"pngjs": "7.0.0",
|
|
55
58
|
"three": "^0.185.0",
|
|
56
59
|
"tsup": "^8.3.0",
|
|
57
60
|
"typescript": "^5.6.0",
|
|
61
|
+
"vite": "^7.1.0",
|
|
58
62
|
"vitest": "^2.1.0"
|
|
59
63
|
},
|
|
60
64
|
"engines": {
|
|
61
65
|
"node": ">=18"
|
|
62
66
|
},
|
|
63
67
|
"scripts": {
|
|
68
|
+
"dev": "pnpm --filter three-vat-example dev",
|
|
69
|
+
"test": "vitest run && pnpm --filter three-vat-example test",
|
|
64
70
|
"build": "tsup",
|
|
65
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"test:watch": "vitest"
|
|
68
|
-
"typecheck": "tsc --noEmit",
|
|
69
|
-
"release": "pnpm publish --otp=\"${NPM_OTP:?set NPM_OTP=<code from your authenticator>}\" && pnpm verify:published",
|
|
70
|
-
"verify:published": "node scripts/verify-published.mjs",
|
|
71
|
-
"example": "pnpm --dir examples dev"
|
|
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"
|
|
72
74
|
}
|
|
73
75
|
}
|
package/dist/types-wVmIj2tC.d.ts
DELETED
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { DataTexture, Box3, BufferGeometry, Material } 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
|
-
* What {@link bakeVAT} returns: a VAT plus the geometry it was baked against.
|
|
46
|
-
*
|
|
47
|
-
* The merged vertex ordering is the baker's own invention and the textures are
|
|
48
|
-
* indexed by it (`x = gl_VertexID`), so the caller can no longer bring its own
|
|
49
|
-
* geometry — it must render the one baked here. `materials` is ordered to match
|
|
50
|
-
* `geometry.groups[].materialIndex`, giving one draw call per material.
|
|
51
|
-
*
|
|
52
|
-
* `loadVAT` returns a plain {@link VAT} without these, which is precisely why the
|
|
53
|
-
* offline format is deprecated and removed in 1.0 (ADR-0010): a serialized VAT
|
|
54
|
-
* cannot be rendered without re-running the merge that produced its ordering.
|
|
55
|
-
*/
|
|
56
|
-
interface BakedVAT extends VAT {
|
|
57
|
-
/** Merged, root-space rest-pose geometry. Its `position` is the delta reference. */
|
|
58
|
-
geometry: BufferGeometry;
|
|
59
|
-
/** Source materials, indexed by `geometry.groups[].materialIndex`. */
|
|
60
|
-
materials: Material[];
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export type { BakedVAT as B, VATClip as V, VAT as a };
|