three-vat 1.0.1 → 2.1.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 +72 -4
- package/dist/carrier-BXaLAPRO.d.ts +515 -0
- package/dist/chunk-3PWAY6MD.js +211 -0
- package/dist/chunk-I4STYOD5.js +53 -0
- package/dist/index.d.ts +113 -14
- package/dist/index.js +622 -68
- package/dist/tsl.d.ts +82 -47
- package/dist/tsl.js +176 -40
- package/dist/webgl.d.ts +31 -26
- package/dist/webgl.js +295 -35
- package/package.json +20 -14
- package/dist/chunk-SXTVASKG.js +0 -40
- package/dist/instance-playback-BrGBIKLe.d.ts +0 -109
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ Bake a glTF `AnimationClip` into GPU textures and animate **hundreds or thousand
|
|
|
16
16
|
npm install three-vat three
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
`three` (>= 0.
|
|
19
|
+
`three` (>= 0.186) is a peer dependency.
|
|
20
20
|
|
|
21
21
|
## One crowd, start to finish
|
|
22
22
|
|
|
@@ -46,10 +46,13 @@ const vat = bakeVAT(gltf.scene, gltf.animations, {
|
|
|
46
46
|
maxTextureSize: getMaxTextureSize(renderer), // this GPU's real ceiling
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
// One entry per character: which clip it plays,
|
|
49
|
+
// One entry per character: which clip it plays, when it started, its rate.
|
|
50
|
+
// Everything but `startTime` is optional — a clip baked from a configured
|
|
51
|
+
// `AnimationAction` carries its own loop, repetition count, end behaviour and
|
|
52
|
+
// speed, and an instance overrides only what it wants to differ.
|
|
50
53
|
const instances = Array.from({ length: 500 }, (_, i) => ({
|
|
51
54
|
clip: vat.clips[i % vat.clips.length],
|
|
52
|
-
|
|
55
|
+
startTime: -Math.random() * 2, // began a moment ago, so the crowd is not in lockstep
|
|
53
56
|
speed: 0.9 + Math.random() * 0.2,
|
|
54
57
|
}))
|
|
55
58
|
|
|
@@ -78,6 +81,11 @@ Two things worth knowing the first time:
|
|
|
78
81
|
- **Materials are never merged.** A 500-robot crowd with 3 materials is 3 draw
|
|
79
82
|
calls — not 1, and not 500. VAT collapses instance count, not material count.
|
|
80
83
|
|
|
84
|
+
**A skinned character?** There is a second encoding: `{ encoding: 'rig' }` bakes
|
|
85
|
+
the posed rig instead of the posed vertices — two orders of magnitude less
|
|
86
|
+
texture, a bake in milliseconds, and faster on a phone, for an asset a rig can
|
|
87
|
+
express. [The rig encoding](./docs/usage.md#the-rig-encoding-encoding-rig).
|
|
88
|
+
|
|
81
89
|
<details>
|
|
82
90
|
<summary><b>Does it work with my model?</b></summary>
|
|
83
91
|
|
|
@@ -97,6 +105,65 @@ of `gltf.animations` rather than spending texture rows on them.
|
|
|
97
105
|
|
|
98
106
|
</details>
|
|
99
107
|
|
|
108
|
+
<details>
|
|
109
|
+
<summary><b>Per-clip playback defaults</b></summary>
|
|
110
|
+
|
|
111
|
+
`bakeVAT` takes an `AnimationClip` **or** an `AnimationAction`, in the same
|
|
112
|
+
array. Configure the action the way three already taught you, and every instance
|
|
113
|
+
of that clip inherits it — and overrides any field it names.
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
const death = mixer.clipAction(deathClip)
|
|
117
|
+
death.loop = THREE.LoopOnce
|
|
118
|
+
|
|
119
|
+
const vat = bakeVAT(gltf.scene, [walkClip, death, idleClip])
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
`loop`, `repetitions` and `timeScale` are read; `time` and `paused` are ignored,
|
|
123
|
+
because a VAT has no playhead of its own to seed; a non-unit `weight` or an
|
|
124
|
+
additive `blendMode` throws, because one baked band cannot be several actions
|
|
125
|
+
blended at once.
|
|
126
|
+
|
|
127
|
+
**Both inputs clamp, where three rewinds.** `clampWhenFinished` defaults to
|
|
128
|
+
`false` in three, which means an untouched action says nothing about the end
|
|
129
|
+
either — and a crowd's answer to nothing is to hold the last frame, because a
|
|
130
|
+
corpse standing back up is the worse default. So the end mode is not read off
|
|
131
|
+
the action; an instance asks for three's rewind with `endMode: EndMode.Rewind`,
|
|
132
|
+
which is the finer grain anyway.
|
|
133
|
+
|
|
134
|
+
[Declaring the defaults at the bake](./docs/usage.md#declaring-the-defaults-at-the-bake).
|
|
135
|
+
|
|
136
|
+
</details>
|
|
137
|
+
|
|
138
|
+
<details>
|
|
139
|
+
<summary><b>Upgrading from 1.x</b></summary>
|
|
140
|
+
|
|
141
|
+
The breaks that reach a 1.x caller, no shims — the package had no users on the
|
|
142
|
+
1.x playback contract, so 2.0 spells it one way rather than two. The full list
|
|
143
|
+
is [the CHANGELOG's 2.0.0 entry](./CHANGELOG.md).
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
{ clip, timeOffset: 1.4, speed: 1 } // 1.x — every field required
|
|
147
|
+
{ clip, startTime: -1.4 } // 2.0 — desync is a start time in the past
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`timeOffset` is gone (`startTime: -timeOffset / speed`), and with it
|
|
151
|
+
`aTimeOffset`. The pack is three `vec4`s — clip, playback, fade — in a
|
|
152
|
+
**playback texture** keyed by the instance's logical index, not instanced
|
|
153
|
+
attributes, which are indexed by the *drawn* slot:
|
|
154
|
+
`addInstancedVATAttributes` is removed, `createVATPlaybackTexture(instances)`
|
|
155
|
+
replaces `addVATInstanceAttributes`, and `setVATInstance(playback, id, instance)`
|
|
156
|
+
takes that texture — `createVATMesh` returns it as `playback` — rather than a
|
|
157
|
+
geometry. A VAT texture's `image.data` is now opaque. Node 20, where
|
|
158
|
+
1.x said 18, and `three >= 0.186`, where `batchIndirectIndex` is exported;
|
|
159
|
+
browsers are unaffected.
|
|
160
|
+
|
|
161
|
+
New, and none of it breaking: per-instance loop modes, one-shots,
|
|
162
|
+
`setVATInstance` after the crowd is built, and a crowd on a `BatchedMesh`.
|
|
163
|
+
[By hand, on either path](./docs/usage.md#by-hand-on-either-path).
|
|
164
|
+
|
|
165
|
+
</details>
|
|
166
|
+
|
|
100
167
|
<details>
|
|
101
168
|
<summary><b>Going bigger: texture limits, bake cost, Web Workers</b></summary>
|
|
102
169
|
|
|
@@ -117,7 +184,8 @@ worker recipe, the draw-call arithmetic, and the primitives underneath
|
|
|
117
184
|
<details>
|
|
118
185
|
<summary><b>What it does not do</b></summary>
|
|
119
186
|
|
|
120
|
-
No clip crossfade (
|
|
187
|
+
No clip crossfade (an instance blends out of a frozen pose, not between two
|
|
188
|
+
clips that are both playing), no LOD, no baking CLI or file
|
|
121
189
|
format, no React/drei binding, glTF input only. Each is a decision rather than a
|
|
122
190
|
gap, and each is written up with its reasoning in
|
|
123
191
|
**[docs/usage.md](./docs/usage.md#what-10-does-not-do)**, alongside the
|
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
import { DataTexture, BufferGeometry, Material, Box3, InstancedMesh, BatchedMesh } from 'three';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* How an instance repeats its clip — the numbers `THREE.LoopRepeat`,
|
|
5
|
+
* `THREE.LoopOnce` and `THREE.LoopPingPong` name, in three's own order, as
|
|
6
|
+
* values a `Float32Array` can carry.
|
|
7
|
+
*
|
|
8
|
+
* What each one *means* is {@link resolveVATFrame}, in one place, transcribed
|
|
9
|
+
* by both decode paths.
|
|
10
|
+
*/
|
|
11
|
+
declare const LoopMode: {
|
|
12
|
+
/** Play the clip end to end, forever (or `repetitions` times). */
|
|
13
|
+
readonly Repeat: 0;
|
|
14
|
+
/** Play the clip through once. */
|
|
15
|
+
readonly Once: 1;
|
|
16
|
+
/** Play forward, then backward, without baking the reversed frames. */
|
|
17
|
+
readonly PingPong: 2;
|
|
18
|
+
};
|
|
19
|
+
type LoopMode = (typeof LoopMode)[keyof typeof LoopMode];
|
|
20
|
+
/**
|
|
21
|
+
* What an instance does once it has finished — three's `clampWhenFinished`, as
|
|
22
|
+
* a pair of numbers.
|
|
23
|
+
*
|
|
24
|
+
* `Clamp` is first because it is this library's default, where three's
|
|
25
|
+
* `clampWhenFinished` defaults to `false`. A one-shot in a crowd — a death, an
|
|
26
|
+
* impact — almost always has to *stay* in its final state, and a rewinding
|
|
27
|
+
* corpse standing back up is the failure a crowd library should not ship by
|
|
28
|
+
* default. The divergence is deliberate.
|
|
29
|
+
*/
|
|
30
|
+
declare const EndMode: {
|
|
31
|
+
/** Hold the last frame. */
|
|
32
|
+
readonly Clamp: 0;
|
|
33
|
+
/** Return to the first frame. */
|
|
34
|
+
readonly Rewind: 1;
|
|
35
|
+
};
|
|
36
|
+
type EndMode = (typeof EndMode)[keyof typeof EndMode];
|
|
37
|
+
/**
|
|
38
|
+
* An endless repeat count, as the pack spells it. `Infinity` does not survive a
|
|
39
|
+
* `Float32Array` usefully, so it is converted once, here, at the boundary.
|
|
40
|
+
*/
|
|
41
|
+
declare const INFINITE_REPETITIONS = -1;
|
|
42
|
+
/**
|
|
43
|
+
* The longest fade {@link setVATInstance} will honour, in seconds.
|
|
44
|
+
*
|
|
45
|
+
* The cap exists because of what this fade *is*: one frozen pose of the
|
|
46
|
+
* outgoing clip, blended away — not a second playback running alongside the
|
|
47
|
+
* first. Over a tenth of a second that is invisible; over half a second the
|
|
48
|
+
* instance visibly skates, because whatever it was doing stopped dead the
|
|
49
|
+
* moment the transition began. A longer fade would not be a better fade, it
|
|
50
|
+
* would be a more visible bug, so the number is clamped rather than trusted.
|
|
51
|
+
*
|
|
52
|
+
* Provisional, like the fade itself: a real two-clip crossfade (#30) replaces
|
|
53
|
+
* both, and nothing else should be built on top of them.
|
|
54
|
+
*/
|
|
55
|
+
declare const MAX_FADE_DURATION = 0.25;
|
|
56
|
+
/**
|
|
57
|
+
* The frozen pose a fade blends away from: one phase of the clip an instance
|
|
58
|
+
* was playing when its animation changed, and the band that phase indexes.
|
|
59
|
+
*
|
|
60
|
+
* Not a second playback state — there is no start time and no speed here,
|
|
61
|
+
* because nothing about it moves. That is the whole of the freeze, and the
|
|
62
|
+
* whole of its limit; see {@link MAX_FADE_DURATION}.
|
|
63
|
+
*/
|
|
64
|
+
interface VATFadeFrom {
|
|
65
|
+
/** First texture row of the outgoing clip's band. */
|
|
66
|
+
startFrame: number;
|
|
67
|
+
/** Rows in that band. */
|
|
68
|
+
frames: number;
|
|
69
|
+
/** The phase of that band the instance was at, in `[0, 1]`. */
|
|
70
|
+
phase: number;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Per-instance playback state consumed by both decode paths.
|
|
74
|
+
*
|
|
75
|
+
* Every policy field is optional because the clip already answers it: a bake
|
|
76
|
+
* handed a configured `AnimationAction` records the answer in the clip table
|
|
77
|
+
* ({@link VATClipDefaults}), and an instance that says nothing inherits it. A
|
|
78
|
+
* crowd of a thousand deaths says "once, clamped" once, at the bake.
|
|
79
|
+
*/
|
|
80
|
+
interface VATInstance {
|
|
81
|
+
/**
|
|
82
|
+
* The clip band to play, straight out of `vat.clips`. Its playback defaults
|
|
83
|
+
* come along with it; a clip table assembled by hand may carry none, and then
|
|
84
|
+
* the library defaults below apply.
|
|
85
|
+
*/
|
|
86
|
+
clip: Pick<VAT['clips'][number], 'startFrame' | 'frames' | 'fps'> & Partial<VATClipDefaults>;
|
|
87
|
+
/**
|
|
88
|
+
* Absolute clock time, in seconds, at which this animation began. May be in
|
|
89
|
+
* the past — and **desync is exactly that**: give each instance of a crowd its
|
|
90
|
+
* own start time a little way back and they stop moving in lockstep.
|
|
91
|
+
*
|
|
92
|
+
* The one field with no clip-level default: when an animation began is a fact
|
|
93
|
+
* about the instance and nothing else.
|
|
94
|
+
*/
|
|
95
|
+
startTime: number;
|
|
96
|
+
/**
|
|
97
|
+
* Playback rate multiplier, `>= 0`. Defaults to the clip's speed, then to
|
|
98
|
+
* `1`. A negative rate is refused when the instance is written: a VAT plays
|
|
99
|
+
* forward, and a clip that must run backwards is baked as a reversed clip.
|
|
100
|
+
* `0` is legal — the instance holds its clip's first row.
|
|
101
|
+
*/
|
|
102
|
+
speed?: number;
|
|
103
|
+
/** How the clip repeats. Defaults to the clip's, then {@link LoopMode.Repeat}. */
|
|
104
|
+
loopMode?: LoopMode;
|
|
105
|
+
/**
|
|
106
|
+
* How many times to play the clip, or {@link INFINITE_REPETITIONS}. Defaults
|
|
107
|
+
* to the clip's count, then to endless for {@link LoopMode.Repeat} and a
|
|
108
|
+
* single play for anything else — the counts three's own `LoopRepeat` and
|
|
109
|
+
* `LoopOnce` imply.
|
|
110
|
+
*
|
|
111
|
+
* A count belongs to the mode it was configured under: replace the clip's
|
|
112
|
+
* `loopMode` and say nothing here, and the count comes from the new mode
|
|
113
|
+
* rather than from the clip — a one-shot over a looping clip finishes.
|
|
114
|
+
*/
|
|
115
|
+
repetitions?: number;
|
|
116
|
+
/**
|
|
117
|
+
* What to do once the repetitions run out. Defaults to the clip's, then
|
|
118
|
+
* {@link EndMode.Clamp} — where three's `clampWhenFinished` defaults to
|
|
119
|
+
* `false`; see {@link EndMode}.
|
|
120
|
+
*/
|
|
121
|
+
endMode?: EndMode;
|
|
122
|
+
/**
|
|
123
|
+
* The frozen outgoing pose to fade away from. Normally you do not write this
|
|
124
|
+
* yourself: {@link setVATInstance} freezes whatever the instance was playing
|
|
125
|
+
* and fills it in when you ask for a {@link fadeDuration}.
|
|
126
|
+
*/
|
|
127
|
+
from?: VATFadeFrom;
|
|
128
|
+
/**
|
|
129
|
+
* Seconds to blend {@link from} away over, capped at {@link MAX_FADE_DURATION}.
|
|
130
|
+
* Wall-clock seconds from {@link startTime}: the clip's `speed` does not
|
|
131
|
+
* stretch a fade.
|
|
132
|
+
*
|
|
133
|
+
* Ignored without a `from` to fade away from — and at creation there is
|
|
134
|
+
* nothing to fade away from, so this is `setVATInstance`'s field in practice.
|
|
135
|
+
*/
|
|
136
|
+
fadeDuration?: number;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Where in its VAT an instance is at a given moment: the two frame rows to
|
|
140
|
+
* sample, the blend between them, and the two facts a decode cannot re-derive
|
|
141
|
+
* from the rows alone.
|
|
142
|
+
*/
|
|
143
|
+
interface VATFrame {
|
|
144
|
+
/** The frame row to sample — an absolute texture row, clip band included. */
|
|
145
|
+
row: number;
|
|
146
|
+
/** The row it interpolates toward. */
|
|
147
|
+
rowNext: number;
|
|
148
|
+
/** Blend between {@link row} and {@link rowNext}, in `[0, 1)`. */
|
|
149
|
+
mix: number;
|
|
150
|
+
/**
|
|
151
|
+
* Whether {@link rowNext} crossed the clip's last row back into its first.
|
|
152
|
+
* True only while a clip is genuinely looping: a ping-pong bounces rather
|
|
153
|
+
* than wraps, and a finished one-shot must not wrap at all or the corpse
|
|
154
|
+
* stands back up for a frame.
|
|
155
|
+
*/
|
|
156
|
+
wraps: boolean;
|
|
157
|
+
/** Whether the repetitions have run out and the instance is holding an end pose. */
|
|
158
|
+
finished: boolean;
|
|
159
|
+
/** How far through the clip this is, in `[0, 1]` — what {@link row} is derived from. */
|
|
160
|
+
phase: number;
|
|
161
|
+
/**
|
|
162
|
+
* The frozen outgoing row a fade blends away from. Equal to {@link row} when
|
|
163
|
+
* the instance is not fading, so a reader that ignores {@link fade} — the
|
|
164
|
+
* demo's texture-panel cursors among them — never points at a row this
|
|
165
|
+
* instance is not sampling.
|
|
166
|
+
*/
|
|
167
|
+
fadeRow: number;
|
|
168
|
+
/**
|
|
169
|
+
* How much of {@link fadeRow} is still showing: `1` at the moment of the
|
|
170
|
+
* write, falling to `0` across `fadeDuration`, and `0` for an instance that
|
|
171
|
+
* is not fading. The decode mixes the sampled clip toward the frozen pose by
|
|
172
|
+
* exactly this weight — the weight of the fade, not the fade itself, which is
|
|
173
|
+
* the pose-freeze fade `CONTEXT.md` names.
|
|
174
|
+
*/
|
|
175
|
+
fadeWeight: number;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* What the vertex shader computes, as a pure function of `(instance, time)` —
|
|
179
|
+
* the **one definition** of the playback semantics. Both decode paths
|
|
180
|
+
* transcribe it (`DECODE_PRELUDE` in src/webgl.ts, `vatDecode` in src/tsl.ts);
|
|
181
|
+
* neither invents it.
|
|
182
|
+
*
|
|
183
|
+
* It exists in TypeScript because the arithmetic is otherwise reachable only
|
|
184
|
+
* inside a GLSL string and a TSL node graph, neither of which CI can evaluate
|
|
185
|
+
* without a GPU — and because a caller scheduling what happens after a one-shot
|
|
186
|
+
* needs to ask the same question the shader answers.
|
|
187
|
+
*
|
|
188
|
+
* There is no accumulated state anywhere in here: an instance is written once,
|
|
189
|
+
* at the moment its animation changes, and every frame after that is this
|
|
190
|
+
* function of the shared clock.
|
|
191
|
+
*/
|
|
192
|
+
declare function resolveVATFrame(instance: VATInstance, time: number): VATFrame;
|
|
193
|
+
/**
|
|
194
|
+
* What carries the pack to the shader: one `DataTexture`, three texels wide,
|
|
195
|
+
* one row per instance, read by the instance's *logical* index (ADR-0016).
|
|
196
|
+
*
|
|
197
|
+
* Held by the caller rather than hidden behind the geometry, because a
|
|
198
|
+
* `BufferGeometry` has nowhere to put a texture and a side channel — a
|
|
199
|
+
* `WeakMap`, or a `userData` the first `clone()` loses — would not say what
|
|
200
|
+
* {@link setVATInstance} writes into. `createVATMesh` hands one back on either
|
|
201
|
+
* decode path; build your own with {@link createVATPlaybackTexture} when you
|
|
202
|
+
* are assembling a crowd by hand.
|
|
203
|
+
*/
|
|
204
|
+
interface VATPlaybackTexture {
|
|
205
|
+
/**
|
|
206
|
+
* The texture both decode paths bind: `x = field`, `y = instance`, RGBA
|
|
207
|
+
* float, {@link PACK_WIDTH} texels wide.
|
|
208
|
+
*/
|
|
209
|
+
texture: DataTexture;
|
|
210
|
+
/** Instances it carries — the rows of {@link texture}. */
|
|
211
|
+
count: number;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Write a crowd's instance playback into a new playback texture. Call once,
|
|
215
|
+
* before rendering, and bind the result into the decode — `createVATMesh` does
|
|
216
|
+
* both for you.
|
|
217
|
+
*
|
|
218
|
+
* The layout below is the shared contract, spelled once in {@link PACK_TEXELS}.
|
|
219
|
+
* Both decode paths read exactly these three texels of row `instanceIndex` —
|
|
220
|
+
* `DECODE_PRELUDE` in `src/webgl.ts` with `texelFetch`, `texturePlayback` in
|
|
221
|
+
* `src/tsl.ts` with `textureLoad`.
|
|
222
|
+
*
|
|
223
|
+
* | Texel | r | g | b | a |
|
|
224
|
+
* | ---------------- | ------------------- | ------------ | ------------- | ------------- |
|
|
225
|
+
* | `x = 0` clip | clip start row | clip frames | clip fps | speed |
|
|
226
|
+
* | `x = 1` playback | start time | loop mode | repetitions | end mode |
|
|
227
|
+
* | `x = 2` fade | from clip start row | from frames | from phase | fade duration |
|
|
228
|
+
*
|
|
229
|
+
* **A texture, not three instanced attributes.** An attribute with divisor 1 is
|
|
230
|
+
* indexed by the *drawn slot*, and the drawn slot stops being the instance the
|
|
231
|
+
* moment a renderer culls or sorts per instance — on a `BatchedMesh` every
|
|
232
|
+
* vertex would read element 0 and the whole crowd would play instance 0's clip
|
|
233
|
+
* (ADR-0016). A row keyed by the logical index is what three itself does for
|
|
234
|
+
* the same problem, in `_matricesTexture`.
|
|
235
|
+
*
|
|
236
|
+
* **Three texels, not thirteen floats.** The move to a texture touched no
|
|
237
|
+
* decode arithmetic, because the layout did not change with it: the pack was
|
|
238
|
+
* already three RGBA-shaped `vec4`s (ADR-0009). Published 1.x is the other
|
|
239
|
+
* story — five one-float attributes there, so a 1.x caller meets both changes
|
|
240
|
+
* at once.
|
|
241
|
+
*
|
|
242
|
+
* **`FloatType`, and it stays that way.** A `startTime` in seconds does not
|
|
243
|
+
* survive half precision — one second of resolution at 2 048 s — so a narrower
|
|
244
|
+
* encoding for the VAT textures does not reach this one.
|
|
245
|
+
*
|
|
246
|
+
* The policy fields, and the clip texel's speed, come from the instance where
|
|
247
|
+
* it names them and from the clip's baked defaults where it does not — resolved
|
|
248
|
+
* in the one place those tiers are spelled — and both decode paths read them as
|
|
249
|
+
* {@link resolveVATFrame} defines them. The fade texel is written as zeroes,
|
|
250
|
+
* which is what "not fading" is: a crowd being created has no pose to fade away
|
|
251
|
+
* from. Fades belong to {@link setVATInstance}, where an instance's animation
|
|
252
|
+
* changes and there is something to fade out of.
|
|
253
|
+
*/
|
|
254
|
+
declare function createVATPlaybackTexture(instances: VATInstance[]): VATPlaybackTexture;
|
|
255
|
+
/**
|
|
256
|
+
* Change one instance's animation, after the crowd is built. The single write
|
|
257
|
+
* the whole event-driven half of this library is made of: an enemy hit at
|
|
258
|
+
* `t = 12.3s` becomes a dying enemy here, and the CPU does not touch it again.
|
|
259
|
+
*
|
|
260
|
+
* ```ts
|
|
261
|
+
* // the moment it is hit — and nothing per frame afterwards
|
|
262
|
+
* setVATInstance(playback, enemyId, {
|
|
263
|
+
* clip: vat.clips[2], // "once, clamped" came with the bake
|
|
264
|
+
* startTime: time.value,
|
|
265
|
+
* fadeDuration: 0.1, // blend out of whatever it was doing
|
|
266
|
+
* })
|
|
267
|
+
* ```
|
|
268
|
+
*
|
|
269
|
+
* `playback` is the crowd's playback texture — `createVATMesh` hands it back
|
|
270
|
+
* beside the mesh and the clock — and `index` the instance's index in the array
|
|
271
|
+
* the crowd was built from, the same one `setMatrixAt` takes.
|
|
272
|
+
*
|
|
273
|
+
* Only that instance's row is marked for upload, so a crowd of a thousand costs
|
|
274
|
+
* one small write rather than a full re-upload. Everything else about the
|
|
275
|
+
* instance — its matrix, its clip's defaults — is untouched. On the TSL path
|
|
276
|
+
* the range is recorded and ignored: three's WebGPU backend re-uploads the
|
|
277
|
+
* whole image on `needsUpdate`, which is 48 bytes per instance once per frame
|
|
278
|
+
* in which anything changed (docs/usage.md says what that costs).
|
|
279
|
+
*
|
|
280
|
+
* Ask for a `fadeDuration` and the pose the instance is in *at `startTime`* is
|
|
281
|
+
* frozen and blended away over that many wall-clock seconds, so the change does
|
|
282
|
+
* not pop. It is a frozen pose and not a second playback: see
|
|
283
|
+
* {@link MAX_FADE_DURATION} for what that costs and how far it can be pushed.
|
|
284
|
+
* One pose, too — writing an instance that is *already* fading freezes the clip
|
|
285
|
+
* it had switched to and drops the older pose, because the pack holds one.
|
|
286
|
+
*
|
|
287
|
+
* A written instance is a pure function of the clock from here on, so what
|
|
288
|
+
* happens *after* it is a matter of scheduling one more of these writes —
|
|
289
|
+
* {@link endsAt} says exactly when. Chaining stays yours: the GPU never learns
|
|
290
|
+
* about a next clip.
|
|
291
|
+
*
|
|
292
|
+
* This is deliberately a function over a playback texture rather than an
|
|
293
|
+
* `InstancedMesh` method. The primitives stay composable for a crowd rendered
|
|
294
|
+
* onto something else, which is the escape hatch ADR-0009 and ADR-0014 commit
|
|
295
|
+
* to — and the playback texture is the object such a caller holds (ADR-0016).
|
|
296
|
+
*/
|
|
297
|
+
declare function setVATInstance(playback: VATPlaybackTexture, index: number, instance: VATInstance): void;
|
|
298
|
+
/**
|
|
299
|
+
* The exact clock time this instance stops animating — when
|
|
300
|
+
* {@link resolveVATFrame} first reports `finished` — or `null` for an animation
|
|
301
|
+
* that never gets there: an endless loop, or a speed of zero.
|
|
302
|
+
*
|
|
303
|
+
* This is what makes chaining one clip to the next a single scheduled write
|
|
304
|
+
* rather than a per-frame poll:
|
|
305
|
+
*
|
|
306
|
+
* ```ts
|
|
307
|
+
* const hit = { clip: vat.clips[1], startTime: now, loopMode: LoopMode.Once }
|
|
308
|
+
* setVATInstance(playback, id, hit)
|
|
309
|
+
*
|
|
310
|
+
* const at = endsAt(hit)
|
|
311
|
+
* if (at !== null) schedule(at, () => setVATInstance(playback, id, { clip: walk, startTime: at }))
|
|
312
|
+
* ```
|
|
313
|
+
*
|
|
314
|
+
* Nothing about the chain reaches the GPU: it reads one pack, and the next clip
|
|
315
|
+
* does not exist to it until that write happens.
|
|
316
|
+
*/
|
|
317
|
+
declare function endsAt(instance: VATInstance): number | null;
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The playback policy a clip carries for every instance that plays it —
|
|
321
|
+
* declared once, at the bake, instead of repeated at every instance. Hand
|
|
322
|
+
* `bakeVAT` a configured `AnimationAction` rather than a bare `AnimationClip`
|
|
323
|
+
* and these come from it; hand it a clip and they are the library defaults
|
|
324
|
+
* (repeat, forever, speed 1, clamping when finished).
|
|
325
|
+
*
|
|
326
|
+
* Defaults, never decisions: an instance overrides any of them, field by field
|
|
327
|
+
* ({@link VATInstance}). Nothing in the texels changes between "once" and
|
|
328
|
+
* "forever" — a bake produces poses, and this is the policy those poses are
|
|
329
|
+
* played under.
|
|
330
|
+
*/
|
|
331
|
+
interface VATClipDefaults {
|
|
332
|
+
/** How the clip repeats, from the action's `loop`. */
|
|
333
|
+
loopMode: LoopMode;
|
|
334
|
+
/**
|
|
335
|
+
* How many times it plays, from the action's `repetitions` — `Infinity`
|
|
336
|
+
* converted to `INFINITE_REPETITIONS`, because a `Float32Array` cannot carry
|
|
337
|
+
* the former.
|
|
338
|
+
*/
|
|
339
|
+
repetitions: number;
|
|
340
|
+
/**
|
|
341
|
+
* What it does once finished — {@link EndMode.Clamp} from the bake, whichever
|
|
342
|
+
* of the two inputs it came from.
|
|
343
|
+
*
|
|
344
|
+
* three defaults `clampWhenFinished` to `false`, and a crowd's answer to a
|
|
345
|
+
* one-shot is to hold the last frame: a corpse standing back up is the worse
|
|
346
|
+
* default to ship (see {@link EndMode}). A bare `AnimationClip` says nothing,
|
|
347
|
+
* and an untouched action's `false` is not a statement either — it is what
|
|
348
|
+
* the field already holds — so the bake gives both the same answer rather
|
|
349
|
+
* than punishing the caller who configured an action. `clampWhenFinished =
|
|
350
|
+
* true` agrees with it; three's rewind is asked for per instance, with
|
|
351
|
+
* `endMode: EndMode.Rewind` (ADR-0017).
|
|
352
|
+
*/
|
|
353
|
+
endMode: EndMode;
|
|
354
|
+
/**
|
|
355
|
+
* Playback rate, from the action's `timeScale`. `>= 0`: a band is sampled
|
|
356
|
+
* forward from its own first row, so a negative rate is refused at the bake
|
|
357
|
+
* rather than held on that row for ever. `0` is a held first row, on purpose.
|
|
358
|
+
*/
|
|
359
|
+
speed: number;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* One baked animation range within a VAT's stacked frame rows, and the playback
|
|
363
|
+
* defaults every instance of it inherits.
|
|
364
|
+
*/
|
|
365
|
+
interface VATClip extends VATClipDefaults {
|
|
366
|
+
/** Clip name, taken from the source `AnimationClip`. */
|
|
367
|
+
name: string;
|
|
368
|
+
/** First frame row (y) of this clip in the texture. */
|
|
369
|
+
startFrame: number;
|
|
370
|
+
/** Number of frame rows baked for this clip. */
|
|
371
|
+
frames: number;
|
|
372
|
+
/** Effective frames-per-second of the bake (`frames / duration`). */
|
|
373
|
+
fps: number;
|
|
374
|
+
/** Source clip duration in seconds. */
|
|
375
|
+
duration: number;
|
|
376
|
+
/**
|
|
377
|
+
* Largest displacement (metres) of any vertex from the rest pose across the
|
|
378
|
+
* clip — the same number under either encoding, because the rig bake skins
|
|
379
|
+
* every vertex on the CPU for the bounds anyway and measures it there.
|
|
380
|
+
* Near-zero means the clip baked as a frozen pose — the diagnostic for a
|
|
381
|
+
* mis-targeted or genuinely static clip.
|
|
382
|
+
*/
|
|
383
|
+
maxDelta: number;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* A baked Vertex Animation Texture: the position `DataTexture` (and the normal
|
|
387
|
+
* one, unless the bake was told to skip it), the geometry they are indexed by,
|
|
388
|
+
* and the clip table and bounds needed to decode and render them. Produced
|
|
389
|
+
* exactly one way — {@link bakeVAT}, at runtime, from a loaded glTF (ADR-0010).
|
|
390
|
+
*
|
|
391
|
+
* The merged vertex ordering is the baker's own invention and the textures are
|
|
392
|
+
* indexed by it (`x = gl_VertexID`), so the caller cannot bring its own
|
|
393
|
+
* geometry — it must render the one baked here. `materials` is ordered to match
|
|
394
|
+
* `geometry.groups[].materialIndex`, giving one draw call per material.
|
|
395
|
+
*
|
|
396
|
+
* The typed array behind either texture's `image.data` is the bake's choice,
|
|
397
|
+
* not part of this contract: `Float32Array` today, and a narrower encoding may
|
|
398
|
+
* change it in a minor release. Move the buffer, hand it to {@link makeVATTexture};
|
|
399
|
+
* do not read numbers out of it.
|
|
400
|
+
*/
|
|
401
|
+
interface VATBase {
|
|
402
|
+
/**
|
|
403
|
+
* Merged rest-pose geometry, in whichever space the encoding stores it: root
|
|
404
|
+
* space under the vertex encoding, where its `position` is the delta
|
|
405
|
+
* reference; each part's own local space under the rig encoding, where the
|
|
406
|
+
* slot carries the placement and also keeps `skinIndex` and `skinWeight`.
|
|
407
|
+
* Carries `normal` always, and `uv`, `color` and `tangent` when every source
|
|
408
|
+
* mesh carried them.
|
|
409
|
+
*/
|
|
410
|
+
geometry: BufferGeometry;
|
|
411
|
+
/** Source materials, indexed by `geometry.groups[].materialIndex`. */
|
|
412
|
+
materials: Material[];
|
|
413
|
+
/** Clip table: name → `{ startFrame, frames, fps, ... }`. */
|
|
414
|
+
clips: VATClip[];
|
|
415
|
+
/** Union of every baked frame's bounds; use as the geometry bounding box. */
|
|
416
|
+
bounds: Box3;
|
|
417
|
+
/** Vertex count (texture width). */
|
|
418
|
+
vertexCount: number;
|
|
419
|
+
/** Total frame rows across all clips (texture height). */
|
|
420
|
+
totalFrames: number;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* A VAT under the **vertex encoding**: a row holds where every vertex ended up,
|
|
424
|
+
* as a position delta and, unless the bake was told to skip it, a normal. The
|
|
425
|
+
* source-agnostic encoding (ADR-0008) and the default; the member every bake
|
|
426
|
+
* produced before there was a second one (ADR-0018).
|
|
427
|
+
*/
|
|
428
|
+
interface DeltaVAT extends VATBase {
|
|
429
|
+
/** Which encoding a row holds — the discriminant of {@link VAT}. */
|
|
430
|
+
encoding: 'delta';
|
|
431
|
+
/** RGBA float texture of per-vertex position deltas (`x = vertex`, `y = frame`). */
|
|
432
|
+
positionTexture: DataTexture;
|
|
433
|
+
/**
|
|
434
|
+
* RGBA float texture of per-vertex absolute normals (`x = vertex`, `y = frame`),
|
|
435
|
+
* or `null` when the bake was told to skip it (`bakeNormals: false`) — halving
|
|
436
|
+
* the VAT for a crowd that never reads a normal. Neither decode path samples
|
|
437
|
+
* it when it is absent; a smooth-shaded lit material paired with such a VAT is
|
|
438
|
+
* refused rather than lit by its rest pose.
|
|
439
|
+
*/
|
|
440
|
+
normalTexture: DataTexture | null;
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* A VAT under the **rig encoding** (ADR-0018): a row holds the posed rig — one
|
|
444
|
+
* **slot** per bone, as a rotation, a translation and a uniform scale — and the
|
|
445
|
+
* vertex shader skins the rest-pose geometry from it. Two orders of magnitude
|
|
446
|
+
* smaller than the vertex encoding and bake-cheap, at the price of source
|
|
447
|
+
* agnosticism: what a rig cannot express is refused at the bake.
|
|
448
|
+
*
|
|
449
|
+
* No normal texture, and none missing: normals and tangents come out of the
|
|
450
|
+
* skin matrix, as in three's own skinning.
|
|
451
|
+
*/
|
|
452
|
+
interface RigVAT extends VATBase {
|
|
453
|
+
/** Which encoding a row holds — the discriminant of {@link VAT}. */
|
|
454
|
+
encoding: 'rig';
|
|
455
|
+
/**
|
|
456
|
+
* RGBA float **rig texture**: two texels per slot — `(qx, qy, qz, qw)` then
|
|
457
|
+
* `(tx, ty, tz, s)` — laid out as `x = slot × 2 + texel`, `y = frame`, clips
|
|
458
|
+
* stacked as bands exactly as in the position texture. Consecutive rows of
|
|
459
|
+
* one slot sit on one quaternion hemisphere; the decode still flips the
|
|
460
|
+
* second row onto the first's side, because a looping clip blends a band's
|
|
461
|
+
* last row into its first and those are not neighbours.
|
|
462
|
+
*/
|
|
463
|
+
rigTexture: DataTexture;
|
|
464
|
+
/** Slots in the rig — the texture is `slotCount × 2` texels wide. */
|
|
465
|
+
slotCount: number;
|
|
466
|
+
}
|
|
467
|
+
/**
|
|
468
|
+
* A baked VAT, discriminated on `encoding`. A decode that samples a texture
|
|
469
|
+
* narrows on `encoding` first, so an encoding it does not decode is refused by
|
|
470
|
+
* name rather than sampled as a texture the VAT does not have (ADR-0018).
|
|
471
|
+
*/
|
|
472
|
+
type VAT = DeltaVAT | RigVAT;
|
|
473
|
+
/**
|
|
474
|
+
* The shared playback clock: one `{ value }` in seconds, read by every material
|
|
475
|
+
* of every VAT mesh driven by it. Set it once per frame. Deliberately the
|
|
476
|
+
* narrowest shape both decode paths satisfy — a WebGL `IUniform<number>` and a
|
|
477
|
+
* TSL uniform node are both one of these — so `createVATMesh` returns the same
|
|
478
|
+
* thing on either renderer.
|
|
479
|
+
*/
|
|
480
|
+
interface VATClock {
|
|
481
|
+
value: number;
|
|
482
|
+
}
|
|
483
|
+
/**
|
|
484
|
+
* A **crowd** ready to render: the mesh to add to the scene, and the clock to
|
|
485
|
+
* advance. What `createVATMesh` returns on either decode path, so moving a
|
|
486
|
+
* crowd between renderers is an import change and nothing else. Named for what
|
|
487
|
+
* it is rather than for its `mesh` field — the clock is half of it.
|
|
488
|
+
*/
|
|
489
|
+
interface VATCrowd {
|
|
490
|
+
/** Add to the scene. Its instance matrices are yours to write. */
|
|
491
|
+
mesh: InstancedMesh;
|
|
492
|
+
/** The shared playback clock — set `.value` once per frame. */
|
|
493
|
+
time: VATClock;
|
|
494
|
+
/**
|
|
495
|
+
* The crowd's **playback texture**: what carries each instance's clip, phase
|
|
496
|
+
* and policy to the shader, and what `setVATInstance` writes one row of
|
|
497
|
+
* (ADR-0016). Exposed rather than hidden behind the mesh because it is the
|
|
498
|
+
* object a caller has to hold to change an instance after the crowd is
|
|
499
|
+
* built — a `BufferGeometry` cannot carry a texture, so there is nowhere
|
|
500
|
+
* else honest to keep it.
|
|
501
|
+
*/
|
|
502
|
+
playback: VATPlaybackTexture;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* A mesh a VAT crowd can ride.
|
|
507
|
+
*
|
|
508
|
+
* `InstancedMesh` is what `createVATMesh` builds on either path.
|
|
509
|
+
* `BatchedMesh` is reached through the primitives, and buys per-instance
|
|
510
|
+
* frustum culling and depth sorting from three.js itself — see
|
|
511
|
+
* docs/usage.md, which also says what it does *not* buy.
|
|
512
|
+
*/
|
|
513
|
+
type VATCarrier = InstancedMesh | BatchedMesh;
|
|
514
|
+
|
|
515
|
+
export { type DeltaVAT as D, EndMode as E, INFINITE_REPETITIONS as I, LoopMode as L, MAX_FADE_DURATION as M, type RigVAT as R, type VAT as V, type VATBase as a, type VATCarrier as b, type VATClip as c, type VATClipDefaults as d, type VATClock as e, type VATCrowd as f, type VATFadeFrom as g, type VATFrame as h, type VATInstance as i, type VATPlaybackTexture as j, createVATPlaybackTexture as k, endsAt as l, resolveVATFrame as r, setVATInstance as s };
|