@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.
@@ -0,0 +1,322 @@
1
+ /**
2
+ * `KITS` and `MSHC`: a mesh carried as painted copies of a few pieces, rather than as its vertices.
3
+ *
4
+ * **What it is for is a world built from a kit.** A city of boxes, columns and cornices is a few
5
+ * thousand distinct shapes placed half a million times, each at its own size and in its own paint.
6
+ * Merged, its vertices are most of a gigabyte even quantised; as placements of the kit they are a
7
+ * few tens of megabytes. So a mesh may arrive as `MSHC` — a table of surfaces and, per copy, which
8
+ * piece, which surface, a 3×4 matrix and how its texture stretches — and is expanded by arithmetic
9
+ * into exactly the `MeshData` a `MESH` would have carried (`assemble.ts`). It stands in place of a
10
+ * `MESH` and counts as one in every ordinal, as `MSHQ` does, so materials and regions name it
11
+ * like any other.
12
+ *
13
+ * **`KITS` names the pieces, and is written ahead of the geometry.** A piece is an ordinary mesh
14
+ * that is drawn only inside the meshes assembled from it; without the declaration a streaming
15
+ * reader would upload every piece as a part and draw the whole kit at the origin. Both chunks are
16
+ * **required**, for the `INST` reason: a reader that skipped `KITS` would draw that kit, and one
17
+ * that skipped `MSHC` would draw nothing where a district stands. A refusal naming them is the
18
+ * honest answer, and what it gives up is opening such a file in a reader before 1.23.
19
+ *
20
+ * **A piece is shape; a copy is shape placed and painted.** A piece's colours, emissive and every
21
+ * other per-vertex constant are ignored — the copy's surface supplies them. A piece carries
22
+ * positions, normals, and texture coordinates with tangents where the mesh is textured, since the
23
+ * tangent is what says which way a coordinate runs, and so how a copy's stretch applies to it.
24
+ * **Its sway is shape too**: where the assembly carries `channel`, each copy takes its piece's, as
25
+ * a tree's rises from its root to its leaves however it is placed.
26
+ *
27
+ * Layout, little-endian:
28
+ * - `KITS`: u32 count, then that many u32 mesh ordinals, ascending;
29
+ * - `MSHC`: u32 attribute bits (a `MESH`'s, less skinning), u32 surface count, then
30
+ * `SURFACE_FLOATS` f32 a surface; u32 copy count, then per copy u32 piece ordinal, u32 surface,
31
+ * 12 f32 of matrix (three columns, then the translation), 3 f32 of u stretch, 3 f32 of v stretch
32
+ * and 2 f32 of offset: 88 bytes a copy.
33
+ */
34
+ import {
35
+ ATTR_EMISSIVE_COLOR,
36
+ ATTR_GRAIN,
37
+ ATTR_LAYERS,
38
+ ATTR_RELIEF,
39
+ ATTR_ROUGHNESS,
40
+ ATTR_SPECULAR,
41
+ ATTR_TANGENT,
42
+ ATTR_UVS,
43
+ ATTR_CHANNEL,
44
+ DrftError,
45
+ align,
46
+ } from './drftFormat.ts';
47
+ import type { MeshData } from './meshData.ts';
48
+
49
+ /** Where each value sits in a surface, whichever of them the mesh carries. */
50
+ export const SURFACE = {
51
+ color: 0,
52
+ emissive: 3,
53
+ specular: 4,
54
+ emissiveColor: 5,
55
+ roughness: 8,
56
+ grain: 9,
57
+ relief: 10,
58
+ layer: 11,
59
+ } as const;
60
+ export const SURFACE_FLOATS = 12;
61
+ /** A copy's matrix: three columns of the linear part, then the translation. */
62
+ export const COPY_MATRIX_FLOATS = 12;
63
+ /** A copy's texture stretch: u along each piece axis, v along each, then the offset. */
64
+ export const COPY_UV_FLOATS = 8;
65
+ const COPY_BYTES = 8 + (COPY_MATRIX_FLOATS + COPY_UV_FLOATS) * 4;
66
+
67
+ /** The optional arrays an assembled mesh may carry. Skinning is a piece of a character, not a kit. */
68
+ export const ASSEMBLY_ATTRIBUTES =
69
+ ATTR_SPECULAR |
70
+ ATTR_UVS |
71
+ ATTR_EMISSIVE_COLOR |
72
+ ATTR_ROUGHNESS |
73
+ ATTR_GRAIN |
74
+ ATTR_RELIEF |
75
+ ATTR_TANGENT |
76
+ ATTR_LAYERS |
77
+ ATTR_CHANNEL;
78
+
79
+ export interface DrftAssembly {
80
+ /** Which optional arrays the expanded mesh carries, as a `MESH`'s attribute bits. */
81
+ readonly attributes: number;
82
+ /** `SURFACE_FLOATS` a surface, laid out as `SURFACE` says. */
83
+ readonly surfaces: Float32Array;
84
+ /** One kit piece a copy, by mesh ordinal. */
85
+ readonly pieces: Uint32Array;
86
+ /** One surface a copy. */
87
+ readonly surfaceOf: Uint32Array;
88
+ /** `COPY_MATRIX_FLOATS` a copy, taking the piece to where the copy stands. */
89
+ readonly transforms: Float32Array;
90
+ /**
91
+ * `COPY_UV_FLOATS` a copy. A coordinate is multiplied by how far the copy stretches the piece's
92
+ * own direction for it — `|stretch ⊙ t|` for the unit tangent `t`, the bitangent for v — so a
93
+ * unit box copied at 13 × 4 m with stretches of size over tile repeats its texture in metres,
94
+ * face by face. `(1, 1, 1)` twice and no offset leave a piece's coordinates as they are.
95
+ */
96
+ readonly uv: Float32Array;
97
+ }
98
+
99
+ /** Whether a mesh slot holds an assembly rather than vertices. */
100
+ export function isAssembly(mesh: object): mesh is DrftAssembly {
101
+ return 'pieces' in mesh && 'surfaceOf' in mesh;
102
+ }
103
+
104
+ export function buildKit(ordinals: readonly number[]): Uint8Array {
105
+ const out = new Uint8Array(align(4 + ordinals.length * 4));
106
+ const view = new DataView(out.buffer);
107
+ view.setUint32(0, ordinals.length, true);
108
+ ordinals.forEach((o, i) => view.setUint32(4 + i * 4, o, true));
109
+ return out;
110
+ }
111
+
112
+ /** The kit's pieces, ascending and each a mesh the file carries. */
113
+ export function readKit(
114
+ buffer: ArrayBufferLike,
115
+ offset: number,
116
+ byteLength: number,
117
+ meshCount: number,
118
+ ): number[] {
119
+ const view = new DataView(buffer, offset, byteLength);
120
+ if (byteLength < 4) throw new DrftError('KITS ends inside its count');
121
+ const count = view.getUint32(0, true);
122
+ if (4 + count * 4 > byteLength) throw new DrftError('KITS ends inside its pieces');
123
+ const out: number[] = [];
124
+ for (let i = 0; i < count; i++) out.push(view.getUint32(4 + i * 4, true));
125
+ checkKit(out, meshCount);
126
+ return out;
127
+ }
128
+
129
+ export function checkKit(ordinals: readonly number[], meshCount: number): void {
130
+ let previous = -1;
131
+ for (const o of ordinals) {
132
+ if (o >= meshCount)
133
+ throw new DrftError(`KITS names mesh ${o}, and the file carries ${meshCount}`);
134
+ if (o <= previous) throw new DrftError('KITS names its pieces out of order or twice');
135
+ previous = o;
136
+ }
137
+ }
138
+
139
+ export function buildAssembly(assembly: DrftAssembly): Uint8Array {
140
+ const copies = assembly.pieces.length;
141
+ const bytes = 12 + assembly.surfaces.length * 4 + copies * COPY_BYTES;
142
+ const out = new Uint8Array(align(bytes));
143
+ const view = new DataView(out.buffer);
144
+ let at = 0;
145
+ const u32 = (v: number): void => {
146
+ view.setUint32(at, v, true);
147
+ at += 4;
148
+ };
149
+ const f32 = (v: number): void => {
150
+ view.setFloat32(at, v, true);
151
+ at += 4;
152
+ };
153
+ u32(assembly.attributes);
154
+ u32(assembly.surfaces.length / SURFACE_FLOATS);
155
+ for (let i = 0; i < assembly.surfaces.length; i++) f32(assembly.surfaces[i] as number);
156
+ u32(copies);
157
+ for (let c = 0; c < copies; c++) {
158
+ u32(assembly.pieces[c] as number);
159
+ u32(assembly.surfaceOf[c] as number);
160
+ for (let i = 0; i < COPY_MATRIX_FLOATS; i++)
161
+ f32(assembly.transforms[c * COPY_MATRIX_FLOATS + i] as number);
162
+ for (let i = 0; i < COPY_UV_FLOATS; i++) f32(assembly.uv[c * COPY_UV_FLOATS + i] as number);
163
+ }
164
+ if (at !== bytes) throw new DrftError(`internal: MSHC wrote ${at} of ${bytes} bytes`);
165
+ return out;
166
+ }
167
+
168
+ /** One `MSHC` payload, checked on its own; its pieces are the caller's to check. */
169
+ export function readAssembly(
170
+ buffer: ArrayBufferLike,
171
+ offset: number,
172
+ byteLength: number,
173
+ ): DrftAssembly {
174
+ const view = new DataView(buffer, offset, byteLength);
175
+ let at = 0;
176
+ const need = (bytes: number, what: string): void => {
177
+ if (at + bytes > byteLength) throw new DrftError(`MSHC ends inside ${what}`);
178
+ };
179
+ need(8, 'its header');
180
+ const attributes = view.getUint32(0, true);
181
+ const surfaceCount = view.getUint32(4, true);
182
+ at = 8;
183
+ need(surfaceCount * SURFACE_FLOATS * 4 + 4, 'its surfaces');
184
+ const surfaces = new Float32Array(surfaceCount * SURFACE_FLOATS);
185
+ for (let i = 0; i < surfaces.length; i++) surfaces[i] = view.getFloat32(at + i * 4, true);
186
+ at += surfaces.length * 4;
187
+ const copies = view.getUint32(at, true);
188
+ at += 4;
189
+ need(copies * COPY_BYTES, 'its copies');
190
+ const pieces = new Uint32Array(copies);
191
+ const surfaceOf = new Uint32Array(copies);
192
+ const transforms = new Float32Array(copies * COPY_MATRIX_FLOATS);
193
+ const uv = new Float32Array(copies * COPY_UV_FLOATS);
194
+ for (let c = 0; c < copies; c++) {
195
+ pieces[c] = view.getUint32(at, true);
196
+ surfaceOf[c] = view.getUint32(at + 4, true);
197
+ at += 8;
198
+ for (let i = 0; i < COPY_MATRIX_FLOATS; i++, at += 4)
199
+ transforms[c * COPY_MATRIX_FLOATS + i] = view.getFloat32(at, true);
200
+ for (let i = 0; i < COPY_UV_FLOATS; i++, at += 4)
201
+ uv[c * COPY_UV_FLOATS + i] = view.getFloat32(at, true);
202
+ }
203
+ const assembly = { attributes, surfaces, pieces, surfaceOf, transforms, uv };
204
+ checkAssembly(assembly, 'MSHC');
205
+ return assembly;
206
+ }
207
+
208
+ /**
209
+ * What an assembly must be on its own: known bits, whole records, surfaces it names, finite
210
+ * numbers, whole layers, and no copy flattened to nothing — whose normals would be a division by
211
+ * zero rather than a direction.
212
+ */
213
+ export function checkAssembly(assembly: DrftAssembly, name: string): void {
214
+ const { attributes, surfaces, pieces, surfaceOf, transforms, uv } = assembly;
215
+ if ((attributes & ~ASSEMBLY_ATTRIBUTES) !== 0) {
216
+ throw new DrftError(`${name} carries attribute bits ${attributes} an assembly cannot`);
217
+ }
218
+ if ((attributes & ATTR_LAYERS) !== 0 && (attributes & ATTR_UVS) === 0) {
219
+ throw new DrftError(`${name} names layers without texture coordinates for them to ride with`);
220
+ }
221
+ if ((attributes & ATTR_UVS) !== 0 && (attributes & ATTR_TANGENT) === 0) {
222
+ throw new DrftError(
223
+ `${name} is textured without tangents; a copy's stretch follows the piece's tangent`,
224
+ );
225
+ }
226
+ const copies = pieces.length;
227
+ if (
228
+ surfaces.length % SURFACE_FLOATS !== 0 ||
229
+ surfaceOf.length !== copies ||
230
+ transforms.length !== copies * COPY_MATRIX_FLOATS ||
231
+ uv.length !== copies * COPY_UV_FLOATS
232
+ ) {
233
+ throw new DrftError(`${name}'s surfaces and copies are not whole records`);
234
+ }
235
+ const surfaceCount = surfaces.length / SURFACE_FLOATS;
236
+ for (let s = 0; s < surfaceCount; s++) {
237
+ for (let i = 0; i < SURFACE_FLOATS; i++) {
238
+ if (!Number.isFinite(surfaces[s * SURFACE_FLOATS + i] as number)) {
239
+ throw new DrftError(`${name}'s surface ${s} is not finite`);
240
+ }
241
+ }
242
+ const layer = surfaces[s * SURFACE_FLOATS + SURFACE.layer] as number;
243
+ if (!(layer >= 0) || Math.floor(layer) !== layer) {
244
+ throw new DrftError(`${name}'s surface ${s} names layer ${layer}, not a whole number`);
245
+ }
246
+ }
247
+ for (let c = 0; c < copies; c++) {
248
+ if ((surfaceOf[c] as number) >= surfaceCount) {
249
+ throw new DrftError(`${name}'s copy ${c} wears surface ${surfaceOf[c]} of ${surfaceCount}`);
250
+ }
251
+ const m = c * COPY_MATRIX_FLOATS;
252
+ for (let i = 0; i < COPY_MATRIX_FLOATS; i++) {
253
+ if (!Number.isFinite(transforms[m + i] as number)) {
254
+ throw new DrftError(`${name}'s copy ${c} has a matrix that is not finite`);
255
+ }
256
+ }
257
+ for (let i = 0; i < COPY_UV_FLOATS; i++) {
258
+ if (!Number.isFinite(uv[c * COPY_UV_FLOATS + i] as number)) {
259
+ throw new DrftError(`${name}'s copy ${c} has a texture stretch that is not finite`);
260
+ }
261
+ }
262
+ if (Math.abs(determinant(transforms, m)) < 1e-12) {
263
+ throw new DrftError(`${name}'s copy ${c} is flattened to nothing by its matrix`);
264
+ }
265
+ }
266
+ }
267
+
268
+ /** The determinant of the linear part of the copy matrix at `m`. */
269
+ export function determinant(t: ArrayLike<number>, m: number): number {
270
+ const a = t[m] as number;
271
+ const b = t[m + 1] as number;
272
+ const c = t[m + 2] as number;
273
+ const d = t[m + 3] as number;
274
+ const e = t[m + 4] as number;
275
+ const f = t[m + 5] as number;
276
+ const g = t[m + 6] as number;
277
+ const h = t[m + 7] as number;
278
+ const i = t[m + 8] as number;
279
+ return a * (e * i - f * h) - d * (b * i - c * h) + g * (b * f - c * e);
280
+ }
281
+
282
+ /**
283
+ * What an assembly must be to the kit around it: every copy a declared piece that comes before it
284
+ * in the file — so a streaming reader holds the piece when the copy lands — and every piece
285
+ * carrying what the assembly asks of it. Shared by the writer and both readers, so a file one
286
+ * refuses the others refuse too.
287
+ */
288
+ export function checkCopies(
289
+ assembly: DrftAssembly,
290
+ ordinal: number,
291
+ kit: ReadonlySet<number>,
292
+ piece: (ordinal: number) => MeshData | undefined,
293
+ ): void {
294
+ const textured = (assembly.attributes & ATTR_UVS) !== 0;
295
+ const tangents = textured || (assembly.attributes & ATTR_TANGENT) !== 0;
296
+ const seen = new Set<number>();
297
+ for (let c = 0; c < assembly.pieces.length; c++) {
298
+ const o = assembly.pieces[c] as number;
299
+ if (seen.has(o)) continue;
300
+ seen.add(o);
301
+ if (!kit.has(o)) {
302
+ throw new DrftError(`mesh ${ordinal} copies mesh ${o}, which KITS does not name a piece`);
303
+ }
304
+ if (o >= ordinal) {
305
+ throw new DrftError(
306
+ `mesh ${ordinal} copies piece ${o}, which comes after it; a piece is written before the meshes assembled from it`,
307
+ );
308
+ }
309
+ const mesh = piece(o);
310
+ if (mesh === undefined)
311
+ throw new DrftError(`mesh ${ordinal} copies piece ${o}, which has not arrived`);
312
+ if (textured && mesh.uvs === undefined) {
313
+ throw new DrftError(`mesh ${ordinal} is textured and piece ${o} has no texture coordinates`);
314
+ }
315
+ if (tangents && mesh.tangents === undefined) {
316
+ throw new DrftError(`mesh ${ordinal} needs tangents and piece ${o} has none`);
317
+ }
318
+ if ((assembly.attributes & ATTR_CHANNEL) !== 0 && mesh.channel === undefined) {
319
+ throw new DrftError(`mesh ${ordinal} sways and piece ${o} carries no channel to sway by`);
320
+ }
321
+ }
322
+ }
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 = 18;
26
+ export const DRFT_VERSION_MINOR = 23;
27
27
 
