@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.
@@ -20,7 +20,7 @@ export declare const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export declare const DRFT_VERSION_MINOR = 18;
23
+ export declare const DRFT_VERSION_MINOR = 23;
24
24
  /** Bytes before the chunk table. */
25
25
  export declare const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -231,6 +231,30 @@ export declare const CHUNK_INST: number;
231
231
  * See `drftQuantised.ts`. Added in 1.18.
232
232
  */
233
233
  export declare const CHUNK_MSHQ: number;
234
+ /**
235
+ * One region of a streamed world: its levels of detail as mesh ordinals, its prop groups, its
236
+ * occluders and its collision. **Required**, one chunk per region, written just ahead of the meshes
237
+ * it introduces; see `drftRegions.ts`. Added in 1.21.
238
+ */
239
+ export declare const CHUNK_REGN: number;
240
+ /**
241
+ * A world's fixed lights summed offline into one dense volume. Optional: a reader that skips it
242
+ * loses the glow of every light past the ones it shades exactly. See `drftLightVolume.ts`. Added in
243
+ * 1.22.
244
+ */
245
+ export declare const CHUNK_LVOL: number;
246
+ /**
247
+ * The meshes that are pieces of a kit, drawn only as copies inside assembled meshes. **Required**,
248
+ * and written ahead of the geometry; see `drftAssembly.ts`. Added in 1.23.
249
+ */
250
+ export declare const CHUNK_KITS: number;
251
+ /**
252
+ * A mesh carried as painted copies of kit pieces, expanded on read into the `MeshData` a `MESH`
253
+ * gives. **Required**, and counted as a mesh in every ordinal. See `drftAssembly.ts`. Added in 1.23.
254
+ */
255
+ export declare const CHUNK_MSHC: number;
256
+ /** Whether a chunk is a mesh in the ordinal sequence: plain, quantised or assembled. */
257
+ export declare function isMeshChunk(code: number): boolean;
234
258
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
235
259
  export declare const SPLAT_BLOCK_PREFIX = 40;
236
260
  /**
@@ -323,6 +347,24 @@ export declare const ATTR_TANGENT: number;
323
347
  export declare const ATTR_JOINTS: number;
324
348
  /** How much each of `ATTR_JOINTS`' four influences moves the vertex. See it for the whole story. */
325
349
  export declare const ATTR_WEIGHTS: number;
350
+ /**
351
+ * Added in 1.20: which image of a texture array each vertex's face wears. See `MeshData.layers`.
352
+ *
353
+ * Next free bit, appended last, for the reason `grain` gives at length: a 1.19 reader meets the
354
+ * arrays it knows where it expects them, stops before this one, and draws every face at layer 0 —
355
+ * the first image of the array, which is what a reader that has never heard of layers should
356
+ * produce.
357
+ */
358
+ export declare const ATTR_LAYERS: number;
359
+ /**
360
+ * Added in 1.23: `MeshData.channel`, four floats a vertex — sway, sky, opacity, and a spare lane.
361
+ *
362
+ * **The writer dropped it without a word until then**, which is the silent kind of missing: a tree
363
+ * baked with its sway came back still, and nothing said the lane had not been written. Next free
364
+ * bit, appended last, so a 1.22 reader meets every array it knows where it expects them, stops
365
+ * before this one, and draws the mesh planted, sunlit and opaque — the absent lane's own meaning.
366
+ */
367
+ export declare const ATTR_CHANNEL: number;
326
368
  /** Everything this version defines, so a reader can spot bits from a later writer. */
327
369
  export declare const ATTR_KNOWN: number;
328
370
  /**
@@ -488,9 +530,18 @@ export interface DrftMaterial {
488
530
  * tree's cards are not drawn. Absent means false, which is how every surface before it was drawn.
489
531
  */
490
532
  readonly doubleSided?: boolean;
533
+ /**
534
+ * Glass, 1.19: the share of light that passes, 0 opaque to 1 clear. Absent or 0 is not glass,
535
+ * which is what every surface before it was. See the engine's `glass.ts`.
536
+ */
537
+ readonly transmission?: number;
538
+ /** How milky the glass is, 0 see-through to 1 fully diffusing. 1.19. */
539
+ readonly frost?: number;
540
+ /** The colour light takes through the glass; white is clear. 1.19. */
541
+ readonly tint?: readonly [number, number, number];
491
542
  }
