@driftengine/drft 4.4.2 → 4.6.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/dist/drftWrite.js CHANGED
@@ -10,7 +10,7 @@
10
10
  */
11
11
  import { buildClip, buildMorph, buildNodes, buildSkin } from './drftSkin.js';
12
12
  import { validateMeshData } from './meshData.js';
13
- import { ALIGNMENT, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_ANIM, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_INST, CHUNK_LITE, CHUNK_MSHQ, CHUNK_SKIN, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_COLL, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_TEXS, MATERIAL_ENTRY_BYTES, DRFT_MAGIC, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR, DrftError, FLAG_LITTLE_ENDIAN, HEADER_BYTES, align, } from './drftFormat.js';
13
+ import { ALIGNMENT, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_ANIM, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, CHUNK_SKIN, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_COLL, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_TEXS, MATERIAL_ENTRY_BYTES, DRFT_MAGIC, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR, DrftError, FLAG_LITTLE_ENDIAN, HEADER_BYTES, align, } from './drftFormat.js';
14
14
  import { coarseFirstOrder } from './coarseFirst.js';
15
15
  import { buildSubs } from './drftSubs.js';
16
16
  import { buildSdfv } from './sdfv.js';
@@ -21,8 +21,17 @@ import { buildNavm } from './navm.js';
21
21
  import { buildNgrf } from './ngrf.js';
22
22
  import { buildColliders } from './drftColliders.js';
23
23
  import { buildInstances } from './drftInstances.js';
24
+ import { buildRegion, planRegions } from './drftRegions.js';
25
+ import { buildLightVolume } from './drftLightVolume.js';
26
+ /** Whether a region names a mesh, as a level or a prop. */
27
+ function regionNames(region, mesh) {
28
+ return (region.levels.some((level) => level.meshes.includes(mesh)) ||
29
+ region.instances.some((group) => group.mesh === mesh));
30
+ }
24
31
  import { buildLights } from './drftLights.js';
25
32
  import { encodeQuantisedMesh } from './drftQuantised.js';
33
+ import { assemblyBounds } from './assemble.js';
34
+ import { buildAssembly, buildKit, checkAssembly, checkCopies, checkKit, isAssembly, } from './drftAssembly.js';
26
35
  /**
27
36
  * The most blocks a capture is split into, and the fewest splats worth having in one.
28
37
  *
@@ -104,7 +113,7 @@ function encodeString(value) {
104
113
  bytes.set(text, 4);
105
114
  return bytes;
106
115
  }
107
- function buildHead(head, meshes, instances = []) {
116
+ function buildHead(head, meshes, instances = [], kit = new Set()) {
108
117
  /*
109
118
  * Bounds are computed rather than taken on trust. A caller that gets them wrong would
110
119
  * produce an asset that culls itself out of frame, which looks like a rendering bug and
@@ -119,8 +128,25 @@ function buildHead(head, meshes, instances = []) {
119
128
  const placed = new Map();
120
129
  for (const group of instances)
121
130
  placed.set(group.mesh, group.transforms);
131
+ const pieceAt = (o) => meshes[o];
122
132
  for (let m = 0; m < meshes.length; m++) {
123
- const mesh = meshes[m];
133
+ /* A kit piece is drawn only where it is copied, and its copies are counted with them. */
134
+ if (kit.has(m))
135
+ continue;
136
+ const slot = meshes[m];
137
+ if (isAssembly(slot)) {
138
+ const b = assemblyBounds(slot, pieceAt);
139
+ if (!(b[0] <= b[3]))
140
+ continue;
141
+ minX = Math.min(minX, b[0]);
142
+ minY = Math.min(minY, b[1]);
143
+ minZ = Math.min(minZ, b[2]);
144
+ maxX = Math.max(maxX, b[3]);
145
+ maxY = Math.max(maxY, b[4]);
146
+ maxZ = Math.max(maxZ, b[5]);
147
+ continue;
148
+ }
149
+ const mesh = slot;
124
150
  const transforms = placed.get(m);
