@driftengine/assets 4.6.1 → 4.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/README.md +1 -1
  2. package/dist/blend.d.ts +25 -0
  3. package/dist/blend.js +43 -0
  4. package/dist/blendAnimation.d.ts +45 -0
  5. package/dist/blendAnimation.js +294 -0
  6. package/dist/blendData.d.ts +114 -0
  7. package/dist/blendData.js +366 -0
  8. package/dist/blendFile.d.ts +69 -0
  9. package/dist/blendFile.js +182 -0
  10. package/dist/blendGeometry.d.ts +45 -0
  11. package/dist/blendGeometry.js +210 -0
  12. package/dist/blendGltf.d.ts +65 -0
  13. package/dist/blendGltf.js +545 -0
  14. package/dist/blendMaterial.d.ts +68 -0
  15. package/dist/blendMaterial.js +545 -0
  16. package/dist/blendMatrix.d.ts +28 -0
  17. package/dist/blendMatrix.js +245 -0
  18. package/dist/blendMesh.d.ts +70 -0
  19. package/dist/blendMesh.js +409 -0
  20. package/dist/blendModifiers.d.ts +25 -0
  21. package/dist/blendModifiers.js +83 -0
  22. package/dist/blendNormals.d.ts +44 -0
  23. package/dist/blendNormals.js +458 -0
  24. package/dist/blendPacked.d.ts +35 -0
  25. package/dist/blendPacked.js +82 -0
  26. package/dist/blendScene.d.ts +50 -0
  27. package/dist/blendScene.js +280 -0
  28. package/dist/gltf.js +8 -2
  29. package/dist/index.d.ts +21 -0
  30. package/dist/index.js +17 -0
  31. package/dist/readModel.d.ts +13 -1
  32. package/dist/readModel.js +43 -13
  33. package/package.json +4 -4
  34. package/src/blend.ts +57 -0
  35. package/src/blendAnimation.ts +328 -0
  36. package/src/blendData.ts +375 -0
  37. package/src/blendFile.ts +233 -0
  38. package/src/blendGeometry.ts +263 -0
  39. package/src/blendGltf.ts +621 -0
  40. package/src/blendMaterial.ts +676 -0
  41. package/src/blendMatrix.ts +257 -0
  42. package/src/blendMesh.ts +496 -0
  43. package/src/blendModifiers.ts +96 -0
  44. package/src/blendNormals.ts +481 -0
  45. package/src/blendPacked.ts +93 -0
  46. package/src/blendScene.ts +307 -0
  47. package/src/gltf.ts +14 -2
  48. package/src/index.ts +41 -0
  49. package/src/readModel.ts +58 -13
