@driftengine/drft 4.3.0 → 4.4.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.
package/src/drftWrite.ts CHANGED
@@ -35,6 +35,9 @@ import {
35
35
  CHUNK_LODF,
36
36
  CHUNK_MORP,
37
37
  CHUNK_NODE,
38
+ CHUNK_INST,
39
+ CHUNK_LITE,
40
+ CHUNK_MSHQ,
38
41
  CHUNK_SKIN,
39
42
  CHUNK_SPLT,
40
43
  SPLAT_BLOCK_PREFIX,
@@ -66,6 +69,11 @@ import { type EntsScene, buildEnts } from './ents.ts';
66
69
  import { type NavPolyMesh, buildNavm } from './navm.ts';
67
70
  import { type DrftGraph, buildNgrf } from './ngrf.ts';
68
71
  import { buildColliders } from './drftColliders.ts';
72
+ import { buildInstances } from './drftInstances.ts';
73
+ import type { DrftInstanceGroup } from './drftInstances.ts';
74
+ import { buildLights } from './drftLights.ts';
75
+ import type { DrftLight } from './drftLights.ts';
76
+ import { encodeQuantisedMesh } from './drftQuantised.ts';
69
77
 
70
78
  /** An image to embed, already compressed, with what its own header said about it. */
71
79
  export interface DrftTextureSource {
@@ -155,6 +163,19 @@ export interface DrftSource {
155
163
  * the meshes exactly as it always did, which is why this is additive.
156
164
  */
157
165
  readonly nodes?: readonly DrftNode[];
166
+ /**
167
+ * Meshes drawn many times: one `INST` chunk, written ahead of the geometry and required. The mesh
168
+ * a group names holds one copy; see `drftInstances.ts`.
169
+ */
170
+ readonly instances?: readonly DrftInstanceGroup[];
171
+ /** The lights the scene was authored with: one `LITE` chunk, optional. See `drftLights.ts`. */
172
+ readonly lights?: readonly DrftLight[];
173
+ /**
174
+ * Write each mesh as `MSHQ` rather than `MESH`: constants, 16-bit ranges and octahedral
175
+ * directions, about a third of the bytes, within the bounds `drftQuantised.ts` states. Off unless
176
+ * asked, so a caller writing a file gets the bytes it always got; the baker asks by default.
177
+ */
178
+ readonly quantise?: boolean;
158
179
  /** Skins, one `SKIN` chunk each, or absent for an asset that deforms nothing. */
159
180
  readonly skins?: readonly DrftSkin[];
160
181
  /** Clips, one `ANIM` chunk each, or absent for an asset that animates nothing. */
@@ -292,7 +313,11 @@ function encodeString(value: string): Uint8Array {
292
313
  return bytes;
293
314
  }
294
315
 
295
- function buildHead(head: Partial<DrftHead> | undefined, meshes: readonly MeshData[]): PendingChunk {
316
+ function buildHead(
317
+ head: Partial<DrftHead> | undefined,
318
+ meshes: readonly MeshData[],
319
+ instances: readonly DrftInstanceGroup[] = [],
320
+ ): PendingChunk {
296
321
  /*
297
322
  * Bounds are computed rather than taken on trust. A caller that gets them wrong would
298
323
  * produce an asset that culls itself out of frame, which looks like a rendering bug and
@@ -304,17 +329,72 @@ function buildHead(head: Partial<DrftHead> | undefined, meshes: readonly MeshDat
304
329
  let maxX = -Infinity;
305
330
  let maxY = -Infinity;
306
331
  let maxZ = -Infinity;
307
- for (const mesh of meshes) {
332
+ const placed = new Map<number, Float32Array>();
333
+ for (const group of instances) placed.set(group.mesh, group.transforms);
334
+ for (let m = 0; m < meshes.length; m++) {
335
+ const mesh = meshes[m] as MeshData;
336
+ const transforms = placed.get(m);
337
+ if (transforms === undefined) {
338
+ for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
339
+ const x = mesh.positions[at] as number;
340
+ const y = mesh.positions[at + 1] as number;
341
+ const z = mesh.positions[at + 2] as number;
342
+ if (x < minX) minX = x;
343
+ if (y < minY) minY = y;
344
+ if (z < minZ) minZ = z;
345
+ if (x > maxX) maxX = x;
346
+ if (y > maxY) maxY = y;
347
+ if (z > maxZ) maxZ = z;
348
+ }
349
+ continue;
350
+ }
351
+ /*
352
+ * **An instanced mesh's bounds are every copy's**, or a file of ten thousand candles reports one
353
+ * candle's box and a consumer fits and culls it as one candle. Each copy's box is the prototype's
354
+ * eight corners through its matrix: conservative for a copy that turns, exact for one that moves.
355
+ */
356
+ let lx = Infinity;
357
+ let ly = Infinity;
358
+ let lz = Infinity;
359
+ let hx = -Infinity;
360
+ let hy = -Infinity;
361
+ let hz = -Infinity;
308
362
  for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
309
- const x = mesh.positions[at] as number;
310
- const y = mesh.positions[at + 1] as number;
311
- const z = mesh.positions[at + 2] as number;
312
- if (x < minX) minX = x;
313
- if (y < minY) minY = y;
314
- if (z < minZ) minZ = z;
315
- if (x > maxX) maxX = x;
316
- if (y > maxY) maxY = y;
317
- if (z > maxZ) maxZ = z;
363
+ lx = Math.min(lx, mesh.positions[at] as number);
364
+ ly = Math.min(ly, mesh.positions[at + 1] as number);
365
+ lz = Math.min(lz, mesh.positions[at + 2] as number);
366
+ hx = Math.max(hx, mesh.positions[at] as number);
367
+ hy = Math.max(hy, mesh.positions[at + 1] as number);
368
+ hz = Math.max(hz, mesh.positions[at + 2] as number);
369
+ }
370
+ if (!Number.isFinite(lx)) continue;
371
+ for (let k = 0; k + 15 < transforms.length; k += 16) {
372
+ for (let corner = 0; corner < 8; corner++) {
373
+ const cx = corner & 1 ? hx : lx;
374
+ const cy = corner & 2 ? hy : ly;
375
+ const cz = corner & 4 ? hz : lz;
376
+ const x =
377
+ (transforms[k] as number) * cx +
378
+ (transforms[k + 4] as number) * cy +
379
+ (transforms[k + 8] as number) * cz +
380
+ (transforms[k + 12] as number);
381
+ const y =
382
+ (transforms[k + 1] as number) * cx +
383
+ (transforms[k + 5] as number) * cy +
384
+ (transforms[k + 9] as number) * cz +
385
+ (transforms[k + 13] as number);
386
+ const z =
387
+ (transforms[k + 2] as number) * cx +
388
+ (transforms[k + 6] as number) * cy +
389
+ (transforms[k + 10] as number) * cz +
390
+ (transforms[k + 14] as number);
391
+ if (x < minX) minX = x;
392
+ if (y < minY) minY = y;
393
+ if (z < minZ) minZ = z;
394
+ if (x > maxX) maxX = x;
395
+ if (y > maxY) maxY = y;
396
+ if (z > maxZ) maxZ = z;
397
+ }
318
398
  }
319
399
  }
320
400
  if (!Number.isFinite(minX)) {
@@ -478,6 +558,11 @@ function buildMaterials(materials: readonly DrftMaterial[]): PendingChunk {
478
558
  view.setFloat32(at + 64, material.metallicScale, true);
479
559
  view.setFloat32(at + 68, material.occlusionStrength, true);
480
560
  view.setFloat32(at + 72, material.cutout, true);
561
+ view.setUint32(
562
+ at + 76,
563
+ (material.blend === true ? 1 : 0) | (material.doubleSided === true ? 2 : 0),
564
+ true,
565
+ );
481
566
  at += MATERIAL_ENTRY_BYTES;
482
567
  }
483
568
  return { code: CHUNK_MATL, flags: 0, bytes };
@@ -537,7 +622,7 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
537
622
  * same asset less its textures rather than a refusal. Moving it earlier in the file changes
538
623
  * nothing about that, because a chunk is found through the table rather than by position.
539
624
  */
540
- const chunks: PendingChunk[] = [buildHead(source.head, source.meshes)];
625
+ const chunks: PendingChunk[] = [buildHead(source.head, source.meshes, source.instances)];
541
626
  /*
542
627
  * The outline first, ahead of even the paint. It is the one thing that puts a recognisable
543
628
  * whole object on screen, it is a few hundred kilobytes against tens of megabytes, and the
@@ -568,6 +653,28 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
568
653
  for (const node of source.nodes === undefined ? [] : [source.nodes]) {
569
654
  chunks.push({ code: CHUNK_NODE, flags: 0, bytes: buildNodes(node) });
570
655
  }
656
+ /*
657
+ * The placements ahead of the meshes they place, for the rig's reason: a streaming consumer must
658
+ * know a mesh is drawn ten thousand times before it lands, or it has already made it one part.
659
+ */
660
+ if ((source.instances?.length ?? 0) > 0) {
661
+ for (const group of source.instances ?? []) {
662
+ if (group.mesh >= source.meshes.length) {
663
+ throw new DrftError(
664
+ `an instance group places mesh ${group.mesh}, and the asset has ${source.meshes.length}`,
665
+ );
666
+ }
667
+ }
668
+ chunks.push({
669
+ code: CHUNK_INST,
670
+ flags: CHUNK_REQUIRED,
671
+ bytes: buildInstances(source.instances as readonly DrftInstanceGroup[]),
672
+ });
673
+ }
674
+ /* Small, and ahead of the geometry, so a streaming consumer can place its lamps early. */
675
+ if ((source.lights?.length ?? 0) > 0) {
676
+ chunks.push({ code: CHUNK_LITE, flags: 0, bytes: buildLights(source.lights ?? []) });
677
+ }
571
678
  for (const skin of source.skins ?? []) {
572
679
  chunks.push({ code: CHUNK_SKIN, flags: 0, bytes: buildSkin(skin) });
573
680
  }
@@ -613,7 +720,12 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
613
720
  }),
614
721
  });
