@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/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_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, 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';
14
14
  import { coarseFirstOrder } from './coarseFirst.js';
15
15
  import { buildSubs } from './drftSubs.js';
16
16
  import { buildSdfv } from './sdfv.js';
@@ -20,6 +20,9 @@ import { buildEnts } from './ents.js';
20
20
  import { buildNavm } from './navm.js';
21
21
  import { buildNgrf } from './ngrf.js';
22
22
  import { buildColliders } from './drftColliders.js';
23
+ import { buildInstances } from './drftInstances.js';
24
+ import { buildLights } from './drftLights.js';
25
+ import { encodeQuantisedMesh } from './drftQuantised.js';
23
26
  /**
24
27
  * The most blocks a capture is split into, and the fewest splats worth having in one.
25
28
  *
@@ -101,7 +104,7 @@ function encodeString(value) {
101
104
  bytes.set(text, 4);
102
105
  return bytes;
103
106
  }
104
- function buildHead(head, meshes) {
107
+ function buildHead(head, meshes, instances = []) {
105
108
  /*
106
109
  * Bounds are computed rather than taken on trust. A caller that gets them wrong would
107
110
  * produce an asset that culls itself out of frame, which looks like a rendering bug and
@@ -113,23 +116,83 @@ function buildHead(head, meshes) {
113
116
  let maxX = -Infinity;
114
117
  let maxY = -Infinity;
115
118
  let maxZ = -Infinity;
116
- for (const mesh of meshes) {
119
+ const placed = new Map();
120
+ for (const group of instances)
121
+ placed.set(group.mesh, group.transforms);
122
+ for (let m = 0; m < meshes.length; m++) {
123
+ const mesh = meshes[m];
124
+ const transforms = placed.get(m);
125
+ if (transforms === undefined) {
126
+ for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
127
+ const x = mesh.positions[at];
128
+ const y = mesh.positions[at + 1];
129
+ const z = mesh.positions[at + 2];
130
+ if (x < minX)
131
+ minX = x;
132
+ if (y < minY)
133
+ minY = y;
134
+ if (z < minZ)
135
+ minZ = z;
136
+ if (x > maxX)
137
+ maxX = x;
138
+ if (y > maxY)
139
+ maxY = y;
140
+ if (z > maxZ)
141
+ maxZ = z;
142
+ }
143
+ continue;
144
+ }
145
+ /*
146
+ * **An instanced mesh's bounds are every copy's**, or a file of ten thousand candles reports one
147
+ * candle's box and a consumer fits and culls it as one candle. Each copy's box is the prototype's
148
+ * eight corners through its matrix: conservative for a copy that turns, exact for one that moves.
149
+ */
150
+ let lx = Infinity;
151
+ let ly = Infinity;
152
+ let lz = Infinity;
153
+ let hx = -Infinity;
154
+ let hy = -Infinity;
155
+ let hz = -Infinity;
117
156
  for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
118
- const x = mesh.positions[at];
119
- const y = mesh.positions[at + 1];
120
- const z = mesh.positions[at + 2];
121
- if (x < minX)
122
- minX = x;
123
- if (y < minY)
124
- minY = y;
125
- if (z < minZ)
126
- minZ = z;
127
- if (x > maxX)
128
- maxX = x;
129
- if (y > maxY)
130
- maxY = y;
131
- if (z > maxZ)
132
- maxZ = z;
157
+ lx = Math.min(lx, mesh.positions[at]);
158
+ ly = Math.min(ly, mesh.positions[at + 1]);
159
+ lz = Math.min(lz, mesh.positions[at + 2]);
160
+ hx = Math.max(hx, mesh.positions[at]);
161
+ hy = Math.max(hy, mesh.positions[at + 1]);
162
+ hz = Math.max(hz, mesh.positions[at + 2]);
163
+ }
164
+ if (!Number.isFinite(lx))
165
+ continue;
166
+ for (let k = 0; k + 15 < transforms.length; k += 16) {
167
+ for (let corner = 0; corner < 8; corner++) {
168
+ const cx = corner & 1 ? hx : lx;
169
+ const cy = corner & 2 ? hy : ly;
170
+ const cz = corner & 4 ? hz : lz;
171
+ const x = transforms[k] * cx +
172
+ transforms[k + 4] * cy +
173
+ transforms[k + 8] * cz +
174
+ transforms[k + 12];
175
+ const y = transforms[k + 1] * cx +
176
+ transforms[k + 5] * cy +
177
+ transforms[k + 9] * cz +
178
+ transforms[k + 13];
179
+ const z = transforms[k + 2] * cx +
180
+ transforms[k + 6] * cy +
181
+ transforms[k + 10] * cz +
182
+ transforms[k + 14];
183
+ if (x < minX)
184
+ minX = x;
185
+ if (y < minY)
186
+ minY = y;
187
+ if (z < minZ)
188
+ minZ = z;
189
+ if (x > maxX)
190
+ maxX = x;
191
+ if (y > maxY)
192
+ maxY = y;
193
+ if (z > maxZ)
194
+ maxZ = z;
195
+ }
133
196
  }