492
543
  /** Floats in a `MATL` entry, then the signed texture indices that follow them. */
493
- export declare const MATERIAL_FLOATS = 15;
544
+ export declare const MATERIAL_FLOATS = 20;
494
545
  /**
495
546
  * Albedo, normal, ORM and emissive.
496
547
  *
@@ -501,7 +552,11 @@ export declare const MATERIAL_FLOATS = 15;
501
552
  * them is speculative — only unbound, and the plans that bind them need no second stride bump.
502
553
  */
503
554
  export declare const MATERIAL_INDICES = 4;
504
- /** A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. */
555
+ /**
556
+ * A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. Glass's five
557
+ * floats — transmission, frost, tint — follow the flags word, since 1.19, which is why the float
558
+ * count is twenty while the entry is still laid out floats, indices, flags, floats.
559
+ */
505
560
  export declare const MATERIAL_FLAGS = 1;
506
561
  export declare const MATERIAL_ENTRY_BYTES: number;
507
562
  /** What `HEAD` says about the asset as a whole. */
@@ -20,7 +20,7 @@ export const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export const DRFT_VERSION_MINOR = 18;
23
+ export const DRFT_VERSION_MINOR = 23;
24
24
  /** Bytes before the chunk table. */
25
25
  export const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -242,6 +242,32 @@ export const CHUNK_INST = fourCC('INST');
242
242
  * See `drftQuantised.ts`. Added in 1.18.
243
243
  */
244
244
  export const CHUNK_MSHQ = fourCC('MSHQ');
245
+ /**
246
+ * One region of a streamed world: its levels of detail as mesh ordinals, its prop groups, its
247
+ * occluders and its collision. **Required**, one chunk per region, written just ahead of the meshes
248
+ * it introduces; see `drftRegions.ts`. Added in 1.21.
249
+ */
250
+ export const CHUNK_REGN = fourCC('REGN');
251
+ /**
252
+ * A world's fixed lights summed offline into one dense volume. Optional: a reader that skips it
253
+ * loses the glow of every light past the ones it shades exactly. See `drftLightVolume.ts`. Added in
254
+ * 1.22.
255
+ */
256
+ export const CHUNK_LVOL = fourCC('LVOL');
257
+ /**
258
+ * The meshes that are pieces of a kit, drawn only as copies inside assembled meshes. **Required**,
259
+ * and written ahead of the geometry; see `drftAssembly.ts`. Added in 1.23.
260
+ */
261
+ export const CHUNK_KITS = fourCC('KITS');
262
+ /**
263
+ * A mesh carried as painted copies of kit pieces, expanded on read into the `MeshData` a `MESH`
264
+ * gives. **Required**, and counted as a mesh in every ordinal. See `drftAssembly.ts`. Added in 1.23.
265
+ */
266
+ export const CHUNK_MSHC = fourCC('MSHC');
267
+ /** Whether a chunk is a mesh in the ordinal sequence: plain, quantised or assembled. */
268
+ export function isMeshChunk(code) {
269
+ return code === CHUNK_MESH || code === CHUNK_MSHQ || code === CHUNK_MSHC;
270
+ }
245
271
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
246
272
  export const SPLAT_BLOCK_PREFIX = 40;
247
273
  /**
@@ -275,6 +301,10 @@ export const KNOWN_CHUNKS = new Set([
275
301
  CHUNK_ENTS,
276
302
  CHUNK_INST,
277
303
  CHUNK_MSHQ,
304
+ CHUNK_REGN,
305
+ CHUNK_LVOL,
306
+ CHUNK_KITS,
307
+ CHUNK_MSHC,
278
308
  ]);
279
309
  /** Chunk flags. */
280
310
  export const CHUNK_REQUIRED = 1 << 0;
@@ -358,6 +388,24 @@ export const ATTR_TANGENT = 1 << 6;
358
388
  export const ATTR_JOINTS = 1 << 7;
359
389
  /** How much each of `ATTR_JOINTS`' four influences moves the vertex. See it for the whole story. */
360
390
  export const ATTR_WEIGHTS = 1 << 8;