28
28
  /** Bytes before the chunk table. */
29
29
  export const HEADER_BYTES = 32;
@@ -272,6 +272,37 @@ export const CHUNK_INST = fourCC('INST');
272
272
  */
273
273
  export const CHUNK_MSHQ = fourCC('MSHQ');
274
274
 
275
+ /**
276
+ * One region of a streamed world: its levels of detail as mesh ordinals, its prop groups, its
277
+ * occluders and its collision. **Required**, one chunk per region, written just ahead of the meshes
278
+ * it introduces; see `drftRegions.ts`. Added in 1.21.
279
+ */
280
+ export const CHUNK_REGN = fourCC('REGN');
281
+
282
+ /**
283
+ * A world's fixed lights summed offline into one dense volume. Optional: a reader that skips it
284
+ * loses the glow of every light past the ones it shades exactly. See `drftLightVolume.ts`. Added in
285
+ * 1.22.
286
+ */
287
+ export const CHUNK_LVOL = fourCC('LVOL');
288
+
289
+ /**
290
+ * The meshes that are pieces of a kit, drawn only as copies inside assembled meshes. **Required**,
291
+ * and written ahead of the geometry; see `drftAssembly.ts`. Added in 1.23.
292
+ */
293
+ export const CHUNK_KITS = fourCC('KITS');
294
+
295
+ /**
296
+ * A mesh carried as painted copies of kit pieces, expanded on read into the `MeshData` a `MESH`
297
+ * gives. **Required**, and counted as a mesh in every ordinal. See `drftAssembly.ts`. Added in 1.23.
298
+ */
299
+ export const CHUNK_MSHC = fourCC('MSHC');
300
+
301
+ /** Whether a chunk is a mesh in the ordinal sequence: plain, quantised or assembled. */
302
+ export function isMeshChunk(code: number): boolean {
303
+ return code === CHUNK_MESH || code === CHUNK_MSHQ || code === CHUNK_MSHC;
304
+ }
305
+
275
306
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
276
307
  export const SPLAT_BLOCK_PREFIX = 40;