615
722
  }
616
- chunks.push(buildMesh(mesh));
723
+ if (source.quantise === true) {
724
+ validateMeshData(mesh);
725
+ chunks.push({ code: CHUNK_MSHQ, flags: CHUNK_REQUIRED, bytes: encodeQuantisedMesh(mesh) });
726
+ } else {
727
+ chunks.push(buildMesh(mesh));
728
+ }
617
729
  });
618
730
  for (const texture of source.textures ?? []) chunks.push(buildTexture(texture));
619
731
  /* One `SUBS` for the file, and only where something was labelled — an empty chunk would cost
package/src/index.ts CHANGED
@@ -17,7 +17,13 @@ export type { DrftMorph, DrftNode } from './drftSkin.ts';
17
17
  export type { DrftSubs, DrftSubstanceEntry } from './drftSubs.ts';
18
18
  export { buildSubs, readSubs } from './drftSubs.ts';
19
19
  export type { DrftSdfv, DrftSdfvEntry } from './sdfv.ts';
20
- export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES } from './sdfv.ts';
20
+ export {
21
+ buildSdfv,
22
+ readSdfv,
23
+ SDFV_ENTRY_BYTES,
24
+ SDFV_MAX_SAMPLES,
25
+ SDFV_WHOLE_FILE,
26
+ } from './sdfv.ts';
21
27
  export {
22
28
  NNET_ENTRY_BYTES,
23
29
  NNET_MAX_HIDDEN,
@@ -59,6 +65,11 @@ export {
59
65
  buildColliders,
60
66
  readColliders,
61
67
  } from './drftColliders.ts';
68
+ /* Meshes drawn many times, one matrix a copy, as the baker finds merged copies again. */
69
+ export { buildInstances, readInstances } from './drftInstances.ts';
70
+ export { buildLights, readLights } from './drftLights.ts';
71
+ export type { DrftLight, DrftLightKind } from './drftLights.ts';
72
+ export type { DrftInstanceGroup } from './drftInstances.ts';
62
73
  /*
63
74
  * **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
64
75
  * carries no semantics — this package is the container and does not know what a channel means —
@@ -83,8 +94,8 @@ export type { EntsScene } from './ents.ts';
83
94
  export { readDrft } from './drftRead.ts';
84
95
  export type { DrftAsset, DrftTexture } from './drftRead.ts';
85
96
  /* Reading one while it is still arriving, for a scene that would rather show a model build. */
