@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,496 @@
1
+ /**
2
+ * A `.blend` mesh's stored data, read the same way whichever Blender wrote it.
3
+ *
4
+ * **Three layouts, one result.** Until 3.4 a mesh was arrays of structs — `MVert`, `MEdge`,
5
+ * `MPoly`, `MLoop` — with UVs and colours in `CustomData` layers beside them. From 3.5 to 4.x the
6
+ * same data became named attributes in `CustomData`: `position`, `.edge_verts`, `.corner_vert`,
7
+ * `sharp_face`. From 5.0 the attributes moved again, into `AttributeStorage`, with a type code of
8
+ * their own. Each is read here into `BlendMeshData`, which is plain arrays and knows nothing about
9
+ * where they came from.
10
+ *
11
+ * **What is read is what is stored.** Nothing is evaluated: a mesh with modifiers is the mesh
12
+ * before them, and `blend.ts` refuses such a file rather than drawing the wrong shape.
13
+ *
14
+ * **Old meaning is translated to current meaning**, because the comparison is with Blender as it
15
+ * is now opening the file. Before 4.1 a sharp edge did nothing unless the mesh had auto smooth on,
16
+ * and Blender drops those edges when it opens such a file; with auto smooth on, it adds a modifier
17
+ * that splits by angle, which is exactly the angle this reader applies.
18
+ */
19
+
20
+ import { DrftError } from '@driftengine/drft';
21
+ import type { BlendData, BlendStruct } from './blendData.ts';
22
+
23
+ /** Domains, in the order both of Blender's attribute systems number them. */
24
+ export const POINT = 0;
25
+ export const EDGE = 1;
26
+ export const FACE = 2;
27
+ export const CORNER = 3;
28
+
29
+ export interface BlendColorLayer {
30
+ readonly name: string;
31
+ readonly domain: typeof POINT | typeof CORNER;
32
+ /** RGBA per element, linear. A byte colour is stored sRGB-encoded and decoded here. */
33
+ readonly rgba: Float32Array;
34
+ }
35
+
36
+ export interface BlendMeshData {
37
+ readonly name: string;
38
+ readonly positions: Float32Array;
39
+ /** Face `f` owns corners `faceOffsets[f]` to `faceOffsets[f + 1]`. */
40
+ readonly faceOffsets: Int32Array;
41
+ readonly cornerVerts: Int32Array;
42
+ readonly cornerEdges: Int32Array;
43
+ readonly edgeVerts: Int32Array;
44
+ readonly materialIndex: Int32Array | null;
45
+ readonly sharpFace: Uint8Array | null;
46
+ readonly sharpEdge: Uint8Array | null;
47
+ /** UV maps in the file's order, two floats per corner. */
48
+ readonly uvs: readonly { name: string; uv: Float32Array }[];
49
+ /** Which UV map renders when a texture names none: Blender's "active render" map. */
50
+ readonly renderUv: number;
51
+ readonly colors: readonly BlendColorLayer[];
52
+ /** The colour attribute a material's unnamed Color Attribute node reads. */
53
+ readonly renderColor: number;
54
+ /** Custom normals in Blender's packed form: two shorts per corner, relative to its fan. */
55
+ readonly packedNormals: Int16Array | null;
56
+ /** Custom normals stored as plain vectors, 4.5 on, in the domain they were written to. */
57
+ readonly freeNormals: { domain: number; normals: Float32Array } | null;
58
+ /** The split angle a pre-4.1 file's auto smooth asked for, or null where it was off. */
59
+ readonly autoSmooth: number | null;
60
+ /** Per vertex, the groups it belongs to and how much: flattened `(group, weight)` pairs. */
61
+ readonly weights: { offsets: Int32Array; groups: Int32Array; values: Float32Array } | null;
62
+ readonly groupNames: readonly string[];
63
+ }
64
+
65
+ /** One stored attribute, before it is interpreted. */
66
+ interface RawAttribute {
67
+ readonly name: string;
68
+ readonly domain: number;
69
+ readonly kind: AttributeKind;
70
+ readonly bytes: Uint8Array;
71
+ /** One value for every element, which `AttributeStorage` can store instead of an array. */
72
+ readonly single: boolean;
73
+ }
74
+
75
+ type AttributeKind =
76
+ | 'bool'
77
+ | 'int8'
78
+ | 'short2'
79
+ | 'int'
80
+ | 'int2'
81
+ | 'float'
82
+ | 'float2'
83
+ | 'float3'
84
+ | 'byteColor'
85
+ | 'color'
86
+ | 'other';
87
+
88
+ /** `CustomData` layer types, by the number a 2.8-to-4.x file stores. */
89
+ const CD_KINDS: Record<number, AttributeKind> = {
90
+ 10: 'float',
91
+ 11: 'int',
92
+ 17: 'byteColor',
93
+ 41: 'short2',
94
+ 45: 'int8',
95
+ 46: 'int2',
96
+ 47: 'color',
97
+ 48: 'float3',
98
+ 49: 'float2',
99
+ 50: 'bool',
100
+ };
101
+
102
+ /**
103
+ * `AttributeStorage` types, 5.0 on: Blender's `AttrType` in declaration order. Read off fixtures
104
+ * rather than assumed: a byte colour attribute is stored as 9, four bytes an element, so the float
105
+ * colour before it is 8.
106
+ */
107
+ const STORAGE_KINDS: readonly AttributeKind[] = [
108
+ 'bool',
109
+ 'int8',
110
+ 'short2',
111
+ 'int',
112
+ 'int2',
113
+ 'float',
114
+ 'float2',
115
+ 'float3',
116
+ 'color',
117
+ 'byteColor',
118
+ ];
119
+
120
+ const ELEMENT_BYTES: Record<AttributeKind, number> = {
121
+ bool: 1,
122
+ int8: 1,
123
+ short2: 4,
124
+ int: 4,
125
+ int2: 8,
126
+ float: 4,
127
+ float2: 8,
128
+ float3: 12,
129
+ byteColor: 4,
130
+ color: 16,
131
+ other: 0,
132
+ };
133
+
134
+ const ME_AUTOSMOOTH = 1 << 5;
135
+ const ME_SMOOTH = 1;
136
+ const ME_SHARP = 1 << 9;
137
+ const CD_MVERT = 0;
138
+ const CD_MDEFORMVERT = 2;
139
+ const CD_MEDGE = 3;
140
+ const CD_MLOOPUV = 16;
141
+ const CD_MLOOPCOL = 17;
142
+ const CD_MPOLY = 25;
143
+ const CD_MLOOP = 26;
144
+
145
+ function counts(mesh: BlendStruct): [number, number, number, number] {
146
+ const get = (...names: string[]): number => {
147
+ for (const name of names) if (mesh.has(name)) return mesh.int(name);
148
+ return 0;
149
+ };
150
+ return [
151
+ get('totvert', 'verts_num'),
152
+ get('totedge', 'edges_num'),
153
+ get('totpoly', 'faces_num'),
154
+ get('totloop', 'corners_num'),
155
+ ];
156
+ }
157
+
158
+ function rawAttributes(mesh: BlendStruct): RawAttribute[] {
159
+ const out: RawAttribute[] = [];
160
+ const sizes = counts(mesh);
161
+ if (mesh.has('attribute_storage')) {
162
+ const storage = mesh.sub('attribute_storage');
163
+ const list = storage.array('dna_attributes', 'Attribute', storage.int('dna_attributes_num'));
164
+ for (const attribute of list) {
165
+ const kind = STORAGE_KINDS[attribute.int('data_type')] ?? 'other';
166
+ const single = attribute.int('storage_type') !== 0;
167
+ const holder = attribute.deref('data', single ? 'AttributeSingle' : 'AttributeArray');
168
+ const bytes = holder?.raw('data');
169
+ if (bytes === null || bytes === undefined) continue;
170
+ out.push({
171
+ name: attribute.text('name'),
172
+ domain: attribute.int('domain'),
173
+ kind,
174
+ bytes,
175
+ single,
176
+ });
177
+ }
178
+ }
179
+ const layers = ['vdata', 'edata', 'pdata', 'ldata'];
180
+ layers.forEach((field, domain) => {
181
+ if (!mesh.has(field)) return;
182
+ const data = mesh.sub(field);
183
+ for (const layer of data.array('layers', 'CustomDataLayer', data.int('totlayer'))) {
184
+ const kind = CD_KINDS[layer.int('type')];
185
+ const bytes = layer.raw('data');
186
+ if (kind === undefined || bytes === null) continue;
187
+ const need = (sizes[domain] ?? 0) * ELEMENT_BYTES[kind];
188
+ if (bytes.length < need) continue;
189
+ out.push({ name: layer.string('name'), domain, kind, bytes, single: false });
190
+ }
191
+ });
192
+ return out;
193
+ }
194
+
195
+ /** A `CustomData` layer of a legacy struct type, as the views of its elements. */
196
+ function legacyLayer(
197
+ mesh: BlendStruct,
198
+ field: string,
199
+ type: number,
200
+ struct: string,
201
+ count: number,
202
+ ): BlendStruct[] {
203
+ if (!mesh.has(field)) return [];
204
+ const data = mesh.sub(field);
205
+ for (const layer of data.array('layers', 'CustomDataLayer', data.int('totlayer'))) {
206
+ if (layer.int('type') === type) return layer.array('data', struct, count);
207
+ }
208
+ return [];
209
+ }
210
+
211
+ function view(bytes: Uint8Array): DataView {
212
+ return new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
213
+ }
214
+
215
+ function readFloats(
216
+ attribute: RawAttribute,
217
+ count: number,
218
+ width: number,
219
+ le: boolean,
220
+ ): Float32Array {
221
+ const out = new Float32Array(count * width);
222
+ const data = view(attribute.bytes);
223
+ for (let i = 0; i < count; i++) {
224
+ const at = attribute.single ? 0 : i * width * 4;
225
+ for (let k = 0; k < width; k++) out[i * width + k] = data.getFloat32(at + k * 4, le);
226
+ }
227
+ return out;
228
+ }
229
+
230
+ function readInts(attribute: RawAttribute, count: number, width: number, le: boolean): Int32Array {
231
+ const out = new Int32Array(count * width);
232
+ const data = view(attribute.bytes);
233
+ for (let i = 0; i < count; i++) {
234
+ const at = attribute.single ? 0 : i * width * 4;
235
+ for (let k = 0; k < width; k++) out[i * width + k] = data.getInt32(at + k * 4, le);
236
+ }
237
+ return out;
238
+ }
239
+
240
+ function readBools(attribute: RawAttribute, count: number): Uint8Array {
241
+ const out = new Uint8Array(count);
242
+ for (let i = 0; i < count; i++)
243
+ out[i] = (attribute.single ? attribute.bytes[0] : attribute.bytes[i]) === 0 ? 0 : 1;
244
+ return out;
245
+ }
246
+
247
+ const srgbToLinear = (byte: number): number => {
248
+ const c = byte / 255;
249
+ return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
250
+ };
251
+
252
+ /** Read a mesh datablock into plain arrays. */
253
+ export function readBlendMesh(blend: BlendData, mesh: BlendStruct): BlendMeshData {
254
+ const le = blend.header.littleEndian;
255
+ const [verts, edges, faces, corners] = counts(mesh);
256
+ const name = mesh.idName();
257
+ if (faces === 0 && mesh.has('totface') && mesh.int('totface') > 0) {
258
+ throw new DrftError(
259
+ `blend: mesh "${name}" stores only tessellated faces, which predates Blender 2.63`,
260
+ );
261
+ }
262
+ const attributes = rawAttributes(mesh);
263
+ const named = (attribute: string, domain: number): RawAttribute | undefined =>
264
+ attributes.find((a) => a.name === attribute && a.domain === domain);
265
+
266
+ let positions: Float32Array;
267
+ const position = named('position', POINT);
268
+ if (position !== undefined) positions = readFloats(position, verts, 3, le);
269
+ else {
270
+ positions = new Float32Array(verts * 3);
271
+ const mvert = mesh.has('mvert')
272
+ ? mesh.array('mvert', 'MVert', verts)
273
+ : legacyLayer(mesh, 'vdata', CD_MVERT, 'MVert', verts);
274
+ mvert.forEach((v, i) => positions.set(v.floats('co'), i * 3));
275
+ }
276
+
277
+ let edgeVerts: Int32Array;
278
+ const edgeAttr = named('.edge_verts', EDGE);
279
+ let legacySharp: Uint8Array | null = null;
280
+ if (edgeAttr !== undefined) edgeVerts = readInts(edgeAttr, edges, 2, le);
281
+ else {
282
+ edgeVerts = new Int32Array(edges * 2);
283
+ const medge = mesh.has('medge')
284
+ ? mesh.array('medge', 'MEdge', edges)
285
+ : legacyLayer(mesh, 'edata', CD_MEDGE, 'MEdge', edges);
286
+ if (medge.length > 0 && medge[0]?.has('flag') === true) legacySharp = new Uint8Array(edges);
287
+ medge.forEach((e, i) => {
288
+ edgeVerts[i * 2] = e.int('v1');
289
+ edgeVerts[i * 2 + 1] = e.int('v2');
290
+ if (legacySharp !== null) legacySharp[i] = (e.int('flag') & ME_SHARP) !== 0 ? 1 : 0;
291
+ });
292
+ }
293
+
294
+ const faceOffsets = new Int32Array(faces + 1);
295
+ let legacyFlat: Uint8Array | null = null;
296
+ let legacyMaterial: Int32Array | null = null;
297
+ if (mesh.has('poly_offset_indices') && mesh.ptr('poly_offset_indices') !== 0n) {
298
+ const raw = mesh.raw('poly_offset_indices');
299
+ if (raw === null)
300
+ throw new DrftError(`blend: mesh "${name}" names face offsets it does not store`);
301
+ const data = view(raw);
302
+ for (let i = 0; i <= faces; i++) faceOffsets[i] = data.getInt32(i * 4, le);
303
+ } else {
304
+ const mpoly = mesh.has('mpoly')
305
+ ? mesh.array('mpoly', 'MPoly', faces)
306
+ : legacyLayer(mesh, 'pdata', CD_MPOLY, 'MPoly', faces);
307
+ legacyFlat = new Uint8Array(faces);
308
+ legacyMaterial = new Int32Array(faces);
309
+ let total = 0;
310
+ mpoly.forEach((p, i) => {
311
+ faceOffsets[i] = p.int('loopstart');
312
+ total = p.int('loopstart') + p.int('totloop');
313
+ (legacyFlat as Uint8Array)[i] = (p.int('flag') & ME_SMOOTH) === 0 ? 1 : 0;
314
+ (legacyMaterial as Int32Array)[i] = p.int('mat_nr');
315
+ });
316
+ faceOffsets[faces] = total;
317
+ }
318
+
319
+ let cornerVerts: Int32Array;
320
+ let cornerEdges: Int32Array;
321
+ const cv = named('.corner_vert', CORNER);
322
+ const ce = named('.corner_edge', CORNER);
323
+ if (cv !== undefined && ce !== undefined) {
324
+ cornerVerts = readInts(cv, corners, 1, le);
325
+ cornerEdges = readInts(ce, corners, 1, le);
326
+ } else {
327
+ cornerVerts = new Int32Array(corners);
328
+ cornerEdges = new Int32Array(corners);
329
+ const mloop = mesh.has('mloop')
330
+ ? mesh.array('mloop', 'MLoop', corners)
331
+ : legacyLayer(mesh, 'ldata', CD_MLOOP, 'MLoop', corners);
332
+ mloop.forEach((l, i) => {
333
+ cornerVerts[i] = l.int('v');
334
+ cornerEdges[i] = l.int('e');
335
+ });
336
+ }
337
+ if (cornerVerts.length !== corners || faceOffsets[faces] !== corners) {
338
+ throw new DrftError(`blend: mesh "${name}" does not store the corners its faces name`);
339
+ }
340
+
341
+ const materialAttr = named('material_index', FACE);
342
+ const materialIndex =
343
+ materialAttr !== undefined ? readInts(materialAttr, faces, 1, le) : legacyMaterial;
344
+ const flatAttr = named('sharp_face', FACE);
345
+ const sharpFace = flatAttr !== undefined ? readBools(flatAttr, faces) : legacyFlat;
346
+ const flag = mesh.has('flag') ? mesh.int('flag') : 0;
347
+ const autoSmooth =
348
+ blend.header.version < 401 && (flag & ME_AUTOSMOOTH) !== 0 && mesh.has('smoothresh')
349
+ ? mesh.float('smoothresh')
350
+ : null;
351
+ const sharpAttr = named('sharp_edge', EDGE);
352
+ const sharpEdge = sharpAttr !== undefined ? readBools(sharpAttr, edges) : legacySharp;
353
+
354
+ const uvs: { name: string; uv: Float32Array }[] = [];
355
+ for (const a of attributes) {
356
+ if (a.kind === 'float2' && a.domain === CORNER && !a.name.startsWith('.')) {
357
+ uvs.push({ name: a.name, uv: readFloats(a, corners, 2, le) });
358
+ }
359
+ }
360
+ if (uvs.length === 0 && mesh.has('ldata')) {
361
+ const data = mesh.sub('ldata');
362
+ for (const layer of data.array('layers', 'CustomDataLayer', data.int('totlayer'))) {
363
+ if (layer.int('type') !== CD_MLOOPUV) continue;
364
+ const uv = new Float32Array(corners * 2);
365
+ layer.array('data', 'MLoopUV', corners).forEach((l, i) => uv.set(l.floats('uv'), i * 2));
366
+ uvs.push({ name: layer.string('name'), uv });
367
+ }
368
+ }
369
+
370
+ const colors: BlendColorLayer[] = [];
371
+ for (const a of attributes) {
372
+ if (
373
+ (a.kind !== 'color' && a.kind !== 'byteColor') ||
374
+ (a.domain !== POINT && a.domain !== CORNER)
375
+ )
376
+ continue;
377
+ const count = a.domain === POINT ? verts : corners;
378
+ const rgba = new Float32Array(count * 4);
379
+ if (a.kind === 'color') rgba.set(readFloats(a, count, 4, le));
380
+ else {
381
+ for (let i = 0; i < count; i++) {
382
+ const at = a.single ? 0 : i * 4;
383
+ for (let k = 0; k < 3; k++) rgba[i * 4 + k] = srgbToLinear(a.bytes[at + k] ?? 0);
384
+ rgba[i * 4 + 3] = (a.bytes[at + 3] ?? 255) / 255;
385
+ }
386
+ }
387
+ colors.push({ name: a.name, domain: a.domain, rgba });
388
+ }
389
+ if (colors.length === 0 && !attributes.some((a) => a.kind === 'byteColor') && mesh.has('ldata')) {
390
+ const data = mesh.sub('ldata');
391
+ for (const layer of data.array('layers', 'CustomDataLayer', data.int('totlayer'))) {
392
+ if (layer.int('type') !== CD_MLOOPCOL) continue;
393
+ const rgba = new Float32Array(corners * 4);
394
+ layer.array('data', 'MLoopCol', corners).forEach((l, i) => {
395
+ rgba[i * 4] = srgbToLinear(l.int('r'));
396
+ rgba[i * 4 + 1] = srgbToLinear(l.int('g'));
397
+ rgba[i * 4 + 2] = srgbToLinear(l.int('b'));
398
+ rgba[i * 4 + 3] = l.int('a') / 255;
399
+ });
400
+ colors.push({ name: layer.string('name'), domain: CORNER, rgba });
401
+ }
402
+ }
403
+
404
+ let packedNormals: Int16Array | null = null;
405
+ let freeNormals: { domain: number; normals: Float32Array } | null = null;
406
+ for (const a of attributes) {
407
+ if (
408
+ a.kind === 'short2' &&
409
+ a.domain === CORNER &&
410
+ (a.name === 'custom_normal' || a.name === '')
411
+ ) {
412
+ packedNormals = new Int16Array(corners * 2);
413
+ const data = view(a.bytes);
414
+ for (let i = 0; i < corners * 2; i++)
415
+ packedNormals[i] = data.getInt16(a.single ? (i % 2) * 2 : i * 2, le);
416
+ }
417
+ if (a.kind === 'float3' && a.name === 'custom_normal') {
418
+ const count = [verts, edges, faces, corners][a.domain] ?? 0;
419
+ freeNormals = { domain: a.domain, normals: readFloats(a, count, 3, le) };
420
+ }
421
+ }
422
+
423
+ /*
424
+ * The active render maps. 5.0 names them on the mesh; before it, the first layer of a type
425
+ * carries the index among its kind in `active_rnd`.
426
+ */
427
+ const named5 = (field: string): string | null =>
428
+ mesh.has(field) && mesh.ptr(field) !== 0n ? mesh.text(field) : null;
429
+ const activeIndex = (type: number): number => {
430
+ if (!mesh.has('ldata')) return 0;
431
+ const data = mesh.sub('ldata');
432
+ const first = data
433
+ .array('layers', 'CustomDataLayer', data.int('totlayer'))
434
+ .find((l) => l.int('type') === type);
435
+ return first?.has('active_rnd') === true ? first.int('active_rnd') : 0;
436
+ };
437
+ const uvName = named5('default_uv_map_attribute');
438
+ let renderUv = uvName === null ? -1 : uvs.findIndex((u) => u.name === uvName);
439
+ if (renderUv < 0)
440
+ renderUv = Math.min(
441
+ activeIndex(position !== undefined ? 49 : CD_MLOOPUV),
442
+ Math.max(0, uvs.length - 1),
443
+ );
444
+ const colorName = named5('default_color_attribute');
445
+ let renderColor = colorName === null ? -1 : colors.findIndex((c) => c.name === colorName);
446
+ if (renderColor < 0) renderColor = 0;
447
+
448
+ const groupNames: string[] = [];
449
+ const groupList = mesh.has('vertex_group_names') ? mesh.list('vertex_group_names') : [];
450
+ for (const group of groupList) groupNames.push(group.string('name'));
451
+ const weights = readWeights(mesh, verts);
452
+
453
+ return {
454
+ name,
455
+ positions,
456
+ faceOffsets,
457
+ cornerVerts,
458
+ cornerEdges,
459
+ edgeVerts,
460
+ materialIndex,
461
+ sharpFace,
462
+ sharpEdge,
463
+ uvs,
464
+ renderUv,
465
+ colors,
466
+ renderColor,
467
+ packedNormals,
468
+ freeNormals,
469
+ autoSmooth,
470
+ weights,
471
+ groupNames,
472
+ };
473
+ }
474
+
475
+ function readWeights(mesh: BlendStruct, verts: number): BlendMeshData['weights'] {
476
+ const dverts =
477
+ mesh.has('dvert') && mesh.ptr('dvert') !== 0n
478
+ ? mesh.array('dvert', 'MDeformVert', verts)
479
+ : legacyLayer(mesh, 'vdata', CD_MDEFORMVERT, 'MDeformVert', verts);
480
+ if (dverts.length !== verts || verts === 0) return null;
481
+ const offsets = new Int32Array(verts + 1);
482
+ const groups: number[] = [];
483
+ const values: number[] = [];
484
+ dverts.forEach((dvert, i) => {
485
+ offsets[i] = groups.length;
486
+ const n = dvert.int('totweight');
487
+ if (n > 0) {
488
+ for (const w of dvert.array('dw', 'MDeformWeight', n)) {
489
+ groups.push(w.int('def_nr'));
490
+ values.push(w.float('weight'));
491
+ }
492
+ }
493
+ });
494
+ offsets[verts] = groups.length;
495
+ return { offsets, groups: Int32Array.from(groups), values: Float32Array.from(values) };
496
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Which of an object's modifiers the direct `.blend` reader can honour, and which it must refuse.
3
+ *
4
+ * **A modifier is a recipe, and the file stores only its ingredients.** The mesh a Subdivision
5
+ * Surface, an Array or a geometry-nodes tree produces is computed by Blender every time and never
6
+ * written, so a reader that drew the stored mesh would draw the wrong shape and say nothing. So
7
+ * every modifier is refused by name unless it is one of the few whose effect is known exactly:
8
+ *
9
+ * - **Disabled for render**: it does nothing to a render, so it does nothing here.
10
+ * - **Smooth by Angle**, and the "Auto Smooth" group Blender 4.1 adds to every older file that had
11
+ * auto smooth on: sharp edges by angle, which `cornerNormals` applies exactly.
12
+ *
13
+ * What would make this wrong: a geometry-nodes group named like Blender's that does something
14
+ * else. The group's own wiring is checked for the nodes that one is built from, not only its name.
15
+ */
16
+
17
+ import type { BlendStruct } from './blendData.ts';
18
+
19
+ /** What an object's modifier stack means for the direct reader. */
20
+ export interface ModifierVerdict {
21
+ /** An angle to split normals at, from a Smooth by Angle modifier, or null. */
22
+ readonly splitAngle: number | null;
23
+ /** The modifiers that need Blender to evaluate them, by name and kind. */
24
+ readonly refusals: readonly string[];
25
+ }
26
+
27
+ /** `eModifierMode_Render`. */
28
+ const MODE_RENDER = 1 << 1;
29
+
30
+ /** The bundled smoothing groups' names, and the nodes every one of them is built from. */
31
+ const SMOOTH_GROUPS = ['Smooth by Angle', 'Auto Smooth'];
32
+ const SMOOTH_NODES = ['GeometryNodeSetShadeSmooth', 'GeometryNodeInputMeshEdgeAngle'];
33
+
34
+ function kindOf(modifier: BlendStruct): string {
35
+ return modifier.type.replace(/ModifierData$/, '').replace(/([a-z])([A-Z])/g, '$1 $2');
36
+ }
37
+
38
+ /** The value a geometry-nodes modifier gives the group input named `input`. */
39
+ function groupInput(modifier: BlendStruct, group: BlendStruct, input: string): number | null {
40
+ if (!group.has('tree_interface')) return null;
41
+ const root = group.sub('tree_interface').sub('root_panel');
42
+ let identifier: string | null = null;
43
+ for (const address of root.pointers('items_array', root.int('items_num'))) {
44
+ const item = root.file.at(address, root.scope);
45
+ if (item?.type === 'bNodeTreeInterfaceSocket' && item.text('name') === input)
46
+ identifier = item.text('identifier');
47
+ }
48
+ if (identifier === null) return null;
49
+ const properties = modifier.sub('settings').deref('properties', 'IDProperty');
50
+ for (const property of properties?.sub('data').list('group', 'IDProperty') ?? []) {
51
+ if (property.string('name') !== identifier) continue;
52
+ const data = property.sub('data');
53
+ const type = property.int('type');
54
+ /* IDP_FLOAT 2 stores its bits in `val`; IDP_DOUBLE 8 in `val` and `val2`; IDP_INT 1, IDP_BOOLEAN 10. */
55
+ if (type === 2)
56
+ return new DataView(new Int32Array([data.int('val')]).buffer).getFloat32(0, true);
57
+ if (type === 8)
58
+ return new DataView(new Int32Array([data.int('val'), data.int('val2')]).buffer).getFloat64(
59
+ 0,
60
+ true,
61
+ );
62
+ if (type === 1 || type === 10) return data.int('val');
63
+ }
64
+ return null;
65
+ }
66
+
67
+ function isSmoothGroup(group: BlendStruct): boolean {
68
+ const name = group.idName().replace(/\.\d{3}$/, '');
69
+ if (!SMOOTH_GROUPS.includes(name)) return false;
70
+ const nodes = new Set(group.list('nodes').map((node) => node.string('idname')));
71
+ return SMOOTH_NODES.every((node) => nodes.has(node));
72
+ }
73
+
74
+ /** Judge an object's modifier stack. */
75
+ export function judgeModifiers(object: BlendStruct): ModifierVerdict {
76
+ let splitAngle: number | null = null;
77
+ const refusals: string[] = [];
78
+ const name = object.idName();
79
+ for (const modifier of object.list('modifiers')) {
80
+ const header = modifier.sub('modifier');
81
+ if ((header.int('mode') & MODE_RENDER) === 0) continue;
82
+ const label = `"${name}" has a ${kindOf(modifier)} modifier ("${header.string('name')}")`;
83
+ if (modifier.type === 'NodesModifierData') {
84
+ const group = modifier.deref('node_group');
85
+ if (group !== null && isSmoothGroup(group)) {
86
+ const ignore = groupInput(modifier, group, 'Ignore Sharpness');
87
+ if (ignore !== null && ignore !== 0)
88
+ refusals.push(`${label} that ignores the mesh's own sharp edges`);
89
+ splitAngle = groupInput(modifier, group, 'Angle') ?? Math.PI / 6;
90
+ continue;
91
+ }
92
+ }
93
+ refusals.push(`${label}, which Blender evaluates`);
94
+ }
95
+ return { splitAngle, refusals };
96
+ }