391
+ /**
392
+ * Added in 1.20: which image of a texture array each vertex's face wears. See `MeshData.layers`.
393
+ *
394
+ * Next free bit, appended last, for the reason `grain` gives at length: a 1.19 reader meets the
395
+ * arrays it knows where it expects them, stops before this one, and draws every face at layer 0 —
396
+ * the first image of the array, which is what a reader that has never heard of layers should
397
+ * produce.
398
+ */
399
+ export const ATTR_LAYERS = 1 << 9;
400
+ /**
401
+ * Added in 1.23: `MeshData.channel`, four floats a vertex — sway, sky, opacity, and a spare lane.
402
+ *
403
+ * **The writer dropped it without a word until then**, which is the silent kind of missing: a tree
404
+ * baked with its sway came back still, and nothing said the lane had not been written. Next free
405
+ * bit, appended last, so a 1.22 reader meets every array it knows where it expects them, stops
406
+ * before this one, and draws the mesh planted, sunlit and opaque — the absent lane's own meaning.
407
+ */
408
+ export const ATTR_CHANNEL = 1 << 10;
361
409
  /** Everything this version defines, so a reader can spot bits from a later writer. */
362
410
  export const ATTR_KNOWN = ATTR_SPECULAR |
363
411
  ATTR_UVS |
@@ -367,7 +415,9 @@ export const ATTR_KNOWN = ATTR_SPECULAR |
367
415
  ATTR_RELIEF |
368
416
  ATTR_TANGENT |
369
417
  ATTR_JOINTS |
370
- ATTR_WEIGHTS;
418
+ ATTR_WEIGHTS |
419
+ ATTR_LAYERS |
420
+ ATTR_CHANNEL;
371
421
  /**
372
422
  * How a `TEXS` payload is encoded.
373
423
  *
@@ -394,7 +444,7 @@ export function codecName(codec) {
394
444
  : `codec ${codec}`;
395
445
  }
396
446
  /** Floats in a `MATL` entry, then the signed texture indices that follow them. */
397
- export const MATERIAL_FLOATS = 15;
447
+ export const MATERIAL_FLOATS = 20;
398
448
  /**
399
449
  * Albedo, normal, ORM and emissive.
400
450
  *
@@ -405,7 +455,11 @@ export const MATERIAL_FLOATS = 15;
405
455
  * them is speculative — only unbound, and the plans that bind them need no second stride bump.
406
456
  */
407
457
  export const MATERIAL_INDICES = 4;
408
- /** A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. */
458
+ /**
459
+ * A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. Glass's five
460
+ * floats — transmission, frost, tint — follow the flags word, since 1.19, which is why the float
461
+ * count is twenty while the entry is still laid out floats, indices, flags, floats.
462
+ */
409
463
  export const MATERIAL_FLAGS = 1;
410
464
  export const MATERIAL_ENTRY_BYTES = MATERIAL_FLOATS * 4 + MATERIAL_INDICES * 4 + MATERIAL_FLAGS * 4;