86
- export { DrftStream, streamDrft } from './drftStream.ts';
87
- export type { DrftManifest, DrftStreamHandlers } from './drftStream.ts';
97
+ export { DEFAULT_STREAM_SLICE_MS, DrftStream, streamDrft } from './drftStream.ts';
98
+ export type { DrftManifest, DrftStreamHandlers, DrftStreamPacing } from './drftStream.ts';
88
99
  export { writeDrft } from './drftWrite.ts';
89
100
  export type { DrftSource, DrftTextureSource } from './drftWrite.ts';
90
101
  export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.ts';
package/src/meshData.ts CHANGED
@@ -288,9 +288,16 @@ export function validateMeshData(data: MeshData): void {
288
288
  * An index past the end is the same hazard from the other side, and the same latitude
289
289
  * applies: a driver may clamp it or refuse the draw. Scanned once at construction, which
290
290
  * is a cost paid when a mesh is built rather than when it is drawn.
291
+ *
292
+ * **The array is read once, outside the loop.** A mesh arrives in as many object shapes as there
293
+ * are combinations of optional fields, so `data.indices` is a megamorphic load, and one inside the
294
+ * loop is a lookup per index the compiler cannot hoist: 0.8 s of a five-pack load, five times what
295
+ * the same scan costs over a local.
291
296
  */
292
- for (let at = 0; at < data.indices.length; at++) {
293
- const index = data.indices[at] as number;
297
+ const indices = data.indices;
298
+ const count = indices.length;
299
+ for (let at = 0; at < count; at++) {
300
+ const index = indices[at] as number;
294
301
  if (index >= vertices) {
295
302
  throw new Error(
296
303
  `Mesh: index ${at} refers to vertex ${index}, but the mesh has ${vertices}. ` +
package/src/sdfv.ts CHANGED
@@ -46,6 +46,22 @@ import { DrftError, align } from './drftFormat.ts';
46
46
  /** `u32` mesh, three `u32` dimensions and six `f32` bounds. A field's samples are elsewhere. */
47
47
  export const SDFV_ENTRY_BYTES = 40;
48
48
 
49
+ /**
50
+ * The mesh ordinal that means **the file's static geometry as a whole**, in the file's own space.
51
+ *
52
+ * **One field for a scene rather than one per mesh, because a scene is where per-mesh fields
53
+ * fail.** A bought courtyard exports its walls as a few meshes spanning the whole of it, so a field
54
+ * of thirty-two samples along each mesh's longest side is 1.2 m voxels on the walls that matter and
55
+ * 12.6 MB across four hundred meshes; one field over all of them at 25 cm is 3 MB and resolves a
56
+ * column. A reader places it with the file's own transform and nothing per mesh.
57
+ *
58
+ * **Additive, and it had to be checked rather than assumed**: no reader has ever compared an
59
+ * entry's ordinal with the mesh count, so a reader that predates this value reads the entry and
60
+ * pairs it with no mesh, which loses only the traced light it could not have used. What would make
61
+ * it wrong is a file whose static geometry moves, which is a file with more than one field.
62
+ */
63
+ export const SDFV_WHOLE_FILE = 0xffffffff;
64
+
49
65
  /**
50
66
  * The most samples one field may declare.
51
67
  *