125
151
  if (transforms === undefined) {
126
152
  for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
@@ -250,6 +276,10 @@ function buildMesh(mesh, code = CHUNK_MESH, flags = CHUNK_REQUIRED) {
250
276
  attributes |= ATTR_JOINTS;
251
277
  if (mesh.weights !== undefined)
252
278
  attributes |= ATTR_WEIGHTS;
279
+ if (mesh.layers !== undefined)
280
+ attributes |= ATTR_LAYERS;
281
+ if (mesh.channel !== undefined)
282
+ attributes |= ATTR_CHANNEL;
253
283
  /* Order is frozen: mandatory arrays, then optional ones by ascending bit. */
254
284
  const arrays = [
255
285
  mesh.positions,
@@ -277,6 +307,12 @@ function buildMesh(mesh, code = CHUNK_MESH, flags = CHUNK_REQUIRED) {
277
307
  arrays.push(mesh.joints);
278
308
  if (mesh.weights !== undefined)
279
309
  arrays.push(mesh.weights);
310
+ /* Newest of all, 1.20, so last. See ATTR_LAYERS. */
311
+ if (mesh.layers !== undefined)
312
+ arrays.push(mesh.layers);
313
+ /* And newer still, 1.23. See ATTR_CHANNEL. */
314
+ if (mesh.channel !== undefined)
315
+ arrays.push(mesh.channel);
280
316
  arrays.push(mesh.indices);
281
317
  const PREFIX = 16;
282
318
  let total = PREFIX;
@@ -366,6 +402,12 @@ function buildMaterials(materials) {
366
402
  view.setFloat32(at + 68, material.occlusionStrength, true);
367
403
  view.setFloat32(at + 72, material.cutout, true);
368
404
  view.setUint32(at + 76, (material.blend === true ? 1 : 0) | (material.doubleSided === true ? 2 : 0), true);
405
+ /* Glass, 1.19: after the flags word, so every earlier offset stays where readers expect it. */
406
+ view.setFloat32(at + 80, material.transmission ?? 0, true);
407
+ view.setFloat32(at + 84, material.frost ?? 0, true);
408
+ view.setFloat32(at + 88, material.tint?.[0] ?? 1, true);
409
+ view.setFloat32(at + 92, material.tint?.[1] ?? 1, true);
410
+ view.setFloat32(at + 96, material.tint?.[2] ?? 1, true);
369
411
  at += MATERIAL_ENTRY_BYTES;
370
412
  }
371
413
  return { code: CHUNK_MATL, flags: 0, bytes };
@@ -416,7 +458,29 @@ export function writeDrft(source) {
416
458
  * same asset less its textures rather than a refusal. Moving it earlier in the file changes
417
459
  * nothing about that, because a chunk is found through the table rather than by position.
418
460
  */
419
- const chunks = [buildHead(source.head, source.meshes, source.instances)];
461
+ const kit = new Set(source.kit ?? []);
462
+ checkKit(source.kit ?? [], source.meshes.length);
463
+ source.meshes.forEach((slot, ordinal) => {
464
+ if (!isAssembly(slot))
465
+ return;
466
+ checkAssembly(slot, `mesh ${ordinal}`);
467
+ checkCopies(slot, ordinal, kit, (o) => {
468
+ const piece = source.meshes[o];
469
+ return piece === undefined || isAssembly(piece) ? undefined : piece;
470
+ });
471
+ });
472
+ for (const o of kit) {
473
+ if (isAssembly(source.meshes[o])) {
474
+ throw new DrftError(`mesh ${o} is a kit piece and an assembly; a piece is vertices`);
475
+ }
476
+ if ((source.instances ?? []).some((g) => g.mesh === o)) {
477
+ throw new DrftError(`mesh ${o} is a kit piece and is placed by INST`);
478
+ }
479
+ if ((source.regions ?? []).some((r) => regionNames(r, o))) {
480
+ throw new DrftError(`mesh ${o} is a kit piece and a region's; a piece is drawn only as copies`);
481
+ }
482
+ }
483
+ const chunks = [buildHead(source.head, source.meshes, source.instances, kit)];
420
484
  /*
421
485
  * The outline first, ahead of even the paint. It is the one thing that puts a recognisable
422
486
  * whole object on screen, it is a few hundred kilobytes against tens of megabytes, and the
@@ -466,6 +530,11 @@ export function writeDrft(source) {
466
530
  bytes: buildInstances(source.instances),
467
531
  });
468
532
  }
533
+ /* The kit ahead of the geometry, for the placements' reason: a streaming consumer must know a
534
+ mesh is a piece before it lands, or it has already drawn it at the origin. */
535
+ if (kit.size > 0) {
536
+ chunks.push({ code: CHUNK_KITS, flags: CHUNK_REQUIRED, bytes: buildKit(source.kit ?? []) });
537
+ }
469
538
  /* Small, and ahead of the geometry, so a streaming consumer can place its lamps early. */
470
539
  if ((source.lights?.length ?? 0) > 0) {
471
540
  chunks.push({ code: CHUNK_LITE, flags: 0, bytes: buildLights(source.lights ?? []) });
@@ -494,7 +563,23 @@ export function writeDrft(source) {
494
563
  if (source.entities !== undefined) {
495
564
  chunks.push({ code: CHUNK_ENTS, flags: 0, bytes: buildEnts(source.entities) });
496
565
  }
566
+ if (source.texturesFirst === true) {
567
+ for (const texture of source.textures ?? [])
568
+ chunks.push(buildTexture(texture));
569
+ }
570
+ const regionsBefore = planRegions(source.regions ?? [], source.meshes.length, (m) => {
571
+ const mesh = source.meshes[m];
572
+ return mesh !== undefined && isAssembly(mesh) ? mesh.pieces : undefined;
573
+ });
574
+ for (const group of source.instances ?? []) {
575
+ if ((source.regions ?? []).some((r) => regionNames(r, group.mesh))) {
576
+ throw new DrftError(`mesh ${group.mesh} is placed by INST and is also a region's`);
577
+ }
578
+ }
497
579
  source.meshes.forEach((mesh, ordinal) => {
580
+ for (const region of regionsBefore[ordinal] ?? []) {
581
+ chunks.push({ code: CHUNK_REGN, flags: CHUNK_REQUIRED, bytes: buildRegion(region) });
582
+ }
498
583
  /*
499
584
  * **Its deltas immediately before it, not after.** A streaming consumer uploads a mesh the
500
585
  * moment its chunk lands, so deltas arriving afterwards are deltas for geometry already on the
@@ -502,6 +587,10 @@ export function writeDrft(source) {
502
587
  * them in hand when it builds. The mesh ordinal in the payload still does the pairing, so a
503
588
  * reader meeting them in another order is unaffected.
504
589
  */
590
+ if (isAssembly(mesh)) {
591
+ chunks.push({ code: CHUNK_MSHC, flags: CHUNK_REQUIRED, bytes: buildAssembly(mesh) });
592
+ return;
593
+ }
505
594
  if (mesh.morphTargets !== undefined && mesh.morphTargetCount !== undefined) {
506
595
  chunks.push({
507
596
  code: CHUNK_MORP,
@@ -521,8 +610,16 @@ export function writeDrft(source) {
521
610
  chunks.push(buildMesh(mesh));
522
611
  }
523
612
  });
524
- for (const texture of source.textures ?? [])
525
- chunks.push(buildTexture(texture));
613
+ for (const region of regionsBefore[source.meshes.length] ?? []) {
614
+ chunks.push({ code: CHUNK_REGN, flags: CHUNK_REQUIRED, bytes: buildRegion(region) });
615
+ }
616
+ if (source.lightVolume !== undefined) {
617
+ chunks.push({ code: CHUNK_LVOL, flags: 0, bytes: buildLightVolume(source.lightVolume) });
618
+ }
619
+ if (source.texturesFirst !== true) {
620
+ for (const texture of source.textures ?? [])
621
+ chunks.push(buildTexture(texture));
622
+ }
526
623
  /* One `SUBS` for the file, and only where something was labelled — an empty chunk would cost
527
624
  sixteen bytes to say nothing, and rule 2 already covers its absence. */
528
625
  if ((source.substances?.length ?? 0) > 0) {
package/dist/index.d.ts CHANGED
@@ -24,6 +24,15 @@ export { buildInstances, readInstances } from './drftInstances.ts';
24
24
  export { buildLights, readLights } from './drftLights.ts';
25
25
  export type { DrftLight, DrftLightKind } from './drftLights.ts';
26
26
  export type { DrftInstanceGroup } from './drftInstances.ts';
27
+ export { REGION_MAX_LEVELS, RegionLedger, buildRegion, planRegions, readRegion, } from './drftRegions.ts';
28
+ export { ATTR_CHANNEL, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_LAYERS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, } from './drftFormat.ts';
29
+ export { ASSEMBLY_ATTRIBUTES, COPY_MATRIX_FLOATS, COPY_UV_FLOATS, SURFACE, SURFACE_FLOATS, buildAssembly, buildKit, checkAssembly, checkCopies, isAssembly, readAssembly, readKit, } from './drftAssembly.ts';
30
+ export type { DrftAssembly } from './drftAssembly.ts';
31
+ export { assemblyBounds, expandAssembly } from './assemble.ts';
32
+ export type { PieceLookup } from './assemble.ts';
33
+ export type { DrftRegion, DrftRegionCollision, DrftRegionLevel } from './drftRegions.ts';
34
+ export { buildLightVolume, readLightVolume } from './drftLightVolume.ts';
35
+ export type { DrftLightVolume } from './drftLightVolume.ts';
27
36
  export { buildDtex, readDtex, DTEX_ADDRESS_MODES, DTEX_MAX_TILES, DTEX_REGISTERS } from './dtex.ts';
28
37
  export type { DtexEntry, DtexMaterial } from './dtex.ts';
29
38
  export { buildNavm, readNavm } from './navm.ts';
package/dist/index.js CHANGED
@@ -23,6 +23,13 @@ export { MAX_COLLIDER_HULLS, MAX_COLLIDER_POINTS, buildColliders, readColliders,
23
23
  /* Meshes drawn many times, one matrix a copy, as the baker finds merged copies again. */
24
24
  export { buildInstances, readInstances } from './drftInstances.js';
25
25
  export { buildLights, readLights } from './drftLights.js';
26
+ export { REGION_MAX_LEVELS, RegionLedger, buildRegion, planRegions, readRegion, } from './drftRegions.js';
27
+ /* A mesh carried as painted copies of kit pieces, expanded where it is needed. The attribute bits
28
+ are a MESH's, which is what an assembly declares its optional arrays with. */
29
+ export { ATTR_CHANNEL, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_LAYERS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, } from './drftFormat.js';
30
+ export { ASSEMBLY_ATTRIBUTES, COPY_MATRIX_FLOATS, COPY_UV_FLOATS, SURFACE, SURFACE_FLOATS, buildAssembly, buildKit, checkAssembly, checkCopies, isAssembly, readAssembly, readKit, } from './drftAssembly.js';
31
+ export { assemblyBounds, expandAssembly } from './assemble.js';
32
+ export { buildLightVolume, readLightVolume } from './drftLightVolume.js';
26
33
  /*
27
34
  * **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
28
35
  * carries no semantics — this package is the container and does not know what a channel means —
@@ -106,6 +106,25 @@ export interface MeshData {
106
106
  * separates asphalt from plaster, and the pass states it: see `Renderer.setSurfaceRelief`.
107
107
  */
108
108
  relief?: Float32Array;
109
+ /**
110
+ * Which image of a texture array each vertex's face wears: one whole number a vertex, 0 the first
111
+ * layer. Absent means layer 0, which is the only layer a plain texture has.
112
+ *
113
+ * **What it is for is a merged mesh wearing many images in one draw.** A block of forty buildings
114
+ * with forty facades is forty materials — forty draws, forty material changes — when each image
115
+ * is its own texture; with the facades as the layers of one array and each face naming its layer,
116
+ * it is one. `Renderer.createSurfaceTextureArray` builds the array, and every map of a material
117
+ * (albedo, normal, ORM, emissive) is read at the same layer, so an albedo and its emissive twin
118
+ * share an index.
119
+ *
120
+ * **It travels in the texture-coordinate attribute, as its third component**, because all
121
+ * sixteen of WebGL2's guaranteed vertex locations are spent in the instanced variant. So it needs
122
+ * `uvs` beside it and is refused without them: a layer with no coordinates addresses nothing.
123
+ *
124
+ * Whole numbers, checked, because the shader rounds to the nearest layer and a 1.5 would land on
125
+ * whichever side rounding favoured on that device.
126
+ */
127
+ layers?: Float32Array;
109
128
  /**
110
129
  * Four floats per vertex, and the only attribute whose lanes mean four different things.
111
130
  *
package/dist/meshData.js CHANGED
@@ -50,6 +50,21 @@ export function validateMeshData(data) {
50
50
  check('grain', data.grain, 1);
51
51
  check('relief', data.relief, 1);
52
52
  check('channel', data.channel, 4);
53
+ check('layers', data.layers, 1);
54
+ if (data.layers !== undefined) {
55
+ if (data.uvs === undefined) {
56
+ throw new Error('Mesh: layers without uvs — a texture-array layer travels as the third texture coordinate, ' +
57
+ 'so a mesh that names layers has to supply the two it rides with');
58
+ }
59
+ const layers = data.layers;
60
+ for (let v = 0; v < layers.length; v++) {
61
+ const layer = layers[v];
62
+ if (!(layer >= 0) || Math.floor(layer) !== layer) {
63
+ throw new Error(`Mesh: layer ${layer} at vertex ${v} — a texture-array layer is a whole number at or ` +
64
+ 'above zero, and anything else lands on whichever layer the device rounds it to');
65
+ }
66
+ }
67
+ }
53
68
  /*
54
69
  * `tangents` was declared optional on 2026-08-22 and was not checked here until 2026-08-25, which
55
70
  * made the widest optional attribute in the format the one a short buffer could reach a driver
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/drft",
3
- "version": "4.4.2",
3
+ "version": "4.6.0",
4
4
  "description": "The .drft container: a streaming format for baked 3D scenes",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -27,9 +27,9 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "devDependencies": {
30
- "@driftengine/core": "4.4.2",
31
- "@driftengine/entities": "4.4.2",
32
- "@driftengine/texture": "4.4.2"
30
+ "@driftengine/core": "4.6.0",
31
+ "@driftengine/entities": "4.6.0",
32
+ "@driftengine/texture": "4.6.0"
33
33
  },
34
34
  "author": "Drift Technologies",
35
35
  "repository": {
@@ -0,0 +1,268 @@
1
+ /**
2
+ * An assembly expanded into the mesh it describes: every copy's piece through its matrix, painted
3
+ * by its surface, its texture coordinates stretched as the copy says.
4
+ *
5
+ * **Positions by the matrix, normals by its inverse transpose**, so a slope stretched along one
6
+ * axis stays perpendicular to itself rather than leaning with the stretch. **A copy that mirrors
7
+ * — a negative determinant — winds its triangles back** and flips its tangent's handedness, so its
8
+ * faces still wind counter-clockwise seen from outside and a mirrored texture still lights the
9
+ * right way up. Tangents follow the matrix, since a tangent is a direction along the surface.
10
+ *
11
+ * **A coordinate is stretched along the piece's own tangent**: u by `|uStretch ⊙ t|` for the
12
+ * piece's unit tangent t, v the same along its bitangent, then offset. That is what lets one unit
13
+ * box serve every box in a city: its face coordinates run 0..1, and a copy 13 m by 4 m carrying
14
+ * stretches of size over tile repeats its facade in metres on every face. What it gives up is a
15
+ * stretch that varies across one face — a cone's side, where the circumference shrinks up the
16
+ * slope — which comes out as the stretch at each vertex's own tangent, and exact only where the
17
+ * piece's coordinates are linear in its surface.
18
+ *
19
+ * One allocation per output array, sized by a first pass over the copies.
20
+ */
21
+ import {
22
+ COPY_MATRIX_FLOATS,
23
+ COPY_UV_FLOATS,
24
+ SURFACE,
25
+ SURFACE_FLOATS,
26
+ determinant,
27
+ } from './drftAssembly.ts';
28
+ import type { DrftAssembly } from './drftAssembly.ts';
29
+ import {
30
+ ATTR_EMISSIVE_COLOR,
31
+ ATTR_GRAIN,
32
+ ATTR_LAYERS,
33
+ ATTR_RELIEF,
34
+ ATTR_ROUGHNESS,
35
+ ATTR_SPECULAR,
36
+ ATTR_TANGENT,
37
+ ATTR_UVS,
38
+ ATTR_CHANNEL,
39
+ DrftError,
40
+ } from './drftFormat.ts';
41
+ import type { MeshData } from './meshData.ts';
42
+
43
+ /** Finds a kit piece by mesh ordinal. */
44
+ export type PieceLookup = (ordinal: number) => MeshData;
45
+
46
+ function pieceOf(piece: PieceLookup, assembly: DrftAssembly, c: number): MeshData {
47
+ const mesh = piece(assembly.pieces[c] as number);
48
+ const textured = (assembly.attributes & ATTR_UVS) !== 0;
49
+ if (textured && (mesh.uvs === undefined || mesh.tangents === undefined)) {
50
+ throw new DrftError(
51
+ `piece ${assembly.pieces[c]} has no ${mesh.uvs === undefined ? 'texture coordinates' : 'tangents'}, and a textured assembly copies it`,
52
+ );
53
+ }
54
+ if ((assembly.attributes & ATTR_TANGENT) !== 0 && mesh.tangents === undefined) {
55
+ throw new DrftError(
56
+ `piece ${assembly.pieces[c]} has no tangents, and the assembly carries them`,
57
+ );
58
+ }
59
+ if ((assembly.attributes & ATTR_CHANNEL) !== 0 && mesh.channel === undefined) {
60
+ throw new DrftError(`piece ${assembly.pieces[c]} has no channel, and the assembly sways`);
61
+ }
62
+ return mesh;
63
+ }
64
+
65
+ export function expandAssembly(assembly: DrftAssembly, piece: PieceLookup): MeshData {
66
+ const { attributes, surfaces, surfaceOf, transforms, uv } = assembly;
67
+ const copies = assembly.pieces.length;
68
+ let vertices = 0;
69
+ let indexCount = 0;
70
+ for (let c = 0; c < copies; c++) {
71
+ const mesh = pieceOf(piece, assembly, c);
72
+ vertices += mesh.positions.length / 3;
73
+ indexCount += mesh.indices.length;
74
+ }
75
+ const has = (bit: number): boolean => (attributes & bit) !== 0;
76
+ const out: MeshData = {
77
+ positions: new Float32Array(vertices * 3),
78
+ normals: new Float32Array(vertices * 3),
79
+ colors: new Float32Array(vertices * 3),
80
+ emissive: new Float32Array(vertices),
81
+ indices: new Uint32Array(indexCount),
82
+ };
83
+ if (has(ATTR_SPECULAR)) out.specular = new Float32Array(vertices);
84
+ if (has(ATTR_UVS)) out.uvs = new Float32Array(vertices * 2);
85
+ if (has(ATTR_EMISSIVE_COLOR)) out.emissiveColor = new Float32Array(vertices * 3);
86
+ if (has(ATTR_ROUGHNESS)) out.roughness = new Float32Array(vertices);
87
+ if (has(ATTR_GRAIN)) out.grain = new Float32Array(vertices);
88
+ if (has(ATTR_RELIEF)) out.relief = new Float32Array(vertices);
89
+ if (has(ATTR_TANGENT)) out.tangents = new Float32Array(vertices * 4);
90
+ if (has(ATTR_LAYERS)) out.layers = new Float32Array(vertices);
91
+ if (has(ATTR_CHANNEL)) out.channel = new Float32Array(vertices * 4);
92
+
93
+ let v0 = 0;
94
+ let i0 = 0;
95
+ for (let c = 0; c < copies; c++) {
96
+ const mesh = pieceOf(piece, assembly, c);
97
+ const m = c * COPY_MATRIX_FLOATS;
98
+ const t = transforms;
99
+ const [a, b, cc, d, e, f, g, h, k] = [
100
+ t[m] as number,
101
+ t[m + 1] as number,
102
+ t[m + 2] as number,
103
+ t[m + 3] as number,
104
+ t[m + 4] as number,
105
+ t[m + 5] as number,
106
+ t[m + 6] as number,
107
+ t[m + 7] as number,
108
+ t[m + 8] as number,
109
+ ];
110
+ const [tx, ty, tz] = [t[m + 9] as number, t[m + 10] as number, t[m + 11] as number];
111
+ const det = determinant(t, m);
112
+ /* The inverse transpose is the cofactor matrix over the determinant. With columns (a b cc),
113
+ (d e f), (g h k) the matrix's rows are (a d g), (b e h), (cc f k), and nRC is the cofactor of
114
+ row R, column C — not of C, R, which would be the plain inverse. */
115
+ const n00 = (e * k - f * h) / det;
116
+ const n01 = (cc * h - b * k) / det;
117
+ const n02 = (b * f - cc * e) / det;
118
+ const n10 = (f * g - d * k) / det;
119
+ const n11 = (a * k - cc * g) / det;
120
+ const n12 = (cc * d - a * f) / det;
121
+ const n20 = (d * h - e * g) / det;
122
+ const n21 = (b * g - a * h) / det;
123
+ const n22 = (a * e - b * d) / det;
124
+ const hand = det < 0 ? -1 : 1;
125
+ const s = (surfaceOf[c] as number) * SURFACE_FLOATS;
126
+ const u = c * COPY_UV_FLOATS;
127
+ const count = mesh.positions.length / 3;
128
+ const p = mesh.positions;
129
+ const n = mesh.normals;
130
+ const tan = mesh.tangents;
131
+ const tex = mesh.uvs;
132
+ for (let i = 0; i < count; i++) {
133
+ const o = v0 + i;
134
+ const px = p[i * 3] as number;
135
+ const py = p[i * 3 + 1] as number;
136
+ const pz = p[i * 3 + 2] as number;
137
+ out.positions[o * 3] = a * px + d * py + g * pz + tx;
138
+ out.positions[o * 3 + 1] = b * px + e * py + h * pz + ty;
139
+ out.positions[o * 3 + 2] = cc * px + f * py + k * pz + tz;
140
+ const nx = n[i * 3] as number;
141
+ const ny = n[i * 3 + 1] as number;
142
+ const nz = n[i * 3 + 2] as number;
143
+ let wx = n00 * nx + n01 * ny + n02 * nz;
144
+ let wy = n10 * nx + n11 * ny + n12 * nz;
145
+ let wz = n20 * nx + n21 * ny + n22 * nz;
146
+ const nl = Math.hypot(wx, wy, wz) || 1;
147
+ wx /= nl;
148
+ wy /= nl;
149
+ wz /= nl;
150
+ out.normals[o * 3] = wx;
151
+ out.normals[o * 3 + 1] = wy;
152
+ out.normals[o * 3 + 2] = wz;
153
+ out.colors[o * 3] = surfaces[s + SURFACE.color] as number;
154
+ out.colors[o * 3 + 1] = surfaces[s + SURFACE.color + 1] as number;
155
+ out.colors[o * 3 + 2] = surfaces[s + SURFACE.color + 2] as number;
156
+ out.emissive[o] = surfaces[s + SURFACE.emissive] as number;
157
+ if (out.specular) out.specular[o] = surfaces[s + SURFACE.specular] as number;
158
+ if (out.emissiveColor) {
159
+ out.emissiveColor[o * 3] = surfaces[s + SURFACE.emissiveColor] as number;
160
+ out.emissiveColor[o * 3 + 1] = surfaces[s + SURFACE.emissiveColor + 1] as number;
161
+ out.emissiveColor[o * 3 + 2] = surfaces[s + SURFACE.emissiveColor + 2] as number;
162
+ }
163
+ if (out.roughness) out.roughness[o] = surfaces[s + SURFACE.roughness] as number;
164
+ if (out.grain) out.grain[o] = surfaces[s + SURFACE.grain] as number;
165
+ if (out.relief) out.relief[o] = surfaces[s + SURFACE.relief] as number;
166
+ if (out.layers) out.layers[o] = surfaces[s + SURFACE.layer] as number;
167
+ /* Sway, sky and opacity are the piece's own: shape, not paint. */
168
+ if (out.channel && mesh.channel !== undefined) {
169
+ for (let lane = 0; lane < 4; lane++) {
170
+ out.channel[o * 4 + lane] = mesh.channel[i * 4 + lane] as number;
171
+ }
172
+ }
173
+ if (tan !== undefined && (out.tangents !== undefined || out.uvs !== undefined)) {
174
+ const qx = tan[i * 4] as number;
175
+ const qy = tan[i * 4 + 1] as number;
176
+ const qz = tan[i * 4 + 2] as number;
177
+ const qw = tan[i * 4 + 3] as number;
178
+ if (out.tangents) {
179
+ let rx = a * qx + d * qy + g * qz;
180
+ let ry = b * qx + e * qy + h * qz;
181
+ let rz = cc * qx + f * qy + k * qz;
182
+ const rl = Math.hypot(rx, ry, rz) || 1;
183
+ rx /= rl;
184
+ ry /= rl;
185
+ rz /= rl;
186
+ out.tangents[o * 4] = rx;
187
+ out.tangents[o * 4 + 1] = ry;
188
+ out.tangents[o * 4 + 2] = rz;
189
+ out.tangents[o * 4 + 3] = qw * hand;
190
+ }
191
+ if (out.uvs && tex !== undefined) {
192
+ /* The piece's own directions for u and v: its tangent, and the bitangent n × t · w. */
193
+ const ql = Math.hypot(qx, qy, qz) || 1;
194
+ const bx = (ny * qz - nz * qy) * qw;
195
+ const by = (nz * qx - nx * qz) * qw;
196
+ const bz = (nx * qy - ny * qx) * qw;
197
+ const bl = Math.hypot(bx, by, bz) || 1;
198
+ const su = Math.hypot(
199
+ ((uv[u] as number) * qx) / ql,
200
+ ((uv[u + 1] as number) * qy) / ql,
201
+ ((uv[u + 2] as number) * qz) / ql,
202
+ );
203
+ const sv = Math.hypot(
204
+ ((uv[u + 3] as number) * bx) / bl,
205
+ ((uv[u + 4] as number) * by) / bl,
206
+ ((uv[u + 5] as number) * bz) / bl,
207
+ );
208
+ out.uvs[o * 2] = (tex[i * 2] as number) * su + (uv[u + 6] as number);
209
+ out.uvs[o * 2 + 1] = (tex[i * 2 + 1] as number) * sv + (uv[u + 7] as number);
210
+ }
211
+ }
212
+ }
213
+ const idx = mesh.indices;
214
+ for (let j = 0; j + 2 < idx.length; j += 3) {
215
+ out.indices[i0 + j] = v0 + (idx[j] as number);
216
+ /* A mirrored copy winds back, or its faces turn inside out. */
217
+ out.indices[i0 + j + 1] = v0 + (idx[hand < 0 ? j + 2 : j + 1] as number);
218
+ out.indices[i0 + j + 2] = v0 + (idx[hand < 0 ? j + 1 : j + 2] as number);
219
+ }
220
+ v0 += count;
221
+ i0 += idx.length;
222
+ }
223
+ return out;
224
+ }
225
+
226
+ /** Min xyz then max xyz of every copy: its piece's box through its matrix, corner by corner. */
227
+ export function assemblyBounds(
228
+ assembly: DrftAssembly,
229
+ piece: PieceLookup,
230
+ out: Float32Array = new Float32Array(6),
231
+ ): Float32Array {
232
+ out.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
233
+ for (let c = 0; c < assembly.pieces.length; c++) {
234
+ const p = piece(assembly.pieces[c] as number).positions;
235
+ let lx = Infinity;
236
+ let ly = Infinity;
237
+ let lz = Infinity;
238
+ let hx = -Infinity;
239
+ let hy = -Infinity;
240
+ let hz = -Infinity;
241
+ for (let i = 0; i + 2 < p.length; i += 3) {
242
+ lx = Math.min(lx, p[i] as number);
243
+ ly = Math.min(ly, p[i + 1] as number);
244
+ lz = Math.min(lz, p[i + 2] as number);
245
+ hx = Math.max(hx, p[i] as number);
246
+ hy = Math.max(hy, p[i + 1] as number);
247
+ hz = Math.max(hz, p[i + 2] as number);
248
+ }
249
+ if (lx > hx) continue;
250
+ const m = c * COPY_MATRIX_FLOATS;
251
+ const t = assembly.transforms;
252
+ for (let corner = 0; corner < 8; corner++) {
253
+ const x = corner & 1 ? hx : lx;
254
+ const y = corner & 2 ? hy : ly;
255
+ const z = corner & 4 ? hz : lz;
256
+ for (let axis = 0; axis < 3; axis++) {
257
+ const w =
258
+ (t[m + axis] as number) * x +
259
+ (t[m + 3 + axis] as number) * y +
260
+ (t[m + 6 + axis] as number) * z +
261
+ (t[m + 9 + axis] as number);
262
+ if (w < (out[axis] as number)) out[axis] = w;
263
+ if (w > (out[axis + 3] as number)) out[axis + 3] = w;
264
+ }
265
+ }
266
+ }
267
+ return out;
268
+ }