134
197
  }
135
198
  if (!Number.isFinite(minX)) {
@@ -302,6 +365,7 @@ function buildMaterials(materials) {
302
365
  view.setFloat32(at + 64, material.metallicScale, true);
303
366
  view.setFloat32(at + 68, material.occlusionStrength, true);
304
367
  view.setFloat32(at + 72, material.cutout, true);
368
+ view.setUint32(at + 76, (material.blend === true ? 1 : 0) | (material.doubleSided === true ? 2 : 0), true);
305
369
  at += MATERIAL_ENTRY_BYTES;
306
370
  }
307
371
  return { code: CHUNK_MATL, flags: 0, bytes };
@@ -352,7 +416,7 @@ export function writeDrft(source) {
352
416
  * same asset less its textures rather than a refusal. Moving it earlier in the file changes
353
417
  * nothing about that, because a chunk is found through the table rather than by position.
354
418
  */
355
- const chunks = [buildHead(source.head, source.meshes)];
419
+ const chunks = [buildHead(source.head, source.meshes, source.instances)];
356
420
  /*
357
421
  * The outline first, ahead of even the paint. It is the one thing that puts a recognisable
358
422
  * whole object on screen, it is a few hundred kilobytes against tens of megabytes, and the
@@ -386,6 +450,26 @@ export function writeDrft(source) {
386
450
  for (const node of source.nodes === undefined ? [] : [source.nodes]) {
387
451
  chunks.push({ code: CHUNK_NODE, flags: 0, bytes: buildNodes(node) });
388
452
  }
453
+ /*
454
+ * The placements ahead of the meshes they place, for the rig's reason: a streaming consumer must
455
+ * know a mesh is drawn ten thousand times before it lands, or it has already made it one part.
456
+ */
457
+ if ((source.instances?.length ?? 0) > 0) {
458
+ for (const group of source.instances ?? []) {
459
+ if (group.mesh >= source.meshes.length) {
460
+ throw new DrftError(`an instance group places mesh ${group.mesh}, and the asset has ${source.meshes.length}`);
461
+ }
462
+ }
463
+ chunks.push({
464
+ code: CHUNK_INST,
465
+ flags: CHUNK_REQUIRED,
466
+ bytes: buildInstances(source.instances),
467
+ });
468
+ }
469
+ /* Small, and ahead of the geometry, so a streaming consumer can place its lamps early. */
470
+ if ((source.lights?.length ?? 0) > 0) {
471
+ chunks.push({ code: CHUNK_LITE, flags: 0, bytes: buildLights(source.lights ?? []) });
472
+ }
389
473
  for (const skin of source.skins ?? []) {
390
474
  chunks.push({ code: CHUNK_SKIN, flags: 0, bytes: buildSkin(skin) });
391
475
  }
@@ -429,7 +513,13 @@ export function writeDrft(source) {
429
513
  }),
430
514
  });
431
515
  }