@@ -0,0 +1,458 @@
1
+ /**
2
+ * The normal Blender shades each face corner with, computed from what a `.blend` stores.
3
+ *
4
+ * **A `.blend` stores no shading normals unless somebody customised them**, so a reader has to
5
+ * derive the ones Blender draws, and "a smooth normal" is not specific enough to agree with it.
6
+ * Blender's rule, which this follows step for step:
7
+ *
8
+ * - Every face flat, or every edge sharp: each corner takes its face's normal.
9
+ * - Every face smooth and no edge sharp: each corner takes its vertex's normal, the sum of the
10
+ * normals of every face at the vertex weighted by the angle each face makes there.
11
+ * - Otherwise the corners around a vertex split into *fans* at every sharp edge — an edge tagged
12
+ * sharp, an edge beside a flat face, an edge with more or fewer than two faces, and an edge whose
13
+ * faces disagree about winding — and each fan takes the angle-weighted sum over its own faces.
14
+ *
15
+ * **Custom normals are stored relative to those fans**, two shorts per corner: an angle away from
16
+ * the fan's normal and an angle around it from the fan's first edge. Decoding them needs the fans
17
+ * walked in exactly Blender's order, which is why the walk below starts each fan where Blender
18
+ * starts it and visits faces in the direction Blender turns. A vector normal stored as an
19
+ * attribute — 4.5 on — needs none of this and is used as it stands.
20
+ *
21
+ * What it gives up: nothing it knows of. Its arithmetic is in doubles where Blender's is in
22
+ * floats, which is a difference of about 1e-4 in a normal and is what the fixtures allow.
23
+ */
24
+ import { FACE, POINT } from './blendMesh.js';
25
+ const EMPTY = -1;
26
+ const UNSET = -2;
27
+ const INVALID = -3;
28
+ /** Blender's limit beyond which two directions are too close to define a frame between them. */
29
+ const TRIGO_THRESHOLD = 1 - 1e-4;
30
+ const TAU = Math.PI * 2;
31
+ const acos = (x) => Math.acos(Math.max(-1, Math.min(1, x)));
32
+ const f32 = Math.fround;
33
+ /** Blender's approximate arc cosine for corner angles, in its single precision. */
34
+ function cornerAngle(x) {
35
+ const f = f32(Math.abs(x));
36
+ const m = f < 1 ? f32(1 - f32(1 - f)) : 1;
37
+ const a = f32(Math.sqrt(f32(1 - m)) *
38
+ f32(1.5707963267 + m * f32(-0.213300989 + m * f32(0.077980478 + m * -0.02164095))));
39
+ return x < 0 ? f32(Math.PI - a) : a;
40
+ }
41
+ const dot = (a, b) => a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
42
+ function normalise(v) {
43
+ const length = Math.hypot(v[0], v[1], v[2]);
44
+ if (length > 1e-35) {
45
+ v[0] /= length;
46
+ v[1] /= length;
47
+ v[2] /= length;
48
+ }
49
+ else {
50
+ v[0] = v[1] = v[2] = 0;
51
+ }
52
+ return length;
53
+ }
54
+ /**
55
+ * Each face's normal, by the rule Blender uses for its size: two edges of a triangle, the two
56
+ * diagonals of a quad, and Newell's method beyond that.
57
+ *
58
+ * **Not Newell's method throughout**, which is what this was until a real file disagreed: a quad
59
+ * with two coincident corners has no area, so Newell gives it no normal, while its diagonals still
60
+ * cross. And where even they cancel — a quad whose corners coincide in pairs — Blender calls the
61
+ * face's normal straight up rather than zero, and averages that into its smooth neighbours. Both
62
+ * were found on a 2.79 wheel, where one such face moved a vertex normal 0.68 from Blender's.
63
+ */
64
+ export function faceNormals(mesh) {
65
+ const { positions: p, faceOffsets, cornerVerts } = mesh;
66
+ const faces = faceOffsets.length - 1;
67
+ const out = new Float64Array(faces * 3);
68
+ const at = (corner, axis) => p[cornerVerts[corner] * 3 + axis];
69
+ for (let f = 0; f < faces; f++) {
70
+ const start = faceOffsets[f];
71
+ const end = faceOffsets[f + 1];
72
+ const n = [0, 0, 0];
73
+ if (end - start === 3 || end - start === 4) {
74
+ /* Triangle: (v0 - v1) x (v1 - v2). Quad: (v0 - v2) x (v1 - v3). */
75
+ const quad = end - start === 4;
76
+ const a = [0, 1, 2].map((k) => at(start, k) - at(start + (quad ? 2 : 1), k));
77
+ const b = [0, 1, 2].map((k) => at(start + 1, k) - at(start + (quad ? 3 : 2), k));
78
+ n[0] = a[1] * b[2] - a[2] * b[1];
79
+ n[1] = a[2] * b[0] - a[0] * b[2];
80
+ n[2] = a[0] * b[1] - a[1] * b[0];
81
+ }
82
+ else {
83
+ let prev = cornerVerts[end - 1] * 3;
84
+ for (let c = start; c < end; c++) {
85
+ const curr = cornerVerts[c] * 3;
86
+ const px = p[prev], py = p[prev + 1], pz = p[prev + 2];
87
+ const cx = p[curr], cy = p[curr + 1], cz = p[curr + 2];
88
+ n[0] += (py - cy) * (pz + cz);
89
+ n[1] += (pz - cz) * (px + cx);
90
+ n[2] += (px - cx) * (py + cy);
91
+ prev = curr;
92
+ }
93
+ }
94
+ /* Blender's convention for a face with no area: it faces up. Its neighbours average it in. */
95
+ if (normalise(n) === 0)
96
+ n[2] = 1;
97
+ out.set(n, f * 3);
98
+ }
99
+ return out;
100
+ }
101
+ function direction(mesh, from, to) {
102
+ const p = mesh.positions;
103
+ const v = [
104
+ p[to * 3] - p[from * 3],
105
+ p[to * 3 + 1] - p[from * 3 + 1],
106
+ p[to * 3 + 2] - p[from * 3 + 2],
107
+ ];
108
+ normalise(v);
109
+ return v;
110
+ }
111
+ /** The angle-weighted vertex normals, what an all-smooth mesh shades with. */
112
+ export function vertexNormals(mesh, face) {
113
+ const { faceOffsets, cornerVerts, positions } = mesh;
114
+ const verts = positions.length / 3;
115
+ const out = new Float64Array(verts * 3);
116
+ for (let f = 0; f < faceOffsets.length - 1; f++) {
117
+ const start = faceOffsets[f];
118
+ const end = faceOffsets[f + 1];
119
+ for (let c = start; c < end; c++) {
120
+ const v = cornerVerts[c];
121
+ const prev = cornerVerts[c === start ? end - 1 : c - 1];
122
+ const next = cornerVerts[c + 1 === end ? start : c + 1];
123
+ const angle = cornerAngle(dot(direction(mesh, v, prev), direction(mesh, v, next)));
124
+ for (let k = 0; k < 3; k++)
125
+ out[v * 3 + k] = out[v * 3 + k] + face[f * 3 + k] * angle;
126
+ }
127
+ }
128
+ for (let v = 0; v < verts; v++) {
129
+ const n = [out[v * 3], out[v * 3 + 1], out[v * 3 + 2]];
130
+ /* Blender's convention for a vertex with no usable faces: point the normal along its position. */
131
+ if (normalise(n) === 0) {
132
+ n[0] = positions[v * 3];
133
+ n[1] = positions[v * 3 + 1];
134
+ n[2] = positions[v * 3 + 2];
135
+ normalise(n);
136
+ }
137
+ out.set(n, v * 3);
138
+ }
139
+ return out;
140
+ }
141
+ /** Every edge that splits a fan: tagged, beside a flat face, not two-faced, or flipped. */
142
+ function edgeLoops(mesh, sharpEdge) {
143
+ const { faceOffsets, cornerVerts, cornerEdges, sharpFace } = mesh;
144
+ const edges = mesh.edgeVerts.length / 2;
145
+ const e2l = new Int32Array(edges * 2).fill(EMPTY);
146
+ for (let f = 0; f < faceOffsets.length - 1; f++) {
147
+ const flat = sharpFace !== null && sharpFace[f] === 1;
148
+ for (let c = faceOffsets[f]; c < faceOffsets[f + 1]; c++) {
149
+ const e = cornerEdges[c];
150
+ if (e2l[e * 2] === EMPTY) {
151
+ e2l[e * 2] = c;
152
+ e2l[e * 2 + 1] = flat ? INVALID : UNSET;
153
+ }
154
+ else if (e2l[e * 2 + 1] === UNSET) {
155
+ const first = e2l[e * 2];
156
+ if (flat ||
157
+ (sharpEdge !== null && sharpEdge[e] === 1) ||
158
+ cornerVerts[c] === cornerVerts[first]) {
159
+ e2l[e * 2 + 1] = INVALID;
160
+ }
161
+ else
162
+ e2l[e * 2 + 1] = c;
163
+ }
164
+ else if (e2l[e * 2 + 1] !== INVALID) {
165
+ e2l[e * 2 + 1] = INVALID;
166
+ }
167
+ }
168
+ }
169
+ return e2l;
170
+ }
171
+ const isSharp = (e2l, edge) => {
172
+ const second = e2l[edge * 2 + 1];
173
+ return second === UNSET || second === INVALID;
174
+ };
175
+ function defineSpace(lnor, ref, other, edges) {
176
+ const space = { lnor: [...lnor], ref: [0, 0, 0], ortho: [0, 0, 0], alpha: 0, beta: 0 };
177
+ const dRef = dot(ref, lnor);
178
+ const dOther = dot(other, lnor);
179
+ if (Math.abs(dRef) >= TRIGO_THRESHOLD || Math.abs(dOther) >= TRIGO_THRESHOLD)
180
+ return space;
181
+ if (edges.length > 0) {
182
+ let alpha = 0;
183
+ for (const e of edges)
184
+ alpha += acos(dot(e, lnor));
185
+ space.alpha = alpha / edges.length;
186
+ }
187
+ else {
188
+ space.alpha = (acos(dRef) + acos(dOther)) / 2;
189
+ }
190
+ const r = [ref[0] - lnor[0] * dRef, ref[1] - lnor[1] * dRef, ref[2] - lnor[2] * dRef];
191
+ normalise(r);
192
+ space.ref = r;
193
+ const o = [
194
+ lnor[1] * r[2] - lnor[2] * r[1],
195
+ lnor[2] * r[0] - lnor[0] * r[2],
196
+ lnor[0] * r[1] - lnor[1] * r[0],
197
+ ];
198
+ normalise(o);
199
+ space.ortho = o;
200
+ const t = [
201
+ other[0] - lnor[0] * dOther,
202
+ other[1] - lnor[1] * dOther,
203
+ other[2] - lnor[2] * dOther,
204
+ ];
205
+ normalise(t);
206
+ const d = dot(r, t);
207
+ if (d < TRIGO_THRESHOLD) {
208
+ const beta = acos(d);
209
+ space.beta = dot(o, t) < 0 ? TAU - beta : beta;
210
+ }
211
+ else
212
+ space.beta = TAU;
213
+ return space;
214
+ }
215
+ function decode(space, a, b) {
216
+ if (a === 0 || space.alpha === 0 || space.beta === 0)
217
+ return [...space.lnor];
218
+ const alphaFac = a / 32767;
219
+ const alpha = (alphaFac > 0 ? space.alpha : TAU - space.alpha) * alphaFac;
220
+ const out = [
221
+ space.lnor[0] * Math.cos(alpha),
222
+ space.lnor[1] * Math.cos(alpha),
223
+ space.lnor[2] * Math.cos(alpha),
224
+ ];
225
+ const sinAlpha = Math.sin(alpha);
226
+ if (b === 0) {
227
+ for (let k = 0; k < 3; k++)
228
+ out[k] = out[k] + space.ref[k] * sinAlpha;
229
+ return out;
230
+ }
231
+ const betaFac = b / 32767;
232
+ const beta = (betaFac > 0 ? space.beta : TAU - space.beta) * betaFac;
233
+ for (let k = 0; k < 3; k++) {
234
+ const i = k;
235
+ out[i] += space.ref[i] * sinAlpha * Math.cos(beta) + space.ortho[i] * sinAlpha * Math.sin(beta);
236
+ }
237
+ return out;
238
+ }
239
+ /**
240
+ * The normal every corner is shaded with, three floats per corner, in the mesh's own space.
241
+ *
242
+ * `splitAngle` also tags as sharp every edge whose faces meet at more than it: a pre-4.1 file's
243
+ * auto smooth by default, or the angle of a Smooth by Angle modifier, which does the same thing.
244
+ */
245
+ export function cornerNormals(mesh, splitAngle = mesh.autoSmooth) {
246
+ const { faceOffsets, cornerVerts, cornerEdges, edgeVerts, sharpFace } = mesh;
247
+ const corners = cornerVerts.length;
248
+ const faces = faceOffsets.length - 1;
249
+ const out = new Float32Array(corners * 3);
250
+ const face = faceNormals(mesh);
251
+ const faceOf = new Int32Array(corners);
252
+ for (let f = 0; f < faces; f++)
253
+ faceOf.fill(f, faceOffsets[f], faceOffsets[f + 1]);
254
+ if (mesh.freeNormals !== null && mesh.packedNormals === null) {
255
+ const { domain, normals } = mesh.freeNormals;
256
+ for (let c = 0; c < corners; c++) {
257
+ const i = domain === POINT ? cornerVerts[c] : domain === FACE ? faceOf[c] : c;
258
+ const n = [
259
+ normals[i * 3],
260
+ normals[i * 3 + 1],
261
+ normals[i * 3 + 2],
262
+ ];
263
+ normalise(n);
264
+ out.set(n, c * 3);
265
+ }
266
+ return out;
267
+ }
268
+ let sharpEdge = mesh.sharpEdge;
269
+ if (splitAngle !== null)
270
+ sharpEdge = byAngle(mesh, face, splitAngle);
271
+ const allFlat = faces > 0 && sharpFace !== null && sharpFace.every((s) => s === 1);
272
+ const allSharp = sharpEdge !== null && sharpEdge.length > 0 && sharpEdge.every((s) => s === 1);
273
+ const anyFlat = sharpFace !== null && sharpFace.some((s) => s === 1);
274
+ const anySharp = sharpEdge !== null && sharpEdge.some((s) => s === 1);
275
+ if (mesh.packedNormals === null && (allFlat || allSharp)) {
276
+ for (let c = 0; c < corners; c++)
277
+ out.set(face.subarray(faceOf[c] * 3, faceOf[c] * 3 + 3), c * 3);
278
+ return out;
279
+ }
280
+ const vertex = vertexNormals(mesh, face);
281
+ if (mesh.packedNormals === null && !anyFlat && !anySharp) {
282
+ for (let c = 0; c < corners; c++) {
283
+ const v = cornerVerts[c];
284
+ out.set(vertex.subarray(v * 3, v * 3 + 3), c * 3);
285
+ }
286
+ return out;
287
+ }
288
+ const e2l = edgeLoops(mesh, sharpEdge);
289
+ const packed = mesh.packedNormals;
290
+ const other = (edge, pivot) => edgeVerts[edge * 2] === pivot
291
+ ? edgeVerts[edge * 2 + 1]
292
+ : edgeVerts[edge * 2];
293
+ const prevCorner = (c) => {
294
+ const f = faceOf[c];
295
+ return c === faceOffsets[f] ? faceOffsets[f + 1] - 1 : c - 1;
296
+ };
297
+ const nextCorner = (c) => {
298
+ const f = faceOf[c];
299
+ return c + 1 === faceOffsets[f + 1] ? faceOffsets[f] : c + 1;
300
+ };
301
+ /* Prefilled as Blender prefills: the vertex normal where smooth, the face normal where flat. */
302
+ for (let c = 0; c < corners; c++) {
303
+ const f = faceOf[c];
304
+ if (sharpFace !== null && sharpFace[f] === 1)
305
+ out.set(face.subarray(f * 3, f * 3 + 3), c * 3);
306
+ else {
307
+ const v = cornerVerts[c];
308
+ out.set(vertex.subarray(v * 3, v * 3 + 3), c * 3);
309
+ }
310
+ }
311
+ /** Cross the edge `curr` names into the next face around `pivot`: Blender's fan step. */
312
+ const step = (curr, pivot) => {
313
+ const edge = cornerEdges[curr];
314
+ const a = e2l[edge * 2];
315
+ const b = e2l[edge * 2 + 1];
316
+ const fromVert = cornerVerts[curr];
317
+ const next = a === curr ? b : a;
318
+ const nextVert = cornerVerts[next];
319
+ if ((fromVert === nextVert && fromVert === pivot) ||
320
+ (fromVert !== nextVert && fromVert !== pivot)) {
321
+ return { curr: prevCorner(next), vert: next };
322
+ }
323
+ const forward = nextCorner(next);
324
+ return { curr: forward, vert: forward };
325
+ };
326
+ const skip = new Uint8Array(corners);
327
+ const cyclic = (start, prev) => {
328
+ const pivot = cornerVerts[start];
329
+ if (isSharp(e2l, cornerEdges[prev]))
330
+ return false;
331
+ let curr = prev;
332
+ let vert = start;
333
+ skip[vert] = 1;
334
+ for (;;) {
335
+ ({ curr, vert } = step(curr, pivot));
336
+ if (isSharp(e2l, cornerEdges[curr]))
337
+ return false;
338
+ if (skip[vert] === 1)
339
+ return vert === start;
340
+ skip[vert] = 1;
341
+ }
342
+ };
343
+ const single = (c) => {
344
+ const f = faceOf[c];
345
+ const n = [face[f * 3], face[f * 3 + 1], face[f * 3 + 2]];
346
+ if (packed !== null) {
347
+ const v = cornerVerts[c];
348
+ const space = defineSpace(n, direction(mesh, v, cornerVerts[nextCorner(c)]), direction(mesh, v, cornerVerts[prevCorner(c)]), []);
349
+ out.set(decode(space, packed[c * 2], packed[c * 2 + 1]), c * 3);
350
+ }
351
+ else
352
+ out.set(n, c * 3);
353
+ };
354
+ const fan = (start, prev) => {
355
+ const pivot = cornerVerts[start];
356
+ const origin = cornerEdges[start];
357
+ const vecOrigin = direction(mesh, pivot, other(origin, pivot));
358
+ let vecPrev = vecOrigin;
359
+ let vecCurr = vecOrigin;
360
+ const edges = [vecOrigin];
361
+ const members = [];
362
+ const lnor = [0, 0, 0];
363
+ let curr = prev;
364
+ let vert = start;
365
+ for (let guard = 0; guard <= corners; guard++) {
366
+ const edge = cornerEdges[curr];
367
+ vecCurr = direction(mesh, pivot, other(edge, pivot));
368
+ const angle = cornerAngle(dot(vecCurr, vecPrev));
369
+ const f = faceOf[curr];
370
+ for (let k = 0; k < 3; k++)
371
+ lnor[k] += face[f * 3 + k] * angle;
372
+ members.push(vert);
373
+ if (edge !== origin)
374
+ edges.push(vecCurr);
375
+ if (isSharp(e2l, edge) || edge === origin)
376
+ break;
377
+ vecPrev = vecCurr;
378
+ ({ curr, vert } = step(curr, pivot));
379
+ }
380
+ let length = normalise(lnor);
381
+ if (packed !== null) {
382
+ if (length === 0) {
383
+ const v = cornerVerts[vert];
384
+ lnor[0] = vertex[v * 3];
385
+ lnor[1] = vertex[v * 3 + 1];
386
+ lnor[2] = vertex[v * 3 + 2];
387
+ length = 1;
388
+ }
389
+ const space = defineSpace(lnor, vecOrigin, vecCurr, edges);
390
+ const first = members[0];
391
+ let a = packed[first * 2];
392
+ let b = packed[first * 2 + 1];
393
+ if (members.some((m) => packed[m * 2] !== a || packed[m * 2 + 1] !== b)) {
394
+ let sa = 0;
395
+ let sb = 0;
396
+ for (const m of members) {
397
+ sa += packed[m * 2];
398
+ sb += packed[m * 2 + 1];
399
+ }
400
+ a = Math.trunc(sa / members.length);
401
+ b = Math.trunc(sb / members.length);
402
+ }
403
+ const custom = decode(space, a, b);
404
+ lnor[0] = custom[0];
405
+ lnor[1] = custom[1];
406
+ lnor[2] = custom[2];
407
+ }
408
+ if (length !== 0)
409
+ for (const m of members)
410
+ out.set(lnor, m * 3);
411
+ };
412
+ for (let f = 0; f < faces; f++) {
413
+ for (let c = faceOffsets[f]; c < faceOffsets[f + 1]; c++) {
414
+ const prev = prevCorner(c);
415
+ const currSharp = isSharp(e2l, cornerEdges[c]);
416
+ const prevSharp = isSharp(e2l, cornerEdges[prev]);
417
+ if (currSharp && prevSharp)
418
+ single(c);
419
+ else if (currSharp || (!prevSharp && skip[c] === 0 && cyclic(c, prev)))
420
+ fan(c, prev);
421
+ }
422
+ }
423
+ return out;
424
+ }
425
+ /** What Blender's smooth-by-angle does to a pre-4.1 file with auto smooth on: tag by angle too. */
426
+ function byAngle(mesh, face, angle) {
427
+ const edges = mesh.edgeVerts.length / 2;
428
+ const out = new Uint8Array(edges);
429
+ if (mesh.sharpEdge !== null)
430
+ out.set(mesh.sharpEdge);
431
+ const first = new Int32Array(edges).fill(-1);
432
+ const count = new Int32Array(edges);
433
+ const second = new Int32Array(edges).fill(-1);
434
+ const { faceOffsets, cornerEdges } = mesh;
435
+ for (let f = 0; f < faceOffsets.length - 1; f++) {
436
+ for (let c = faceOffsets[f]; c < faceOffsets[f + 1]; c++) {
437
+ const e = cornerEdges[c];
438
+ if (count[e] === 0)
439
+ first[e] = f;
440
+ else if (count[e] === 1)
441
+ second[e] = f;
442
+ count[e] = count[e] + 1;
443
+ }
444
+ }
445
+ const cos = Math.cos(angle);
446
+ for (let e = 0; e < edges; e++) {
447
+ if (count[e] !== 2)
448
+ continue;
449
+ const a = first[e];
450
+ const b = second[e];
451
+ const d = face[a * 3] * face[b * 3] +
452
+ face[a * 3 + 1] * face[b * 3 + 1] +
453
+ face[a * 3 + 2] * face[b * 3 + 2];
454
+ if (d < cos)
455
+ out[e] = 1;
456
+ }
457
+ return out;
458
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Undoing the compression a `.blend` was saved with, through a decompressor the host supplies.
3
+ *
4
+ * **Blender has saved compressed files two ways.** Before 3.0 the whole file was one gzip stream.
5
+ * From 3.0 it is zstd in the *seekable* layout: the file cut into independent frames, and a table
6
+ * at the end in a skippable frame saying how long each one is. This module reads that table and
7
+ * hands each frame to the host's decoder, because a decoder asked for the whole file may stop at
8
+ * the end of the first frame and say nothing: Node 22's `zstdDecompressSync` returned 67 KB of a
9
+ * three-frame, 330 KB fixture, which then read as a file that ends before its `ENDB`.
10
+ *
11
+ * **The decoder is the host's, not this module's.** Node has both; a browser has gzip in
12
+ * `DecompressionStream` and, depending on the browser, no zstd at all. Shipping a zstd decoder
13
+ * here would be owning a correctness risk the platform already carries, which is the argument
14
+ * `fbxInflate.ts` makes for deflate. A host without one is told the name of what to pass.
15
+ *
16
+ * What it gives up: a zstd file with no seek table — one no Blender writes — is handed to the
17
+ * decoder whole, and a decoder that stops after one frame then truncates it. The truncation is
18
+ * caught downstream, by the missing `ENDB`, rather than here.
19
+ */
20
+ /** Decompress one gzip stream or one zstd frame. Supplied by the host. */
21
+ export type BlendDecompress = (compressed: Uint8Array, codec: 'gzip' | 'zstd') => Uint8Array | Promise<Uint8Array>;
22
+ /** Each frame's compressed and decompressed length, from the seek table, or null where there is none. */
23
+ export declare function zstdFrames(bytes: Uint8Array): {
24
+ packed: number;
25
+ plain: number;
26
+ }[] | null;
27
+ /**
28
+ * The plain bytes of a `.blend`, decompressing first where the file says it is compressed.
29
+ *
30
+ * **Allocated once, from the seek table.** The table states every frame's decompressed length, so
31
+ * the output is sized before the first frame is decoded and each frame is copied into place and
32
+ * dropped. Collecting the frames and joining them afterwards held the file twice at its peak, which
33
+ * is 14 GB for a 7 GB city and was the difference between reading it and not.
34
+ */
35
+ export declare function unpackBlend(bytes: Uint8Array, decompress: BlendDecompress | undefined): Promise<Uint8Array>;
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Undoing the compression a `.blend` was saved with, through a decompressor the host supplies.
3
+ *
4
+ * **Blender has saved compressed files two ways.** Before 3.0 the whole file was one gzip stream.
5
+ * From 3.0 it is zstd in the *seekable* layout: the file cut into independent frames, and a table
6
+ * at the end in a skippable frame saying how long each one is. This module reads that table and
7
+ * hands each frame to the host's decoder, because a decoder asked for the whole file may stop at
8
+ * the end of the first frame and say nothing: Node 22's `zstdDecompressSync` returned 67 KB of a
9
+ * three-frame, 330 KB fixture, which then read as a file that ends before its `ENDB`.
10
+ *
11
+ * **The decoder is the host's, not this module's.** Node has both; a browser has gzip in
12
+ * `DecompressionStream` and, depending on the browser, no zstd at all. Shipping a zstd decoder
13
+ * here would be owning a correctness risk the platform already carries, which is the argument
14
+ * `fbxInflate.ts` makes for deflate. A host without one is told the name of what to pass.
15
+ *
16
+ * What it gives up: a zstd file with no seek table — one no Blender writes — is handed to the
17
+ * decoder whole, and a decoder that stops after one frame then truncates it. The truncation is
18
+ * caught downstream, by the missing `ENDB`, rather than here.
19
+ */
20
+ import { DrftError } from '@driftengine/drft';
21
+ import { blendCompression } from './blendFile.js';
22
+ const SEEKABLE_MAGIC = 0x8f92eab1;
23
+ const SKIPPABLE_MAGIC = 0x184d2a5e;
24
+ /** Each frame's compressed and decompressed length, from the seek table, or null where there is none. */
25
+ export function zstdFrames(bytes) {
26
+ if (bytes.length < 17)
27
+ return null;
28
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
29
+ const footer = bytes.length - 9;
30
+ if (view.getUint32(footer + 5, true) !== SEEKABLE_MAGIC)
31
+ return null;
32
+ const frames = view.getUint32(footer, true);
33
+ const descriptor = bytes[footer + 4] ?? 0;
34
+ const entry = descriptor & 0x80 ? 12 : 8;
35
+ const table = footer - frames * entry;
36
+ const header = table - 8;
37
+ if (header < 0 || view.getUint32(header, true) !== SKIPPABLE_MAGIC)
38
+ return null;
39
+ const sizes = [];
40
+ let total = 0;
41
+ for (let i = 0; i < frames; i++) {
42
+ const packed = view.getUint32(table + i * entry, true);
43
+ sizes.push({ packed, plain: view.getUint32(table + i * entry + 4, true) });
44
+ total += packed;
45
+ }
46
+ return total === header ? sizes : null;
47
+ }
48
+ /**
49
+ * The plain bytes of a `.blend`, decompressing first where the file says it is compressed.
50
+ *
51
+ * **Allocated once, from the seek table.** The table states every frame's decompressed length, so
52
+ * the output is sized before the first frame is decoded and each frame is copied into place and
53
+ * dropped. Collecting the frames and joining them afterwards held the file twice at its peak, which
54
+ * is 14 GB for a 7 GB city and was the difference between reading it and not.
55
+ */
56
+ export async function unpackBlend(bytes, decompress) {
57
+ const codec = blendCompression(bytes);
58
+ if (codec === null)
59
+ return bytes;
60
+ if (decompress === undefined) {
61
+ throw new DrftError(`.blend saved with ${codec} compression needs a "decompress" to be read. Bake it with ` +
62
+ '`npm run bake`, which supplies one, or pass decompress to readModel.');
63
+ }
64
+ if (codec === 'gzip')
65
+ return decompress(bytes, 'gzip');
66
+ const frames = zstdFrames(bytes);
67
+ if (frames === null)
68
+ return decompress(bytes, 'zstd');
69
+ const out = new Uint8Array(frames.reduce((n, frame) => n + frame.plain, 0));
70
+ let from = 0;
71
+ let to = 0;
72
+ for (const frame of frames) {
73
+ const part = await decompress(bytes.subarray(from, from + frame.packed), 'zstd');
74
+ if (part.length !== frame.plain) {
75
+ throw new DrftError(`blend: a zstd frame decoded to ${part.length} bytes where the seek table says ${frame.plain}`);
76
+ }
77
+ out.set(part, to);
78
+ from += frame.packed;
79
+ to += part.length;
80
+ }
81
+ return out;
82
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Which objects of a `.blend` are drawn, and where: the scene walk, in Blender's coordinates.
3
+ *
4
+ * **"Drawn" means what a render of the open scene shows**, the same rule `scripts/blender/
5
+ * export_gltf.py` applies before it hands the scene to Blender's exporter, so both routes into
6
+ * the engine agree on what an asset contains. An object is drawn when it is linked into the scene
7
+ * through at least one collection that is neither excluded from the view layer nor disabled for
8
+ * rendering, and is not itself hidden from renders. Parenting does not hide: a child of a hidden
9
+ * object is drawn where its parent would have put it, which is what Blender does.
10
+ *
11
+ * **A collection instance is expanded** into its objects, placed by the instancer and shifted by
12
+ * the collection's instance offset. Before 2.8 there were no collections, and the scene's bases and
13
+ * layer bits say the same thing.
14
+ *
15
+ * **What has to be evaluated is refused rather than guessed.** A constraint, a vertex parent or a
16
+ * particle instancer changes where things are by running Blender, so the walk names it in
17
+ * `refusals` and the caller declines the whole file. Animation does not refuse: Blender writes the
18
+ * evaluated value of every animated property back to the datablock, so a saved file holds the
19
+ * pose of the frame it was saved on.
20
+ */
21
+ import type { BlendData, BlendStruct } from './blendData.ts';
22
+ import type { Mat4 } from './blendMatrix.ts';
23
+ /** One drawn object, or an empty that carries others, at its place in the world. */
24
+ export interface BlendPlaced {
25
+ readonly object: BlendStruct;
26
+ readonly name: string;
27
+ /** Blender's object type: 0 empty, 1 mesh, 10 light, 25 armature, and the rest. */
28
+ readonly kind: number;
29
+ /** World matrix, Blender space (Z up), column-major. */
30
+ readonly world: Mat4;
31
+ /** Index of the nearest placed ancestor, or -1. */
32
+ readonly parent: number;
33
+ }
34
+ export declare const OB_EMPTY = 0;
35
+ export declare const OB_MESH = 1;
36
+ export declare const OB_LAMP = 10;
37
+ export declare const OB_CAMERA = 11;
38
+ export declare const OB_ARMATURE = 25;
39
+ /** The scene the file was saved looking at. */
40
+ export declare function activeScene(blend: BlendData): BlendStruct | null;
41
+ /**
42
+ * The object's transform relative to its parent: location, rotation and scale, deltas folded in.
43
+ *
44
+ * `values`, where given, overrides what the file stores with what an animation says at a frame,
45
+ * keyed by Blender's data path and index — `location[0]`, `rotation_quaternion[3]` — which is what
46
+ * `Animation.values` fills (`blendAnimation.ts`).
47
+ */
48
+ export declare function localMatrix(object: BlendStruct, values?: ReadonlyMap<string, number>): Mat4;
49
+ /** Walk the scene into placed objects, naming in `refusals` anything that needs Blender to run. */
50
+ export declare function placeObjects(blend: BlendData, refusals: string[], bonePose: (armature: BlendStruct, bone: string) => Mat4 | null): BlendPlaced[];