277
308
 
@@ -306,6 +337,10 @@ export const KNOWN_CHUNKS: ReadonlySet<number> = new Set([
306
337
  CHUNK_ENTS,
307
338
  CHUNK_INST,
308
339
  CHUNK_MSHQ,
340
+ CHUNK_REGN,
341
+ CHUNK_LVOL,
342
+ CHUNK_KITS,
343
+ CHUNK_MSHC,
309
344
  ]);
310
345
 
311
346
  /** Chunk flags. */
@@ -393,6 +428,26 @@ export const ATTR_JOINTS = 1 << 7;
393
428
  /** How much each of `ATTR_JOINTS`' four influences moves the vertex. See it for the whole story. */
394
429
  export const ATTR_WEIGHTS = 1 << 8;
395
430
 
431
+ /**
432
+ * Added in 1.20: which image of a texture array each vertex's face wears. See `MeshData.layers`.
433
+ *
434
+ * Next free bit, appended last, for the reason `grain` gives at length: a 1.19 reader meets the
435
+ * arrays it knows where it expects them, stops before this one, and draws every face at layer 0 —
436
+ * the first image of the array, which is what a reader that has never heard of layers should
437
+ * produce.
438
+ */
439
+ export const ATTR_LAYERS = 1 << 9;
440
+
441
+ /**
442
+ * Added in 1.23: `MeshData.channel`, four floats a vertex — sway, sky, opacity, and a spare lane.
443
+ *
444
+ * **The writer dropped it without a word until then**, which is the silent kind of missing: a tree
445
+ * baked with its sway came back still, and nothing said the lane had not been written. Next free
446
+ * bit, appended last, so a 1.22 reader meets every array it knows where it expects them, stops
447
+ * before this one, and draws the mesh planted, sunlit and opaque — the absent lane's own meaning.
448
+ */
449
+ export const ATTR_CHANNEL = 1 << 10;
450
+
396
451
  /** Everything this version defines, so a reader can spot bits from a later writer. */