432
- chunks.push(buildMesh(mesh));
516
+ if (source.quantise === true) {
517
+ validateMeshData(mesh);
518
+ chunks.push({ code: CHUNK_MSHQ, flags: CHUNK_REQUIRED, bytes: encodeQuantisedMesh(mesh) });
519
+ }
520
+ else {
521
+ chunks.push(buildMesh(mesh));
522
+ }
433
523
  });
434
524
  for (const texture of source.textures ?? [])
435
525
  chunks.push(buildTexture(texture));
package/dist/index.d.ts CHANGED
@@ -13,13 +13,17 @@ export type { DrftMorph, DrftNode } from './drftSkin.ts';
13
13
  export type { DrftSubs, DrftSubstanceEntry } from './drftSubs.ts';
14
14
  export { buildSubs, readSubs } from './drftSubs.ts';
15
15
  export type { DrftSdfv, DrftSdfvEntry } from './sdfv.ts';
16
- export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES } from './sdfv.ts';
16
+ export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES, SDFV_WHOLE_FILE, } from './sdfv.ts';
17
17
  export { NNET_ENTRY_BYTES, NNET_MAX_HIDDEN, NNET_MAX_WIDTH, buildNnet, nnetWeightCount, readNnet, } from './nnet.ts';
18
18
  export type { DrftNetwork, DrftNnet } from './nnet.ts';
19
19
  export { buildNgrf, readNgrf } from './ngrf.ts';
20
20
  export type { DrftAttribute, DrftGraph, DrftGraphNode, DrftGraphTensor, DrftGraphValue, } from './ngrf.ts';
21
21
  export { buildClip, buildMorph, buildNodes, buildSkin, readClip, readMorph, readNodes, readSkin, } from './drftSkin.ts';
22
22
  export { MAX_COLLIDER_HULLS, MAX_COLLIDER_POINTS, buildColliders, readColliders, } from './drftColliders.ts';
23
+ export { buildInstances, readInstances } from './drftInstances.ts';
24
+ export { buildLights, readLights } from './drftLights.ts';
25
+ export type { DrftLight, DrftLightKind } from './drftLights.ts';
26
+ export type { DrftInstanceGroup } from './drftInstances.ts';
23
27
  export { buildDtex, readDtex, DTEX_ADDRESS_MODES, DTEX_MAX_TILES, DTEX_REGISTERS } from './dtex.ts';
24
28
  export type { DtexEntry, DtexMaterial } from './dtex.ts';
25
29
  export { buildNavm, readNavm } from './navm.ts';
@@ -28,8 +32,8 @@ export { buildEnts, readEnts } from './ents.ts';
28
32
  export type { EntsScene } from './ents.ts';
29
33
  export { readDrft } from './drftRead.ts';
30
34
  export type { DrftAsset, DrftTexture } from './drftRead.ts';
31
- export { DrftStream, streamDrft } from './drftStream.ts';
32
- export type { DrftManifest, DrftStreamHandlers } from './drftStream.ts';
35
+ export { DEFAULT_STREAM_SLICE_MS, DrftStream, streamDrft } from './drftStream.ts';
36
+ export type { DrftManifest, DrftStreamHandlers, DrftStreamPacing } from './drftStream.ts';
33
37
  export { writeDrft } from './drftWrite.ts';
34
38
  export type { DrftSource, DrftTextureSource } from './drftWrite.ts';
35
39
  export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.ts';
package/dist/index.js CHANGED
@@ -8,7 +8,7 @@
8
8
  */
9
9
  export { validateMeshData } from './meshData.js';
10
10
  export { buildSubs, readSubs } from './drftSubs.js';
11
- export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES } from './sdfv.js';
11
+ export { buildSdfv, readSdfv, SDFV_ENTRY_BYTES, SDFV_MAX_SAMPLES, SDFV_WHOLE_FILE, } from './sdfv.js';
12
12
  export { NNET_ENTRY_BYTES, NNET_MAX_HIDDEN, NNET_MAX_WIDTH, buildNnet, nnetWeightCount, readNnet, } from './nnet.js';
13
13
  export { buildNgrf, readNgrf } from './ngrf.js';
