three-vat 1.0.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +164 -308
- package/dist/carrier-BFCPmcQK.d.ts +472 -0
- package/dist/chunk-2PGZ44TP.js +202 -0
- package/dist/chunk-W2ZAFMPB.js +53 -0
- package/dist/index.d.ts +78 -14
- package/dist/index.js +137 -47
- package/dist/tsl.d.ts +43 -31
- package/dist/tsl.js +94 -38
- package/dist/webgl.d.ts +31 -26
- package/dist/webgl.js +135 -33
- package/package.json +24 -21
- package/dist/chunk-SXTVASKG.js +0 -40
- package/dist/instance-playback-BrGBIKLe.d.ts +0 -109
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/baked-normals.ts
|
|
2
|
+
var NORMAL_READING_FLAGS = [
|
|
3
|
+
"isMeshStandardMaterial",
|
|
4
|
+
"isMeshPhongMaterial",
|
|
5
|
+
"isMeshLambertMaterial",
|
|
6
|
+
"isMeshToonMaterial",
|
|
7
|
+
"isMeshNormalMaterial",
|
|
8
|
+
"isMeshMatcapMaterial"
|
|
9
|
+
];
|
|
10
|
+
function needsBakedNormal(material) {
|
|
11
|
+
if (material.flatShading === true) return false;
|
|
12
|
+
const flags = material;
|
|
13
|
+
return NORMAL_READING_FLAGS.some((flag) => flags[flag] === true);
|
|
14
|
+
}
|
|
15
|
+
function assertBakedNormal(vat, material) {
|
|
16
|
+
if (vat.normalTexture !== null || !needsBakedNormal(material)) return;
|
|
17
|
+
throw new Error(
|
|
18
|
+
`three-vat: material "${material.name || "(unnamed)"}" (${material.type}) shades from a normal, but this VAT was baked with \`bakeNormals: false\` and carries none \u2014 the crowd would be lit by its rest pose. Set \`flatShading: true\` on the material (three then derives the normal from the deformed position, per fragment, which is the right normal for a posed mesh), or use an unlit material such as MeshBasicMaterial, or bake with \`bakeNormals: true\`.`
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/carrier.ts
|
|
23
|
+
function isBatchedCarrier(carrier) {
|
|
24
|
+
return carrier?.isBatchedMesh === true;
|
|
25
|
+
}
|
|
26
|
+
function rangeOf(batch, geometryId) {
|
|
27
|
+
try {
|
|
28
|
+
return batch.getGeometryRangeAt(geometryId);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function assertVATCarrier(carrier, vat) {
|
|
34
|
+
if (!isBatchedCarrier(carrier)) return;
|
|
35
|
+
if (rangeOf(carrier, 1)) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
"three-vat: a BatchedMesh carrier must hold exactly one geometry \u2014 the VAT\u2019s own. Both decode paths index the VAT by the vertex index, which on a batch is the geometry\u2019s vertexStart plus the vertex, so a second geometry\u2019s instances read another character\u2019s rows. One VAT, one geometry, N instances: mixing characters in one draw needs one VAT texture each, and a sampler is a uniform per draw call."
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
const range = rangeOf(carrier, 0);
|
|
41
|
+
if (!range) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"three-vat: this BatchedMesh holds no geometry \u2014 add `vat.geometry` with `addGeometry` before patching a material for it."
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (range.vertexStart !== 0 || range.vertexCount !== vat.vertexCount) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`three-vat: this BatchedMesh\u2019s geometry spans ${range.vertexCount} vertices from ${range.vertexStart}, and the VAT has ${vat.vertexCount} from 0. The decode reads the VAT at the batch\u2019s own vertex index, so the batch must hold \`vat.geometry\` and nothing before it.`
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { assertBakedNormal, assertVATCarrier, isBatchedCarrier };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { V as VAT } from './
|
|
3
|
-
export { a as
|
|
1
|
+
import { AnimationClip, AnimationAction, Object3D, TypedArray, TextureDataType, DataTexture } from 'three';
|
|
2
|
+
import { V as VAT } from './carrier-BFCPmcQK.js';
|
|
3
|
+
export { E as EndMode, I as INFINITE_REPETITIONS, L as LoopMode, M as MAX_FADE_DURATION, a as VATCarrier, b as VATClip, c as VATClipDefaults, d as VATClock, e as VATCrowd, f as VATFadeFrom, g as VATFrame, h as VATInstance, i as VATPlaybackTexture, j as createVATPlaybackTexture, k as endsAt, r as resolveVATFrame, s as setVATInstance } from './carrier-BFCPmcQK.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
6
|
+
* What {@link bakeVAT} takes for each animation: the clip itself, or an
|
|
7
|
+
* `AnimationAction` already configured the way three taught you.
|
|
8
|
+
*
|
|
9
|
+
* An action costs the baker nothing — it builds an `AnimationMixer` to pose the
|
|
10
|
+
* mesh either way — and buys the caller per-clip defaults every instance of
|
|
11
|
+
* that clip inherits ({@link VATClipDefaults}).
|
|
12
12
|
*/
|
|
13
|
-
|
|
13
|
+
type BakeInput = AnimationClip | AnimationAction;
|
|
14
14
|
interface BakeOptions {
|
|
15
15
|
/** Sample rate in frames per second. Default `30`. */
|
|
16
16
|
fps?: number;
|
|
@@ -21,10 +21,55 @@ interface BakeOptions {
|
|
|
21
21
|
* VAT that allocates on your desktop and fails on a phone.
|
|
22
22
|
*/
|
|
23
23
|
maxTextureSize?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Bake the normal texture. Default `true`.
|
|
26
|
+
*
|
|
27
|
+
* Turning it off halves the VAT — `verts x frames x 16 B x 2` becomes `x 1` —
|
|
28
|
+
* and is correct for exactly two material setups:
|
|
29
|
+
*
|
|
30
|
+
* - **Unlit** (`MeshBasicMaterial`, and its node twin), which never reads a
|
|
31
|
+
* normal, so the texture was pure waste.
|
|
32
|
+
* - **`flatShading: true`**, where three derives the normal from screen-space
|
|
33
|
+
* derivatives of the *deformed* position in the fragment stage. That is the
|
|
34
|
+
* correct normal for the posed mesh, computed for free — the baked one is
|
|
35
|
+
* not merely unnecessary there, it is redundant work.
|
|
36
|
+
*
|
|
37
|
+
* Anything else that shades — a smooth-shaded lit material — would light the
|
|
38
|
+
* crowd by its rest-pose normals, which is visibly wrong (ADR-0002). Both
|
|
39
|
+
* decode paths refuse that pairing loudly rather than render it.
|
|
40
|
+
*/
|
|
41
|
+
bakeNormals?: boolean;
|
|
24
42
|
}
|
|
25
43
|
/**
|
|
26
|
-
* Bake
|
|
27
|
-
*
|
|
44
|
+
* Bake animations into a VAT by sampling the posed subtree frame by frame on
|
|
45
|
+
* the CPU.
|
|
46
|
+
*
|
|
47
|
+
* Each entry of `animations` is an `AnimationClip`, or an `AnimationAction`
|
|
48
|
+
* already configured the way three taught you:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* const action = mixer.clipAction(deathClip)
|
|
52
|
+
* action.loop = THREE.LoopOnce
|
|
53
|
+
*
|
|
54
|
+
* const vat = bakeVAT(gltf.scene, [walkAction, action, idleClip])
|
|
55
|
+
* ```
|
|
56
|
+
*
|
|
57
|
+
* Every instance that plays `death` then inherits "once, clamped" without the
|
|
58
|
+
* caller saying so again, and may still override any of it. An action costs the
|
|
59
|
+
* bake nothing — it builds an `AnimationMixer` to pose the mesh either way — and
|
|
60
|
+
* a plain clip carries no configuration, so the simple case still needs no
|
|
61
|
+
* mixer at all and takes the library defaults ({@link VATClipDefaults}).
|
|
62
|
+
*
|
|
63
|
+
* `loop`, `repetitions` and `timeScale` are read. **`clampWhenFinished` is
|
|
64
|
+
* not**: it is `false` on every untouched action, so a crowd reads it as the
|
|
65
|
+
* silence it usually is and clamps either way — an instance names
|
|
66
|
+
* `endMode: EndMode.Rewind` to get three's behaviour back.
|
|
67
|
+
* **`time` and `paused` are ignored**: they say where a playhead is sitting,
|
|
68
|
+
* not how the animation is meant to play, and a VAT has no playhead of its own
|
|
69
|
+
* to seed — every instance's position is a function of the shared clock and its
|
|
70
|
+
* own `startTime`. A non-unit `weight` and an additive `blendMode` are refused
|
|
71
|
+
* outright; both describe several actions blended at once, which one baked band
|
|
72
|
+
* cannot be.
|
|
28
73
|
*
|
|
29
74
|
* The unit of a bake is the whole subtree under `root`, merged into one vertex
|
|
30
75
|
* set and recorded in root space (ADR-0008) — so it handles a single
|
|
@@ -45,7 +90,26 @@ interface BakeOptions {
|
|
|
45
90
|
* Renderer-agnostic — touches no WebGL/WebGPU context — so it runs identically
|
|
46
91
|
* at runtime, in a Web Worker, and in Node.
|
|
47
92
|
*/
|
|
48
|
-
declare function bakeVAT(root: Object3D,
|
|
93
|
+
declare function bakeVAT(root: Object3D, animations: BakeInput[], { fps, maxTextureSize, bakeNormals }?: BakeOptions): VAT;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The WebGL2 *spec floor for high-end desktop*, which two things lean on.
|
|
97
|
+
*
|
|
98
|
+
* - **The bake's fallback cap**, used when the caller does not pass
|
|
99
|
+
* `maxTextureSize`. The baker is renderer-agnostic by design (it runs in
|
|
100
|
+
* Node, and in a Web Worker) so it cannot query the real limit itself: pass
|
|
101
|
+
* `getMaxTextureSize(renderer)` from `three-vat/webgl` or `three-vat/tsl`
|
|
102
|
+
* whenever a renderer exists.
|
|
103
|
+
* - **The instance ceiling**, because the playback texture is one row per
|
|
104
|
+
* instance (`createVATPlaybackTexture`, ADR-0016). That one takes no
|
|
105
|
+
* override, the crowd being built long after the bake was sized.
|
|
106
|
+
*
|
|
107
|
+
* Not a guarantee either way: plenty of mobile GPUs report 4096 or 8192, and
|
|
108
|
+
* a crowd between that and this number is refused by the driver at upload
|
|
109
|
+
* rather than here. The bake is where the real limit is worth passing, because
|
|
110
|
+
* it is where the numbers get large.
|
|
111
|
+
*/
|
|
112
|
+
declare const MAX_TEXTURE_SIZE = 16384;
|
|
49
113
|
/**
|
|
50
114
|
* Build a VAT `DataTexture` with the fixed sampling flags every path relies on:
|
|
51
115
|
* RGBA, nearest filtering, no mipmaps. Frame interpolation is done manually in
|
|
@@ -53,4 +117,4 @@ declare function bakeVAT(root: Object3D, clips: AnimationClip[], { fps, maxTextu
|
|
|
53
117
|
*/
|
|
54
118
|
declare function makeVATTexture(data: TypedArray, width: number, height: number, type?: TextureDataType): DataTexture;
|
|
55
119
|
|
|
56
|
-
export { type BakeOptions, MAX_TEXTURE_SIZE, VAT, bakeVAT, makeVATTexture };
|
|
120
|
+
export { type BakeInput, type BakeOptions, MAX_TEXTURE_SIZE, VAT, bakeVAT, makeVATTexture };
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,54 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import { LoopMode, MAX_TEXTURE_SIZE, makeVATTexture, LIBRARY_PLAYBACK_DEFAULTS, FORWARD_ONLY_REASON, INFINITE_REPETITIONS } from './chunk-2PGZ44TP.js';
|
|
2
|
+
export { EndMode, INFINITE_REPETITIONS, LoopMode, MAX_FADE_DURATION, MAX_TEXTURE_SIZE, createVATPlaybackTexture, endsAt, makeVATTexture, resolveVATFrame, setVATInstance } from './chunk-2PGZ44TP.js';
|
|
3
|
+
import { LoopRepeat, LoopOnce, LoopPingPong, Matrix4, AnimationMixer, Box3, Vector4, Vector3, Sphere, AdditiveAnimationBlendMode, BufferAttribute, BufferGeometry } from 'three';
|
|
3
4
|
|
|
4
|
-
var
|
|
5
|
+
var LOOP_MODES = /* @__PURE__ */ new Map([
|
|
6
|
+
[LoopRepeat, LoopMode.Repeat],
|
|
7
|
+
[LoopOnce, LoopMode.Once],
|
|
8
|
+
[LoopPingPong, LoopMode.PingPong]
|
|
9
|
+
]);
|
|
10
|
+
function isAction(input) {
|
|
11
|
+
return typeof input.getClip === "function";
|
|
12
|
+
}
|
|
13
|
+
function resolveAnimation(input) {
|
|
14
|
+
if (!isAction(input)) return { clip: input, defaults: { ...LIBRARY_PLAYBACK_DEFAULTS } };
|
|
15
|
+
const clip = input.getClip();
|
|
16
|
+
const name = clip.name || "(unnamed)";
|
|
17
|
+
const cannotBlend = (field, value) => new Error(
|
|
18
|
+
`three-vat: action for clip "${name}" has ${field} ${value}; that blends several actions at once, which a single baked band cannot represent \u2014 bake the clips separately and crossfade between them (three-vat#30), or reset the action before baking`
|
|
19
|
+
);
|
|
20
|
+
if (input.weight !== 1) throw cannotBlend("weight", String(input.weight));
|
|
21
|
+
if (input.blendMode === AdditiveAnimationBlendMode) throw cannotBlend("blendMode", "additive");
|
|
22
|
+
if (input.timeScale < 0) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
`three-vat: action for clip "${name}" has timeScale ${input.timeScale}; ${FORWARD_ONLY_REASON}.`
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const loopMode = LOOP_MODES.get(input.loop);
|
|
28
|
+
if (loopMode === void 0) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`three-vat: action for clip "${name}" has an unrecognised loop mode ${input.loop}; expected THREE.LoopRepeat, LoopOnce or LoopPingPong`
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
clip,
|
|
35
|
+
defaults: {
|
|
36
|
+
loopMode,
|
|
37
|
+
// three's `repetitions` defaults to Infinity and `LoopOnce` ignores it
|
|
38
|
+
// outright — so a one-shot is one play whatever the field says, and an
|
|
39
|
+
// endless count becomes the sentinel a Float32Array can carry.
|
|
40
|
+
repetitions: loopMode === LoopMode.Once ? 1 : Number.isFinite(input.repetitions) ? input.repetitions : INFINITE_REPETITIONS,
|
|
41
|
+
// Not read off the action (see the rule above), so a bake clamps whichever
|
|
42
|
+
// input it was handed. Uniform across loop modes on purpose: an instance
|
|
43
|
+
// may override a repeating clip's `loopMode` to `Once` and inherit this
|
|
44
|
+
// field, so there is no mode whose end mode is safely unreachable and
|
|
45
|
+
// could hold a different answer. Rewind is per instance:
|
|
46
|
+
// `{ ..., endMode: EndMode.Rewind }` (#43, ADR-0017).
|
|
47
|
+
endMode: LIBRARY_PLAYBACK_DEFAULTS.endMode,
|
|
48
|
+
speed: input.timeScale
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
5
52
|
function asAttribute(value, mesh, name) {
|
|
6
53
|
if (!(value instanceof BufferAttribute)) {
|
|
7
54
|
throw new Error(
|
|
@@ -44,6 +91,8 @@ function collectParts(root) {
|
|
|
44
91
|
baseNrm: asAttribute(geometry.attributes.normal, mesh, "normal"),
|
|
45
92
|
isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
|
|
46
93
|
skeleton: skinned.skeleton,
|
|
94
|
+
pose: void 0,
|
|
95
|
+
// assigned below, once the distinct rigs are known
|
|
47
96
|
skinIndex: geometry.attributes.skinIndex,
|
|
48
97
|
skinWeight: geometry.attributes.skinWeight,
|
|
49
98
|
morphPos: geometry.morphAttributes.position,
|
|
@@ -64,18 +113,25 @@ function mergeGeometry(parts, restMatrices, total) {
|
|
|
64
113
|
const normal = new Float32Array(total * 3);
|
|
65
114
|
const wantUV = parts.every((p) => !!p.mesh.geometry.attributes.uv);
|
|
66
115
|
const wantColor = parts.every((p) => !!p.mesh.geometry.attributes.color);
|
|
116
|
+
const wantTangent = parts.every((p) => {
|
|
117
|
+
const t = p.mesh.geometry.attributes.tangent;
|
|
118
|
+
return t instanceof BufferAttribute && t.itemSize === 4;
|
|
119
|
+
});
|
|
67
120
|
const uv = wantUV ? new Float32Array(total * 2) : null;
|
|
68
121
|
const color = wantColor ? new Float32Array(total * 3) : null;
|
|
122
|
+
const tangent = wantTangent ? new Float32Array(total * 4) : null;
|
|
69
123
|
const indices = [];
|
|
70
124
|
const groups = [];
|
|
71
125
|
const _v = new Vector3();
|
|
72
126
|
const _n = new Vector3();
|
|
127
|
+
const _t = new Vector3();
|
|
73
128
|
parts.forEach((part, pi) => {
|
|
74
129
|
const m = restMatrices[pi];
|
|
75
130
|
const geometry = part.mesh.geometry;
|
|
76
131
|
const start = part.vertexStart;
|
|
77
132
|
const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
|
|
78
133
|
const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
|
|
134
|
+
const srcTangent = tangent ? geometry.attributes.tangent : null;
|
|
79
135
|
for (let v = 0; v < part.vertexCount; v++) {
|
|
80
136
|
_v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
|
|
81
137
|
_n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
|
|
@@ -95,6 +151,14 @@ function mergeGeometry(parts, restMatrices, total) {
|
|
|
95
151
|
color[o3 + 1] = srcColor.getY(v);
|
|
96
152
|
color[o3 + 2] = srcColor.getZ(v);
|
|
97
153
|
}
|
|
154
|
+
if (tangent && srcTangent) {
|
|
155
|
+
_t.fromBufferAttribute(srcTangent, v).transformDirection(m);
|
|
156
|
+
const o4 = (start + v) * 4;
|
|
157
|
+
tangent[o4] = _t.x;
|
|
158
|
+
tangent[o4 + 1] = _t.y;
|
|
159
|
+
tangent[o4 + 2] = _t.z;
|
|
160
|
+
tangent[o4 + 3] = srcTangent.getW(v);
|
|
161
|
+
}
|
|
98
162
|
}
|
|
99
163
|
const groupStart = indices.length;
|
|
100
164
|
const index = geometry.index;
|
|
@@ -119,11 +183,14 @@ function mergeGeometry(parts, restMatrices, total) {
|
|
|
119
183
|
merged.setAttribute("normal", new BufferAttribute(normal, 3));
|
|
120
184
|
if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
|
|
121
185
|
if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
|
|
186
|
+
if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
|
|
122
187
|
merged.setIndex(indices);
|
|
123
188
|
for (const g of groups) merged.addGroup(g.start, g.count, g.materialIndex);
|
|
124
189
|
return merged;
|
|
125
190
|
}
|
|
126
|
-
function bakeVAT(root,
|
|
191
|
+
function bakeVAT(root, animations, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE, bakeNormals = true } = {}) {
|
|
192
|
+
const resolved = animations.map(resolveAnimation);
|
|
193
|
+
const clips = resolved.map((a) => a.clip);
|
|
127
194
|
root.updateMatrixWorld(true);
|
|
128
195
|
const parts = collectParts(root);
|
|
129
196
|
const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
|
|
@@ -146,13 +213,13 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
146
213
|
const geometry = mergeGeometry(parts, restMatrices, vertexCount);
|
|
147
214
|
const mergedBase = geometry.attributes.position;
|
|
148
215
|
const posData = new Float32Array(vertexCount * totalFrames * 4);
|
|
149
|
-
const nrmData = new Float32Array(vertexCount * totalFrames * 4);
|
|
216
|
+
const nrmData = bakeNormals ? new Float32Array(vertexCount * totalFrames * 4) : null;
|
|
217
|
+
const bakeNormal = nrmData !== null;
|
|
150
218
|
const mixer = new AnimationMixer(root);
|
|
151
219
|
const bounds = new Box3();
|
|
152
220
|
const clipTable = [];
|
|
153
221
|
const _si = new Vector4();
|
|
154
222
|
const _sw = new Vector4();
|
|
155
|
-
const _bone = new Matrix4();
|
|
156
223
|
const _acc = new Matrix4();
|
|
157
224
|
const _skin = new Matrix4();
|
|
158
225
|
const _partMatrix = new Matrix4();
|
|
@@ -162,7 +229,8 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
162
229
|
const _mt = new Vector3();
|
|
163
230
|
const _mb = new Vector3();
|
|
164
231
|
const _mbn = new Vector3();
|
|
165
|
-
const
|
|
232
|
+
const poses = attachPoseBuffers(parts);
|
|
233
|
+
const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
|
|
166
234
|
let warnedNonUniformScale = false;
|
|
167
235
|
let rowOffset = 0;
|
|
168
236
|
clips.forEach((clip, ci) => {
|
|
@@ -174,22 +242,24 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
174
242
|
mixer.setTime(f / frames * clip.duration);
|
|
175
243
|
root.updateMatrixWorld(true);
|
|
176
244
|
const row = rowOffset + f;
|
|
245
|
+
poseSkeletons(poses);
|
|
177
246
|
if (!warnedNonUniformScale) {
|
|
178
|
-
warnedNonUniformScale = warnOnNonUniformBoneScale(influencers
|
|
247
|
+
warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
|
|
179
248
|
}
|
|
180
249
|
for (const part of parts) {
|
|
181
250
|
_partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
|
|
182
251
|
const influences = part.mesh.morphTargetInfluences;
|
|
183
|
-
const { morphPos, morphNrm, morphRelative, isSkinned,
|
|
252
|
+
const { morphPos, morphNrm, morphRelative, isSkinned, skinIndex, skinWeight } = part;
|
|
253
|
+
const boneMatrices = part.pose?.matrices;
|
|
184
254
|
const morphCount = influences ? Math.min(influences.length, Math.max(morphPos?.length ?? 0, morphNrm?.length ?? 0)) : 0;
|
|
185
255
|
for (let v = 0; v < part.vertexCount; v++) {
|
|
186
256
|
const vi = part.vertexStart + v;
|
|
187
257
|
_p.fromBufferAttribute(part.basePos, v);
|
|
188
|
-
_n.fromBufferAttribute(part.baseNrm, v);
|
|
258
|
+
if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
|
|
189
259
|
if (influences && morphCount > 0) {
|
|
190
260
|
if (!morphRelative) {
|
|
191
261
|
_mb.fromBufferAttribute(part.basePos, v);
|
|
192
|
-
_mbn.fromBufferAttribute(part.baseNrm, v);
|
|
262
|
+
if (bakeNormal) _mbn.fromBufferAttribute(part.baseNrm, v);
|
|
193
263
|
}
|
|
194
264
|
for (let t = 0; t < morphCount; t++) {
|
|
195
265
|
const w = influences[t];
|
|
@@ -200,7 +270,7 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
200
270
|
if (!morphRelative) _mt.sub(_mb);
|
|
201
271
|
_p.addScaledVector(_mt, w);
|
|
202
272
|
}
|
|
203
|
-
const targetNrm = morphNrm?.[t];
|
|
273
|
+
const targetNrm = bakeNormal ? morphNrm?.[t] : void 0;
|
|
204
274
|
if (targetNrm) {
|
|
205
275
|
_mt.fromBufferAttribute(targetNrm, v);
|
|
206
276
|
if (!morphRelative) _mt.sub(_mbn);
|
|
@@ -208,26 +278,24 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
208
278
|
}
|
|
209
279
|
}
|
|
210
280
|
}
|
|
211
|
-
if (isSkinned &&
|
|
281
|
+
if (isSkinned && boneMatrices) {
|
|
212
282
|
const skinned = part.mesh;
|
|
213
283
|
_si.fromBufferAttribute(skinIndex, v);
|
|
214
284
|
_sw.fromBufferAttribute(skinWeight, v);
|
|
215
|
-
_acc.elements
|
|
285
|
+
const ae = _acc.elements;
|
|
286
|
+
ae.fill(0);
|
|
216
287
|
for (let i = 0; i < 4; i++) {
|
|
217
288
|
const w = _sw.getComponent(i);
|
|
218
289
|
if (w === 0) continue;
|
|
219
|
-
const
|
|
220
|
-
|
|
221
|
-
const ae = _acc.elements;
|
|
222
|
-
const be = _bone.elements;
|
|
223
|
-
for (let e = 0; e < 16; e++) ae[e] += be[e] * w;
|
|
290
|
+
const b = _si.getComponent(i) * BONE_STRIDE;
|
|
291
|
+
for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
|
|
224
292
|
}
|
|
225
293
|
_skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
|
|
226
294
|
_p.applyMatrix4(_skin);
|
|
227
|
-
_n.transformDirection(_skin);
|
|
295
|
+
if (bakeNormal) _n.transformDirection(_skin);
|
|
228
296
|
}
|
|
229
297
|
_p.applyMatrix4(_partMatrix);
|
|
230
|
-
_n.transformDirection(_partMatrix);
|
|
298
|
+
if (bakeNormal) _n.transformDirection(_partMatrix);
|
|
231
299
|
bounds.expandByPoint(_p);
|
|
232
300
|
_bp.fromBufferAttribute(mergedBase, vi);
|
|
233
301
|
const o = (row * vertexCount + vi) * 4;
|
|
@@ -239,10 +307,12 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
239
307
|
posData[o + 1] = dy;
|
|
240
308
|
posData[o + 2] = dz;
|
|
241
309
|
posData[o + 3] = 1;
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
310
|
+
if (bakeNormal) {
|
|
311
|
+
nrmData[o] = _n.x;
|
|
312
|
+
nrmData[o + 1] = _n.y;
|
|
313
|
+
nrmData[o + 2] = _n.z;
|
|
314
|
+
nrmData[o + 3] = 1;
|
|
315
|
+
}
|
|
246
316
|
}
|
|
247
317
|
}
|
|
248
318
|
}
|
|
@@ -253,7 +323,10 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
253
323
|
frames,
|
|
254
324
|
fps: frames / clip.duration,
|
|
255
325
|
duration: clip.duration,
|
|
256
|
-
maxDelta: Math.sqrt(maxDeltaSq)
|
|
326
|
+
maxDelta: Math.sqrt(maxDeltaSq),
|
|
327
|
+
// Declared once, here, rather than repeated at every instance that plays
|
|
328
|
+
// this band. An instance overrides any of them, field by field.
|
|
329
|
+
...resolved[ci].defaults
|
|
257
330
|
});
|
|
258
331
|
rowOffset += frames;
|
|
259
332
|
});
|
|
@@ -266,7 +339,7 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
266
339
|
for (const part of parts) materials[part.materialIndex] = part.material;
|
|
267
340
|
return {
|
|
268
341
|
positionTexture: makeVATTexture(posData, vertexCount, totalFrames),
|
|
269
|
-
normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames),
|
|
342
|
+
normalTexture: nrmData ? makeVATTexture(nrmData, vertexCount, totalFrames) : null,
|
|
270
343
|
clips: clipTable,
|
|
271
344
|
bounds,
|
|
272
345
|
vertexCount,
|
|
@@ -276,6 +349,32 @@ function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } =
|
|
|
276
349
|
materials
|
|
277
350
|
};
|
|
278
351
|
}
|
|
352
|
+
var BONE_STRIDE = 16;
|
|
353
|
+
var IDENTITY = /* @__PURE__ */ new Matrix4();
|
|
354
|
+
function attachPoseBuffers(parts) {
|
|
355
|
+
const byRig = /* @__PURE__ */ new Map();
|
|
356
|
+
for (const part of parts) {
|
|
357
|
+
if (!part.isSkinned || !part.skeleton) continue;
|
|
358
|
+
let pose = byRig.get(part.skeleton);
|
|
359
|
+
if (!pose) {
|
|
360
|
+
const matrices = new Float64Array(part.skeleton.bones.length * BONE_STRIDE);
|
|
361
|
+
pose = { skeleton: part.skeleton, matrices };
|
|
362
|
+
byRig.set(part.skeleton, pose);
|
|
363
|
+
}
|
|
364
|
+
part.pose = pose;
|
|
365
|
+
}
|
|
366
|
+
return [...byRig.values()];
|
|
367
|
+
}
|
|
368
|
+
function poseSkeletons(poses) {
|
|
369
|
+
const scratch = new Matrix4();
|
|
370
|
+
for (const { skeleton, matrices } of poses) {
|
|
371
|
+
const { bones, boneInverses } = skeleton;
|
|
372
|
+
for (let b = 0; b < bones.length; b++) {
|
|
373
|
+
scratch.multiplyMatrices(bones[b]?.matrixWorld ?? IDENTITY, boneInverses[b]);
|
|
374
|
+
matrices.set(scratch.elements, b * BONE_STRIDE);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
279
378
|
function influencedBones(part) {
|
|
280
379
|
const used = /* @__PURE__ */ new Set();
|
|
281
380
|
const index = part.skinIndex;
|
|
@@ -285,37 +384,28 @@ function influencedBones(part) {
|
|
|
285
384
|
if (weight.getComponent(v, i) !== 0) used.add(index.getComponent(v, i));
|
|
286
385
|
}
|
|
287
386
|
}
|
|
288
|
-
return {
|
|
387
|
+
return { pose: part.pose, bones: [...used] };
|
|
289
388
|
}
|
|
290
389
|
var SCALE_UNIFORMITY_EPSILON = 1e-4;
|
|
291
|
-
function hasNonUniformScale(
|
|
292
|
-
const
|
|
293
|
-
const x =
|
|
294
|
-
const y =
|
|
295
|
-
const z =
|
|
390
|
+
function hasNonUniformScale(matrices, b) {
|
|
391
|
+
const o = b * BONE_STRIDE;
|
|
392
|
+
const x = matrices[o] ** 2 + matrices[o + 1] ** 2 + matrices[o + 2] ** 2;
|
|
393
|
+
const y = matrices[o + 4] ** 2 + matrices[o + 5] ** 2 + matrices[o + 6] ** 2;
|
|
394
|
+
const z = matrices[o + 8] ** 2 + matrices[o + 9] ** 2 + matrices[o + 10] ** 2;
|
|
296
395
|
const max = Math.max(x, y, z);
|
|
297
396
|
return max - Math.min(x, y, z) > SCALE_UNIFORMITY_EPSILON * max;
|
|
298
397
|
}
|
|
299
|
-
function warnOnNonUniformBoneScale(influencers
|
|
300
|
-
for (const {
|
|
398
|
+
function warnOnNonUniformBoneScale(influencers) {
|
|
399
|
+
for (const { pose, bones } of influencers) {
|
|
301
400
|
for (const b of bones) {
|
|
302
|
-
|
|
303
|
-
if (!hasNonUniformScale(scratch)) continue;
|
|
401
|
+
if (!hasNonUniformScale(pose.matrices, b)) continue;
|
|
304
402
|
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.`
|
|
403
|
+
`three-vat: bone "${pose.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
404
|
);
|
|
307
405
|
return true;
|
|
308
406
|
}
|
|
309
407
|
}
|
|
310
408
|
return false;
|
|
311
409
|
}
|
|
312
|
-
function makeVATTexture(data, width, height, type = FloatType) {
|
|
313
|
-
const tex = new DataTexture(data, width, height, RGBAFormat, type);
|
|
314
|
-
tex.minFilter = NearestFilter;
|
|
315
|
-
tex.magFilter = NearestFilter;
|
|
316
|
-
tex.generateMipmaps = false;
|
|
317
|
-
tex.needsUpdate = true;
|
|
318
|
-
return tex;
|
|
319
|
-
}
|
|
320
410
|
|
|
321
|
-
export {
|
|
411
|
+
export { bakeVAT };
|
package/dist/tsl.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { BufferGeometry, InstancedMesh } from 'three';
|
|
2
1
|
import { Node } from 'three/webgpu';
|
|
3
|
-
import {
|
|
2
|
+
import { d as VATClock, i as VATPlaybackTexture, a as VATCarrier, V as VAT, h as VATInstance, e as VATCrowd } from './carrier-BFCPmcQK.js';
|
|
3
|
+
import 'three';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The real maximum texture dimension this renderer accepts, for
|
|
@@ -31,40 +31,50 @@ interface VATNodeOptions {
|
|
|
31
31
|
*/
|
|
32
32
|
time?: VATTimeUniform;
|
|
33
33
|
/**
|
|
34
|
-
* The
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* `desync`.
|
|
34
|
+
* The crowd's playback texture — build it with `createVATPlaybackTexture`
|
|
35
|
+
* from `three-vat` *before* calling this, or let `createVATMesh` do it — and
|
|
36
|
+
* each instance plays its own clip, at its own phase and rate, read from its
|
|
37
|
+
* own row — which row that is, is the carrier's answer (see `carrier`). Without
|
|
38
|
+
* it, every instance plays `clipIndex`, phase-desynced by `desync`.
|
|
39
39
|
*
|
|
40
|
-
* Which decode the graph compiles is decided here, at build time:
|
|
41
|
-
*
|
|
42
|
-
* fallback cannot be a shader-side branch.
|
|
40
|
+
* Which decode the graph compiles is decided here, at build time: the
|
|
41
|
+
* fallback is a different graph, not a shader-side branch.
|
|
43
42
|
*/
|
|
44
|
-
|
|
43
|
+
playback?: VATPlaybackTexture;
|
|
45
44
|
/**
|
|
46
|
-
* The
|
|
45
|
+
* The crowd's **carrier** — the mesh these nodes will render on, an
|
|
46
|
+
* `InstancedMesh` or a `BatchedMesh`. Named as the WebGL path's
|
|
47
|
+
* `patchVATMaterial` names it, because it is the one concept (CONTEXT.md).
|
|
47
48
|
*
|
|
48
|
-
* Required for a crowd, and for
|
|
49
|
-
* to `positionLocal` *before* it reads `positionNode`, so
|
|
50
|
-
* displace in the geometry's own space and then re-apply
|
|
51
|
-
* itself. Without this the delta is added in instance space —
|
|
52
|
-
* unscaled — and every instance deforms according to its own
|
|
49
|
+
* Required for a crowd, and for two reasons. First: three applies the
|
|
50
|
+
* carrier's transform to `positionLocal` *before* it reads `positionNode`, so
|
|
51
|
+
* the decode has to displace in the geometry's own space and then re-apply
|
|
52
|
+
* that transform itself. Without this the delta is added in instance space —
|
|
53
|
+
* unrotated and unscaled — and every instance deforms according to its own
|
|
54
|
+
* matrix. Second: the carrier decides how this instance's *logical* index is
|
|
55
|
+
* spelled, which is the row of the playback texture the pack is read from —
|
|
56
|
+
* `instanceIndex` on an `InstancedMesh`, `batchIndirectIndex` on a
|
|
57
|
+
* `BatchedMesh`, whose drawn slot is a permutation that changes every frame
|
|
58
|
+
* (ADR-0016).
|
|
59
|
+
*
|
|
60
|
+
* One option rather than one per carrier, because those two answers have to
|
|
61
|
+
* come from the same object: a decode that re-applied one mesh's transform
|
|
62
|
+
* while reading another's index would render a crowd nothing could explain.
|
|
53
63
|
*
|
|
54
64
|
* Omit it for a single, non-instanced mesh, where `positionLocal` is the
|
|
55
65
|
* geometry position and there is nothing to re-apply.
|
|
56
66
|
*/
|
|
57
|
-
|
|
67
|
+
carrier?: VATCarrier;
|
|
58
68
|
/**
|
|
59
69
|
* Which clip to play (index into `vat.clips`). Ignored — along with
|
|
60
|
-
* `desync` — when `
|
|
61
|
-
*
|
|
70
|
+
* `desync` — when a `playback` texture is given, which says all of this per
|
|
71
|
+
* instance. Default `0`.
|
|
62
72
|
*/
|
|
63
73
|
clipIndex?: number;
|
|
64
74
|
/**
|
|
65
|
-
* Max random per-instance time offset in seconds, hashed from
|
|
66
|
-
* `0` (default) plays every instance in lockstep. Ignored when
|
|
67
|
-
*
|
|
75
|
+
* Max random per-instance time offset in seconds, hashed from the instance's
|
|
76
|
+
* logical index. `0` (default) plays every instance in lockstep. Ignored when
|
|
77
|
+
* a `playback` texture is given.
|
|
68
78
|
*/
|
|
69
79
|
desync?: number;
|
|
70
80
|
}
|
|
@@ -97,11 +107,11 @@ interface VATNodes {
|
|
|
97
107
|
* Build TSL decode nodes for a baked VAT, for the WebGPU/TSL renderer path.
|
|
98
108
|
* Shadows work automatically because `positionNode` also feeds the depth pass.
|
|
99
109
|
*
|
|
100
|
-
* Pass the `
|
|
101
|
-
*
|
|
110
|
+
* Pass the crowd's `playback` texture and each instance plays the clip, phase
|
|
111
|
+
* and rate written into its row by `createVATPlaybackTexture` — the same
|
|
102
112
|
* instance-playback contract the WebGL path reads (ADR-0009), so a mixed-clip
|
|
103
|
-
* crowd renders identically on either renderer. Without
|
|
104
|
-
*
|
|
113
|
+
* crowd renders identically on either renderer. Without it every instance plays
|
|
114
|
+
* `clipIndex`, desynced by a phase hashed from the instance index.
|
|
105
115
|
*
|
|
106
116
|
* Coverage note: the node graph is tested structurally in CI (no GPU); that the
|
|
107
117
|
* two paths decode *identically* is a pixel-diff release gate.
|
|
@@ -110,6 +120,7 @@ declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
|
|
|
110
120
|
/**
|
|
111
121
|
* The decode's arithmetic: the position delta and the normal this instance reads
|
|
112
122
|
* at this moment, as nodes — before the vertex-stage writes that place them.
|
|
123
|
+
* `normal` is `null` when the VAT was baked without a normal texture.
|
|
113
124
|
*
|
|
114
125
|
* @internal Split out and exported for the structural tests. A `Fn` body is
|
|
115
126
|
* opaque to graph traversal (its statements are not built until the shader is),
|
|
@@ -119,7 +130,7 @@ declare function vatNodes(vat: VAT, options?: VATNodeOptions): VATNodes;
|
|
|
119
130
|
*/
|
|
120
131
|
declare function vatDecode(vat: VAT, options?: VATNodeOptions): {
|
|
121
132
|
position: Vec3Node;
|
|
122
|
-
normal: Vec3Node;
|
|
133
|
+
normal: Vec3Node | null;
|
|
123
134
|
};
|
|
124
135
|
/** Options for {@link createVATMesh}. */
|
|
125
136
|
interface CreateVATMeshOptions {
|
|
@@ -137,11 +148,12 @@ interface CreateVATMeshOptions {
|
|
|
137
148
|
}
|
|
138
149
|
/**
|
|
139
150
|
* Turn a baked VAT and a list of instances into a crowd ready to render: an
|
|
140
|
-
* `InstancedMesh`
|
|
141
|
-
*
|
|
151
|
+
* `InstancedMesh` rendering the bake's geometry, a playback texture carrying
|
|
152
|
+
* the instance-playback contract, and materials that decode the VAT on the
|
|
153
|
+
* vertex stage.
|
|
142
154
|
*
|
|
143
155
|
* ```ts
|
|
144
|
-
* const { mesh, time } = createVATMesh(vat, instances)
|
|
156
|
+
* const { mesh, time, playback } = createVATMesh(vat, instances)
|
|
145
157
|
* mesh.castShadow = mesh.receiveShadow = true
|
|
146
158
|
* scene.add(mesh)
|
|
147
159
|
* // per frame:
|