397
452
  export const ATTR_KNOWN =
398
453
  ATTR_SPECULAR |
@@ -403,7 +458,9 @@ export const ATTR_KNOWN =
403
458
  ATTR_RELIEF |
404
459
  ATTR_TANGENT |
405
460
  ATTR_JOINTS |
406
- ATTR_WEIGHTS;
461
+ ATTR_WEIGHTS |
462
+ ATTR_LAYERS |
463
+ ATTR_CHANNEL;
407
464
 
408
465
  /**
409
466
  * How a `TEXS` payload is encoded.
@@ -582,10 +639,19 @@ export interface DrftMaterial {
582
639
  * tree's cards are not drawn. Absent means false, which is how every surface before it was drawn.
583
640
  */
584
641
  readonly doubleSided?: boolean;
642
+ /**
643
+ * Glass, 1.19: the share of light that passes, 0 opaque to 1 clear. Absent or 0 is not glass,
644
+ * which is what every surface before it was. See the engine's `glass.ts`.
645
+ */
646
+ readonly transmission?: number;
647
+ /** How milky the glass is, 0 see-through to 1 fully diffusing. 1.19. */
648
+ readonly frost?: number;
649
+ /** The colour light takes through the glass; white is clear. 1.19. */
650
+ readonly tint?: readonly [number, number, number];
585
651
  }
