three-vat 4.1.0 → 4.2.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 +37 -8
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +585 -0
- package/dist/carrier-DmKhB4QL.d.ts +13 -0
- package/dist/{chunk-OEKKEHRQ.js → chunk-4IKOXUSX.js} +85 -26
- package/dist/chunk-7TT7B5WS.js +263 -0
- package/dist/chunk-P6AFXEBT.js +1104 -0
- package/dist/index.d.ts +37 -5
- package/dist/index.js +142 -965
- package/dist/tsl.d.ts +2 -1
- package/dist/tsl.js +18 -15
- package/dist/{carrier-VLSJLgjX.d.ts → types-CDPXKe0_.d.ts} +99 -34
- package/dist/webgl.d.ts +2 -1
- package/dist/webgl.js +25 -14
- package/dist/write.d.ts +53 -0
- package/dist/write.js +3 -0
- package/package.json +88 -81
|
@@ -0,0 +1,1104 @@
|
|
|
1
|
+
import { LoopMode, MAX_TEXTURE_SIZE, vertexLayoutFor, makeVATNormalTexture, makeVATTexture, RIG_TEXELS_PER_SLOT, LIBRARY_PLAYBACK_DEFAULTS, INFINITE_REPETITIONS, HALF_FLOAT_MAX, RIG_TEXELS } from './chunk-4IKOXUSX.js';
|
|
2
|
+
import { LoopRepeat, LoopOnce, LoopPingPong, DataUtils, Matrix4, Box3, Vector4, Vector3, Sphere, HalfFloatType, Quaternion, BufferGeometry, BufferAttribute, FloatType, AdditiveAnimationBlendMode, AnimationMixer, PropertyBinding, Color, InterleavedBufferAttribute, InterleavedBuffer } from 'three';
|
|
3
|
+
|
|
4
|
+
// src/octahedral.ts
|
|
5
|
+
var signNotZero = (v) => v >= 0 ? 1 : -1;
|
|
6
|
+
var toByte = (v) => Math.max(0, Math.min(255, Math.round((v * 0.5 + 0.5) * 255)));
|
|
7
|
+
function encodeOctahedral(x, y, z, out, offset) {
|
|
8
|
+
const l1 = Math.abs(x) + Math.abs(y) + Math.abs(z);
|
|
9
|
+
if (l1 === 0) {
|
|
10
|
+
out[offset] = toByte(0);
|
|
11
|
+
out[offset + 1] = toByte(0);
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
const px = x / l1;
|
|
15
|
+
const py = y / l1;
|
|
16
|
+
const fold = z <= 0;
|
|
17
|
+
out[offset] = toByte(fold ? (1 - Math.abs(py)) * signNotZero(px) : px);
|
|
18
|
+
out[offset + 1] = toByte(fold ? (1 - Math.abs(px)) * signNotZero(py) : py);
|
|
19
|
+
}
|
|
20
|
+
function decodeOctahedral(u, v, out = { x: 0, y: 0, z: 0 }) {
|
|
21
|
+
const ex = u / 255 * 2 - 1;
|
|
22
|
+
const ey = v / 255 * 2 - 1;
|
|
23
|
+
const z = 1 - Math.abs(ex) - Math.abs(ey);
|
|
24
|
+
const t = Math.max(-z, 0);
|
|
25
|
+
const x = ex - signNotZero(ex) * t;
|
|
26
|
+
const y = ey - signNotZero(ey) * t;
|
|
27
|
+
const length = Math.hypot(x, y, z) || 1;
|
|
28
|
+
out.x = x / length;
|
|
29
|
+
out.y = y / length;
|
|
30
|
+
out.z = z / length;
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
var IGNORED = ["uuid", "name", "color", "userData", "metadata"];
|
|
34
|
+
function flatFacts(material) {
|
|
35
|
+
const m = material;
|
|
36
|
+
if (!m.color?.isColor || m.vertexColors || m.isShadowMaterial) return null;
|
|
37
|
+
const own = (key) => Object.prototype.hasOwnProperty.call(m, key);
|
|
38
|
+
if (own("onBeforeCompile") || own("customProgramCacheKey")) return null;
|
|
39
|
+
for (const value of Object.values(m)) {
|
|
40
|
+
const v = value;
|
|
41
|
+
if (v?.isTexture || v?.isNode) return null;
|
|
42
|
+
}
|
|
43
|
+
const json = m.toJSON();
|
|
44
|
+
for (const key of IGNORED) delete json[key];
|
|
45
|
+
return { key: JSON.stringify(json), color: [m.color.r, m.color.g, m.color.b] };
|
|
46
|
+
}
|
|
47
|
+
function mergedFlatMaterial(members) {
|
|
48
|
+
const merged = members[0].clone();
|
|
49
|
+
merged.color.setRGB(1, 1, 1);
|
|
50
|
+
merged.vertexColors = true;
|
|
51
|
+
merged.name = members.map((m) => m.name).filter(Boolean).join(" + ");
|
|
52
|
+
return merged;
|
|
53
|
+
}
|
|
54
|
+
var FLAT_MERGE = { facts: flatFacts, merge: mergedFlatMaterial };
|
|
55
|
+
function planFlatMerge(materials, hooks) {
|
|
56
|
+
const facts = /* @__PURE__ */ new Map();
|
|
57
|
+
const groups = /* @__PURE__ */ new Map();
|
|
58
|
+
for (const material of materials) {
|
|
59
|
+
if (facts.has(material)) continue;
|
|
60
|
+
const f = hooks.facts(material);
|
|
61
|
+
if (!f) continue;
|
|
62
|
+
facts.set(material, f);
|
|
63
|
+
groups.set(f.key, [...groups.get(f.key) ?? [], material]);
|
|
64
|
+
}
|
|
65
|
+
const targets = /* @__PURE__ */ new Map();
|
|
66
|
+
for (const members of groups.values()) {
|
|
67
|
+
if (members.length < 2) continue;
|
|
68
|
+
const merged = hooks.merge(members);
|
|
69
|
+
for (const m of members) targets.set(m, { material: merged, tint: new Color().fromArray(facts.get(m).color) });
|
|
70
|
+
}
|
|
71
|
+
return { targetOf: (material) => targets.get(material) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// src/bake.ts
|
|
75
|
+
var LOOP_MODES = /* @__PURE__ */ new Map([
|
|
76
|
+
[LoopRepeat, LoopMode.Repeat],
|
|
77
|
+
[LoopOnce, LoopMode.Once],
|
|
78
|
+
[LoopPingPong, LoopMode.PingPong]
|
|
79
|
+
]);
|
|
80
|
+
function isAction(input) {
|
|
81
|
+
return typeof input.getClip === "function";
|
|
82
|
+
}
|
|
83
|
+
function resolveAnimation(input) {
|
|
84
|
+
if (!isAction(input)) return { clip: input, defaults: { ...LIBRARY_PLAYBACK_DEFAULTS } };
|
|
85
|
+
const clip = input.getClip();
|
|
86
|
+
const name = clip.name || "(unnamed)";
|
|
87
|
+
const cannotBlend = (field, value) => new Error(
|
|
88
|
+
`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`
|
|
89
|
+
);
|
|
90
|
+
if (input.weight !== 1) throw cannotBlend("weight", String(input.weight));
|
|
91
|
+
if (input.blendMode === AdditiveAnimationBlendMode) throw cannotBlend("blendMode", "additive");
|
|
92
|
+
const loopMode = LOOP_MODES.get(input.loop);
|
|
93
|
+
if (loopMode === void 0) {
|
|
94
|
+
throw new Error(
|
|
95
|
+
`three-vat: action for clip "${name}" has an unrecognised loop mode ${input.loop}; expected THREE.LoopRepeat, LoopOnce or LoopPingPong`
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
clip,
|
|
100
|
+
defaults: {
|
|
101
|
+
loopMode,
|
|
102
|
+
// three's `repetitions` defaults to Infinity and `LoopOnce` ignores it
|
|
103
|
+
// outright — so a one-shot is one play whatever the field says, and an
|
|
104
|
+
// endless count becomes the sentinel a Float32Array can carry.
|
|
105
|
+
repetitions: loopMode === LoopMode.Once ? 1 : Number.isFinite(input.repetitions) ? input.repetitions : INFINITE_REPETITIONS,
|
|
106
|
+
// Not read off the action (see the rule above), so a bake clamps whichever
|
|
107
|
+
// input it was handed. Uniform across loop modes on purpose: an instance
|
|
108
|
+
// may override a repeating clip's `loopMode` to `Once` and inherit this
|
|
109
|
+
// field, so there is no mode whose end mode is safely unreachable and
|
|
110
|
+
// could hold a different answer. Rewind is per instance:
|
|
111
|
+
// `{ ..., endMode: EndMode.Rewind }` (#43, ADR-0017).
|
|
112
|
+
endMode: LIBRARY_PLAYBACK_DEFAULTS.endMode,
|
|
113
|
+
speed: input.timeScale
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
function morphCountOf({ mesh, morphPos, morphNrm }) {
|
|
118
|
+
const influences = mesh.morphTargetInfluences;
|
|
119
|
+
return influences ? Math.min(influences.length, Math.max(morphPos?.length ?? 0, morphNrm?.length ?? 0)) : 0;
|
|
120
|
+
}
|
|
121
|
+
var _mt = /* @__PURE__ */ new Vector3();
|
|
122
|
+
var _mb = /* @__PURE__ */ new Vector3();
|
|
123
|
+
var _mbn = /* @__PURE__ */ new Vector3();
|
|
124
|
+
function morphVertex(part, v, weights, count, p, n) {
|
|
125
|
+
const { basePos, baseNrm, morphPos, morphNrm, morphRelative } = part;
|
|
126
|
+
if (!morphRelative) {
|
|
127
|
+
_mb.fromBufferAttribute(basePos, v);
|
|
128
|
+
if (n) _mbn.fromBufferAttribute(baseNrm, v);
|
|
129
|
+
}
|
|
130
|
+
for (let t = 0; t < count; t++) {
|
|
131
|
+
const w = weights[t];
|
|
132
|
+
if (w === 0) continue;
|
|
133
|
+
const targetPos = morphPos?.[t];
|
|
134
|
+
if (targetPos) {
|
|
135
|
+
_mt.fromBufferAttribute(targetPos, v);
|
|
136
|
+
if (!morphRelative) _mt.sub(_mb);
|
|
137
|
+
p.addScaledVector(_mt, w);
|
|
138
|
+
}
|
|
139
|
+
const targetNrm = n ? morphNrm?.[t] : void 0;
|
|
140
|
+
if (n && targetNrm) {
|
|
141
|
+
_mt.fromBufferAttribute(targetNrm, v);
|
|
142
|
+
if (!morphRelative) _mt.sub(_mbn);
|
|
143
|
+
n.addScaledVector(_mt, w);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
function asAttribute(value, mesh, name) {
|
|
148
|
+
if (!(value instanceof BufferAttribute)) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
`three-vat: mesh ${meshName(mesh)} has an interleaved or unsupported "${name}" attribute; VAT bakes plain BufferAttributes`
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
return value;
|
|
154
|
+
}
|
|
155
|
+
function deriveNormals(position, index) {
|
|
156
|
+
const scratch = new BufferGeometry();
|
|
157
|
+
scratch.setAttribute("position", position);
|
|
158
|
+
scratch.setIndex(index);
|
|
159
|
+
scratch.computeVertexNormals();
|
|
160
|
+
return scratch.attributes.normal;
|
|
161
|
+
}
|
|
162
|
+
function piecesOf(mesh) {
|
|
163
|
+
const { geometry } = mesh;
|
|
164
|
+
if (!Array.isArray(mesh.material)) return [{ geometry, material: mesh.material }];
|
|
165
|
+
const materials = mesh.material;
|
|
166
|
+
if (geometry.groups.length === 0) {
|
|
167
|
+
throw new Error(
|
|
168
|
+
`three-vat: mesh ${meshName(mesh)} uses a material array but its geometry has no groups; give it one group per material, or a single material`
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
const normal = geometry.attributes.normal ?? deriveNormals(asAttribute(geometry.attributes.position, mesh, "position"), geometry.index);
|
|
172
|
+
return geometry.groups.flatMap((group, g) => {
|
|
173
|
+
const index = group.materialIndex ?? 0;
|
|
174
|
+
const material = materials[index];
|
|
175
|
+
if (!material) {
|
|
176
|
+
throw new Error(
|
|
177
|
+
`three-vat: mesh ${meshName(mesh)}: group ${g} draws material ${index}, which its material array of ${materials.length} does not hold`
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
const piece = groupGeometry(geometry, normal, group);
|
|
181
|
+
return piece ? [{ geometry: piece, material }] : [];
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function groupGeometry(source, normal, group) {
|
|
185
|
+
const index = source.index;
|
|
186
|
+
const end = Math.min(group.start + group.count, index ? index.count : source.attributes.position.count);
|
|
187
|
+
const vertices = [];
|
|
188
|
+
const local = /* @__PURE__ */ new Map();
|
|
189
|
+
const remapped = [];
|
|
190
|
+
for (let i = group.start; i < end; i++) {
|
|
191
|
+
const v = index ? index.getX(i) : i;
|
|
192
|
+
let at = local.get(v);
|
|
193
|
+
if (at === void 0) {
|
|
194
|
+
at = vertices.push(v) - 1;
|
|
195
|
+
local.set(v, at);
|
|
196
|
+
}
|
|
197
|
+
remapped.push(at);
|
|
198
|
+
}
|
|
199
|
+
if (vertices.length === 0) return null;
|
|
200
|
+
const copy = vertexCopier(vertices);
|
|
201
|
+
const geometry = new BufferGeometry();
|
|
202
|
+
for (const [name, attribute] of Object.entries(source.attributes)) {
|
|
203
|
+
geometry.setAttribute(name, copy(name === "normal" ? normal : attribute));
|
|
204
|
+
}
|
|
205
|
+
if (!source.attributes.normal) geometry.setAttribute("normal", copy(normal));
|
|
206
|
+
for (const [name, targets] of Object.entries(source.morphAttributes)) {
|
|
207
|
+
geometry.morphAttributes[name] = targets.map(copy);
|
|
208
|
+
}
|
|
209
|
+
geometry.morphTargetsRelative = source.morphTargetsRelative;
|
|
210
|
+
if (index) geometry.setIndex(remapped);
|
|
211
|
+
return geometry;
|
|
212
|
+
}
|
|
213
|
+
function vertexCopier(vertices) {
|
|
214
|
+
const buffers = /* @__PURE__ */ new Map();
|
|
215
|
+
const rows = (array, stride) => {
|
|
216
|
+
const out = new array.constructor(vertices.length * stride);
|
|
217
|
+
vertices.forEach((v, i) => out.set(array.subarray(v * stride, (v + 1) * stride), i * stride));
|
|
218
|
+
return out;
|
|
219
|
+
};
|
|
220
|
+
return (attribute) => {
|
|
221
|
+
const { itemSize, normalized } = attribute;
|
|
222
|
+
if (attribute instanceof InterleavedBufferAttribute) {
|
|
223
|
+
const { data } = attribute;
|
|
224
|
+
let copied = buffers.get(data);
|
|
225
|
+
if (!copied) buffers.set(data, copied = new InterleavedBuffer(rows(data.array, data.stride), data.stride));
|
|
226
|
+
return new InterleavedBufferAttribute(copied, itemSize, attribute.offset, normalized);
|
|
227
|
+
}
|
|
228
|
+
return new attribute.constructor(rows(attribute.array, itemSize), itemSize, normalized);
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
function collectParts(root, hooks) {
|
|
232
|
+
const found = [];
|
|
233
|
+
root.traverse((o) => {
|
|
234
|
+
const mesh = o;
|
|
235
|
+
if (mesh.isMesh && mesh.geometry) found.push(mesh);
|
|
236
|
+
});
|
|
237
|
+
if (found.length === 0) {
|
|
238
|
+
throw new Error("three-vat: no Mesh found under root; nothing to bake");
|
|
239
|
+
}
|
|
240
|
+
const pieces = found.flatMap((mesh) => piecesOf(mesh).map((piece) => ({ mesh, ...piece })));
|
|
241
|
+
if (pieces.length === 0) {
|
|
242
|
+
throw new Error("three-vat: no geometry group under root draws anything; nothing to bake");
|
|
243
|
+
}
|
|
244
|
+
const flat = hooks ? planFlatMerge(
|
|
245
|
+
pieces.map((piece) => piece.material),
|
|
246
|
+
hooks
|
|
247
|
+
) : null;
|
|
248
|
+
const materials = [];
|
|
249
|
+
const parts = [];
|
|
250
|
+
for (const { mesh, geometry, material: source } of pieces) {
|
|
251
|
+
const merged = flat?.targetOf(source);
|
|
252
|
+
const material = merged?.material ?? source;
|
|
253
|
+
let materialIndex = materials.indexOf(material);
|
|
254
|
+
if (materialIndex === -1) materialIndex = materials.push(material) - 1;
|
|
255
|
+
const skinned = mesh;
|
|
256
|
+
const basePos = asAttribute(geometry.attributes.position, mesh, "position");
|
|
257
|
+
parts.push({
|
|
258
|
+
mesh,
|
|
259
|
+
geometry,
|
|
260
|
+
material,
|
|
261
|
+
materialIndex,
|
|
262
|
+
vertexStart: 0,
|
|
263
|
+
// assigned below, after material sorting
|
|
264
|
+
vertexCount: geometry.attributes.position.count,
|
|
265
|
+
basePos,
|
|
266
|
+
baseNrm: geometry.attributes.normal ? asAttribute(geometry.attributes.normal, mesh, "normal") : deriveNormals(basePos, geometry.index),
|
|
267
|
+
isSkinned: !!geometry.attributes.skinWeight && !!skinned.skeleton,
|
|
268
|
+
skeleton: skinned.skeleton,
|
|
269
|
+
pose: void 0,
|
|
270
|
+
// assigned below, once the distinct rigs are known
|
|
271
|
+
skinIndex: geometry.attributes.skinIndex,
|
|
272
|
+
skinWeight: geometry.attributes.skinWeight,
|
|
273
|
+
morphPos: geometry.morphAttributes.position,
|
|
274
|
+
morphNrm: geometry.morphAttributes.normal,
|
|
275
|
+
morphRelative: geometry.morphTargetsRelative,
|
|
276
|
+
tint: merged?.tint ?? null
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
parts.sort((a, b) => a.materialIndex - b.materialIndex);
|
|
280
|
+
let offset = 0;
|
|
281
|
+
for (const part of parts) {
|
|
282
|
+
part.vertexStart = offset;
|
|
283
|
+
offset += part.vertexCount;
|
|
284
|
+
}
|
|
285
|
+
return parts;
|
|
286
|
+
}
|
|
287
|
+
function mergeGeometry(parts, restMatrices, total) {
|
|
288
|
+
const position = new Float32Array(total * 3);
|
|
289
|
+
const normal = new Float32Array(total * 3);
|
|
290
|
+
const _v = new Vector3();
|
|
291
|
+
const _n = new Vector3();
|
|
292
|
+
parts.forEach((part, pi) => {
|
|
293
|
+
const m = restMatrices[pi];
|
|
294
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
295
|
+
_v.fromBufferAttribute(part.basePos, v).applyMatrix4(m);
|
|
296
|
+
_n.fromBufferAttribute(part.baseNrm, v).transformDirection(m);
|
|
297
|
+
_v.toArray(position, (part.vertexStart + v) * 3);
|
|
298
|
+
_n.toArray(normal, (part.vertexStart + v) * 3);
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
const merged = new BufferGeometry();
|
|
302
|
+
merged.setAttribute("position", new BufferAttribute(position, 3));
|
|
303
|
+
merged.setAttribute("normal", new BufferAttribute(normal, 3));
|
|
304
|
+
mergeSurface(merged, parts, total, restMatrices);
|
|
305
|
+
return merged;
|
|
306
|
+
}
|
|
307
|
+
function mergeSurface(merged, parts, total, restMatrices) {
|
|
308
|
+
const merging = parts.some((p) => p.tint);
|
|
309
|
+
const want = optionalAttributes(parts, merging);
|
|
310
|
+
const uv = want.uv ? new Float32Array(total * 2) : null;
|
|
311
|
+
const color = want.color ? new Float32Array(total * 3) : null;
|
|
312
|
+
const tangent = want.tangent ? new Float32Array(total * 4) : null;
|
|
313
|
+
const _t = new Vector3();
|
|
314
|
+
parts.forEach((part, pi) => {
|
|
315
|
+
const geometry = part.geometry;
|
|
316
|
+
const start = part.vertexStart;
|
|
317
|
+
const srcUV = uv ? asAttribute(geometry.attributes.uv, part.mesh, "uv") : null;
|
|
318
|
+
const srcColor = color ? colorSource(part, merging) : null;
|
|
319
|
+
const srcTangent = tangent ? geometry.attributes.tangent : null;
|
|
320
|
+
const m = restMatrices?.[pi];
|
|
321
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
322
|
+
const vi = start + v;
|
|
323
|
+
if (uv && srcUV) {
|
|
324
|
+
uv[vi * 2] = srcUV.getX(v);
|
|
325
|
+
uv[vi * 2 + 1] = srcUV.getY(v);
|
|
326
|
+
}
|
|
327
|
+
if (color) writeColor(color, vi * 3, part, srcColor, v);
|
|
328
|
+
if (tangent && srcTangent) {
|
|
329
|
+
_t.fromBufferAttribute(srcTangent, v);
|
|
330
|
+
if (m) _t.transformDirection(m);
|
|
331
|
+
const o4 = vi * 4;
|
|
332
|
+
tangent[o4] = _t.x;
|
|
333
|
+
tangent[o4 + 1] = _t.y;
|
|
334
|
+
tangent[o4 + 2] = _t.z;
|
|
335
|
+
tangent[o4 + 3] = srcTangent.getW(v);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
if (uv) merged.setAttribute("uv", new BufferAttribute(uv, 2));
|
|
340
|
+
if (color) merged.setAttribute("color", new BufferAttribute(color, 3));
|
|
341
|
+
if (tangent) merged.setAttribute("tangent", new BufferAttribute(tangent, 4));
|
|
342
|
+
indexParts(merged, parts);
|
|
343
|
+
}
|
|
344
|
+
function colorSource(part, merging) {
|
|
345
|
+
const source = part.geometry.attributes.color;
|
|
346
|
+
if (part.tint || !source) return null;
|
|
347
|
+
if (merging && source instanceof InterleavedBufferAttribute) return source;
|
|
348
|
+
return asAttribute(source, part.mesh, "color");
|
|
349
|
+
}
|
|
350
|
+
function writeColor(out, o3, part, source, v) {
|
|
351
|
+
if (part.tint) {
|
|
352
|
+
out[o3] = part.tint.r;
|
|
353
|
+
out[o3 + 1] = part.tint.g;
|
|
354
|
+
out[o3 + 2] = part.tint.b;
|
|
355
|
+
} else if (source) {
|
|
356
|
+
out[o3] = source.getX(v);
|
|
357
|
+
out[o3 + 1] = source.getY(v);
|
|
358
|
+
out[o3 + 2] = source.getZ(v);
|
|
359
|
+
} else {
|
|
360
|
+
out[o3] = out[o3 + 1] = out[o3 + 2] = 1;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function optionalAttributes(parts, merging) {
|
|
364
|
+
return {
|
|
365
|
+
uv: parts.every((p) => !!p.geometry.attributes.uv),
|
|
366
|
+
// A merge writes a colour for every part it touched, so the attribute is
|
|
367
|
+
// there whenever one happened; a part it did not touch keeps its own, or
|
|
368
|
+
// white, which leaves an untinted material's shading as it was.
|
|
369
|
+
color: merging || parts.every((p) => !!p.geometry.attributes.color),
|
|
370
|
+
tangent: parts.every((p) => {
|
|
371
|
+
const t = p.geometry.attributes.tangent;
|
|
372
|
+
return t instanceof BufferAttribute && t.itemSize === 4;
|
|
373
|
+
})
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function indexParts(merged, parts) {
|
|
377
|
+
const indices = [];
|
|
378
|
+
const groups = [];
|
|
379
|
+
for (const part of parts) {
|
|
380
|
+
const start = part.vertexStart;
|
|
381
|
+
const groupStart = indices.length;
|
|
382
|
+
const index = part.geometry.index;
|
|
383
|
+
if (index) {
|
|
384
|
+
for (let i = 0; i < index.count; i++) indices.push(start + index.getX(i));
|
|
385
|
+
} else {
|
|
386
|
+
for (let v = 0; v < part.vertexCount; v++) indices.push(start + v);
|
|
387
|
+
}
|
|
388
|
+
const last = groups[groups.length - 1];
|
|
389
|
+
if (last && last.materialIndex === part.materialIndex) {
|
|
390
|
+
last.count += indices.length - groupStart;
|
|
391
|
+
} else {
|
|
392
|
+
groups.push({
|
|
393
|
+
start: groupStart,
|
|
394
|
+
count: indices.length - groupStart,
|
|
395
|
+
materialIndex: part.materialIndex
|
|
396
|
+
});
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
merged.setIndex(indices);
|
|
400
|
+
for (const g of groups) merged.addGroup(g.start, g.count, g.materialIndex);
|
|
401
|
+
}
|
|
402
|
+
function frameCountsFor(clips, fps, maxTextureSize) {
|
|
403
|
+
const frameCounts = clips.map((c) => Math.max(2, Math.round(c.duration * fps)));
|
|
404
|
+
const totalFrames = frameCounts.reduce((a, b) => a + b, 0);
|
|
405
|
+
if (totalFrames > maxTextureSize) {
|
|
406
|
+
throw new Error(
|
|
407
|
+
`three-vat: totalFrames ${totalFrames} exceeds maxTextureSize ${maxTextureSize}; lower fps or bake fewer clips`
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
return { frameCounts, totalFrames };
|
|
411
|
+
}
|
|
412
|
+
function bakeVAT(root, animations, options = {}) {
|
|
413
|
+
return bakeVATWith(root, animations, options, FLAT_MERGE);
|
|
414
|
+
}
|
|
415
|
+
function bakeVATWith(root, animations, options, hooks) {
|
|
416
|
+
const {
|
|
417
|
+
fps = 30,
|
|
418
|
+
maxTextureSize = MAX_TEXTURE_SIZE,
|
|
419
|
+
bakeNormals = true,
|
|
420
|
+
encoding = "auto",
|
|
421
|
+
mergeFlatMaterials = false
|
|
422
|
+
} = options;
|
|
423
|
+
const resolved = animations.map(resolveAnimation);
|
|
424
|
+
root.updateMatrixWorld(true);
|
|
425
|
+
const parts = collectParts(root, mergeFlatMaterials ? hooks : null);
|
|
426
|
+
if (encoding === "rig") return bakeRig(root, parts, resolved, { fps, maxTextureSize });
|
|
427
|
+
if (encoding === "delta") {
|
|
428
|
+
return { ...bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }), fallback: null };
|
|
429
|
+
}
|
|
430
|
+
let refusal;
|
|
431
|
+
try {
|
|
432
|
+
return bakeRig(root, parts, resolved, { fps, maxTextureSize });
|
|
433
|
+
} catch (error) {
|
|
434
|
+
if (!(error instanceof RigRefusal)) throw error;
|
|
435
|
+
refusal = error;
|
|
436
|
+
}
|
|
437
|
+
try {
|
|
438
|
+
return { ...bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }), fallback: refusal.message };
|
|
439
|
+
} catch (error) {
|
|
440
|
+
if (!(error instanceof Error)) throw error;
|
|
441
|
+
throw new Error(
|
|
442
|
+
`three-vat: neither encoding can bake this subtree. The vertex encoding: ${unprefixed(error)}. The rig encoding, tried first: ${unprefixed(refusal)}`,
|
|
443
|
+
{ cause: refusal }
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
function unprefixed(error) {
|
|
448
|
+
return error.message.replace(/^three-vat: /, "");
|
|
449
|
+
}
|
|
450
|
+
function sampleClips(root, resolved, frameCounts, poses, writeFrame) {
|
|
451
|
+
const mixer = new AnimationMixer(root);
|
|
452
|
+
const clipTable = [];
|
|
453
|
+
let rowOffset = 0;
|
|
454
|
+
try {
|
|
455
|
+
for (let ci = 0; ci < resolved.length; ci++) {
|
|
456
|
+
const { clip, defaults } = resolved[ci];
|
|
457
|
+
const frames = frameCounts[ci];
|
|
458
|
+
const action = mixer.clipAction(clip);
|
|
459
|
+
action.play();
|
|
460
|
+
let maxDeltaSq = 0;
|
|
461
|
+
for (let f = 0; f < frames; f++) {
|
|
462
|
+
mixer.setTime(f / frames * clip.duration);
|
|
463
|
+
root.updateMatrixWorld(true);
|
|
464
|
+
poseSkeletons(poses);
|
|
465
|
+
maxDeltaSq = Math.max(maxDeltaSq, writeFrame(clip, f, rowOffset + f));
|
|
466
|
+
}
|
|
467
|
+
action.stop();
|
|
468
|
+
clipTable.push({
|
|
469
|
+
name: clip.name,
|
|
470
|
+
startFrame: rowOffset,
|
|
471
|
+
frames,
|
|
472
|
+
fps: frames / clip.duration,
|
|
473
|
+
duration: clip.duration,
|
|
474
|
+
maxDelta: Math.sqrt(maxDeltaSq),
|
|
475
|
+
// Declared once, here, rather than repeated at every instance that plays
|
|
476
|
+
// this band. An instance overrides any of them, field by field.
|
|
477
|
+
...defaults
|
|
478
|
+
});
|
|
479
|
+
rowOffset += frames;
|
|
480
|
+
}
|
|
481
|
+
} finally {
|
|
482
|
+
mixer.stopAllAction();
|
|
483
|
+
mixer.setTime(0);
|
|
484
|
+
root.updateMatrixWorld(true);
|
|
485
|
+
}
|
|
486
|
+
return clipTable;
|
|
487
|
+
}
|
|
488
|
+
function bakeVertices(root, parts, resolved, { fps, maxTextureSize, bakeNormals }) {
|
|
489
|
+
const clips = resolved.map((a) => a.clip);
|
|
490
|
+
const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
|
|
491
|
+
const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
|
|
492
|
+
const { rowsPerFrame, width, height, frameStride } = vertexLayoutFor(vertexCount, totalFrames, maxTextureSize);
|
|
493
|
+
const rootInverse = root.matrixWorld.clone().invert();
|
|
494
|
+
const restMatrices = parts.map(
|
|
495
|
+
(p) => new Matrix4().multiplyMatrices(rootInverse, p.mesh.matrixWorld)
|
|
496
|
+
);
|
|
497
|
+
const geometry = mergeGeometry(parts, restMatrices, vertexCount);
|
|
498
|
+
const mergedBase = geometry.attributes.position;
|
|
499
|
+
const posData = new Uint16Array(frameStride * totalFrames * 4);
|
|
500
|
+
const nrmData = bakeNormals ? new Uint8Array(frameStride * totalFrames * 2) : null;
|
|
501
|
+
const bakeNormal = nrmData !== null;
|
|
502
|
+
const bounds = new Box3();
|
|
503
|
+
const _si = new Vector4();
|
|
504
|
+
const _sw = new Vector4();
|
|
505
|
+
const _acc = new Matrix4();
|
|
506
|
+
const _skin = new Matrix4();
|
|
507
|
+
const _partMatrix = new Matrix4();
|
|
508
|
+
const _p = new Vector3();
|
|
509
|
+
const _bp = new Vector3();
|
|
510
|
+
const _n = new Vector3();
|
|
511
|
+
const poses = attachPoseBuffers(parts);
|
|
512
|
+
const influencers = bakeNormals ? parts.filter((p) => p.pose).map(influencedBones) : [];
|
|
513
|
+
let warnedNonUniformScale = false;
|
|
514
|
+
const writeFrame = (clip, f, row) => {
|
|
515
|
+
let maxDeltaSq = 0;
|
|
516
|
+
if (!warnedNonUniformScale) {
|
|
517
|
+
warnedNonUniformScale = warnOnNonUniformBoneScale(influencers);
|
|
518
|
+
}
|
|
519
|
+
for (const part of parts) {
|
|
520
|
+
_partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
|
|
521
|
+
const influences = part.mesh.morphTargetInfluences;
|
|
522
|
+
const { isSkinned, skinIndex, skinWeight } = part;
|
|
523
|
+
const boneMatrices = part.pose?.matrices;
|
|
524
|
+
const morphCount = morphCountOf(part);
|
|
525
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
526
|
+
const vi = part.vertexStart + v;
|
|
527
|
+
_p.fromBufferAttribute(part.basePos, v);
|
|
528
|
+
if (bakeNormal) _n.fromBufferAttribute(part.baseNrm, v);
|
|
529
|
+
if (influences && morphCount > 0) morphVertex(part, v, influences, morphCount, _p, bakeNormal ? _n : null);
|
|
530
|
+
if (isSkinned && boneMatrices) {
|
|
531
|
+
const skinned = part.mesh;
|
|
532
|
+
_si.fromBufferAttribute(skinIndex, v);
|
|
533
|
+
_sw.fromBufferAttribute(skinWeight, v);
|
|
534
|
+
const ae = _acc.elements;
|
|
535
|
+
for (let e = 0; e < 16; e++) ae[e] = 0;
|
|
536
|
+
for (let i = 0; i < 4; i++) {
|
|
537
|
+
const w = _sw.getComponent(i);
|
|
538
|
+
if (w === 0) continue;
|
|
539
|
+
const b = _si.getComponent(i) * BONE_STRIDE;
|
|
540
|
+
for (let e = 0; e < 16; e++) ae[e] += boneMatrices[b + e] * w;
|
|
541
|
+
}
|
|
542
|
+
_skin.multiplyMatrices(_acc, skinned.bindMatrix).premultiply(skinned.bindMatrixInverse);
|
|
543
|
+
_p.applyMatrix4(_skin);
|
|
544
|
+
if (bakeNormal) _n.transformDirection(_skin);
|
|
545
|
+
}
|
|
546
|
+
_p.applyMatrix4(_partMatrix);
|
|
547
|
+
if (bakeNormal) _n.transformDirection(_partMatrix);
|
|
548
|
+
bounds.expandByPoint(_p);
|
|
549
|
+
_bp.fromBufferAttribute(mergedBase, vi);
|
|
550
|
+
const texel = row * frameStride + vi;
|
|
551
|
+
const o = texel * 4;
|
|
552
|
+
const dx = _p.x - _bp.x;
|
|
553
|
+
const dy = _p.y - _bp.y;
|
|
554
|
+
const dz = _p.z - _bp.z;
|
|
555
|
+
maxDeltaSq = Math.max(maxDeltaSq, dx * dx + dy * dy + dz * dz);
|
|
556
|
+
const largest = Math.max(Math.abs(dx), Math.abs(dy), Math.abs(dz));
|
|
557
|
+
if (largest > HALF_FLOAT_MAX) {
|
|
558
|
+
throw new Error(
|
|
559
|
+
`three-vat: position delta component ${largest} (clip "${clip.name}", frame ${f}, vertex ${vi}) exceeds the half-float limit of ${HALF_FLOAT_MAX} the position texture stores; a unit smaller than the metre is the usual cause`
|
|
560
|
+
);
|
|
561
|
+
}
|
|
562
|
+
posData[o] = toHalfFloat(dx);
|
|
563
|
+
posData[o + 1] = toHalfFloat(dy);
|
|
564
|
+
posData[o + 2] = toHalfFloat(dz);
|
|
565
|
+
posData[o + 3] = HALF_ONE;
|
|
566
|
+
if (bakeNormal) encodeOctahedral(_n.x, _n.y, _n.z, nrmData, texel * 2);
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
return maxDeltaSq;
|
|
570
|
+
};
|
|
571
|
+
const clipTable = sampleClips(root, resolved, frameCounts, poses, writeFrame);
|
|
572
|
+
geometry.boundingBox = bounds.clone();
|
|
573
|
+
geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
|
|
574
|
+
const materials = [];
|
|
575
|
+
for (const part of parts) materials[part.materialIndex] = part.material;
|
|
576
|
+
return {
|
|
577
|
+
positionTexture: makeVATTexture(posData, width, height, HalfFloatType),
|
|
578
|
+
normalTexture: nrmData ? makeVATNormalTexture(nrmData, width, height) : null,
|
|
579
|
+
clips: clipTable,
|
|
580
|
+
bounds,
|
|
581
|
+
vertexCount,
|
|
582
|
+
totalFrames,
|
|
583
|
+
rowsPerFrame,
|
|
584
|
+
encoding: "delta",
|
|
585
|
+
geometry,
|
|
586
|
+
materials
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
var toHalfFloat = DataUtils.toHalfFloat;
|
|
590
|
+
var HALF_ONE = /* @__PURE__ */ toHalfFloat(1);
|
|
591
|
+
var BONE_STRIDE = 16;
|
|
592
|
+
var IDENTITY = /* @__PURE__ */ new Matrix4();
|
|
593
|
+
function attachPoseBuffers(parts) {
|
|
594
|
+
const byRig = /* @__PURE__ */ new Map();
|
|
595
|
+
for (const part of parts) {
|
|
596
|
+
if (!part.isSkinned || !part.skeleton) continue;
|
|
597
|
+
let pose = byRig.get(part.skeleton);
|
|
598
|
+
if (!pose) {
|
|
599
|
+
const matrices = new Float64Array(part.skeleton.bones.length * BONE_STRIDE);
|
|
600
|
+
pose = { skeleton: part.skeleton, matrices };
|
|
601
|
+
byRig.set(part.skeleton, pose);
|
|
602
|
+
}
|
|
603
|
+
part.pose = pose;
|
|
604
|
+
}
|
|
605
|
+
return [...byRig.values()];
|
|
606
|
+
}
|
|
607
|
+
function poseSkeletons(poses) {
|
|
608
|
+
const scratch = new Matrix4();
|
|
609
|
+
for (const { skeleton, matrices } of poses) {
|
|
610
|
+
const { bones, boneInverses } = skeleton;
|
|
611
|
+
for (let b = 0; b < bones.length; b++) {
|
|
612
|
+
scratch.multiplyMatrices(bones[b]?.matrixWorld ?? IDENTITY, boneInverses[b]);
|
|
613
|
+
matrices.set(scratch.elements, b * BONE_STRIDE);
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
function influencedBones(part) {
|
|
618
|
+
const used = /* @__PURE__ */ new Set();
|
|
619
|
+
const index = part.skinIndex;
|
|
620
|
+
const weight = part.skinWeight;
|
|
621
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
622
|
+
for (let i = 0; i < 4; i++) {
|
|
623
|
+
if (weight.getComponent(v, i) !== 0) used.add(index.getComponent(v, i));
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return { pose: part.pose, bones: [...used] };
|
|
627
|
+
}
|
|
628
|
+
var SCALE_UNIFORMITY_EPSILON = 1e-4;
|
|
629
|
+
var MATRIX_BASIS = [0, 1, 2, 4, 5, 6, 8, 9, 10];
|
|
630
|
+
function hasNonUniformScale(matrices, b) {
|
|
631
|
+
const o = b * BONE_STRIDE;
|
|
632
|
+
return unevenSquares(
|
|
633
|
+
matrices[o] ** 2 + matrices[o + 1] ** 2 + matrices[o + 2] ** 2,
|
|
634
|
+
matrices[o + 4] ** 2 + matrices[o + 5] ** 2 + matrices[o + 6] ** 2,
|
|
635
|
+
matrices[o + 8] ** 2 + matrices[o + 9] ** 2 + matrices[o + 10] ** 2
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function hasShear(matrices, b) {
|
|
639
|
+
const m = (i) => matrices[b * BONE_STRIDE + i];
|
|
640
|
+
const lengthSq = (a) => m(a) ** 2 + m(a + 1) ** 2 + m(a + 2) ** 2;
|
|
641
|
+
const dot = (a, c) => m(a) * m(c) + m(a + 1) * m(c + 1) + m(a + 2) * m(c + 2);
|
|
642
|
+
const square = (a, c) => dot(a, c) ** 2 <= SCALE_UNIFORMITY_EPSILON ** 2 * lengthSq(a) * lengthSq(c);
|
|
643
|
+
return !(square(0, 4) && square(0, 8) && square(4, 8));
|
|
644
|
+
}
|
|
645
|
+
function unevenSquares(x, y, z) {
|
|
646
|
+
const max = Math.max(x, y, z);
|
|
647
|
+
return max - Math.min(x, y, z) > SCALE_UNIFORMITY_EPSILON * max;
|
|
648
|
+
}
|
|
649
|
+
function warnOnNonUniformBoneScale(influencers) {
|
|
650
|
+
for (const { pose, bones } of influencers) {
|
|
651
|
+
for (const b of bones) {
|
|
652
|
+
if (!hasNonUniformScale(pose.matrices, b)) continue;
|
|
653
|
+
console.warn(
|
|
654
|
+
`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.`
|
|
655
|
+
);
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
return false;
|
|
660
|
+
}
|
|
661
|
+
var VERTEX_ENCODING_STORES_IT = "the vertex encoding (`encoding: 'delta'`) stores what a rig cannot";
|
|
662
|
+
var RigRefusal = class extends Error {
|
|
663
|
+
};
|
|
664
|
+
function placementOf(part, rootInverse, out) {
|
|
665
|
+
out.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
|
|
666
|
+
if (part.pose) out.multiply(part.mesh.bindMatrixInverse);
|
|
667
|
+
return out;
|
|
668
|
+
}
|
|
669
|
+
var MATRIX_EPSILON = 1e-6;
|
|
670
|
+
function matricesClose(a, b) {
|
|
671
|
+
const ae = a.elements;
|
|
672
|
+
const be = b.elements;
|
|
673
|
+
for (let e = 0; e < 16; e++) if (Math.abs(ae[e] - be[e]) > MATRIX_EPSILON) return false;
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
function slotLabel(slot) {
|
|
677
|
+
const parts = partNames(slot.parts);
|
|
678
|
+
if (!slot.rig) return `rigid part ${parts}`;
|
|
679
|
+
const { pose, bone } = slot.rig;
|
|
680
|
+
return `bone "${pose.skeleton.bones[bone]?.name || `bone ${bone}`}" of ${parts}`;
|
|
681
|
+
}
|
|
682
|
+
function meshName(mesh) {
|
|
683
|
+
return `"${mesh.name || "(unnamed)"}"`;
|
|
684
|
+
}
|
|
685
|
+
function partName(part) {
|
|
686
|
+
return meshName(part.mesh);
|
|
687
|
+
}
|
|
688
|
+
function partNames(parts) {
|
|
689
|
+
return [...new Set(parts.map((p) => p.mesh))].map(meshName).join(", ");
|
|
690
|
+
}
|
|
691
|
+
function slotReads(slot, bone, boneInverse, bindMatrix, placement) {
|
|
692
|
+
const { rig } = slot;
|
|
693
|
+
return rig !== void 0 && rig.pose.skeleton.bones[rig.bone] === bone && matricesClose(rig.pose.skeleton.boneInverses[rig.bone], boneInverse) && matricesClose(rig.bindMatrix, bindMatrix) && matricesClose(slot.restPlacement, placement);
|
|
694
|
+
}
|
|
695
|
+
function layoutSlots(parts, rootInverse) {
|
|
696
|
+
const slots = [];
|
|
697
|
+
const slotMaps = /* @__PURE__ */ new Map();
|
|
698
|
+
const rigidSlots = /* @__PURE__ */ new Map();
|
|
699
|
+
for (const part of parts) {
|
|
700
|
+
const restPlacement = placementOf(part, rootInverse, new Matrix4());
|
|
701
|
+
if (part.pose) {
|
|
702
|
+
const { pose } = part;
|
|
703
|
+
const { bones, boneInverses } = pose.skeleton;
|
|
704
|
+
const bindMatrix = part.mesh.bindMatrix.clone();
|
|
705
|
+
const map = [];
|
|
706
|
+
for (let b = 0; b < bones.length; b++) {
|
|
707
|
+
const bone = bones[b];
|
|
708
|
+
const shared = bone ? slots.findIndex((s) => slotReads(s, bone, boneInverses[b], bindMatrix, restPlacement)) : -1;
|
|
709
|
+
if (shared >= 0) {
|
|
710
|
+
const slot = slots[shared];
|
|
711
|
+
if (!slot.parts.includes(part)) slot.parts.push(part);
|
|
712
|
+
map.push(shared);
|
|
713
|
+
} else {
|
|
714
|
+
map.push(slots.length);
|
|
715
|
+
slots.push({ rig: { pose, bone: b, bindMatrix }, parts: [part], restPlacement });
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
slotMaps.set(part, map);
|
|
719
|
+
} else {
|
|
720
|
+
const shared = rigidSlots.get(part.mesh);
|
|
721
|
+
if (shared !== void 0) {
|
|
722
|
+
slots[shared].parts.push(part);
|
|
723
|
+
slotMaps.set(part, [shared]);
|
|
724
|
+
} else {
|
|
725
|
+
rigidSlots.set(part.mesh, slots.length);
|
|
726
|
+
slotMaps.set(part, [slots.length]);
|
|
727
|
+
slots.push({ rig: void 0, parts: [part], restPlacement });
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return { slots, slotMaps };
|
|
732
|
+
}
|
|
733
|
+
function morphTracksOn(mesh, clip, root) {
|
|
734
|
+
const driven = /* @__PURE__ */ new Map();
|
|
735
|
+
for (const track of clip.tracks) {
|
|
736
|
+
const parsed = PropertyBinding.parseTrackName(track.name);
|
|
737
|
+
if (parsed.propertyName !== "morphTargetInfluences") continue;
|
|
738
|
+
if (PropertyBinding.findNode(root, parsed.nodeName) !== mesh) continue;
|
|
739
|
+
const size = track.getValueSize();
|
|
740
|
+
const keys = track.times.length;
|
|
741
|
+
const values = track.values;
|
|
742
|
+
const add = (t, read) => {
|
|
743
|
+
const list = driven.get(t) ?? [];
|
|
744
|
+
for (let k = 0; k < keys; k++) list.push(read(k));
|
|
745
|
+
driven.set(t, list);
|
|
746
|
+
};
|
|
747
|
+
if (parsed.propertyIndex === void 0) {
|
|
748
|
+
for (let t = 0; t < size; t++) add(t, (k) => values[k * size + t]);
|
|
749
|
+
} else {
|
|
750
|
+
const byIndex = Number(parsed.propertyIndex);
|
|
751
|
+
const t = Number.isInteger(byIndex) ? byIndex : mesh.morphTargetDictionary?.[parsed.propertyIndex];
|
|
752
|
+
if (t !== void 0) add(t, (k) => values[k]);
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return driven;
|
|
756
|
+
}
|
|
757
|
+
function morphInfluences(part, clips, root) {
|
|
758
|
+
const { mesh } = part;
|
|
759
|
+
const influences = mesh.morphTargetInfluences;
|
|
760
|
+
const count = morphCountOf(part);
|
|
761
|
+
if (!influences || count === 0) return { statics: [], animated: [] };
|
|
762
|
+
const driven = clips.map((clip) => morphTracksOn(mesh, clip, root));
|
|
763
|
+
const statics = [];
|
|
764
|
+
const animated = [];
|
|
765
|
+
for (let t = 0; t < count; t++) {
|
|
766
|
+
const seen = /* @__PURE__ */ new Set();
|
|
767
|
+
const offending = [];
|
|
768
|
+
driven.forEach((tracks, ci) => {
|
|
769
|
+
const values = tracks.get(t);
|
|
770
|
+
if (!values) {
|
|
771
|
+
seen.add(influences[t]);
|
|
772
|
+
return;
|
|
773
|
+
}
|
|
774
|
+
offending.push(`"${clips[ci].name}"`);
|
|
775
|
+
for (const value of values) seen.add(value);
|
|
776
|
+
});
|
|
777
|
+
if (seen.size > 1) {
|
|
778
|
+
const name = Object.entries(mesh.morphTargetDictionary ?? {}).find(([, i]) => i === t)?.[0];
|
|
779
|
+
animated.push({ part, target: name ? `"${name}"` : String(t), clips: offending });
|
|
780
|
+
}
|
|
781
|
+
statics.push(seen.values().next().value);
|
|
782
|
+
}
|
|
783
|
+
return { statics, animated };
|
|
784
|
+
}
|
|
785
|
+
function refuseAnimatedMorphs(animated) {
|
|
786
|
+
const byDriver = /* @__PURE__ */ new Map();
|
|
787
|
+
for (const { part, target, clips } of animated) {
|
|
788
|
+
const key = `${target}
|
|
789
|
+
${clips.join("\n")}`;
|
|
790
|
+
const entry = byDriver.get(key) ?? { target, clips, parts: [] };
|
|
791
|
+
if (!entry.parts.includes(part)) entry.parts.push(part);
|
|
792
|
+
byDriver.set(key, entry);
|
|
793
|
+
}
|
|
794
|
+
const offences = [...byDriver.values()].map(({ target, clips, parts }) => {
|
|
795
|
+
const one = clips.length === 1;
|
|
796
|
+
const single = new Set(parts.map((p) => p.mesh)).size === 1;
|
|
797
|
+
const its = single ? "its" : "their";
|
|
798
|
+
return `clip${one ? "" : "s"} ${clips.join(", ")} animate${one ? "s" : ""} morph target ${target} of part${single ? "" : "s"} ${partNames(parts)}, ${its} vertices moving where no bone does`;
|
|
799
|
+
});
|
|
800
|
+
return new RigRefusal(
|
|
801
|
+
`three-vat: the rig encoding cannot bake this subtree: ${offences.join("; ")}. A slot stores a rotation, a translation and one scale, not a per-vertex delta, and ${VERTEX_ENCODING_STORES_IT}`
|
|
802
|
+
);
|
|
803
|
+
}
|
|
804
|
+
function foldMorphs(part, statics) {
|
|
805
|
+
const { vertexCount, basePos, baseNrm } = part;
|
|
806
|
+
const position = new Float32Array(vertexCount * 3);
|
|
807
|
+
const normal = new Float32Array(vertexCount * 3);
|
|
808
|
+
const active = statics.some((w) => w !== 0);
|
|
809
|
+
const _p = new Vector3();
|
|
810
|
+
const _n = new Vector3();
|
|
811
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
812
|
+
_p.fromBufferAttribute(basePos, v);
|
|
813
|
+
_n.fromBufferAttribute(baseNrm, v);
|
|
814
|
+
if (active) {
|
|
815
|
+
morphVertex(part, v, statics, statics.length, _p, _n);
|
|
816
|
+
_n.normalize();
|
|
817
|
+
}
|
|
818
|
+
_p.toArray(position, v * 3);
|
|
819
|
+
_n.toArray(normal, v * 3);
|
|
820
|
+
}
|
|
821
|
+
return { position, normal };
|
|
822
|
+
}
|
|
823
|
+
function scaleSensitiveNodes(slots, root, influenced) {
|
|
824
|
+
const sensitive = /* @__PURE__ */ new Map();
|
|
825
|
+
slots.forEach((slot, i) => {
|
|
826
|
+
if (!influenced[i]) return;
|
|
827
|
+
if (slot.rig) {
|
|
828
|
+
const { bones } = slot.rig.pose.skeleton;
|
|
829
|
+
for (let node = bones[slot.rig.bone] ?? null; node && bones.includes(node); node = node.parent) {
|
|
830
|
+
sensitive.set(node, slot);
|
|
831
|
+
}
|
|
832
|
+
} else {
|
|
833
|
+
for (let node = slot.parts[0].mesh; node && node !== root; node = node.parent) {
|
|
834
|
+
sensitive.set(node, slot);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
return sensitive;
|
|
839
|
+
}
|
|
840
|
+
function refuseAnimatedNonUniformScale(slots, clips, root, influenced) {
|
|
841
|
+
const sensitive = scaleSensitiveNodes(slots, root, influenced);
|
|
842
|
+
for (const clip of clips) {
|
|
843
|
+
for (const track of clip.tracks) {
|
|
844
|
+
const parsed = PropertyBinding.parseTrackName(track.name);
|
|
845
|
+
if (parsed.propertyName !== "scale" || track.getValueSize() !== 3) continue;
|
|
846
|
+
const node = PropertyBinding.findNode(root, parsed.nodeName);
|
|
847
|
+
const reached = node && sensitive.get(node);
|
|
848
|
+
if (!reached) continue;
|
|
849
|
+
const values = track.values;
|
|
850
|
+
for (let k = 0; k < track.times.length; k++) {
|
|
851
|
+
const o = k * 3;
|
|
852
|
+
if (unevenSquares(values[o] ** 2, values[o + 1] ** 2, values[o + 2] ** 2)) {
|
|
853
|
+
throw nonUniformScale(slotLabel(reached), clip);
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
function nonUniformScale(label, clip) {
|
|
860
|
+
return new RigRefusal(
|
|
861
|
+
`three-vat: ${label} animates with non-uniform scale in clip "${clip.name}", which the rig encoding cannot store \u2014 a slot is a rotation, a translation and one scale, and ${VERTEX_ENCODING_STORES_IT}. Authored with a uniform scale, it bakes as a rig`
|
|
862
|
+
);
|
|
863
|
+
}
|
|
864
|
+
function bakeRig(root, parts, resolved, { fps, maxTextureSize }) {
|
|
865
|
+
const clips = resolved.map((a) => a.clip);
|
|
866
|
+
const rootInverse = root.matrixWorld.clone().invert();
|
|
867
|
+
const poses = attachPoseBuffers(parts);
|
|
868
|
+
const { slots, slotMaps } = layoutSlots(parts, rootInverse);
|
|
869
|
+
const slotCount = slots.length;
|
|
870
|
+
const morphs = parts.map((part) => morphInfluences(part, clips, root));
|
|
871
|
+
const animated = morphs.flatMap((m) => m.animated);
|
|
872
|
+
if (animated.length > 0) throw refuseAnimatedMorphs(animated);
|
|
873
|
+
const rigParts = parts.map((part, i) => ({
|
|
874
|
+
part,
|
|
875
|
+
slotOf: slotMaps.get(part),
|
|
876
|
+
...foldMorphs(part, morphs[i].statics)
|
|
877
|
+
}));
|
|
878
|
+
const width = slotCount * RIG_TEXELS_PER_SLOT;
|
|
879
|
+
if (width > maxTextureSize) {
|
|
880
|
+
throw new RigRefusal(
|
|
881
|
+
`three-vat: rig texture width ${width} (${slotCount} slots \xD7 ${RIG_TEXELS_PER_SLOT} texels) exceeds maxTextureSize ${maxTextureSize}`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
const { frameCounts, totalFrames } = frameCountsFor(clips, fps, maxTextureSize);
|
|
885
|
+
const vertexCount = parts.reduce((n, p) => n + p.vertexCount, 0);
|
|
886
|
+
const geometry = mergeRigGeometry(rigParts, vertexCount);
|
|
887
|
+
const localPosition = geometry.attributes.position.array;
|
|
888
|
+
const skinIndex = geometry.attributes.skinIndex.array;
|
|
889
|
+
const skinWeight = geometry.attributes.skinWeight.array;
|
|
890
|
+
const influenced = new Uint8Array(slotCount);
|
|
891
|
+
for (let i = 0; i < skinWeight.length; i++) {
|
|
892
|
+
if (skinWeight[i] !== 0) influenced[skinIndex[i]] = 1;
|
|
893
|
+
}
|
|
894
|
+
refuseAnimatedNonUniformScale(slots, clips, root, influenced);
|
|
895
|
+
const _p = new Vector3();
|
|
896
|
+
const _slot = new Matrix4();
|
|
897
|
+
const _partMatrix = new Matrix4();
|
|
898
|
+
const placements = new Map(parts.map((part) => [part, new Matrix4()]));
|
|
899
|
+
const _q = new Quaternion();
|
|
900
|
+
const _t = new Vector3();
|
|
901
|
+
const _s = new Vector3();
|
|
902
|
+
const restRoot = new Float64Array(vertexCount * 3);
|
|
903
|
+
for (const part of parts) {
|
|
904
|
+
_partMatrix.multiplyMatrices(rootInverse, part.mesh.matrixWorld);
|
|
905
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
906
|
+
_p.fromBufferAttribute(part.basePos, v).applyMatrix4(_partMatrix);
|
|
907
|
+
_p.toArray(restRoot, (part.vertexStart + v) * 3);
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
const restSlots = new Float32Array(slotCount * 16);
|
|
911
|
+
for (let slot = 0; slot < slotCount; slot++) {
|
|
912
|
+
_slot.multiplyMatrices(rootInverse, slots[slot].parts[0].mesh.matrixWorld).toArray(restSlots, slot * 16);
|
|
913
|
+
}
|
|
914
|
+
const data = new Float32Array(width * totalFrames * 4);
|
|
915
|
+
const slotMatrices = new Float64Array(slotCount * BONE_STRIDE);
|
|
916
|
+
const previous = new Float64Array(slotCount * 4);
|
|
917
|
+
const bounds = new Box3();
|
|
918
|
+
let minX = Infinity;
|
|
919
|
+
let minY = Infinity;
|
|
920
|
+
let minZ = Infinity;
|
|
921
|
+
let maxX = -Infinity;
|
|
922
|
+
let maxY = -Infinity;
|
|
923
|
+
let maxZ = -Infinity;
|
|
924
|
+
const writeFrame = (clip, _f, row) => {
|
|
925
|
+
let maxDeltaSq = 0;
|
|
926
|
+
for (const [part, placement] of placements) placementOf(part, rootInverse, placement);
|
|
927
|
+
for (let slot = 0; slot < slotCount; slot++) {
|
|
928
|
+
const { rig, parts: readers } = slots[slot];
|
|
929
|
+
const lead = readers[0];
|
|
930
|
+
const placement = placements.get(lead);
|
|
931
|
+
for (let pi = 1; pi < readers.length; pi++) {
|
|
932
|
+
const other = readers[pi];
|
|
933
|
+
if (!matricesClose(placement, placements.get(other))) {
|
|
934
|
+
throw new RigRefusal(
|
|
935
|
+
`three-vat: parts ${partName(lead)} and ${partName(other)} read the same slots but move apart in clip "${clip.name}"; the rig encoding gives them one set of slots, which cannot place them differently, and ${VERTEX_ENCODING_STORES_IT}`
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
if (rig) {
|
|
940
|
+
_slot.fromArray(rig.pose.matrices, rig.bone * BONE_STRIDE).multiply(rig.bindMatrix).premultiply(placement);
|
|
941
|
+
} else {
|
|
942
|
+
_slot.copy(placement);
|
|
943
|
+
}
|
|
944
|
+
slotMatrices.set(_slot.elements, slot * BONE_STRIDE);
|
|
945
|
+
if (influenced[slot] && (hasNonUniformScale(slotMatrices, slot) || hasShear(slotMatrices, slot))) {
|
|
946
|
+
throw nonUniformScale(slotLabel(slots[slot]), clip);
|
|
947
|
+
}
|
|
948
|
+
const p4 = slot * 4;
|
|
949
|
+
const det = _slot.determinant();
|
|
950
|
+
let scale;
|
|
951
|
+
if (det === 0) {
|
|
952
|
+
_t.setFromMatrixPosition(_slot);
|
|
953
|
+
_q.fromArray(previous, p4);
|
|
954
|
+
if (_q.lengthSq() === 0) _q.identity();
|
|
955
|
+
scale = 0;
|
|
956
|
+
} else {
|
|
957
|
+
if (det < 0) for (const e of MATRIX_BASIS) _slot.elements[e] = -_slot.elements[e];
|
|
958
|
+
_slot.decompose(_t, _q, _s);
|
|
959
|
+
scale = det < 0 ? -_s.x : _s.x;
|
|
960
|
+
}
|
|
961
|
+
if (previous[p4] * _q.x + previous[p4 + 1] * _q.y + previous[p4 + 2] * _q.z + previous[p4 + 3] * _q.w < 0) {
|
|
962
|
+
_q.set(-_q.x, -_q.y, -_q.z, -_q.w);
|
|
963
|
+
}
|
|
964
|
+
previous[p4] = _q.x;
|
|
965
|
+
previous[p4 + 1] = _q.y;
|
|
966
|
+
previous[p4 + 2] = _q.z;
|
|
967
|
+
previous[p4 + 3] = _q.w;
|
|
968
|
+
const o = (row * width + slot * RIG_TEXELS_PER_SLOT) * 4;
|
|
969
|
+
const rotation = o + RIG_TEXELS.rotation * 4;
|
|
970
|
+
data[rotation] = _q.x;
|
|
971
|
+
data[rotation + 1] = _q.y;
|
|
972
|
+
data[rotation + 2] = _q.z;
|
|
973
|
+
data[rotation + 3] = _q.w;
|
|
974
|
+
const placementTexel = o + RIG_TEXELS.placement * 4;
|
|
975
|
+
data[placementTexel] = _t.x;
|
|
976
|
+
data[placementTexel + 1] = _t.y;
|
|
977
|
+
data[placementTexel + 2] = _t.z;
|
|
978
|
+
data[placementTexel + 3] = scale;
|
|
979
|
+
}
|
|
980
|
+
for (let v = 0; v < vertexCount; v++) {
|
|
981
|
+
const o3 = v * 3;
|
|
982
|
+
const o4 = v * 4;
|
|
983
|
+
const x = localPosition[o3];
|
|
984
|
+
const y = localPosition[o3 + 1];
|
|
985
|
+
const z = localPosition[o3 + 2];
|
|
986
|
+
let px = 0;
|
|
987
|
+
let py = 0;
|
|
988
|
+
let pz = 0;
|
|
989
|
+
for (let i = 0; i < 4; i++) {
|
|
990
|
+
const w = skinWeight[o4 + i];
|
|
991
|
+
if (w === 0) continue;
|
|
992
|
+
const m = skinIndex[o4 + i] * BONE_STRIDE;
|
|
993
|
+
px += w * (slotMatrices[m] * x + slotMatrices[m + 4] * y + slotMatrices[m + 8] * z + slotMatrices[m + 12]);
|
|
994
|
+
py += w * (slotMatrices[m + 1] * x + slotMatrices[m + 5] * y + slotMatrices[m + 9] * z + slotMatrices[m + 13]);
|
|
995
|
+
pz += w * (slotMatrices[m + 2] * x + slotMatrices[m + 6] * y + slotMatrices[m + 10] * z + slotMatrices[m + 14]);
|
|
996
|
+
}
|
|
997
|
+
if (px < minX) minX = px;
|
|
998
|
+
if (px > maxX) maxX = px;
|
|
999
|
+
if (py < minY) minY = py;
|
|
1000
|
+
if (py > maxY) maxY = py;
|
|
1001
|
+
if (pz < minZ) minZ = pz;
|
|
1002
|
+
if (pz > maxZ) maxZ = pz;
|
|
1003
|
+
const dx = px - restRoot[o3];
|
|
1004
|
+
const dy = py - restRoot[o3 + 1];
|
|
1005
|
+
const dz = pz - restRoot[o3 + 2];
|
|
1006
|
+
const deltaSq = dx * dx + dy * dy + dz * dz;
|
|
1007
|
+
if (deltaSq > maxDeltaSq) maxDeltaSq = deltaSq;
|
|
1008
|
+
}
|
|
1009
|
+
return maxDeltaSq;
|
|
1010
|
+
};
|
|
1011
|
+
const clipTable = sampleClips(root, resolved, frameCounts, poses, writeFrame);
|
|
1012
|
+
bounds.min.set(minX, minY, minZ);
|
|
1013
|
+
bounds.max.set(maxX, maxY, maxZ);
|
|
1014
|
+
geometry.boundingBox = bounds.clone();
|
|
1015
|
+
geometry.boundingSphere = bounds.getBoundingSphere(new Sphere());
|
|
1016
|
+
const materials = [];
|
|
1017
|
+
for (const part of parts) materials[part.materialIndex] = part.material;
|
|
1018
|
+
const vat = {
|
|
1019
|
+
encoding: "rig",
|
|
1020
|
+
rigTexture: makeVATTexture(data, width, totalFrames),
|
|
1021
|
+
slotCount,
|
|
1022
|
+
clips: clipTable,
|
|
1023
|
+
bounds,
|
|
1024
|
+
vertexCount,
|
|
1025
|
+
totalFrames,
|
|
1026
|
+
geometry,
|
|
1027
|
+
materials
|
|
1028
|
+
};
|
|
1029
|
+
REST_SLOTS.set(vat, restSlots);
|
|
1030
|
+
return vat;
|
|
1031
|
+
}
|
|
1032
|
+
var REST_SLOTS = /* @__PURE__ */ new WeakMap();
|
|
1033
|
+
function restSlotsOf(vat) {
|
|
1034
|
+
return REST_SLOTS.get(vat);
|
|
1035
|
+
}
|
|
1036
|
+
function keepRestSlots(vat, rest) {
|
|
1037
|
+
REST_SLOTS.set(vat, rest);
|
|
1038
|
+
}
|
|
1039
|
+
function mergeRigGeometry(rigParts, total) {
|
|
1040
|
+
const position = new Float32Array(total * 3);
|
|
1041
|
+
const normal = new Float32Array(total * 3);
|
|
1042
|
+
const slotIndex = new Uint16Array(total * 4);
|
|
1043
|
+
const slotWeight = new Float32Array(total * 4);
|
|
1044
|
+
for (const { part, slotOf, position: restPos, normal: restNrm } of rigParts) {
|
|
1045
|
+
const start = part.vertexStart;
|
|
1046
|
+
const srcIndex = part.pose ? part.skinIndex : null;
|
|
1047
|
+
const srcWeight = part.pose ? part.skinWeight : null;
|
|
1048
|
+
position.set(restPos, start * 3);
|
|
1049
|
+
normal.set(restNrm, start * 3);
|
|
1050
|
+
for (let v = 0; v < part.vertexCount; v++) {
|
|
1051
|
+
const o4 = (start + v) * 4;
|
|
1052
|
+
if (srcIndex && srcWeight) {
|
|
1053
|
+
for (let i = 0; i < 4; i++) {
|
|
1054
|
+
slotIndex[o4 + i] = slotOf[srcIndex.getComponent(v, i)];
|
|
1055
|
+
slotWeight[o4 + i] = srcWeight.getComponent(v, i);
|
|
1056
|
+
}
|
|
1057
|
+
} else {
|
|
1058
|
+
slotIndex[o4] = slotOf[0];
|
|
1059
|
+
slotWeight[o4] = 1;
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
const merged = new BufferGeometry();
|
|
1064
|
+
merged.setAttribute("position", new BufferAttribute(position, 3));
|
|
1065
|
+
merged.setAttribute("normal", new BufferAttribute(normal, 3));
|
|
1066
|
+
merged.setAttribute("skinIndex", new BufferAttribute(slotIndex, 4));
|
|
1067
|
+
merged.setAttribute("skinWeight", new BufferAttribute(slotWeight, 4));
|
|
1068
|
+
mergeSurface(merged, rigParts.map((r) => r.part), total, null);
|
|
1069
|
+
return merged;
|
|
1070
|
+
}
|
|
1071
|
+
var BAKED_FILE_EXTENSION = "THREEVAT_vat";
|
|
1072
|
+
var BAKED_FILE_VERSION = 1;
|
|
1073
|
+
var BAKED_LAYER_FORMATS = {
|
|
1074
|
+
/** The position layer: four half-floats a texel, written as the unsigned shorts that hold their bits. */
|
|
1075
|
+
RGBA16F: {
|
|
1076
|
+
componentType: 5123,
|
|
1077
|
+
bytesPerTexel: 8,
|
|
1078
|
+
build: (texels, width, height) => makeVATTexture(new Uint16Array(texels), width, height, HalfFloatType)
|
|
1079
|
+
},
|
|
1080
|
+
/** The normal layer: an octahedral pair of unsigned bytes a texel. */
|
|
1081
|
+
RG8: {
|
|
1082
|
+
componentType: 5121,
|
|
1083
|
+
bytesPerTexel: 2,
|
|
1084
|
+
build: (texels, width, height) => makeVATNormalTexture(new Uint8Array(texels), width, height)
|
|
1085
|
+
},
|
|
1086
|
+
/** The rig texture: four floats a texel, two texels a slot. */
|
|
1087
|
+
RGBA32F: {
|
|
1088
|
+
componentType: 5126,
|
|
1089
|
+
bytesPerTexel: 16,
|
|
1090
|
+
build: (texels, width, height) => makeVATTexture(new Float32Array(texels), width, height, FloatType)
|
|
1091
|
+
}
|
|
1092
|
+
};
|
|
1093
|
+
function positionDigest(position) {
|
|
1094
|
+
const values = new Float32Array(position.count * position.itemSize);
|
|
1095
|
+
for (let v = 0; v < position.count; v++) {
|
|
1096
|
+
for (let c = 0; c < position.itemSize; c++) values[v * position.itemSize + c] = position.getComponent(v, c);
|
|
1097
|
+
}
|
|
1098
|
+
const bytes = new Uint8Array(values.buffer);
|
|
1099
|
+
let hash = 2166136261;
|
|
1100
|
+
for (let i = 0; i < bytes.length; i++) hash = Math.imul(hash ^ bytes[i], 16777619);
|
|
1101
|
+
return `fnv1a32:${(hash >>> 0).toString(16).padStart(8, "0")}`;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
export { BAKED_FILE_EXTENSION, BAKED_FILE_VERSION, BAKED_LAYER_FORMATS, bakeVAT, bakeVATWith, decodeOctahedral, encodeOctahedral, flatFacts, keepRestSlots, mergedFlatMaterial, positionDigest, restSlotsOf };
|