@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.
- package/README.md +1 -1
- package/dist/assemble.d.ts +7 -0
- package/dist/assemble.js +242 -0
- package/dist/drftAssembly.d.ts +62 -0
- package/dist/drftAssembly.js +267 -0
- package/dist/drftFormat.d.ts +43 -1
- package/dist/drftFormat.js +52 -2
- package/dist/drftLightVolume.d.ts +12 -0
- package/dist/drftLightVolume.js +172 -0
- package/dist/drftQuantised.js +4 -1
- package/dist/drftRead.d.ts +21 -1
- package/dist/drftRead.js +71 -4
- package/dist/drftRegions.d.ts +58 -0
- package/dist/drftRegions.js +313 -0
- package/dist/drftStream.d.ts +49 -1
- package/dist/drftStream.js +74 -5
- package/dist/drftWrite.d.ts +35 -1
- package/dist/drftWrite.js +97 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/meshData.d.ts +19 -0
- package/dist/meshData.js +15 -0
- package/package.json +4 -4
- package/src/assemble.ts +268 -0
- package/src/drftAssembly.ts +322 -0
- package/src/drftFormat.ts +59 -2
- package/src/drftLightVolume.ts +179 -0
- package/src/drftQuantised.ts +8 -1
- package/src/drftRead.ts +97 -4
- package/src/drftRegions.ts +350 -0
- package/src/drftStream.ts +112 -7
- package/src/drftWrite.ts +141 -5
- package/src/index.ts +40 -0
- package/src/meshData.ts +38 -0
package/src/drftRead.ts
CHANGED
|
@@ -31,6 +31,8 @@ import {
|
|
|
31
31
|
ATTR_SPECULAR,
|
|
32
32
|
ATTR_UVS,
|
|
33
33
|
ATTR_WEIGHTS,
|
|
34
|
+
ATTR_LAYERS,
|
|
35
|
+
ATTR_CHANNEL,
|
|
34
36
|
CHUNK_ENTRY_BYTES,
|
|
35
37
|
CHUNK_HEAD,
|
|
36
38
|
CHUNK_LODM,
|
|
@@ -55,8 +57,12 @@ import {
|
|
|
55
57
|
CHUNK_ANIM,
|
|
56
58
|
CHUNK_NODE,
|
|
57
59
|
CHUNK_INST,
|
|
60
|
+
CHUNK_REGN,
|
|
61
|
+
CHUNK_LVOL,
|
|
58
62
|
CHUNK_LITE,
|
|
59
63
|
CHUNK_MSHQ,
|
|
64
|
+
CHUNK_KITS,
|
|
65
|
+
CHUNK_MSHC,
|
|
60
66
|
CHUNK_SKIN,
|
|
61
67
|
CHUNK_MORP,
|
|
62
68
|
CHUNK_NNET,
|
|
@@ -74,6 +80,13 @@ import { type NavPolyMesh, readNavm } from './navm.ts';
|
|
|
74
80
|
import { type DrftNetwork, type DrftNnet, readNnet } from './nnet.ts';
|
|
75
81
|
import { type DrftGraph, readNgrf } from './ngrf.ts';
|
|
76
82
|
import { readColliders } from './drftColliders.ts';
|
|
83
|
+
import { RegionLedger, readRegion } from './drftRegions.ts';
|
|
84
|
+
import { expandAssembly } from './assemble.ts';
|
|
85
|
+
import { checkCopies, readAssembly, readKit } from './drftAssembly.ts';
|
|
86
|
+
import type { DrftAssembly } from './drftAssembly.ts';
|
|
87
|
+
import { readLightVolume } from './drftLightVolume.ts';
|
|
88
|
+
import type { DrftLightVolume } from './drftLightVolume.ts';
|
|
89
|
+
import type { DrftRegion } from './drftRegions.ts';
|
|
77
90
|
import { readInstances } from './drftInstances.ts';
|
|
78
91
|
import type { DrftInstanceGroup } from './drftInstances.ts';
|
|
79
92
|
import { readLights } from './drftLights.ts';
|
|
@@ -208,6 +221,24 @@ export interface DrftAsset {
|
|
|
208
221
|
* ```
|
|
209
222
|
*/
|
|
210
223
|
readonly colliders: readonly Float32Array[];
|
|
224
|
+
/**
|
|
225
|
+
* The regions of a streamed world, in the order they were written, or empty. `REGN`, defined at
|
|
226
|
+
* 1.21: each names the meshes it draws at every level, its props, occluders and collision.
|
|
227
|
+
*/
|
|
228
|
+
readonly regions: readonly DrftRegion[];
|
|
229
|
+
/** A world's summed lights, or null for a file carrying no `LVOL`. Defined at 1.22. */
|
|
230
|
+
readonly lightVolume: DrftLightVolume | null;
|
|
231
|
+
/**
|
|
232
|
+
* The meshes that are kit pieces, ascending, or empty. `KITS`, defined at 1.23. A piece is in
|
|
233
|
+
* `meshes` like any other, and is drawn only as copies inside the assemblies.
|
|
234
|
+
*/
|
|
235
|
+
readonly kit: readonly number[];
|
|
236
|
+
/**
|
|
237
|
+
* Each `MSHC` mesh as the file carried it, by ordinal, or empty. Its slot in `meshes` holds it
|
|
238
|
+
* expanded — which for a world built from a kit is most of a gigabyte, so a consumer of such a
|
|
239
|
+
* file streams it (`DrftStream.onAssembly`) rather than reading it whole.
|
|
240
|
+
*/
|
|
241
|
+
readonly assemblies: ReadonlyMap<number, DrftAssembly>;
|
|
211
242
|
/** Optional chunks this reader did not understand, in file order. Diagnostics only. */
|
|
212
243
|
readonly skipped: readonly string[];
|
|
213
244
|
readonly versionMajor: number;
|
|
@@ -290,6 +321,10 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
|
|
|
290
321
|
writer set only one bit is rejected rather than half-skinned. See ATTR_JOINTS. */
|
|
291
322
|
const joints = (attributes & ATTR_JOINTS) !== 0 ? floats(vertices * 4, 'joints') : undefined;
|
|
292
323
|
const weights = (attributes & ATTR_WEIGHTS) !== 0 ? floats(vertices * 4, 'weights') : undefined;
|
|
324
|
+
/* Texture-array layers, 1.20, one float a vertex and last. See ATTR_LAYERS. */
|
|
325
|
+
const layers = (attributes & ATTR_LAYERS) !== 0 ? floats(vertices, 'layers') : undefined;
|
|
326
|
+
/* Sway, sky and opacity, 1.23, four floats a vertex and last. See ATTR_CHANNEL. */
|
|
327
|
+
const channel = (attributes & ATTR_CHANNEL) !== 0 ? floats(vertices * 4, 'channel') : undefined;
|
|
293
328
|
|
|
294
329
|
const indexBytes = indexCount * 4;
|
|
295
330
|
if (at + indexBytes > limit) {
|
|
@@ -312,6 +347,8 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
|
|
|
312
347
|
...(tangents === undefined ? {} : { tangents }),
|
|
313
348
|
...(joints === undefined ? {} : { joints }),
|
|
314
349
|
...(weights === undefined ? {} : { weights }),
|
|
350
|
+
...(layers === undefined ? {} : { layers }),
|
|
351
|
+
...(channel === undefined ? {} : { channel }),
|
|
315
352
|
};
|
|
316
353
|
|
|
317
354
|
/*
|
|
@@ -324,6 +361,15 @@ export function readMesh(buffer: ArrayBuffer, chunk: DrftChunk): MeshData {
|
|
|
324
361
|
}
|
|
325
362
|
|
|
326
363
|
/** Decode a `.drft` from a buffer. Throws `DrftError` on anything it cannot read. */
|
|
364
|
+
/** A slot held for an assembly until its pieces are in hand. Never returned. */
|
|
365
|
+
const EMPTY_MESH: MeshData = {
|
|
366
|
+
positions: new Float32Array(0),
|
|
367
|
+
normals: new Float32Array(0),
|
|
368
|
+
colors: new Float32Array(0),
|
|
369
|
+
emissive: new Float32Array(0),
|
|
370
|
+
indices: new Uint32Array(0),
|
|
371
|
+
};
|
|
372
|
+
|
|
327
373
|
export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
328
374
|
if (buffer.byteLength < HEADER_BYTES) {
|
|
329
375
|
throw new DrftError(`a file is at least ${HEADER_BYTES} bytes, got ${buffer.byteLength}`);
|
|
@@ -382,6 +428,10 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
382
428
|
let colliders: readonly Float32Array[] = [];
|
|
383
429
|
/* Read once every mesh is counted, because a group is checked against the mesh count. */
|
|
384
430
|
let instanceChunk: { offset: number; byteLength: number } | null = null;
|
|
431
|
+
const regionChunks: { offset: number; byteLength: number }[] = [];
|
|
432
|
+
let lightVolume: DrftLightVolume | null = null;
|
|
433
|
+
let kitChunk: { offset: number; byteLength: number } | null = null;
|
|
434
|
+
const assemblies = new Map<number, DrftAssembly>();
|
|
385
435
|
let lights: DrftLight[] = [];
|
|
386
436
|
const skipped: string[] = [];
|
|
387
437
|
let head: DrftHead | null = null;
|
|
@@ -427,6 +477,11 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
427
477
|
/* A mesh like any other, in the same ordinal sequence: see `drftQuantised.ts`. */
|
|
428
478
|
else if (chunk.code === CHUNK_MSHQ)
|
|
429
479
|
meshes.push(decodeQuantisedMesh(buffer, chunk.offset, chunk.byteLength));
|
|
480
|
+
else if (chunk.code === CHUNK_MSHC) {
|
|
481
|
+
/* Its slot held, and filled once every piece is in hand. */
|
|
482
|
+
assemblies.set(meshes.length, readAssembly(buffer, chunk.offset, chunk.byteLength));
|
|
483
|
+
meshes.push(EMPTY_MESH);
|
|
484
|
+
} else if (chunk.code === CHUNK_KITS) kitChunk = chunk;
|
|
430
485
|
/* A level of detail is a mesh payload under another code, so it takes the same reader. */
|
|
431
486
|
else if (chunk.code === CHUNK_LODM)
|
|
432
487
|
levels.push({ level: chunk.index, mesh: readMesh(buffer, chunk) });
|
|
@@ -443,6 +498,10 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
443
498
|
else if (chunk.code === CHUNK_ENTS) entities = readEnts(buffer, chunk.offset, chunk.byteLength);
|
|
444
499
|
else if (chunk.code === CHUNK_NODE) nodes = readNodes(buffer, chunk.offset, chunk.byteLength);
|
|
445
500
|
else if (chunk.code === CHUNK_INST) instanceChunk = chunk;
|
|
501
|
+
/* Read once every mesh is counted, since a region names meshes that follow it. */
|
|
502
|
+
else if (chunk.code === CHUNK_REGN) regionChunks.push(chunk);
|
|
503
|
+
else if (chunk.code === CHUNK_LVOL)
|
|
504
|
+
lightVolume = readLightVolume(buffer, chunk.offset, chunk.byteLength);
|
|
446
505
|
else if (chunk.code === CHUNK_LITE) lights = readLights(buffer, chunk.offset, chunk.byteLength);
|
|
447
506
|
else if (chunk.code === CHUNK_SKIN)
|
|
448
507
|
skins.push(readSkin(buffer, chunk.offset, chunk.byteLength));
|
|
@@ -530,6 +589,39 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
530
589
|
};
|
|
531
590
|
}
|
|
532
591
|
|
|
592
|
+
const kit =
|
|
593
|
+
kitChunk === null ? [] : readKit(buffer, kitChunk.offset, kitChunk.byteLength, meshes.length);
|
|
594
|
+
const pieces = new Set(kit);
|
|
595
|
+
for (const o of kit) {
|
|
596
|
+
if (assemblies.has(o)) throw new DrftError(`mesh ${o} is a kit piece and an assembly`);
|
|
597
|
+
}
|
|
598
|
+
for (const [ordinal, assembly] of assemblies) {
|
|
599
|
+
checkCopies(assembly, ordinal, pieces, (o) => meshes[o]);
|
|
600
|
+
meshes[ordinal] = expandAssembly(assembly, (o) => meshes[o] as MeshData);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
const ledger = new RegionLedger();
|
|
604
|
+
const regions = regionChunks.map((chunk) => {
|
|
605
|
+
const region = readRegion(buffer, chunk.offset, chunk.byteLength, meshes.length);
|
|
606
|
+
ledger.admit(region);
|
|
607
|
+
return region;
|
|
608
|
+
});
|
|
609
|
+
const instances =
|
|
610
|
+
instanceChunk === null
|
|
611
|
+
? []
|
|
612
|
+
: readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length);
|
|
613
|
+
/* A mesh placed by `INST` across the whole file and also a region's would be drawn twice. */
|
|
614
|
+
for (const group of instances) {
|
|
615
|
+
if (ledger.names(group.mesh)) {
|
|
616
|
+
throw new DrftError(`mesh ${group.mesh} is placed by INST and is also a region's`);
|
|
617
|
+
}
|
|
618
|
+
if (pieces.has(group.mesh))
|
|
619
|
+
throw new DrftError(`mesh ${group.mesh} is a kit piece and is placed by INST`);
|
|
620
|
+
}
|
|
621
|
+
for (const o of kit) {
|
|
622
|
+
if (ledger.names(o)) throw new DrftError(`mesh ${o} is a kit piece and a region's`);
|
|
623
|
+
}
|
|
624
|
+
|
|
533
625
|
/* By the ordinal the file gave them rather than by where they sat, coarsest first. */
|
|
534
626
|
levels.sort((a, b) => a.level - b.level);
|
|
535
627
|
const lods = levels.map((entry) => entry.mesh);
|
|
@@ -540,10 +632,7 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
540
632
|
lods,
|
|
541
633
|
levels: discreteLevels,
|
|
542
634
|
nodes,
|
|
543
|
-
instances
|
|
544
|
-
instanceChunk === null
|
|
545
|
-
? []
|
|
546
|
-
: readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length),
|
|
635
|
+
instances,
|
|
547
636
|
lights,
|
|
548
637
|
skins,
|
|
549
638
|
clips,
|
|
@@ -554,6 +643,10 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
554
643
|
networks: nnet?.networks ?? [],
|
|
555
644
|
graphs,
|
|
556
645
|
colliders,
|
|
646
|
+
regions,
|
|
647
|
+
lightVolume,
|
|
648
|
+
kit,
|
|
649
|
+
assemblies,
|
|
557
650
|
dtex,
|
|
558
651
|
navigation,
|
|
559
652
|
entities,
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `REGN`: one region of a streamed world — its geometry at every level of detail, its prop
|
|
3
|
+
* batches, its occluders and its collision — as one chunk, written just ahead of its meshes.
|
|
4
|
+
*
|
|
5
|
+
* **Required, because a reader that skipped it would draw every level of every region at once.**
|
|
6
|
+
* A region's levels are ordinary meshes, so an old reader would open a city as each district drawn
|
|
7
|
+
* three times over itself, and every prop it places as one copy at the origin — the `INST`
|
|
8
|
+
* argument. A refusal naming `REGN` is the honest answer. What that gives up is opening such a file
|
|
9
|
+
* at all in a reader before 1.21.
|
|
10
|
+
*
|
|
11
|
+
* **Levels are meshes named by ordinal, finest first, each with a geometric error**: how far, in
|
|
12
|
+
* metres, that level's surface may stand from the real one. The runtime turns an error into a
|
|
13
|
+
* switch distance against its own screen, which a file cannot know. A mesh belongs to at most one
|
|
14
|
+
* region's levels. An instance group's prototype may be shared by any number of regions — a lamp
|
|
15
|
+
* post is one mesh however many districts stand it — but is never also a level mesh.
|
|
16
|
+
*
|
|
17
|
+
* Layout, little-endian:
|
|
18
|
+
* - u32 id, then f32 × 6 bounds (min xyz, max xyz);
|
|
19
|
+
* - u32 level count, then per level: f32 error, u32 mesh count, that many u32 mesh ordinals;
|
|
20
|
+
* - u32 group count, then per group: u32 mesh ordinal, u32 copies, that many 16-float matrices;
|
|
21
|
+
* - u32 occluder count, then f32 × 6 a box (min xyz, max xyz);
|
|
22
|
+
* - u32 vertex count, u32 index count, f32 × 3 a vertex, u32 an index: the region's collision.
|
|
23
|
+
*/
|
|
24
|
+
import { DrftError, align } from './drftFormat.ts';
|
|
25
|
+
import type { DrftInstanceGroup } from './drftInstances.ts';
|
|
26
|
+
|
|
27
|
+
export interface DrftRegionLevel {
|
|
28
|
+
/** How far, in metres, this level's surface may stand from the real one. 0 for exact. */
|
|
29
|
+
readonly error: number;
|
|
30
|
+
/** The ordinals of the meshes this level draws. */
|
|
31
|
+
readonly meshes: readonly number[];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DrftRegionCollision {
|
|
35
|
+
readonly positions: Float32Array;
|
|
36
|
+
readonly indices: Uint32Array;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DrftRegion {
|
|
40
|
+
readonly id: number;
|
|
41
|
+
/** Everything the region holds, min xyz then max xyz. */
|
|
42
|
+
readonly bounds: ArrayLike<number>;
|
|
43
|
+
/** Finest first; at least one. */
|
|
44
|
+
readonly levels: readonly DrftRegionLevel[];
|
|
45
|
+
/** Its props, drawn at its finest level. */
|
|
46
|
+
readonly instances: readonly DrftInstanceGroup[];
|
|
47
|
+
/** Boxes that hide what stands behind them, six floats each. */
|
|
48
|
+
readonly occluders: Float32Array;
|
|
49
|
+
/** One triangle mesh the region collides as, or null. */
|
|
50
|
+
readonly collision: DrftRegionCollision | null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The most levels a region may carry; the runtime keeps one value of a byte for "none". */
|
|
54
|
+
export const REGION_MAX_LEVELS = 254;
|
|
55
|
+
|
|
56
|
+
export function buildRegion(region: DrftRegion): Uint8Array {
|
|
57
|
+
checkRegion(region, Number.POSITIVE_INFINITY);
|
|
58
|
+
let bytes = 4 + 24 + 4 + 4 + 4 + 8;
|
|
59
|
+
for (const level of region.levels) bytes += 8 + level.meshes.length * 4;
|
|
60
|
+
for (const group of region.instances) bytes += 8 + group.transforms.length * 4;
|
|
61
|
+
bytes += region.occluders.length * 4;
|
|
62
|
+
const vertices = region.collision?.positions.length ?? 0;
|
|
63
|
+
const indices = region.collision?.indices.length ?? 0;
|
|
64
|
+
bytes += vertices * 4 + indices * 4;
|
|
65
|
+
const out = new Uint8Array(align(bytes));
|
|
66
|
+
const view = new DataView(out.buffer);
|
|
67
|
+
let at = 0;
|
|
68
|
+
const u32 = (value: number): void => {
|
|
69
|
+
view.setUint32(at, value, true);
|
|
70
|
+
at += 4;
|
|
71
|
+
};
|
|
72
|
+
const f32 = (value: number): void => {
|
|
73
|
+
view.setFloat32(at, value, true);
|
|
74
|
+
at += 4;
|
|
75
|
+
};
|
|
76
|
+
u32(region.id);
|
|
77
|
+
for (let i = 0; i < 6; i++) f32(region.bounds[i] as number);
|
|
78
|
+
u32(region.levels.length);
|
|
79
|
+
for (const level of region.levels) {
|
|
80
|
+
f32(level.error);
|
|
81
|
+
u32(level.meshes.length);
|
|
82
|
+
for (const mesh of level.meshes) u32(mesh);
|
|
83
|
+
}
|
|
84
|
+
u32(region.instances.length);
|
|
85
|
+
for (const group of region.instances) {
|
|
86
|
+
u32(group.mesh);
|
|
87
|
+
u32(group.transforms.length / 16);
|
|
88
|
+
for (let i = 0; i < group.transforms.length; i++) f32(group.transforms[i] as number);
|
|
89
|
+
}
|
|
90
|
+
u32(region.occluders.length / 6);
|
|
91
|
+
for (let i = 0; i < region.occluders.length; i++) f32(region.occluders[i] as number);
|
|
92
|
+
u32(vertices / 3);
|
|
93
|
+
u32(indices);
|
|
94
|
+
for (let i = 0; i < vertices; i++) f32(region.collision?.positions[i] as number);
|
|
95
|
+
for (let i = 0; i < indices; i++) u32(region.collision?.indices[i] as number);
|
|
96
|
+
/* The one check the builder owns: it wrote exactly what it measured. */
|
|
97
|
+
if (at !== bytes) throw new DrftError(`internal: REGN wrote ${at} of ${bytes} bytes`);
|
|
98
|
+
return out;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One `REGN` payload, checked on its own against the file's mesh count. */
|
|
102
|
+
export function readRegion(
|
|
103
|
+
buffer: ArrayBufferLike,
|
|
104
|
+
offset: number,
|
|
105
|
+
byteLength: number,
|
|
106
|
+
meshCount: number,
|
|
107
|
+
): DrftRegion {
|
|
108
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
109
|
+
let at = 0;
|
|
110
|
+
const need = (bytes: number, what: string): void => {
|
|
111
|
+
if (at + bytes > byteLength) throw new DrftError(`REGN ends inside ${what}`);
|
|
112
|
+
};
|
|
113
|
+
const u32 = (): number => {
|
|
114
|
+
const value = view.getUint32(at, true);
|
|
115
|
+
at += 4;
|
|
116
|
+
return value;
|
|
117
|
+
};
|
|
118
|
+
const floats = (count: number): Float32Array => {
|
|
119
|
+
const out = new Float32Array(count);
|
|
120
|
+
for (let i = 0; i < count; i++) out[i] = view.getFloat32(at + i * 4, true);
|
|
121
|
+
at += count * 4;
|
|
122
|
+
return out;
|
|
123
|
+
};
|
|
124
|
+
need(32, 'its header');
|
|
125
|
+
const id = u32();
|
|
126
|
+
const bounds = floats(6);
|
|
127
|
+
const levelCount = u32();
|
|
128
|
+
const levels: DrftRegionLevel[] = [];
|
|
129
|
+
for (let l = 0; l < levelCount; l++) {
|
|
130
|
+
need(8, `region ${id}'s level ${l}`);
|
|
131
|
+
const error = view.getFloat32(at, true);
|
|
132
|
+
at += 4;
|
|
133
|
+
const count = u32();
|
|
134
|
+
need(count * 4, `region ${id}'s level ${l}`);
|
|
135
|
+
const meshes: number[] = [];
|
|
136
|
+
for (let m = 0; m < count; m++) meshes.push(u32());
|
|
137
|
+
levels.push({ error, meshes });
|
|
138
|
+
}
|
|
139
|
+
need(4, `region ${id}'s groups`);
|
|
140
|
+
const groupCount = u32();
|
|
141
|
+
const instances: DrftInstanceGroup[] = [];
|
|
142
|
+
for (let g = 0; g < groupCount; g++) {
|
|
143
|
+
need(8, `region ${id}'s group ${g}`);
|
|
144
|
+
const mesh = u32();
|
|
145
|
+
const copies = u32();
|
|
146
|
+
need(copies * 64, `region ${id}'s group ${g}`);
|
|
147
|
+
instances.push({ mesh, transforms: floats(copies * 16) });
|
|
148
|
+
}
|
|
149
|
+
need(4, `region ${id}'s occluders`);
|
|
150
|
+
const occluderCount = u32();
|
|
151
|
+
need(occluderCount * 24, `region ${id}'s occluders`);
|
|
152
|
+
const occluders = floats(occluderCount * 6);
|
|
153
|
+
need(8, `region ${id}'s collision`);
|
|
154
|
+
const vertices = u32();
|
|
155
|
+
const indexCount = u32();
|
|
156
|
+
need(vertices * 12 + indexCount * 4, `region ${id}'s collision`);
|
|
157
|
+
const positions = floats(vertices * 3);
|
|
158
|
+
const indices = new Uint32Array(indexCount);
|
|
159
|
+
for (let i = 0; i < indexCount; i++) indices[i] = u32();
|
|
160
|
+
const region: DrftRegion = {
|
|
161
|
+
id,
|
|
162
|
+
bounds,
|
|
163
|
+
levels,
|
|
164
|
+
instances,
|
|
165
|
+
occluders,
|
|
166
|
+
collision: vertices === 0 && indexCount === 0 ? null : { positions, indices },
|
|
167
|
+
};
|
|
168
|
+
checkRegion(region, meshCount);
|
|
169
|
+
return region;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** What one region must be on its own, whoever is asking. */
|
|
173
|
+
function checkRegion(region: DrftRegion, meshCount: number): void {
|
|
174
|
+
const name = `region ${region.id}`;
|
|
175
|
+
const b = region.bounds;
|
|
176
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
177
|
+
if (!((b[axis] as number) <= (b[axis + 3] as number))) {
|
|
178
|
+
throw new DrftError(`${name}'s bounds are inside out on axis ${axis}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (region.levels.length < 1 || region.levels.length > REGION_MAX_LEVELS) {
|
|
182
|
+
throw new DrftError(
|
|
183
|
+
`${name} has ${region.levels.length} levels; 1 to ${REGION_MAX_LEVELS} are allowed`,
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
let previous = 0;
|
|
187
|
+
for (let l = 0; l < region.levels.length; l++) {
|
|
188
|
+
const level = region.levels[l] as DrftRegionLevel;
|
|
189
|
+
if (!(level.error >= previous) || !Number.isFinite(level.error)) {
|
|
190
|
+
throw new DrftError(
|
|
191
|
+
`${name}'s level ${l} has error ${level.error}, finer than the one before`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
previous = level.error;
|
|
195
|
+
for (const mesh of level.meshes) {
|
|
196
|
+
if (mesh >= meshCount) {
|
|
197
|
+
throw new DrftError(`${name} draws mesh ${mesh}, and the file carries ${meshCount}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
const grouped = new Set<number>();
|
|
202
|
+
for (const group of region.instances) {
|
|
203
|
+
if (group.mesh >= meshCount) {
|
|
204
|
+
throw new DrftError(`${name} places mesh ${group.mesh}, and the file carries ${meshCount}`);
|
|
205
|
+
}
|
|
206
|
+
if (group.transforms.length === 0 || group.transforms.length % 16 !== 0) {
|
|
207
|
+
throw new DrftError(`${name}'s group for mesh ${group.mesh} is not whole matrices`);
|
|
208
|
+
}
|
|
209
|
+
if (grouped.has(group.mesh)) throw new DrftError(`${name} groups mesh ${group.mesh} twice`);
|
|
210
|
+
grouped.add(group.mesh);
|
|
211
|
+
}
|
|
212
|
+
if (region.occluders.length % 6 !== 0) {
|
|
213
|
+
throw new DrftError(`${name}'s occluders are not whole boxes`);
|
|
214
|
+
}
|
|
215
|
+
const collision = region.collision;
|
|
216
|
+
if (collision !== null) {
|
|
217
|
+
const vertices = collision.positions.length / 3;
|
|
218
|
+
if (collision.positions.length % 3 !== 0 || collision.indices.length % 3 !== 0) {
|
|
219
|
+
throw new DrftError(`${name}'s collision is not whole vertices and triangles`);
|
|
220
|
+
}
|
|
221
|
+
for (let i = 0; i < collision.indices.length; i++) {
|
|
222
|
+
if ((collision.indices[i] as number) >= vertices) {
|
|
223
|
+
throw new DrftError(
|
|
224
|
+
`${name}'s collision names vertex ${collision.indices[i]} of ${vertices}`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* What regions must be to each other: an id once, a level mesh in one region's one level, and a
|
|
233
|
+
* group's prototype never also a level mesh. Kept across chunks by the writer, the whole-file reader
|
|
234
|
+
* and the stream alike, so a file one of them refuses the others refuse too.
|
|
235
|
+
*/
|
|
236
|
+
export class RegionLedger {
|
|
237
|
+
private readonly ids = new Set<number>();
|
|
238
|
+
private readonly levelMeshes = new Set<number>();
|
|
239
|
+
private readonly prototypes = new Set<number>();
|
|
240
|
+
|
|
241
|
+
admit(region: DrftRegion): void {
|
|
242
|
+
if (this.ids.has(region.id)) throw new DrftError(`region ${region.id} appears twice`);
|
|
243
|
+
this.ids.add(region.id);
|
|
244
|
+
for (const level of region.levels) {
|
|
245
|
+
for (const mesh of level.meshes) {
|
|
246
|
+
if (this.levelMeshes.has(mesh) || this.prototypes.has(mesh)) {
|
|
247
|
+
throw new DrftError(
|
|
248
|
+
`mesh ${mesh} is region ${region.id}'s and something else's; a level mesh belongs to one region's one level`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
this.levelMeshes.add(mesh);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
for (const group of region.instances) {
|
|
255
|
+
if (this.levelMeshes.has(group.mesh)) {
|
|
256
|
+
throw new DrftError(
|
|
257
|
+
`mesh ${group.mesh} is both a level mesh and a prop region ${region.id} places`,
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
this.prototypes.add(group.mesh);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Whether a mesh is any region's, as a level or a prop. */
|
|
265
|
+
names(mesh: number): boolean {
|
|
266
|
+
return this.levelMeshes.has(mesh) || this.prototypes.has(mesh);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Where each region's chunk goes among the meshes: `before[m]` is the regions written just ahead of
|
|
272
|
+
* mesh `m`, and `before[meshCount]` those after the last. A streaming reader then always has a
|
|
273
|
+
* region before the meshes it introduces.
|
|
274
|
+
*
|
|
275
|
+
* The meshes no region introduces come first, then each region's new meshes, in the order the
|
|
276
|
+
* regions are given and ascending. **A region introduces the kit pieces its assemblies copy, where
|
|
277
|
+
* no earlier region did** (`piecesOf` names them), so a piece travels with the first region that
|
|
278
|
+
* needs it rather than at the head of the file: a world's kit is most of its bytes, and a walker's
|
|
279
|
+
* first regions need a sliver of it. A piece ahead of the first mesh any region names is the
|
|
280
|
+
* file's own, as every piece was before a region could carry one, so either layout is valid. That
|
|
281
|
+
* a piece comes ahead of every assembly copying it is the kit's own rule (`checkCopies`). Anything
|
|
282
|
+
* else is refused by name rather than reordered, because the ordinals are what the regions and the
|
|
283
|
+
* materials already point at.
|
|
284
|
+
*/
|
|
285
|
+
export function planRegions(
|
|
286
|
+
regions: readonly DrftRegion[],
|
|
287
|
+
meshCount: number,
|
|
288
|
+
piecesOf: (mesh: number) => ArrayLike<number> | undefined = () => undefined,
|
|
289
|
+
): DrftRegion[][] {
|
|
290
|
+
const ledger = new RegionLedger();
|
|
291
|
+
for (const region of regions) {
|
|
292
|
+
checkRegion(region, meshCount);
|
|
293
|
+
ledger.admit(region);
|
|
294
|
+
}
|
|
295
|
+
/* What each region introduces: its levels' meshes and its props, then the pieces its level
|
|
296
|
+
meshes copy that nobody introduced before it — those past the first mesh any region names;
|
|
297
|
+
a piece ahead of it is the file's own, written first as every piece once was. */
|
|
298
|
+
let firstNamed = meshCount;
|
|
299
|
+
for (let m = 0; m < meshCount; m++) {
|
|
300
|
+
if (ledger.names(m)) {
|
|
301
|
+
firstNamed = m;
|
|
302
|
+
break;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const introduced = new Set<number>();
|
|
306
|
+
const fresh = regions.map((region) => {
|
|
307
|
+
const out: number[] = [];
|
|
308
|
+
const take = (mesh: number): void => {
|
|
309
|
+
if (introduced.has(mesh)) return;
|
|
310
|
+
introduced.add(mesh);
|
|
311
|
+
out.push(mesh);
|
|
312
|
+
};
|
|
313
|
+
for (const level of region.levels) for (const mesh of level.meshes) take(mesh);
|
|
314
|
+
for (const group of region.instances) take(group.mesh);
|
|
315
|
+
for (const level of region.levels) {
|
|
316
|
+
for (const mesh of level.meshes) {
|
|
317
|
+
const pieces = piecesOf(mesh);
|
|
318
|
+
for (let i = 0; i < (pieces?.length ?? 0); i++) {
|
|
319
|
+
const piece = pieces?.[i] as number;
|
|
320
|
+
if (piece > firstNamed && !ledger.names(piece)) take(piece);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return out.sort((a, b) => a - b);
|
|
325
|
+
});
|
|
326
|
+
let next = meshCount - introduced.size;
|
|
327
|
+
for (let m = 0; m < next; m++) {
|
|
328
|
+
if (introduced.has(m)) {
|
|
329
|
+
throw new DrftError(
|
|
330
|
+
`mesh ${m} is a region's and comes before mesh ${next - 1}, which no region names; ` +
|
|
331
|
+
'a file with regions writes the meshes no region names first',
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const before: DrftRegion[][] = [];
|
|
336
|
+
for (let m = 0; m <= meshCount; m++) before.push([]);
|
|
337
|
+
regions.forEach((region, r) => {
|
|
338
|
+
(before[next] as DrftRegion[]).push(region);
|
|
339
|
+
for (const mesh of fresh[r] as number[]) {
|
|
340
|
+
if (mesh !== next) {
|
|
341
|
+
throw new DrftError(
|
|
342
|
+
`region ${region.id} introduces mesh ${mesh} where mesh ${next} is next; ` +
|
|
343
|
+
"write each region's new meshes in the order the regions are written",
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
next++;
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
return before;
|
|
350
|
+
}
|