586
652
 
587
653
  /** Floats in a `MATL` entry, then the signed texture indices that follow them. */
588
- export const MATERIAL_FLOATS = 15;
654
+ export const MATERIAL_FLOATS = 20;
589
655
  /**
590
656
  * Albedo, normal, ORM and emissive.
591
657
  *
@@ -596,7 +662,11 @@ export const MATERIAL_FLOATS = 15;
596
662
  * them is speculative — only unbound, and the plans that bind them need no second stride bump.
597
663
  */
598
664
  export const MATERIAL_INDICES = 4;
599
- /** A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. */
665
+ /**
666
+ * A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. Glass's five
667
+ * floats — transmission, frost, tint — follow the flags word, since 1.19, which is why the float
668
+ * count is twenty while the entry is still laid out floats, indices, flags, floats.
669
+ */
600
670
  export const MATERIAL_FLAGS = 1;
601
671
  export const MATERIAL_ENTRY_BYTES = MATERIAL_FLOATS * 4 + MATERIAL_INDICES * 4 + MATERIAL_FLAGS * 4;
602
672
 
@@ -0,0 +1,179 @@
1
+ /**
2
+ * `LVOL`: a world's fixed lights summed into one dense volume offline, as the renderer reads it.
3
+ *
4
+ * **Optional, because the file is whole without it.** A reader that skips it draws the world lit by
5
+ * the lights it shades exactly and loses the glow of every light past them — less, not wrong.
6
+ *
7
+ * **Runs, because most of a city's volume is dark.** Lamps reach thirty metres and the volume stands
8
+ * a hundred and more, so the samples above the light are nothing, and a sample inside a building is
9
+ * nothing of another kind: it is not a sample at all, which the shader's filtering must be told.
10
+ * So samples are written in order as runs of three kinds — dark air, solid, and literal — and only a
11
+ * literal run carries values, eight half floats a sample: the colour and whether it is a sample,
12
+ * then the direction light arrives from.
13
+ *
14
+ * Layout, little-endian:
15
+ * - f32 × 3 origin, f32 spacing, u32 × 3 samples on each axis;
16
+ * - runs until every sample is covered, each a u32 whose top two bits are the kind (0 dark air,
17
+ * 1 solid, 2 literal) and whose low thirty are the count, a literal followed by its halves.
18
+ */
19
+ import { DrftError, align } from './drftFormat.ts';
20
+
21
+ /** The same shape as core's `DenseLightVolume`, so a read volume goes straight to the renderer. */
22
+ export interface DrftLightVolume {
23
+ readonly origin: readonly [number, number, number];
24
+ readonly spacing: number;
25
+ readonly dims: readonly [number, number, number];
26
+ /** Four floats a sample, x fastest: the colour, then 1 for a sample and 0 inside solid. */
27
+ readonly light: Float32Array;
28
+ /** Four floats a sample: the direction light arrives from, then 0. */
29
+ readonly direction: Float32Array;
30
+ }
31
+
32
+ const DARK = 0;
33
+ const SOLID = 1;
34
+ const LITERAL = 2;
35
+ const COUNT_MASK = 0x3fffffff;
36
+
37
+ function kindOf(volume: DrftLightVolume, sample: number): number {
38
+ const at = sample * 4;
39
+ const light = volume.light;
40
+ const lit =
41
+ light[at] !== 0 ||
42
+ light[at + 1] !== 0 ||
43
+ light[at + 2] !== 0 ||
44
+ volume.direction[at] !== 0 ||
45
+ volume.direction[at + 1] !== 0 ||
46
+ volume.direction[at + 2] !== 0;
47
+ if (lit) return LITERAL;
48
+ if (light[at + 3] === 1) return DARK;
49
+ if (light[at + 3] === 0) return SOLID;
50
+ return LITERAL;
51
+ }
52
+
53
+ export function buildLightVolume(volume: DrftLightVolume): Uint8Array {
54
+ const count = volume.dims[0] * volume.dims[1] * volume.dims[2];
55
+ if (volume.light.length !== count * 4 || volume.direction.length !== count * 4) {
56
+ throw new DrftError(`a light volume of ${count} samples needs ${count * 4} floats a channel`);
57
+ }
58
+ if (!(volume.spacing > 0)) throw new DrftError(`a light volume's spacing is ${volume.spacing}`);
59
+ /* Measured first, so the payload is allocated once. */
60
+ let bytes = 28;
61
+ for (let sample = 0; sample < count;) {
62
+ const kind = kindOf(volume, sample);
63
+ let end = sample + 1;
64
+ while (end < count && end - sample < COUNT_MASK && kindOf(volume, end) === kind) end++;
65
+ bytes += 4 + (kind === LITERAL ? (end - sample) * 16 : 0);
66
+ sample = end;
67
+ }
68
+ const out = new Uint8Array(align(bytes));
69
+ const view = new DataView(out.buffer);
70
+ for (let i = 0; i < 3; i++) view.setFloat32(i * 4, volume.origin[i] as number, true);
71
+ view.setFloat32(12, volume.spacing, true);
72
+ for (let i = 0; i < 3; i++) view.setUint32(16 + i * 4, volume.dims[i] as number, true);
73
+ let at = 28;
74
+ for (let sample = 0; sample < count;) {
75
+ const kind = kindOf(volume, sample);
76
+ let end = sample + 1;
77
+ while (end < count && end - sample < COUNT_MASK && kindOf(volume, end) === kind) end++;
78
+ view.setUint32(at, ((kind << 30) | (end - sample)) >>> 0, true);
79
+ at += 4;
80
+ if (kind === LITERAL) {
81
+ for (let s = sample; s < end; s++) {
82
+ for (let c = 0; c < 4; c++) {
83
+ view.setUint16(at, halfOf(volume.light[s * 4 + c] as number), true);
84
+ view.setUint16(at + 8, halfOf(volume.direction[s * 4 + c] as number), true);
85
+ at += 2;
86
+ }
87
+ at += 8;
88
+ }
89
+ }
90
+ sample = end;
91
+ }
92
+ return out;
93
+ }
94
+
95
+ export function readLightVolume(
96
+ buffer: ArrayBufferLike,
97
+ offset: number,
98
+ byteLength: number,
99
+ ): DrftLightVolume {
100
+ if (byteLength < 28) throw new DrftError('LVOL ends inside its header');
101
+ const view = new DataView(buffer, offset, byteLength);
102
+ const origin: [number, number, number] = [
103
+ view.getFloat32(0, true),
104
+ view.getFloat32(4, true),
105
+ view.getFloat32(8, true),
106
+ ];
107
+ const spacing = view.getFloat32(12, true);
108
+ const dims: [number, number, number] = [
109
+ view.getUint32(16, true),
110
+ view.getUint32(20, true),
111
+ view.getUint32(24, true),
112
+ ];
113
+ const count = dims[0] * dims[1] * dims[2];
114
+ if (!(spacing > 0) || count === 0) {
115
+ throw new DrftError(`LVOL has spacing ${spacing} and ${dims.join(' x ')} samples`);
116
+ }
117
+ const light = new Float32Array(count * 4);
118
+ const direction = new Float32Array(count * 4);
119
+ let at = 28;
120
+ let sample = 0;
121
+ while (sample < count) {
122
+ if (at + 4 > byteLength) throw new DrftError(`LVOL ends at sample ${sample} of ${count}`);
123
+ const word = view.getUint32(at, true);
124
+ at += 4;
125
+ const kind = word >>> 30;
126
+ const run = word & COUNT_MASK;
127
+ if (run === 0 || sample + run > count || kind > LITERAL) {
128
+ throw new DrftError(`LVOL has a run of ${run} of kind ${kind} at sample ${sample}`);
129
+ }
130
+ if (kind === DARK) {
131
+ for (let s = sample; s < sample + run; s++) light[s * 4 + 3] = 1;
132
+ } else if (kind === LITERAL) {
133
+ if (at + run * 16 > byteLength) throw new DrftError(`LVOL ends inside sample ${sample}`);
134
+ for (let s = sample; s < sample + run; s++) {
135
+ for (let c = 0; c < 4; c++) {
136
+ light[s * 4 + c] = floatOf(view.getUint16(at, true));
137
+ direction[s * 4 + c] = floatOf(view.getUint16(at + 8, true));
138
+ at += 2;
139
+ }
140
+ at += 8;
141
+ }
142
+ }
143
+ sample += run;
144
+ }
145
+ return { origin, spacing, dims, light, direction };
146
+ }
147
+
148
+ const scratchFloat = new Float32Array(1);
149
+ const scratchWord = new Uint32Array(scratchFloat.buffer);
150
+
151
+ /** A float as IEEE half-precision bits, rounded to nearest, overflow to infinity. */
152
+ function halfOf(value: number): number {
153
+ scratchFloat[0] = value;
154
+ const word = scratchWord[0] as number;
155
+ const sign = (word >>> 16) & 0x8000;
156
+ const exponent = ((word >>> 23) & 0xff) - 127 + 15;
157
+ const mantissa = word & 0x7fffff;
158
+ if (exponent >= 31)
159
+ return sign | 0x7c00 | (((word >>> 23) & 0xff) === 0xff && mantissa ? 0x200 : 0);
160
+ if (exponent <= 0) {
161
+ if (exponent < -10) return sign;
162
+ const full = mantissa | 0x800000;
163
+ const shift = 14 - exponent;
164
+ const rounded = (full + (1 << (shift - 1))) >> shift;
165
+ return sign | rounded;
166
+ }
167
+ const rounded = (exponent << 10) + ((mantissa + 0x1000) >> 13);
168
+ return sign | Math.min(rounded, 0x7c00);
169
+ }
170
+
171
+ /** IEEE half-precision bits as a float. */
172
+ function floatOf(bits: number): number {
173
+ const sign = bits & 0x8000 ? -1 : 1;
174
+ const exponent = (bits >> 10) & 0x1f;
175
+ const mantissa = bits & 0x3ff;
176
+ if (exponent === 0) return sign * mantissa * 2 ** -24;
177
+ if (exponent === 31) return mantissa ? Number.NaN : sign * Number.POSITIVE_INFINITY;
178
+ return sign * (1 + mantissa / 1024) * 2 ** (exponent - 15);
179
+ }
@@ -33,6 +33,8 @@ import {
33
33
  ATTR_TANGENT,
34
34
  ATTR_UVS,
35
35
  ATTR_WEIGHTS,
36
+ ATTR_LAYERS,
37
+ ATTR_CHANNEL,
36
38
  DrftError,
37
39
  align,
38
40
  } from './drftFormat.ts';
@@ -60,7 +62,9 @@ type Name =
60
62
  | 'relief'
61
63
  | 'tangents'
62
64
  | 'joints'
63
- | 'weights';
65
+ | 'weights'
66
+ | 'layers'
67
+ | 'channel';
64
68
 
65
69
  /** The frozen order `MESH` uses, with each optional array's bit: `FORMAT.md` §4.3. */
66
70
  const ORDER: readonly {
@@ -82,6 +86,9 @@ const ORDER: readonly {
82
86
  { name: 'tangents', width: 4, bit: ATTR_TANGENT, kind: 'direction' },
83
87
  { name: 'joints', width: 4, bit: ATTR_JOINTS, kind: 'exact' },
84
88
  { name: 'weights', width: 4, bit: ATTR_WEIGHTS, kind: 'value' },
89
+ /* Whole numbers naming an image, so exact: a layer quantised onto its neighbour is another picture. */
90
+ { name: 'layers', width: 1, bit: ATTR_LAYERS, kind: 'exact' },
91
+ { name: 'channel', width: 4, bit: ATTR_CHANNEL, kind: 'value' },
85
92
  ];
86
93
 
87
94
  /** A growing little-endian byte list, for the writer: offline, so it may allocate. */