three-vat 0.1.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +302 -29
- package/dist/chunk-SXTVASKG.js +40 -0
- package/dist/index.d.ts +41 -51
- package/dist/index.js +256 -89
- package/dist/instance-playback-BrGBIKLe.d.ts +109 -0
- package/dist/tsl.d.ts +136 -16
- package/dist/tsl.js +73 -16
- package/dist/webgl.d.ts +64 -14
- package/dist/webgl.js +16 -24
- package/package.json +17 -11
- package/dist/types-DVtQvVjk.d.ts +0 -45
package/dist/index.js
CHANGED
|
@@ -1,24 +1,152 @@
|
|
|
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
|
-
function
|
|
6
|
-
|
|
7
|
-
const vertexCount = geometry.attributes.position.count;
|
|
8
|
-
if (vertexCount > MAX_TEXTURE_SIZE) {
|
|
5
|
+
function asAttribute(value, mesh, name) {
|
|
6
|
+
if (!(value instanceof BufferAttribute)) {
|
|
9
7
|
throw new Error(
|
|
10
|
-
`three-vat:
|
|
8
|
+
`three-vat: mesh "${mesh.name || "(unnamed)"}" has an interleaved or unsupported "${name}" attribute; VAT bakes plain BufferAttributes`
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
return value;
|
|
12
|
+
}
|
|
13
|
+
function collectParts(root) {
|
|
14
|
+
const found = [];
|
|
15
|
+
root.traverse((o) => {
|
|
16
|
+
const mesh = o;
|
|
17
|
+
if (mesh.isMesh && mesh.geometry) found.push(mesh);
|
|
18
|
+
});
|
|
19
|
+
if (found.length === 0) {
|
|
20
|
+
throw new Error("three-vat: no Mesh found under root; nothing to bake");
|
|
21
|
+
}
|
|
22
|
+
const materials = [];
|
|
23
|
+
const parts = [];
|
|
24
|
+
for (const mesh of found) {
|
|
25
|
+
if (Array.isArray(mesh.material)) {
|
|
26
|
+
throw new Error(
|
|
27
|
+
`three-vat: mesh "${mesh.name || "(unnamed)"}" uses a material array; split it into one mesh per material before baking`
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
const geometry = mesh.geometry;
|
|
31
|
+
if (!geometry.attributes.normal) geometry.computeVertexNormals();
|
|
32
|
+
const material = mesh.material;
|
|
33
|
+
let materialIndex = materials.indexOf(material);
|
|
34
|
+
if (materialIndex === -1) materialIndex = materials.push(material) - 1;
|
|
35
|
+
const skinned = mesh;
|
|
36
|
+
parts.push({
|
|
37
|
+
mesh,
|
|
38
|
+
material,
|
|
39
|
+
materialIndex,
|
|
40
|
+
vertexStart: 0,
|
|
41
|
+
// assigned below, after material sorting
|
|
42
|
+
vertexCount: geometry.attributes.position.count,
|
|
43
|
+
basePos: asAttribute(geometry.attributes.position, mesh, "position"),
|
|
44
|
+
baseNrm: asAttribute(geometry.attributes.normal, mesh, "normal"),
|
|
45
|
+
isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
|
|
46
|
+
skeleton: skinned.skeleton,
|
|
47
|
+
skinIndex: geometry.attributes.skinIndex,
|
|
48
|
+
skinWeight: geometry.attributes.skinWeight,
|
|
49
|
+
morphPos: geometry.morphAttributes.position,
|
|
50
|
+
morphNrm: geometry.morphAttributes.normal,
|
|
51
|
+
morphRelative: geometry.morphTargetsRelative
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
parts.sort((a, b) => a.materialIndex - b.materialIndex);
|
|
55
|
+
let offset = 0;
|
|
56
|
+
for (const part of parts) {
|
|
57
|
+
part.vertexStart = offset;
|
|
58
|
+
offset += part.vertexCount;
|
|
59
|
+
}
|
|
60
|
+
return parts;
|
|
61
|
+
}
|
|
62
|
+
function mergeGeometry(parts, restMatrices, total) {
|
|
63
|
+
const position = new Float32Array(total * 3);
|
|
64
|
+
const normal = new Float32Array(total * 3);
|
|
65
|
+
const wantUV = parts.every((p) => !!p.mesh.geometry.attributes.uv);
|
|
66
|
+
const wantColor = parts.every((p) => !!p.mesh.geometry.attributes.color);
|
|
67
|
+
const uv = wantUV ? new Float32Array(total * 2) : null;
|
|
68
|
+
const color = wantColor ? new Float32Array(total * 3) : null;
|
|
69
|
+
const indices = [];
|
|
70
|
+
const groups = [];
|
|
71
|
+
const _v = new Vector3();
|
|
72
|
+
const _n = new Vector3();
|
|
73
|
+
parts.forEach((part, pi) => {
|
|
74
|
+
const m = restMatrices[pi];
|
|
75
|
+
const geometry = part.mesh.geometry;
|
|
76
|
+
const start = part.vertexStart;
|
|
77
|
+
const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
|
|
78
|
+
const srcColor = color ? asAttribute(geometry.attributes.color, part.mesh, "color") : null;
|
|
79
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
80
|
+
_v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
|
|
81
|
+
_n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
|
|
82
|
+
const o3 = (start + v) * 3;
|
|
83
|
+
position[o3] = _v.x;
|
|
84
|
+
position[o3 + 1] = _v.y;
|
|
85
|
+
position[o3 + 2] = _v.z;
|
|
86
|
+
normal[o3] = _n.x;
|
|
87
|
+
normal[o3 + 1] = _n.y;
|
|
88
|
+
normal[o3 + 2] = _n.z;
|
|
89
|
+
if (uv && srcUV) {
|
|
90
|
+
uv[(start + v) * 2] = srcUV.getX(v);
|
|
91
|
+
uv[(start + v) * 2 + 1] = srcUV.getY(v);
|
|
92
|
+
}
|
|
93
|
+
if (color && srcColor) {
|
|
94
|
+
color[o3] = srcColor.getX(v);
|
|
95
|
+
color[o3 + 1] = srcColor.getY(v);
|
|
96
|
+
color[o3 + 2] = srcColor.getZ(v);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const groupStart = indices.length;
|
|
100
|
+
const index = geometry.index;
|
|
101
|
+
if (index) {
|
|
102
|
+
for (let i = 0; i < index.count; i++) indices.push(start + index.getX(i));
|
|
103
|
+
} else {
|
|
104
|
+
for (let v = 0; v < part.vertexCount; v++) indices.push(start + v);
|
|
105
|
+
}
|
|
106
|
+
const last = groups[groups.length - 1];
|
|
107
|
+
if (last && last.materialIndex === part.materialIndex) {
|
|
108
|
+
last.count += indices.length - groupStart;
|
|
109
|
+
} else {
|
|
110
|
+
groups.push({
|
|
111
|
+
start: groupStart,
|
|
112
|
+
count: indices.length - groupStart,
|
|
113
|
+
materialIndex: part.materialIndex
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
const merged = new BufferGeometry();
|
|
118
|
+
merged.setAttribute("position", new BufferAttribute(position, 3));
|
|
119
|
+
merged.setAttribute("normal", new BufferAttribute(normal, 3));
|
|
120
|
+
if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
|
|
121
|
+
if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
|
|
122
|
+
merged.setIndex(indices);
|
|
123
|
+
for (const g of groups) merged.addGroup(g.start, g.count, g.materialIndex);
|
|
124
|
+
return merged;
|
|
125
|
+
}
|
|
126
|
+
function bakeVAT(root, clips, { fps = 30, maxTextureSize = MAX_TEXTURE_SIZE } = {}) {
|
|
127
|
+
root.updateMatrixWorld(true);
|
|
128
|
+
const parts = collectParts(root);
|
|
129
|
+
const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
|
|
130
|
+
if (vertexCount > maxTextureSize) {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`three-vat: vertexCount ${vertexCount} exceeds maxTextureSize ${maxTextureSize}; row wrapping is not implemented`
|
|
11
133
|
);
|
|
12
134
|
}
|
|
13
135
|
const frameCounts = clips.map((c) => Math.max(2, Math.round(c.duration * fps)));
|
|
14
136
|
const totalFrames = frameCounts.reduce((a, b) => a + b, 0);
|
|
137
|
+
if (totalFrames > maxTextureSize) {
|
|
138
|
+
throw new Error(
|
|
139
|
+
`three-vat: totalFrames ${totalFrames} exceeds maxTextureSize ${maxTextureSize}; lower fps or bake fewer clips`
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
const rootInverse = root.matrixWorld.clone().invert();
|
|
143
|
+
const restMatrices = parts.map(
|
|
144
|
+
(p) => new Matrix4().multiplyMatrices(rootInverse, p.mesh.matrixWorld)
|
|
145
|
+
);
|
|
146
|
+
const geometry = mergeGeometry(parts, restMatrices, vertexCount);
|
|
147
|
+
const mergedBase = geometry.attributes.position;
|
|
15
148
|
const posData = new Float32Array(vertexCount * totalFrames * 4);
|
|
16
149
|
const nrmData = new Float32Array(vertexCount * totalFrames * 4);
|
|
17
|
-
const basePos = geometry.attributes.position;
|
|
18
|
-
const baseNrm = geometry.attributes.normal;
|
|
19
|
-
const skinIndex = geometry.attributes.skinIndex;
|
|
20
|
-
const skinWeight = geometry.attributes.skinWeight;
|
|
21
|
-
const skeleton = skinnedMesh.skeleton;
|
|
22
150
|
const mixer = new AnimationMixer(root);
|
|
23
151
|
const bounds = new Box3();
|
|
24
152
|
const clipTable = [];
|
|
@@ -27,9 +155,15 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
|
|
|
27
155
|
const _bone = new Matrix4();
|
|
28
156
|
const _acc = new Matrix4();
|
|
29
157
|
const _skin = new Matrix4();
|
|
158
|
+
const _partMatrix = new Matrix4();
|
|
30
159
|
const _p = new Vector3();
|
|
31
160
|
const _bp = new Vector3();
|
|
32
161
|
const _n = new Vector3();
|
|
162
|
+
const _mt = new Vector3();
|
|
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;
|
|
33
167
|
let rowOffset = 0;
|
|
34
168
|
clips.forEach((clip, ci) => {
|
|
35
169
|
const frames = frameCounts[ci];
|
|
@@ -40,37 +174,76 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
|
|
|
40
174
|
mixer.setTime(f / frames * clip.duration);
|
|
41
175
|
root.updateMatrixWorld(true);
|
|
42
176
|
const row = rowOffset + f;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
177
|
+
if (!warnedNonUniformScale) {
|
|
178
|
+
warnedNonUniformScale = warnOnNonUniformBoneScale(influencers, _bone);
|
|
179
|
+
}
|
|
180
|
+
for (const part of parts) {
|
|
181
|
+
_partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
|
|
182
|
+
const influences = part.mesh.morphTargetInfluences;
|
|
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;
|
|
185
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
186
|
+
const vi = part.vertexStart + v;
|
|
187
|
+
_p.fromBufferAttribute(part.basePos, v);
|
|
188
|
+
_n.fromBufferAttribute(part.baseNrm, v);
|
|
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++) {
|
|
195
|
+
const w = influences[t];
|
|
196
|
+
if (w === 0) continue;
|
|
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
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (isSkinned && skeleton) {
|
|
212
|
+
const skinned = part.mesh;
|
|
213
|
+
_si.fromBufferAttribute(skinIndex, v);
|
|
214
|
+
_sw.fromBufferAttribute(skinWeight, v);
|
|
215
|
+
_acc.elements.fill(0);
|
|
216
|
+
for (let i = 0; i < 4; i++) {
|
|
217
|
+
const w = _sw.getComponent(i);
|
|
218
|
+
if (w === 0) continue;
|
|
219
|
+
const bi = _si.getComponent(i);
|
|
220
|
+
_bone.multiplyMatrices(skeleton.bones[bi].matrixWorld, skeleton.boneInverses[bi]);
|
|
221
|
+
const ae = _acc.elements;
|
|
222
|
+
const be = _bone.elements;
|
|
223
|
+
for (let e = 0; e < 16; e++) ae[e] += be[e] * w;
|
|
224
|
+
}
|
|
225
|
+
_skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
|
|
226
|
+
_p.applyMatrix4(_skin);
|
|
227
|
+
_n.transformDirection(_skin);
|
|
228
|
+
}
|
|
229
|
+
_p.applyMatrix4(_partMatrix);
|
|
230
|
+
_n.transformDirection(_partMatrix);
|
|
231
|
+
bounds.expandByPoint(_p);
|
|
232
|
+
_bp.fromBufferAttribute(mergedBase, vi);
|
|
233
|
+
const o = (row * vertexCount + vi) * 4;
|
|
234
|
+
const dx = _p.x - _bp.x;
|
|
235
|
+
const dy = _p.y - _bp.y;
|
|
236
|
+
const dz = _p.z - _bp.z;
|
|
237
|
+
maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
|
|
238
|
+
posData[o] = dx;
|
|
239
|
+
posData[o + 1] = dy;
|
|
240
|
+
posData[o + 2] = dz;
|
|
241
|
+
posData[o + 3] = 1;
|
|
242
|
+
nrmData[o] = _n.x;
|
|
243
|
+
nrmData[o + 1] = _n.y;
|
|
244
|
+
nrmData[o + 2] = _n.z;
|
|
245
|
+
nrmData[o + 3] = 1;
|
|
55
246
|
}
|
|
56
|
-
_skin.multiplyMatrices(_acc, skinnedMesh.bindMatrix).premultiply(skinnedMesh.bindMatrixInverse);
|
|
57
|
-
_bp.fromBufferAttribute(basePos, v);
|
|
58
|
-
_p.copy(_bp).applyMatrix4(_skin);
|
|
59
|
-
bounds.expandByPoint(_p);
|
|
60
|
-
_n.fromBufferAttribute(baseNrm, v).transformDirection(_skin);
|
|
61
|
-
const o = (row * vertexCount + v) * 4;
|
|
62
|
-
const dx = _p.x - _bp.x;
|
|
63
|
-
const dy = _p.y - _bp.y;
|
|
64
|
-
const dz = _p.z - _bp.z;
|
|
65
|
-
maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
|
|
66
|
-
posData[o] = dx;
|
|
67
|
-
posData[o + 1] = dy;
|
|
68
|
-
posData[o + 2] = dz;
|
|
69
|
-
posData[o + 3] = 1;
|
|
70
|
-
nrmData[o] = _n.x;
|
|
71
|
-
nrmData[o + 1] = _n.y;
|
|
72
|
-
nrmData[o + 2] = _n.z;
|
|
73
|
-
nrmData[o + 3] = 1;
|
|
74
247
|
}
|
|
75
248
|
}
|
|
76
249
|
action.stop();
|
|
@@ -87,6 +260,10 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
|
|
|
87
260
|
mixer.stopAllAction();
|
|
88
261
|
mixer.setTime(0);
|
|
89
262
|
root.updateMatrixWorld(true);
|
|
263
|
+
geometry.boundingBox = bounds.clone();
|
|
264
|
+
geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
|
|
265
|
+
const materials = [];
|
|
266
|
+
for (const part of parts) materials[part.materialIndex] = part.material;
|
|
90
267
|
return {
|
|
91
268
|
positionTexture: makeVATTexture(posData, vertexCount, totalFrames),
|
|
92
269
|
normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames),
|
|
@@ -94,9 +271,44 @@ function bakeVAT(root, skinnedMesh, clips, { fps = 30 } = {}) {
|
|
|
94
271
|
bounds,
|
|
95
272
|
vertexCount,
|
|
96
273
|
totalFrames,
|
|
97
|
-
encoding: "delta"
|
|
274
|
+
encoding: "delta",
|
|
275
|
+
geometry,
|
|
276
|
+
materials
|
|
98
277
|
};
|
|
99
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
|
+
}
|
|
100
312
|
function makeVATTexture(data, width, height, type = FloatType) {
|
|
101
313
|
const tex = new DataTexture(data, width, height, RGBAFormat, type);
|
|
102
314
|
tex.minFilter = NearestFilter;
|
|
@@ -105,50 +317,5 @@ function makeVATTexture(data, width, height, type = FloatType) {
|
|
|
105
317
|
tex.needsUpdate = true;
|
|
106
318
|
return tex;
|
|
107
319
|
}
|
|
108
|
-
function serializeVAT(vat, { precision = "float16" } = {}) {
|
|
109
|
-
const pos = vat.positionTexture.image.data;
|
|
110
|
-
const nrm = vat.normalTexture.image.data;
|
|
111
|
-
const manifest = {
|
|
112
|
-
version: 1,
|
|
113
|
-
vertexCount: vat.vertexCount,
|
|
114
|
-
totalFrames: vat.totalFrames,
|
|
115
|
-
encoding: vat.encoding,
|
|
116
|
-
precision,
|
|
117
|
-
clips: vat.clips,
|
|
118
|
-
bounds: {
|
|
119
|
-
min: [vat.bounds.min.x, vat.bounds.min.y, vat.bounds.min.z],
|
|
120
|
-
max: [vat.bounds.max.x, vat.bounds.max.y, vat.bounds.max.z]
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
return { manifest, position: encode(pos, precision), normal: encode(nrm, precision) };
|
|
124
|
-
}
|
|
125
|
-
function encode(src, precision) {
|
|
126
|
-
if (precision === "float32") {
|
|
127
|
-
return src.slice().buffer;
|
|
128
|
-
}
|
|
129
|
-
const half = new Uint16Array(src.length);
|
|
130
|
-
for (let i = 0; i < src.length; i++) half[i] = DataUtils.toHalfFloat(src[i]);
|
|
131
|
-
return half.buffer;
|
|
132
|
-
}
|
|
133
|
-
function loadVAT(serialized) {
|
|
134
|
-
const { manifest, position, normal } = serialized;
|
|
135
|
-
const { vertexCount, totalFrames, precision } = manifest;
|
|
136
|
-
const type = precision === "float32" ? FloatType : HalfFloatType;
|
|
137
|
-
const posData = precision === "float32" ? new Float32Array(position) : new Uint16Array(position);
|
|
138
|
-
const nrmData = precision === "float32" ? new Float32Array(normal) : new Uint16Array(normal);
|
|
139
|
-
const bounds = new Box3(
|
|
140
|
-
new Vector3().fromArray(manifest.bounds.min),
|
|
141
|
-
new Vector3().fromArray(manifest.bounds.max)
|
|
142
|
-
);
|
|
143
|
-
return {
|
|
144
|
-
positionTexture: makeVATTexture(posData, vertexCount, totalFrames, type),
|
|
145
|
-
normalTexture: makeVATTexture(nrmData, vertexCount, totalFrames, type),
|
|
146
|
-
clips: manifest.clips,
|
|
147
|
-
bounds,
|
|
148
|
-
vertexCount,
|
|
149
|
-
totalFrames,
|
|
150
|
-
encoding: manifest.encoding
|
|
151
|
-
};
|
|
152
|
-
}
|
|
153
320
|
|
|
154
|
-
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 };
|