@driftengine/drft 4.5.0 → 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 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 = 19;
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
  *
@@ -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,
@@ -0,0 +1,58 @@
1
+ import type { DrftInstanceGroup } from './drftInstances.ts';
2
+ export interface DrftRegionLevel {
3
+ /** How far, in metres, this level's surface may stand from the real one. 0 for exact. */
4
+ readonly error: number;
5
+ /** The ordinals of the meshes this level draws. */
6
+ readonly meshes: readonly number[];
7
+ }
8
+ export interface DrftRegionCollision {
9
+ readonly positions: Float32Array;
10
+ readonly indices: Uint32Array;
11
+ }
12
+ export interface DrftRegion {
13
+ readonly id: number;
14
+ /** Everything the region holds, min xyz then max xyz. */
15
+ readonly bounds: ArrayLike<number>;
16
+ /** Finest first; at least one. */
17
+ readonly levels: readonly DrftRegionLevel[];
18
+ /** Its props, drawn at its finest level. */
19
+ readonly instances: readonly DrftInstanceGroup[];
20
+ /** Boxes that hide what stands behind them, six floats each. */
21
+ readonly occluders: Float32Array;
22
+ /** One triangle mesh the region collides as, or null. */
23
+ readonly collision: DrftRegionCollision | null;
24
+ }
25
+ /** The most levels a region may carry; the runtime keeps one value of a byte for "none". */
26
+ export declare const REGION_MAX_LEVELS = 254;
27
+ export declare function buildRegion(region: DrftRegion): Uint8Array;
28
+ /** One `REGN` payload, checked on its own against the file's mesh count. */
29
+ export declare function readRegion(buffer: ArrayBufferLike, offset: number, byteLength: number, meshCount: number): DrftRegion;
30
+ /**
31
+ * What regions must be to each other: an id once, a level mesh in one region's one level, and a
32
+ * group's prototype never also a level mesh. Kept across chunks by the writer, the whole-file reader
33
+ * and the stream alike, so a file one of them refuses the others refuse too.
34
+ */
35
+ export declare class RegionLedger {
36
+ private readonly ids;
37
+ private readonly levelMeshes;
38
+ private readonly prototypes;
39
+ admit(region: DrftRegion): void;
40
+ /** Whether a mesh is any region's, as a level or a prop. */
41
+ names(mesh: number): boolean;
42
+ }
43
+ /**
44
+ * Where each region's chunk goes among the meshes: `before[m]` is the regions written just ahead of
45
+ * mesh `m`, and `before[meshCount]` those after the last. A streaming reader then always has a
46
+ * region before the meshes it introduces.
47
+ *
48
+ * The meshes no region introduces come first, then each region's new meshes, in the order the
49
+ * regions are given and ascending. **A region introduces the kit pieces its assemblies copy, where
50
+ * no earlier region did** (`piecesOf` names them), so a piece travels with the first region that
51
+ * needs it rather than at the head of the file: a world's kit is most of its bytes, and a walker's
52
+ * first regions need a sliver of it. A piece ahead of the first mesh any region names is the
53
+ * file's own, as every piece was before a region could carry one, so either layout is valid. That
54
+ * a piece comes ahead of every assembly copying it is the kit's own rule (`checkCopies`). Anything
55
+ * else is refused by name rather than reordered, because the ordinals are what the regions and the
56
+ * materials already point at.
57
+ */
58
+ export declare function planRegions(regions: readonly DrftRegion[], meshCount: number, piecesOf?: (mesh: number) => ArrayLike<number> | undefined): DrftRegion[][];