411
465
  /**
@@ -0,0 +1,12 @@
1
+ /** The same shape as core's `DenseLightVolume`, so a read volume goes straight to the renderer. */
2
+ export interface DrftLightVolume {
3
+ readonly origin: readonly [number, number, number];
4
+ readonly spacing: number;
5
+ readonly dims: readonly [number, number, number];
6
+ /** Four floats a sample, x fastest: the colour, then 1 for a sample and 0 inside solid. */
7
+ readonly light: Float32Array;
8
+ /** Four floats a sample: the direction light arrives from, then 0. */
9
+ readonly direction: Float32Array;
10
+ }
11
+ export declare function buildLightVolume(volume: DrftLightVolume): Uint8Array;
12
+ export declare function readLightVolume(buffer: ArrayBufferLike, offset: number, byteLength: number): DrftLightVolume;
@@ -0,0 +1,172 @@
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.js';
20
+ const DARK = 0;
21
+ const SOLID = 1;
22
+ const LITERAL = 2;
23
+ const COUNT_MASK = 0x3fffffff;
24
+ function kindOf(volume, sample) {
25
+ const at = sample * 4;
26
+ const light = volume.light;
27
+ const lit = light[at] !== 0 ||
28
+ light[at + 1] !== 0 ||
29
+ light[at + 2] !== 0 ||
30
+ volume.direction[at] !== 0 ||
31
+ volume.direction[at + 1] !== 0 ||
32
+ volume.direction[at + 2] !== 0;
33
+ if (lit)
34
+ return LITERAL;
35
+ if (light[at + 3] === 1)
36
+ return DARK;
37
+ if (light[at + 3] === 0)
38
+ return SOLID;
39
+ return LITERAL;
40
+ }
41
+ export function buildLightVolume(volume) {
42
+ const count = volume.dims[0] * volume.dims[1] * volume.dims[2];
43
+ if (volume.light.length !== count * 4 || volume.direction.length !== count * 4) {
44
+ throw new DrftError(`a light volume of ${count} samples needs ${count * 4} floats a channel`);
45
+ }
46
+ if (!(volume.spacing > 0))
47
+ throw new DrftError(`a light volume's spacing is ${volume.spacing}`);
48
+ /* Measured first, so the payload is allocated once. */
49
+ let bytes = 28;
50
+ for (let sample = 0; sample < count;) {
51
+ const kind = kindOf(volume, sample);
52
+ let end = sample + 1;
53
+ while (end < count && end - sample < COUNT_MASK && kindOf(volume, end) === kind)
54
+ end++;
55
+ bytes += 4 + (kind === LITERAL ? (end - sample) * 16 : 0);
56
+ sample = end;
57
+ }
58
+ const out = new Uint8Array(align(bytes));
59
+ const view = new DataView(out.buffer);
60
+ for (let i = 0; i < 3; i++)
61
+ view.setFloat32(i * 4, volume.origin[i], true);
62
+ view.setFloat32(12, volume.spacing, true);
63
+ for (let i = 0; i < 3; i++)
64
+ view.setUint32(16 + i * 4, volume.dims[i], true);
65
+ let at = 28;
66
+ for (let sample = 0; sample < count;) {
67
+ const kind = kindOf(volume, sample);
68
+ let end = sample + 1;
69
+ while (end < count && end - sample < COUNT_MASK && kindOf(volume, end) === kind)
70
+ end++;
71
+ view.setUint32(at, ((kind << 30) | (end - sample)) >>> 0, true);
72
+ at += 4;
73
+ if (kind === LITERAL) {
74
+ for (let s = sample; s < end; s++) {
75
+ for (let c = 0; c < 4; c++) {
76
+ view.setUint16(at, halfOf(volume.light[s * 4 + c]), true);
77
+ view.setUint16(at + 8, halfOf(volume.direction[s * 4 + c]), true);
78
+ at += 2;
79
+ }
80
+ at += 8;
81
+ }
82
+ }
83
+ sample = end;
84
+ }
85
+ return out;
86
+ }
87
+ export function readLightVolume(buffer, offset, byteLength) {
88
+ if (byteLength < 28)
89
+ throw new DrftError('LVOL ends inside its header');
90
+ const view = new DataView(buffer, offset, byteLength);
91
+ const origin = [
92
+ view.getFloat32(0, true),
93
+ view.getFloat32(4, true),
94
+ view.getFloat32(8, true),
95
+ ];
96
+ const spacing = view.getFloat32(12, true);
97
+ const dims = [
98
+ view.getUint32(16, true),
99
+ view.getUint32(20, true),
100
+ view.getUint32(24, true),
101
+ ];
102
+ const count = dims[0] * dims[1] * dims[2];
103
+ if (!(spacing > 0) || count === 0) {
104
+ throw new DrftError(`LVOL has spacing ${spacing} and ${dims.join(' x ')} samples`);
105
+ }
106
+ const light = new Float32Array(count * 4);
107
+ const direction = new Float32Array(count * 4);
108
+ let at = 28;
109
+ let sample = 0;
110
+ while (sample < count) {
111
+ if (at + 4 > byteLength)
112
+ throw new DrftError(`LVOL ends at sample ${sample} of ${count}`);
113
+ const word = view.getUint32(at, true);
114
+ at += 4;
115
+ const kind = word >>> 30;
116
+ const run = word & COUNT_MASK;
117
+ if (run === 0 || sample + run > count || kind > LITERAL) {
118
+ throw new DrftError(`LVOL has a run of ${run} of kind ${kind} at sample ${sample}`);
119
+ }
120
+ if (kind === DARK) {
121
+ for (let s = sample; s < sample + run; s++)
122
+ light[s * 4 + 3] = 1;
123
+ }
124
+ else if (kind === LITERAL) {
125
+ if (at + run * 16 > byteLength)
126
+ throw new DrftError(`LVOL ends inside sample ${sample}`);
127
+ for (let s = sample; s < sample + run; s++) {
128
+ for (let c = 0; c < 4; c++) {
129
+ light[s * 4 + c] = floatOf(view.getUint16(at, true));
130
+ direction[s * 4 + c] = floatOf(view.getUint16(at + 8, true));
131
+ at += 2;
132
+ }
133
+ at += 8;
134
+ }
135
+ }
136
+ sample += run;
137
+ }
138
+ return { origin, spacing, dims, light, direction };
139
+ }
140
+ const scratchFloat = new Float32Array(1);
141
+ const scratchWord = new Uint32Array(scratchFloat.buffer);
142
+ /** A float as IEEE half-precision bits, rounded to nearest, overflow to infinity. */
143
+ function halfOf(value) {
144
+ scratchFloat[0] = value;
145
+ const word = scratchWord[0];
146
+ const sign = (word >>> 16) & 0x8000;
147
+ const exponent = ((word >>> 23) & 0xff) - 127 + 15;
148
+ const mantissa = word & 0x7fffff;
149
+ if (exponent >= 31)
150
+ return sign | 0x7c00 | (((word >>> 23) & 0xff) === 0xff && mantissa ? 0x200 : 0);
151
+ if (exponent <= 0) {
152
+ if (exponent < -10)
153
+ return sign;
154
+ const full = mantissa | 0x800000;
155
+ const shift = 14 - exponent;
156
+ const rounded = (full + (1 << (shift - 1))) >> shift;
157
+ return sign | rounded;
158
+ }
159
+ const rounded = (exponent << 10) + ((mantissa + 0x1000) >> 13);
160
+ return sign | Math.min(rounded, 0x7c00);
161
+ }
162
+ /** IEEE half-precision bits as a float. */
163
+ function floatOf(bits) {
164
+ const sign = bits & 0x8000 ? -1 : 1;
165
+ const exponent = (bits >> 10) & 0x1f;
166
+ const mantissa = bits & 0x3ff;
167
+ if (exponent === 0)
168
+ return sign * mantissa * 2 ** -24;
169
+ if (exponent === 31)
170
+ return mantissa ? Number.NaN : sign * Number.POSITIVE_INFINITY;
171
+ return sign * (1 + mantissa / 1024) * 2 ** (exponent - 15);
172
+ }
@@ -23,7 +23,7 @@
23
23
  * compares vertices bit for bit, or one that cannot afford the decode's copy; `writeDrft`'s