14
14
  export { buildClip, buildMorph, buildNodes, buildSkin, readClip, readMorph, readNodes, readSkin, } from './drftSkin.js';
@@ -20,6 +20,9 @@ export { buildClip, buildMorph, buildNodes, buildSkin, readClip, readMorph, read
20
20
  * of the format unable to disagree about a byte.
21
21
  */
22
22
  export { MAX_COLLIDER_HULLS, MAX_COLLIDER_POINTS, buildColliders, readColliders, } from './drftColliders.js';
23
+ /* Meshes drawn many times, one matrix a copy, as the baker finds merged copies again. */
24
+ export { buildInstances, readInstances } from './drftInstances.js';
25
+ export { buildLights, readLights } from './drftLights.js';
23
26
  /*
24
27
  * **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
25
28
  * carries no semantics — this package is the container and does not know what a channel means —
@@ -40,7 +43,7 @@ export { buildNavm, readNavm } from './navm.js';
40
43
  export { buildEnts, readEnts } from './ents.js';
41
44
  export { readDrft } from './drftRead.js';
42
45
  /* Reading one while it is still arriving, for a scene that would rather show a model build. */
43
- export { DrftStream, streamDrft } from './drftStream.js';
46
+ export { DEFAULT_STREAM_SLICE_MS, DrftStream, streamDrft } from './drftStream.js';
44
47
  export { writeDrft } from './drftWrite.js';
45
48
  export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.js';
46
49
  /* A consumer decoding an embedded texture has to know which codec it is holding. */
package/dist/meshData.js CHANGED
@@ -94,9 +94,16 @@ export function validateMeshData(data) {
94
94
  * An index past the end is the same hazard from the other side, and the same latitude
95
95
  * applies: a driver may clamp it or refuse the draw. Scanned once at construction, which
96
96
  * is a cost paid when a mesh is built rather than when it is drawn.
97
+ *
98
+ * **The array is read once, outside the loop.** A mesh arrives in as many object shapes as there
99
+ * are combinations of optional fields, so `data.indices` is a megamorphic load, and one inside the
100
+ * loop is a lookup per index the compiler cannot hoist: 0.8 s of a five-pack load, five times what
101
+ * the same scan costs over a local.
97
102
  */
98
- for (let at = 0; at < data.indices.length; at++) {
99
- const index = data.indices[at];
103
+ const indices = data.indices;
104
+ const count = indices.length;
105
+ for (let at = 0; at < count; at++) {
106
+ const index = indices[at];
100
107
  if (index >= vertices) {
101
108
  throw new Error(`Mesh: index ${at} refers to vertex ${index}, but the mesh has ${vertices}. ` +
102
109
  `Out-of-range indices may be clamped on one driver and reject the draw on another.`);
package/dist/sdfv.d.ts CHANGED
@@ -42,6 +42,21 @@
42
42
  */
43
43
  /** `u32` mesh, three `u32` dimensions and six `f32` bounds. A field's samples are elsewhere. */
44
44
  export declare const SDFV_ENTRY_BYTES = 40;
45
+ /**
46
+ * The mesh ordinal that means **the file's static geometry as a whole**, in the file's own space.
47
+ *
48
+ * **One field for a scene rather than one per mesh, because a scene is where per-mesh fields
49
+ * fail.** A bought courtyard exports its walls as a few meshes spanning the whole of it, so a field
50
+ * of thirty-two samples along each mesh's longest side is 1.2 m voxels on the walls that matter and
51
+ * 12.6 MB across four hundred meshes; one field over all of them at 25 cm is 3 MB and resolves a
52
+ * column. A reader places it with the file's own transform and nothing per mesh.
53
+ *
54
+ * **Additive, and it had to be checked rather than assumed**: no reader has ever compared an
55
+ * entry's ordinal with the mesh count, so a reader that predates this value reads the entry and
56
+ * pairs it with no mesh, which loses only the traced light it could not have used. What would make
57
+ * it wrong is a file whose static geometry moves, which is a file with more than one field.
58
+ */
59
+ export declare const SDFV_WHOLE_FILE = 4294967295;
45
60
  /**
46
61
  * The most samples one field may declare.
47
62
  *
package/dist/sdfv.js CHANGED
@@ -43,6 +43,21 @@
43
43
  import { DrftError, align } from './drftFormat.js';
44
44
  /** `u32` mesh, three `u32` dimensions and six `f32` bounds. A field's samples are elsewhere. */
45
45
  export const SDFV_ENTRY_BYTES = 40;
46
+ /**
47
+ * The mesh ordinal that means **the file's static geometry as a whole**, in the file's own space.
48
+ *
49
+ * **One field for a scene rather than one per mesh, because a scene is where per-mesh fields
50
+ * fail.** A bought courtyard exports its walls as a few meshes spanning the whole of it, so a field
51
+ * of thirty-two samples along each mesh's longest side is 1.2 m voxels on the walls that matter and
52
+ * 12.6 MB across four hundred meshes; one field over all of them at 25 cm is 3 MB and resolves a
53
+ * column. A reader places it with the file's own transform and nothing per mesh.
54
+ *
55
+ * **Additive, and it had to be checked rather than assumed**: no reader has ever compared an
56
+ * entry's ordinal with the mesh count, so a reader that predates this value reads the entry and
57
+ * pairs it with no mesh, which loses only the traced light it could not have used. What would make
58
+ * it wrong is a file whose static geometry moves, which is a file with more than one field.
59
+ */
60
+ export const SDFV_WHOLE_FILE = 0xffffffff;
46
61
  /**
47
62
  * The most samples one field may declare.
48
63
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@driftengine/drft",
3
- "version": "4.3.0",
3
+ "version": "4.4.1",
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.3.0",
31
- "@driftengine/entities": "4.3.0",
32
- "@driftengine/texture": "4.3.0"
30
+ "@driftengine/core": "4.4.1",
31
+ "@driftengine/entities": "4.4.1",
32
+ "@driftengine/texture": "4.4.1"
33
33
  },
34
34
  "author": "Drift Technologies",
35
35
  "repository": {
package/src/drftFormat.ts CHANGED
@@ -23,7 +23,7 @@ export const DRFT_VERSION_MAJOR = 1;
23
23
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
24
24
  * enum values with a defined fallback. Never a changed meaning.
25
25
  */
26
- export const DRFT_VERSION_MINOR = 17;
26
+ export const DRFT_VERSION_MINOR = 18;
27
27
 
28
28
  /** Bytes before the chunk table. */
29
29
  export const HEADER_BYTES = 32;
@@ -259,6 +259,19 @@ export const CHUNK_NAVM = fourCC('NAVM');
259
259
  */
260
260
  export const CHUNK_ENTS = fourCC('ENTS');
261
261
 
262
+ /**
263
+ * Meshes drawn many times, one matrix a copy. **Required**: see `drftInstances.ts` for why a reader
264
+ * that skipped it would be worse than one that refused. Added in 1.18.
265
+ */
266
+ export const CHUNK_INST = fourCC('INST');
267
+
268
+ /**
269
+ * A mesh in the bytes its data needs: constants, 16-bit ranges and octahedral directions, decoded on
270
+ * read into the `MeshData` a `MESH` gives. **Required**, and counted as a mesh in every ordinal.
271
+ * See `drftQuantised.ts`. Added in 1.18.
272
+ */
273
+ export const CHUNK_MSHQ = fourCC('MSHQ');
274
+
262
275
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
263
276
  export const SPLAT_BLOCK_PREFIX = 40;
264
277
 
@@ -291,6 +304,8 @@ export const KNOWN_CHUNKS: ReadonlySet<number> = new Set([
291
304
  CHUNK_NGRF,
292
305
  CHUNK_NAVM,
293
306
  CHUNK_ENTS,
307
+ CHUNK_INST,
308
+ CHUNK_MSHQ,
294
309
  ]);
295
310
 
296
311
  /** Chunk flags. */
@@ -550,6 +565,23 @@ export interface DrftMaterial {
550
565
  * tested.
551
566
  */
552
567
  readonly cutout: number;
568
+ /**
569
+ * Whether the surface blends: its texture's alpha is coverage, times `opacity`. 1.18.
570
+ *
571
+ * **A flag, because `opacity` could not say it.** glTF's `BLEND` makes alpha the factor times the
572
+ * texture, and a factor of 1 left `opacity` at 1: every loader drew the material opaque and a
573
+ * leaf's shape, which is all in its texture's alpha, was never read. Absent means false, which is
574
+ * what every file before 1.18 meant.
575
+ */
576
+ readonly blend?: boolean;
577
+ /**
578
+ * Whether both faces of the surface are seen: bit 1 of the flags word, 1.18.
579
+ *
580
+ * glTF's `doubleSided`, and what a curtain, a flag and a leaf card are. A one-sided surface is
581
+ * culled from behind, so a curtain seen from the other side of its arch is a hole, and half a
582
+ * tree's cards are not drawn. Absent means false, which is how every surface before it was drawn.
583
+ */
584
+ readonly doubleSided?: boolean;
553
585
  }
554
586
 
555
587
  /** Floats in a `MATL` entry, then the signed texture indices that follow them. */
@@ -564,7 +596,9 @@ export const MATERIAL_FLOATS = 15;
564
596
  * them is speculative — only unbound, and the plans that bind them need no second stride bump.
565
597
  */
566
598
  export const MATERIAL_INDICES = 4;
567
- export const MATERIAL_ENTRY_BYTES = MATERIAL_FLOATS * 4 + MATERIAL_INDICES * 4;
599
+ /** A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. */
600
+ export const MATERIAL_FLAGS = 1;
601
+ export const MATERIAL_ENTRY_BYTES = MATERIAL_FLOATS * 4 + MATERIAL_INDICES * 4 + MATERIAL_FLAGS * 4;
568
602
 
569
603
  /** What `HEAD` says about the asset as a whole. */
570
604
  export interface DrftHead {
@@ -0,0 +1,81 @@
1
+ /**
2
+ * `INST`: meshes drawn many times, each copy placed by a column-major 4×4 matrix.
3
+ *
4
+ * **Required, because a reader that skipped it would draw one copy where there are thousands.** The
5
+ * mesh an instance group names holds one prototype, so an old reader that did not know the chunk
6
+ * would open a scene of ten thousand candles as a single candle at the origin and say nothing. A
7
+ * refusal naming `INST` is the honest answer. What that gives up is opening such a file at all in a
8
+ * reader before 1.18.
9
+ *
10
+ * Layout, little-endian:
11
+ * - u32 group count;
12
+ * - per group: u32 mesh ordinal, u32 instance count, then that many 16-float matrices.
13
+ *
14
+ * A mesh appears in at most one group.
15
+ */
16
+ import { DrftError, align } from './drftFormat.ts';
17
+
18
+ export interface DrftInstanceGroup {
19
+ /** The ordinal of the `MESH` this group draws, which holds one copy. */
20
+ readonly mesh: number;
21
+ /** Sixteen floats a copy, column-major, taking the mesh to where that copy stands. */
22
+ readonly transforms: Float32Array;
23
+ }
24
+
25
+ export function buildInstances(groups: readonly DrftInstanceGroup[]): Uint8Array {
26
+ const seen = new Set<number>();
27
+ let floats = 0;
28
+ for (const group of groups) {
29
+ if (group.transforms.length === 0 || group.transforms.length % 16 !== 0) {
30
+ throw new DrftError(
31
+ `instance group for mesh ${group.mesh} has ${group.transforms.length} floats, which is not whole matrices`,
32
+ );
33
+ }
34
+ if (seen.has(group.mesh)) throw new DrftError(`mesh ${group.mesh} is instanced twice`);
35
+ seen.add(group.mesh);
36
+ floats += group.transforms.length;
37
+ }
38
+ const bytes = new Uint8Array(align(4 + groups.length * 8 + floats * 4));
39
+ const view = new DataView(bytes.buffer);
40
+ view.setUint32(0, groups.length, true);
41
+ let at = 4;
42
+ for (const group of groups) {
43
+ view.setUint32(at, group.mesh, true);
44
+ view.setUint32(at + 4, group.transforms.length / 16, true);
45
+ at += 8;
46
+ for (let i = 0; i < group.transforms.length; i++) {
47
+ view.setFloat32(at, group.transforms[i] as number, true);
48
+ at += 4;
49
+ }
50
+ }
51
+ return bytes;
52
+ }
53
+
54
+ /** The groups in an `INST` payload, each checked against the file's mesh count. */
55
+ export function readInstances(
56
+ buffer: ArrayBufferLike,
57
+ offset: number,
58
+ byteLength: number,
59
+ meshCount: number,
60
+ ): DrftInstanceGroup[] {
61
+ const view = new DataView(buffer, offset, byteLength);
62
+ const count = view.getUint32(0, true);
63
+ const out: DrftInstanceGroup[] = [];
64
+ let at = 4;
65
+ for (let g = 0; g < count; g++) {
66
+ if (at + 8 > byteLength) throw new DrftError('INST ends inside a group header');
67
+ const mesh = view.getUint32(at, true);
68
+ const copies = view.getUint32(at + 4, true);
69
+ at += 8;
70
+ if (mesh >= meshCount) {
71
+ throw new DrftError(`INST places mesh ${mesh}, and the file carries ${meshCount} meshes`);
72
+ }
73
+ if (at + copies * 64 > byteLength)
74
+ throw new DrftError(`INST ends inside mesh ${mesh}'s placements`);
75
+ const transforms = new Float32Array(copies * 16);
76
+ for (let i = 0; i < transforms.length; i++) transforms[i] = view.getFloat32(at + i * 4, true);
77
+ at += copies * 64;
78
+ out.push({ mesh, transforms });
79
+ }
80
+ return out;
81
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * `LITE`: the lights a scene was authored with, in glTF's own terms.
3
+ *
4
+ * **The source's numbers, unconverted.** Intensity is candela for a point or a spot and lux for a
5
+ * directional light, as `KHR_lights_punctual` states it, because what a candela is in a consumer's
6
+ * units is that consumer's exposure decision and no two agree. The chunk says where the lamps are and
7
+ * what their author said about them. Deciding how bright they draw is the consumer's job.
8
+ *
9
+ * **Optional.** A reader that skips it loses the lights and nothing else; they were only ever the
10
+ * consumer's to place. What that gives up is a file whose lights are essential saying so, which
11
+ * none is: a scene without its lamps is darker, not wrong.
12
+ *
13
+ * Layout, little-endian:
14
+ * - u32 light count, u32 name count;
15
+ * - the names, each a u32 byte length and UTF-8, padded to four bytes. **Each distinct name is
16
+ * written once**: a pack of ten thousand candles names every one "Instance";
17
+ * - per light, 64 bytes: u32 kind (0 point, 1 spot, 2 directional), u32 name index, position,
18
+ * direction, colour (three f32 each), intensity, range (0 is unbounded), inner and outer cone
19
+ * in radians, and one reserved f32.
20
+ */
21
+ import { DrftError, align } from './drftFormat.ts';
22
+
23
+ export type DrftLightKind = 'point' | 'spot' | 'directional';
24
+
25
+ export interface DrftLight {
26
+ readonly kind: DrftLightKind;
27
+ /** The node's name in the source, or empty. */
28
+ readonly name: string;
29
+ /** World space, metres. */
30
+ readonly position: readonly [number, number, number];
31
+ /** Where a spot or a directional light points, world space and unit length. glTF's local −z. */
32
+ readonly direction: readonly [number, number, number];
33
+ /** Linear, 0 to 1 a channel. */
34
+ readonly color: readonly [number, number, number];
35
+ /** Candela for a point or a spot, lux for a directional light. */
36
+ readonly intensity: number;
37
+ /** Metres at which the source's author cut the light off, or 0 for no cut. */
38
+ readonly range: number;
39
+ readonly innerConeRad: number;
40
+ readonly outerConeRad: number;
41
+ }
42
+
43
+ const KINDS: readonly DrftLightKind[] = ['point', 'spot', 'directional'];
44
+ const LIGHT_BYTES = 64;
45
+ const encoder = new TextEncoder();
46
+ const decoder = new TextDecoder();
47
+
48
+ export function buildLights(lights: readonly DrftLight[]): Uint8Array {
49
+ const names: string[] = [];
50
+ const nameIndex = new Map<string, number>();
51
+ for (const light of lights) {
52
+ if (!nameIndex.has(light.name)) {
53
+ nameIndex.set(light.name, names.length);
54
+ names.push(light.name);
55
+ }
56
+ }
57
+ const encoded = names.map((name) => encoder.encode(name));
58
+ let nameBytes = 0;
59
+ for (const text of encoded) nameBytes += align(4 + text.length);
60
+ const bytes = new Uint8Array(8 + nameBytes + lights.length * LIGHT_BYTES);
61
+ const view = new DataView(bytes.buffer);
62
+ view.setUint32(0, lights.length, true);
63
+ view.setUint32(4, names.length, true);
64
+ let at = 8;
65
+ for (const text of encoded) {
66
+ view.setUint32(at, text.length, true);
67
+ bytes.set(text, at + 4);
68
+ at += align(4 + text.length);
69
+ }
70
+ for (const light of lights) {
71
+ view.setUint32(at, KINDS.indexOf(light.kind), true);
72
+ view.setUint32(at + 4, nameIndex.get(light.name) ?? 0, true);
73
+ const floats = [
74
+ ...light.position,
75
+ ...light.direction,
76
+ ...light.color,
77
+ light.intensity,
78
+ light.range,
79
+ light.innerConeRad,
80
+ light.outerConeRad,
81
+ ];
82
+ for (let i = 0; i < floats.length; i++)
83
+ view.setFloat32(at + 8 + i * 4, floats[i] as number, true);
84
+ at += LIGHT_BYTES;
85
+ }
86
+ return bytes;
87
+ }
88
+
89
+ /** The lights in a `LITE` payload. */
90
+ export function readLights(
91
+ buffer: ArrayBufferLike,
92
+ offset: number,
93
+ byteLength: number,
94
+ ): DrftLight[] {
95
+ const view = new DataView(buffer, offset, byteLength);
96
+ if (byteLength < 8) throw new DrftError('LITE is shorter than its own header');
97
+ const count = view.getUint32(0, true);
98
+ const nameCount = view.getUint32(4, true);
99
+ const names: string[] = [];
100
+ let at = 8;
101
+ for (let n = 0; n < nameCount; n++) {
102
+ if (at + 4 > byteLength) throw new DrftError('LITE ends inside its names');
103
+ const length = view.getUint32(at, true);
104
+ if (at + 4 + length > byteLength) throw new DrftError('a LITE name runs past the chunk');
105
+ names.push(decoder.decode(new Uint8Array(buffer, offset + at + 4, length)));
106
+ at += align(4 + length);
107
+ }
108
+ if (at + count * LIGHT_BYTES > byteLength) {
109
+ throw new DrftError(`LITE says ${count} lights and ends inside them`);
110
+ }
111
+ const f = (o: number): number => view.getFloat32(o, true);
112
+ const out: DrftLight[] = [];
113
+ for (let i = 0; i < count; i++) {
114
+ const kind = KINDS[view.getUint32(at, true)];
115
+ if (kind === undefined)
116
+ throw new DrftError(`LITE light ${i} is of a kind this reader does not know`);
117
+ out.push({
118
+ kind,
119
+ name: names[view.getUint32(at + 4, true)] ?? '',
120
+ position: [f(at + 8), f(at + 12), f(at + 16)],
121
+ direction: [f(at + 20), f(at + 24), f(at + 28)],
122
+ color: [f(at + 32), f(at + 36), f(at + 40)],
123
+ intensity: f(at + 44),
124
+ range: f(at + 48),
125
+ innerConeRad: f(at + 52),
126
+ outerConeRad: f(at + 56),
127
+ });
128
+ at += LIGHT_BYTES;
129
+ }
130
+ return out;
131
+ }