24
24
  * `quantise: false` writes `MESH` as before.
25
25
  */
26
- import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, ATTR_WEIGHTS, DrftError, align, } from './drftFormat.js';
26
+ import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, DrftError, align, } from './drftFormat.js';
27
27
  const RAW = 0;
28
28
  const CONSTANT = 1;
29
29
  const RANGE16 = 2;
@@ -46,6 +46,9 @@ const ORDER = [
46
46
  { name: 'tangents', width: 4, bit: ATTR_TANGENT, kind: 'direction' },
47
47
  { name: 'joints', width: 4, bit: ATTR_JOINTS, kind: 'exact' },
48
48
  { name: 'weights', width: 4, bit: ATTR_WEIGHTS, kind: 'value' },
49
+ /* Whole numbers naming an image, so exact: a layer quantised onto its neighbour is another picture. */
50
+ { name: 'layers', width: 1, bit: ATTR_LAYERS, kind: 'exact' },
51
+ { name: 'channel', width: 4, bit: ATTR_CHANNEL, kind: 'value' },
49
52
  ];
50
53
  /** A growing little-endian byte list, for the writer: offline, so it may allocate. */
51
54
  class Bytes {
@@ -24,6 +24,9 @@ import { type EntsScene } from './ents.ts';
24
24
  import { type NavPolyMesh } from './navm.ts';
25
25
  import { type DrftNetwork } from './nnet.ts';
26
26
  import { type DrftGraph } from './ngrf.ts';
27
+ import type { DrftAssembly } from './drftAssembly.ts';
28
+ import type { DrftLightVolume } from './drftLightVolume.ts';
29
+ import type { DrftRegion } from './drftRegions.ts';
27
30
  import type { DrftInstanceGroup } from './drftInstances.ts';
28
31
  import type { DrftLight } from './drftLights.ts';
29
32
  /** An embedded image, still compressed. Decoding is the consumer's, through `createImageBitmap`. */
@@ -156,6 +159,24 @@ export interface DrftAsset {
156
159
  * ```
157
160
  */
158
161
  readonly colliders: readonly Float32Array[];
162
+ /**
163
+ * The regions of a streamed world, in the order they were written, or empty. `REGN`, defined at
164
+ * 1.21: each names the meshes it draws at every level, its props, occluders and collision.
165
+ */
166
+ readonly regions: readonly DrftRegion[];
167
+ /** A world's summed lights, or null for a file carrying no `LVOL`. Defined at 1.22. */
168
+ readonly lightVolume: DrftLightVolume | null;
169
+ /**
170
+ * The meshes that are kit pieces, ascending, or empty. `KITS`, defined at 1.23. A piece is in
171
+ * `meshes` like any other, and is drawn only as copies inside the assemblies.
172
+ */
173
+ readonly kit: readonly number[];
174
+ /**
175
+ * Each `MSHC` mesh as the file carried it, by ordinal, or empty. Its slot in `meshes` holds it
176
+ * expanded — which for a world built from a kit is most of a gigabyte, so a consumer of such a
177
+ * file streams it (`DrftStream.onAssembly`) rather than reading it whole.
178
+ */
179
+ readonly assemblies: ReadonlyMap<number, DrftAssembly>;
159
180
  /** Optional chunks this reader did not understand, in file order. Diagnostics only. */
160
181
  readonly skipped: readonly string[];
161
182
  readonly versionMajor: number;
@@ -163,7 +184,6 @@ export interface DrftAsset {
163
184
  }
164
185
  export declare function readHead(buffer: ArrayBuffer, chunk: DrftChunk): DrftHead;
165
186
  export declare function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData;
166
- /** Decode a `.drft` from a buffer. Throws `DrftError` on anything it cannot read. */
167
187
  export declare function readDrft(buffer: ArrayBuffer): DrftAsset;
168
188
  /**
169
189
  * One `SPLT` block, viewed in place.
package/dist/drftRead.js CHANGED
@@ -16,7 +16,7 @@
16
16
  */
17
17
  import { readClip, readMorph, readNodes, readSkin } from './drftSkin.js';
18
18
  import { validateMeshData } from './meshData.js';
19
- import { 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_LODM, CHUNK_LODF, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MESH, CHUNK_REQUIRED, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, CHUNK_MATL, CHUNK_TEXS, KNOWN_CHUNKS, MATERIAL_ENTRY_BYTES, align, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_INST, CHUNK_LITE, CHUNK_MSHQ, CHUNK_SKIN, CHUNK_MORP, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_COLL, } from './drftFormat.js';
19
+ import { 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_LODM, CHUNK_LODF, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MESH, CHUNK_REQUIRED, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, CHUNK_MATL, CHUNK_TEXS, KNOWN_CHUNKS, MATERIAL_ENTRY_BYTES, align, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, CHUNK_SKIN, CHUNK_MORP, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_COLL, } from './drftFormat.js';
20
20
  import { readSubs } from './drftSubs.js';
21
21
  import { readSdfv } from './sdfv.js';
22
22
  import { readDtex } from './dtex.js';
@@ -25,6 +25,10 @@ import { readNavm } from './navm.js';
25
25
  import { readNnet } from './nnet.js';
26
26
  import { readNgrf } from './ngrf.js';
27
27
  import { readColliders } from './drftColliders.js';
28
+ import { RegionLedger, readRegion } from './drftRegions.js';
29
+ import { expandAssembly } from './assemble.js';
30
+ import { checkCopies, readAssembly, readKit } from './drftAssembly.js';
31
+ import { readLightVolume } from './drftLightVolume.js';
28
32
  import { readInstances } from './drftInstances.js';
29
33
  import { readLights } from './drftLights.js';
30
34
  import { decodeQuantisedMesh } from './drftQuantised.js';
@@ -96,6 +100,10 @@ export function readMesh(buffer, chunk) {
96
100
  writer set only one bit is rejected rather than half-skinned. See ATTR_JOINTS. */
97
101
  const joints = (attributes & ATTR_JOINTS) !== 0 ? floats(vertices * 4, 'joints') : undefined;
98
102
  const weights = (attributes & ATTR_WEIGHTS) !== 0 ? floats(vertices * 4, 'weights') : undefined;
103
+ /* Texture-array layers, 1.20, one float a vertex and last. See ATTR_LAYERS. */
104
+ const layers = (attributes & ATTR_LAYERS) !== 0 ? floats(vertices, 'layers') : undefined;
105
+ /* Sway, sky and opacity, 1.23, four floats a vertex and last. See ATTR_CHANNEL. */
106
+ const channel = (attributes & ATTR_CHANNEL) !== 0 ? floats(vertices * 4, 'channel') : undefined;
99
107
  const indexBytes = indexCount * 4;
100
108
  if (at + indexBytes > limit) {
101
109
  throw new DrftError(`indices need ${indexBytes} bytes but only ${limit - at} remain at ${at}`);
@@ -116,6 +124,8 @@ export function readMesh(buffer, chunk) {
116
124
  ...(tangents === undefined ? {} : { tangents }),
117
125
  ...(joints === undefined ? {} : { joints }),
118
126
  ...(weights === undefined ? {} : { weights }),
127
+ ...(layers === undefined ? {} : { layers }),
128
+ ...(channel === undefined ? {} : { channel }),
119
129
  };
120
130
  /*
121
131
  * Checked again on the way in, even though the writer checked on the way out. The file
@@ -126,6 +136,14 @@ export function readMesh(buffer, chunk) {
126
136
  return mesh;
127
137
  }
128
138
  /** Decode a `.drft` from a buffer. Throws `DrftError` on anything it cannot read. */
139
+ /** A slot held for an assembly until its pieces are in hand. Never returned. */
140
+ const EMPTY_MESH = {
141
+ positions: new Float32Array(0),
142
+ normals: new Float32Array(0),
143
+ colors: new Float32Array(0),
144
+ emissive: new Float32Array(0),
145
+ indices: new Uint32Array(0),
146
+ };
129
147
  export function readDrft(buffer) {
130
148
  if (buffer.byteLength < HEADER_BYTES) {
131
149
  throw new DrftError(`a file is at least ${HEADER_BYTES} bytes, got ${buffer.byteLength}`);
@@ -176,6 +194,10 @@ export function readDrft(buffer) {
176
194
  let colliders = [];
177
195
  /* Read once every mesh is counted, because a group is checked against the mesh count. */
178
196
  let instanceChunk = null;
197
+ const regionChunks = [];
198
+ let lightVolume = null;
199
+ let kitChunk = null;
200
+ const assemblies = new Map();
179
201
  let lights = [];
180
202
  const skipped = [];
181
203
  let head = null;
@@ -215,6 +237,13 @@ export function readDrft(buffer) {
215
237
  /* A mesh like any other, in the same ordinal sequence: see `drftQuantised.ts`. */
216
238
  else if (chunk.code === CHUNK_MSHQ)
217
239
  meshes.push(decodeQuantisedMesh(buffer, chunk.offset, chunk.byteLength));
240
+ else if (chunk.code === CHUNK_MSHC) {
241
+ /* Its slot held, and filled once every piece is in hand. */
242
+ assemblies.set(meshes.length, readAssembly(buffer, chunk.offset, chunk.byteLength));
243
+ meshes.push(EMPTY_MESH);
244
+ }
245
+ else if (chunk.code === CHUNK_KITS)
246
+ kitChunk = chunk;
218
247
  /* A level of detail is a mesh payload under another code, so it takes the same reader. */
219
248
  else if (chunk.code === CHUNK_LODM)
220
249
  levels.push({ level: chunk.index, mesh: readMesh(buffer, chunk) });
@@ -238,6 +267,11 @@ export function readDrft(buffer) {
238
267
  nodes = readNodes(buffer, chunk.offset, chunk.byteLength);
239
268
  else if (chunk.code === CHUNK_INST)
240
269
  instanceChunk = chunk;
270
+ /* Read once every mesh is counted, since a region names meshes that follow it. */
271
+ else if (chunk.code === CHUNK_REGN)
272
+ regionChunks.push(chunk);
273
+ else if (chunk.code === CHUNK_LVOL)
274
+ lightVolume = readLightVolume(buffer, chunk.offset, chunk.byteLength);
241
275
  else if (chunk.code === CHUNK_LITE)
242
276
  lights = readLights(buffer, chunk.offset, chunk.byteLength);
243
277
  else if (chunk.code === CHUNK_SKIN)
@@ -319,6 +353,37 @@ export function readDrft(buffer) {
319
353
  morphTargetCount: morph.targetCount,
320
354
  };
321
355
  }
356
+ const kit = kitChunk === null ? [] : readKit(buffer, kitChunk.offset, kitChunk.byteLength, meshes.length);
357
+ const pieces = new Set(kit);
358
+ for (const o of kit) {
359
+ if (assemblies.has(o))
360
+ throw new DrftError(`mesh ${o} is a kit piece and an assembly`);
361
+ }
362
+ for (const [ordinal, assembly] of assemblies) {
363
+ checkCopies(assembly, ordinal, pieces, (o) => meshes[o]);
364
+ meshes[ordinal] = expandAssembly(assembly, (o) => meshes[o]);
365
+ }
366
+ const ledger = new RegionLedger();
367
+ const regions = regionChunks.map((chunk) => {
368
+ const region = readRegion(buffer, chunk.offset, chunk.byteLength, meshes.length);
369
+ ledger.admit(region);
370
+ return region;
371
+ });
372
+ const instances = instanceChunk === null
373
+ ? []
374
+ : readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length);
375
+ /* A mesh placed by `INST` across the whole file and also a region's would be drawn twice. */
376
+ for (const group of instances) {
377
+ if (ledger.names(group.mesh)) {
378
+ throw new DrftError(`mesh ${group.mesh} is placed by INST and is also a region's`);
379
+ }
380
+ if (pieces.has(group.mesh))
381
+ throw new DrftError(`mesh ${group.mesh} is a kit piece and is placed by INST`);
382
+ }
383
+ for (const o of kit) {
384
+ if (ledger.names(o))
385
+ throw new DrftError(`mesh ${o} is a kit piece and a region's`);
386
+ }
322
387
  /* By the ordinal the file gave them rather than by where they sat, coarsest first. */
323
388
  levels.sort((a, b) => a.level - b.level);
324
389
  const lods = levels.map((entry) => entry.mesh);
@@ -328,9 +393,7 @@ export function readDrft(buffer) {
328
393
  lods,
329
394
  levels: discreteLevels,
330
395
  nodes,
331
- instances: instanceChunk === null
332
- ? []
333
- : readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length),
396
+ instances,
334
397
  lights,
335
398
  skins,
336
399
  clips,
@@ -341,6 +404,10 @@ export function readDrft(buffer) {
341
404
  networks: nnet?.networks ?? [],
342
405
  graphs,
343
406
  colliders,
407
+ regions,
408
+ lightVolume,
409
+ kit,
410
+ assemblies,
344
411
  dtex,
345
412
  navigation,
346
413
  entities,
@@ -536,6 +603,16 @@ export function readMaterials(buffer, chunk) {
536
603
  blend: has(76, 4) ? (view.getUint32(at + 76, true) & 1) !== 0 : false,
537
604
  /* One-sided unless it says otherwise, which is how every file before the bit was drawn. */
538
605
  doubleSided: has(76, 4) ? (view.getUint32(at + 76, true) & 2) !== 0 : false,
606
+ /* Not glass, which is what every file before 1.19 meant. */
607
+ transmission: has(80, 4) ? view.getFloat32(at + 80, true) : 0,
608
+ frost: has(84, 4) ? view.getFloat32(at + 84, true) : 0,
609
+ tint: has(88, 12)
610
+ ? [
611
+ view.getFloat32(at + 88, true),
612
+ view.getFloat32(at + 92, true),
613
+ view.getFloat32(at + 96, true),
614
+ ]
615
+ : [1, 1, 1],
539
616
  });
540
617
  }
541
618
  return out;