@driftengine/drft 4.5.0 → 4.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/drftStream.ts
CHANGED
|
@@ -19,9 +19,17 @@ import {
|
|
|
19
19
|
CHUNK_ANIM,
|
|
20
20
|
CHUNK_NODE,
|
|
21
21
|
CHUNK_INST,
|
|
22
|
+
CHUNK_REGN,
|
|
23
|
+
CHUNK_LVOL,
|
|
22
24
|
CHUNK_LITE,
|
|
23
25
|
CHUNK_SDFV,
|
|
26
|
+
CHUNK_COLL,
|
|
27
|
+
CHUNK_NAVM,
|
|
28
|
+
CHUNK_ENTS,
|
|
24
29
|
CHUNK_MSHQ,
|
|
30
|
+
CHUNK_KITS,
|
|
31
|
+
CHUNK_MSHC,
|
|
32
|
+
isMeshChunk,
|
|
25
33
|
CHUNK_SKIN,
|
|
26
34
|
CHUNK_MORP,
|
|
27
35
|
} from './drftFormat.ts';
|
|
@@ -39,6 +47,17 @@ import { readSdfv } from './sdfv.ts';
|
|
|
39
47
|
import type { DrftSdfvEntry } from './sdfv.ts';
|
|
40
48
|
import type { DrftLight } from './drftLights.ts';
|
|
41
49
|
import { decodeQuantisedMesh } from './drftQuantised.ts';
|
|
50
|
+
import { readColliders } from './drftColliders.ts';
|
|
51
|
+
import { expandAssembly } from './assemble.ts';
|
|
52
|
+
import type { PieceLookup } from './assemble.ts';
|
|
53
|
+
import { checkCopies, readAssembly, readKit } from './drftAssembly.ts';
|
|
54
|
+
import type { DrftAssembly } from './drftAssembly.ts';
|
|
55
|
+
import { RegionLedger, readRegion } from './drftRegions.ts';
|
|
56
|
+
import { readLightVolume } from './drftLightVolume.ts';
|
|
57
|
+
import type { DrftLightVolume } from './drftLightVolume.ts';
|
|
58
|
+
import type { DrftRegion } from './drftRegions.ts';
|
|
59
|
+
import { type NavPolyMesh, readNavm } from './navm.ts';
|
|
60
|
+
import { type EntsScene, readEnts } from './ents.ts';
|
|
42
61
|
|
|
43
62
|
/**
|
|
44
63
|
* What the file says is coming, known from the first few kilobytes.
|
|
@@ -93,8 +112,25 @@ export interface DrftStreamHandlers {
|
|
|
93
112
|
* stop drawing it in the same frame it starts drawing the model, or the two are both there.
|
|
94
113
|
*/
|
|
95
114
|
readonly onLod?: (mesh: MeshData, level: number) => void;
|
|
96
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* One mesh, in the order the file lays them out, with its ordinal in that order. A kit piece is
|
|
117
|
+
* not one — it goes to `onPiece` — and an assembly is one only where `onAssembly` is absent, in
|
|
118
|
+
* which case it arrives here expanded.
|
|
119
|
+
*/
|
|
97
120
|
readonly onMesh?: (mesh: MeshData, ordinal: number) => void;
|
|
121
|
+
/**
|
|
122
|
+
* The ordinals of the kit's pieces, once, when `KITS` lands — ahead of the geometry, so a
|
|
123
|
+
* consumer knows which meshes are drawn only as copies before any of them arrives.
|
|
124
|
+
*/
|
|
125
|
+
readonly onKit?: (pieces: readonly number[]) => void;
|
|
126
|
+
/** One kit piece, as it lands. The stream keeps it for the assemblies that copy it. */
|
|
127
|
+
readonly onPiece?: (mesh: MeshData, ordinal: number) => void;
|
|
128
|
+
/**
|
|
129
|
+
* One assembly as the file carried it, with a lookup for its pieces, which have all arrived. A
|
|
130
|
+
* consumer that pages its world keeps it this small and expands it (`expandAssembly`) when it is
|
|
131
|
+
* needed; without this handler the stream expands it and calls `onMesh`.
|
|
132
|
+
*/
|
|
133
|
+
readonly onAssembly?: (assembly: DrftAssembly, ordinal: number, piece: PieceLookup) => void;
|
|
98
134
|
/**
|
|
99
135
|
* One block of a Gaussian splat capture, with its ordinal.
|
|
100
136
|
*
|
|
@@ -117,6 +153,13 @@ export interface DrftStreamHandlers {
|
|
|
117
153
|
* puts it, so a consumer knows a mesh is drawn many times before that mesh arrives.
|
|
118
154
|
*/
|
|
119
155
|
readonly onInstances?: (groups: readonly DrftInstanceGroup[]) => void;
|
|
156
|
+
/**
|
|
157
|
+
* One region of a streamed world, when its `REGN` lands — ahead of the meshes it introduces, so
|
|
158
|
+
* a consumer knows a mesh is a region's level or prop before it arrives. See `drftRegions.ts`.
|
|
159
|
+
*/
|
|
160
|
+
readonly onRegion?: (region: DrftRegion) => void;
|
|
161
|
+
/** A world's summed lights, once, when `LVOL` lands. See `drftLightVolume.ts`. */
|
|
162
|
+
readonly onLightVolume?: (volume: DrftLightVolume) => void;
|
|
120
163
|
/** The lights the scene was authored with, once, when `LITE` lands. See `drftLights.ts`. */
|
|
121
164
|
readonly onLights?: (lights: readonly DrftLight[]) => void;
|
|
122
165
|
/**
|
|
@@ -136,6 +179,18 @@ export interface DrftStreamHandlers {
|
|
|
136
179
|
* the geometry so this is usually possible.
|
|
137
180
|
*/
|
|
138
181
|
readonly onClip?: (clip: AnimationClip, ordinal: number) => void;
|
|
182
|
+
/**
|
|
183
|
+
* The convex hulls the asset collides as, once, when `COLL` lands. See `drftColliders.ts`.
|
|
184
|
+
*
|
|
185
|
+
* These three — collision, the navigation mesh and the entities — were `readDrft`'s alone, so a
|
|
186
|
+
* scene that streamed its geometry had to fetch the file again, whole, to learn where it could
|
|
187
|
+
* walk and what stood in it.
|
|
188
|
+
*/
|
|
189
|
+
readonly onColliders?: (hulls: readonly Float32Array[]) => void;
|
|
190
|
+
/** The navigation mesh, once, when `NAVM` lands. See `navm.ts`. */
|
|
191
|
+
readonly onNavigation?: (navigation: NavPolyMesh) => void;
|
|
192
|
+
/** The serialised entities, once, when `ENTS` lands. See `ents.ts`. */
|
|
193
|
+
readonly onEntities?: (entities: EntsScene) => void;
|
|
139
194
|
readonly onMaterials?: (materials: readonly DrftMaterial[]) => void;
|
|
140
195
|
readonly onTexture?: (texture: DrftTexture, ordinal: number) => void;
|
|
141
196
|
}
|
|
@@ -171,10 +226,20 @@ export class DrftStream {
|
|
|
171
226
|
private preludeBytes = 0;
|
|
172
227
|
private received = 0;
|
|
173
228
|
private chunks: DrftChunk[] = [];
|
|
229
|
+
/** What the regions so far claim, so a second claim on a mesh is refused as `readDrft` refuses it. */
|
|
230
|
+
private readonly regions = new RegionLedger();
|
|
174
231
|
/** How many table entries have been reported complete, in table order. */
|
|
175
232
|
private settled = 0;
|
|
176
233
|
private meshOrdinal = 0;
|
|
177
234
|
private textureOrdinal = 0;
|
|
235
|
+
/** The kit's pieces once `KITS` lands, and each piece as it arrives. */
|
|
236
|
+
private kit: Set<number> | null = null;
|
|
237
|
+
private readonly pieces = new Map<number, MeshData>();
|
|
238
|
+
private readonly pieceAt: PieceLookup = (o) => {
|
|
239
|
+
const piece = this.pieces.get(o);
|
|
240
|
+
if (piece === undefined) throw new DrftError(`kit piece ${o} has not arrived`);
|
|
241
|
+
return piece;
|
|
242
|
+
};
|
|
178
243
|
private head: DrftHead | null = null;
|
|
179
244
|
private meshCount = 0;
|
|
180
245
|
private materialCount = 0;
|
|
@@ -350,7 +415,7 @@ export class DrftStream {
|
|
|
350
415
|
* counted in `meshCount`, which is the number of parts — an outline is not a part, and
|
|
351
416
|
* a readout saying "1 of 188 parts" would be counting the model twice.
|
|
352
417
|
*/
|
|
353
|
-
if (chunk.code
|
|
418
|
+
if (isMeshChunk(chunk.code) || chunk.code === CHUNK_LODM) {
|
|
354
419
|
meshBytes += chunk.byteLength;
|
|
355
420
|
}
|
|
356
421
|
if (chunk.code === CHUNK_TEXS) textureBytes += chunk.byteLength;
|
|
@@ -371,7 +436,7 @@ export class DrftStream {
|
|
|
371
436
|
this.manifestSent = true;
|
|
372
437
|
this.handlers.onManifest?.({
|
|
373
438
|
totalBytes: this.buffer.byteLength,
|
|
374
|
-
meshCount: this.chunks.filter((c) => c.code
|
|
439
|
+
meshCount: this.chunks.filter((c) => isMeshChunk(c.code)).length,
|
|
375
440
|
textureCount: this.chunks.filter((c) => c.code === CHUNK_TEXS).length,
|
|
376
441
|
lodCount: this.chunks.filter((c) => c.code === CHUNK_LODM).length,
|
|
377
442
|
splatBlockCount: this.chunks.filter((c) => c.code === CHUNK_SPLT).length,
|
|
@@ -400,7 +465,36 @@ export class DrftStream {
|
|
|
400
465
|
? readMesh(this.buffer, chunk)
|
|
401
466
|
: decodeQuantisedMesh(this.buffer, chunk.offset, chunk.byteLength);
|
|
402
467
|
this.meshCount++;
|
|
403
|
-
|
|
468
|
+
const ordinal = this.meshOrdinal++;
|
|
469
|
+
if (this.kit?.has(ordinal) === true) {
|
|
470
|
+
this.pieces.set(ordinal, mesh);
|
|
471
|
+
this.handlers.onPiece?.(mesh, ordinal);
|
|
472
|
+
} else {
|
|
473
|
+
this.handlers.onMesh?.(mesh, ordinal);
|
|
474
|
+
}
|
|
475
|
+
} else if (chunk.code === CHUNK_MSHC) {
|
|
476
|
+
const assembly = readAssembly(this.buffer, chunk.offset, chunk.byteLength);
|
|
477
|
+
this.meshCount++;
|
|
478
|
+
const ordinal = this.meshOrdinal++;
|
|
479
|
+
if (this.kit?.has(ordinal) === true) {
|
|
480
|
+
throw new DrftError(`mesh ${ordinal} is a kit piece and an assembly`);
|
|
481
|
+
}
|
|
482
|
+
checkCopies(assembly, ordinal, this.kit ?? new Set(), (o) => this.pieces.get(o));
|
|
483
|
+
if (this.handlers.onAssembly !== undefined) {
|
|
484
|
+
this.handlers.onAssembly(assembly, ordinal, this.pieceAt);
|
|
485
|
+
} else {
|
|
486
|
+
this.handlers.onMesh?.(expandAssembly(assembly, this.pieceAt), ordinal);
|
|
487
|
+
}
|
|
488
|
+
} else if (chunk.code === CHUNK_KITS) {
|
|
489
|
+
const meshes = this.chunks.filter((c) => isMeshChunk(c.code)).length;
|
|
490
|
+
const kit = readKit(this.buffer, chunk.offset, chunk.byteLength, meshes);
|
|
491
|
+
/* A piece already handed over as a mesh has been drawn; the writer puts the kit first. */
|
|
492
|
+
const early = kit.find((o) => o < this.meshOrdinal);
|
|
493
|
+
if (early !== undefined) {
|
|
494
|
+
throw new DrftError(`KITS arrives after mesh ${early}, one of its pieces`);
|
|
495
|
+
}
|
|
496
|
+
this.kit = new Set(kit);
|
|
497
|
+
this.handlers.onKit?.(kit);
|
|
404
498
|
} else if (chunk.code === CHUNK_SPLT) {
|
|
405
499
|
/* The ordinal the file gave it, so blocks append in the writer's order however they
|
|
406
500
|
arrived — which for a sequential fetch is the same order, and for a range-fetching
|
|
@@ -417,12 +511,23 @@ export class DrftStream {
|
|
|
417
511
|
} else if (chunk.code === CHUNK_NODE) {
|
|
418
512
|
this.handlers.onNodes?.(readNodes(this.buffer, chunk.offset, chunk.byteLength));
|
|
419
513
|
} else if (chunk.code === CHUNK_INST) {
|
|
420
|
-
const meshes = this.chunks.filter(
|
|
421
|
-
(c) => c.code === CHUNK_MESH || c.code === CHUNK_MSHQ,
|
|
422
|
-
).length;
|
|
514
|
+
const meshes = this.chunks.filter((c) => isMeshChunk(c.code)).length;
|
|
423
515
|
this.handlers.onInstances?.(
|
|
424
516
|
readInstances(this.buffer, chunk.offset, chunk.byteLength, meshes),
|
|
425
517
|
);
|
|
518
|
+
} else if (chunk.code === CHUNK_REGN) {
|
|
519
|
+
const meshes = this.chunks.filter((c) => isMeshChunk(c.code)).length;
|
|
520
|
+
const region = readRegion(this.buffer, chunk.offset, chunk.byteLength, meshes);
|
|
521
|
+
this.regions.admit(region);
|
|
522
|
+
this.handlers.onRegion?.(region);
|
|
523
|
+
} else if (chunk.code === CHUNK_LVOL) {
|
|
524
|
+
this.handlers.onLightVolume?.(readLightVolume(this.buffer, chunk.offset, chunk.byteLength));
|
|
525
|
+
} else if (chunk.code === CHUNK_COLL) {
|
|
526
|
+
this.handlers.onColliders?.(readColliders(this.buffer, chunk.offset, chunk.byteLength));
|
|
527
|
+
} else if (chunk.code === CHUNK_NAVM) {
|
|
528
|
+
this.handlers.onNavigation?.(readNavm(this.buffer, chunk.offset, chunk.byteLength));
|
|
529
|
+
} else if (chunk.code === CHUNK_ENTS) {
|
|
530
|
+
this.handlers.onEntities?.(readEnts(this.buffer, chunk.offset, chunk.byteLength));
|
|
426
531
|
} else if (chunk.code === CHUNK_LITE) {
|
|
427
532
|
this.handlers.onLights?.(readLights(this.buffer, chunk.offset, chunk.byteLength));
|
|
428
533
|
} else if (chunk.code === CHUNK_SDFV) {
|
package/src/drftWrite.ts
CHANGED
|
@@ -25,6 +25,8 @@ import {
|
|
|
25
25
|
ATTR_SPECULAR,
|
|
26
26
|
ATTR_UVS,
|
|
27
27
|
ATTR_WEIGHTS,
|
|
28
|
+
ATTR_LAYERS,
|
|
29
|
+
ATTR_CHANNEL,
|
|
28
30
|
CHUNK_ENTRY_BYTES,
|
|
29
31
|
CHUNK_HEAD,
|
|
30
32
|
CHUNK_ANIM,
|
|
@@ -36,8 +38,12 @@ import {
|
|
|
36
38
|
CHUNK_MORP,
|
|
37
39
|
CHUNK_NODE,
|
|
38
40
|
CHUNK_INST,
|
|
41
|
+
CHUNK_REGN,
|
|
42
|
+
CHUNK_LVOL,
|
|
39
43
|
CHUNK_LITE,
|
|
40
44
|
CHUNK_MSHQ,
|
|
45
|
+
CHUNK_KITS,
|
|
46
|
+
CHUNK_MSHC,
|
|
41
47
|
CHUNK_SKIN,
|
|
42
48
|
CHUNK_SPLT,
|
|
43
49
|
SPLAT_BLOCK_PREFIX,
|
|
@@ -70,10 +76,32 @@ import { type NavPolyMesh, buildNavm } from './navm.ts';
|
|
|
70
76
|
import { type DrftGraph, buildNgrf } from './ngrf.ts';
|
|
71
77
|
import { buildColliders } from './drftColliders.ts';
|
|
72
78
|
import { buildInstances } from './drftInstances.ts';
|
|
79
|
+
import { buildRegion, planRegions } from './drftRegions.ts';
|
|
80
|
+
import { buildLightVolume } from './drftLightVolume.ts';
|
|
81
|
+
import type { DrftLightVolume } from './drftLightVolume.ts';
|
|
82
|
+
import type { DrftRegion } from './drftRegions.ts';
|
|
83
|
+
|
|
84
|
+
/** Whether a region names a mesh, as a level or a prop. */
|
|
85
|
+
function regionNames(region: DrftRegion, mesh: number): boolean {
|
|
86
|
+
return (
|
|
87
|
+
region.levels.some((level) => level.meshes.includes(mesh)) ||
|
|
88
|
+
region.instances.some((group) => group.mesh === mesh)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
73
91
|
import type { DrftInstanceGroup } from './drftInstances.ts';
|
|
74
92
|
import { buildLights } from './drftLights.ts';
|
|
75
93
|
import type { DrftLight } from './drftLights.ts';
|
|
76
94
|
import { encodeQuantisedMesh } from './drftQuantised.ts';
|
|
95
|
+
import { assemblyBounds } from './assemble.ts';
|
|
96
|
+
import {
|
|
97
|
+
buildAssembly,
|
|
98
|
+
buildKit,
|
|
99
|
+
checkAssembly,
|
|
100
|
+
checkCopies,
|
|
101
|
+
checkKit,
|
|
102
|
+
isAssembly,
|
|
103
|
+
} from './drftAssembly.ts';
|
|
104
|
+
import type { DrftAssembly } from './drftAssembly.ts';
|
|
77
105
|
|
|
78
106
|
/** An image to embed, already compressed, with what its own header said about it. */
|
|
79
107
|
export interface DrftTextureSource {
|
|
@@ -94,7 +122,18 @@ export interface DrftTextureSource {
|
|
|
94
122
|
/** What a caller hands over to be baked. */
|
|
95
123
|
export interface DrftSource {
|
|
96
124
|
readonly head?: Partial<DrftHead>;
|
|
97
|
-
|
|
125
|
+
/**
|
|
126
|
+
* The meshes by ordinal. An assembly — painted copies of kit pieces — stands in a slot like any
|
|
127
|
+
* mesh and is written as one required `MSHC` chunk; see `drftAssembly.ts`.
|
|
128
|
+
*/
|
|
129
|
+
readonly meshes: readonly (MeshData | DrftAssembly)[];
|
|
130
|
+
/**
|
|
131
|
+
* The ordinals of the meshes that are kit pieces, ascending: drawn only as copies inside the
|
|
132
|
+
* assemblies, never on their own. One required `KITS` chunk, ahead of the geometry. A piece
|
|
133
|
+
* comes before every assembly that copies it, is never an assembly itself, and is neither placed
|
|
134
|
+
* by `INST` nor named by a region.
|
|
135
|
+
*/
|
|
136
|
+
readonly kit?: readonly number[];
|
|
98
137
|
/**
|
|
99
138
|
* One material per mesh, by ordinal, or absent for an asset that names none.
|
|
100
139
|
*
|
|
@@ -168,6 +207,19 @@ export interface DrftSource {
|
|
|
168
207
|
* a group names holds one copy; see `drftInstances.ts`.
|
|
169
208
|
*/
|
|
170
209
|
readonly instances?: readonly DrftInstanceGroup[];
|
|
210
|
+
/**
|
|
211
|
+
* The regions of a streamed world: one required `REGN` chunk each, written just ahead of the
|
|
212
|
+
* meshes it introduces. The meshes no region names come first, then each region's new meshes in
|
|
213
|
+
* region order — the writer refuses any other order rather than renumbering what the regions and
|
|
214
|
+
* the materials point at. See `drftRegions.ts`.
|
|
215
|
+
*/
|
|
216
|
+
readonly regions?: readonly DrftRegion[];
|
|
217
|
+
/**
|
|
218
|
+
* A world's fixed lights summed offline into one dense volume: one optional `LVOL` chunk, after
|
|
219
|
+
* the geometry, since the world draws lit by its exact lights until it lands. See
|
|
220
|
+
* `drftLightVolume.ts`.
|
|
221
|
+
*/
|
|
222
|
+
readonly lightVolume?: DrftLightVolume;
|
|
171
223
|
/** The lights the scene was authored with: one `LITE` chunk, optional. See `drftLights.ts`. */
|
|
172
224
|
readonly lights?: readonly DrftLight[];
|
|
173
225
|
/**
|
|
@@ -211,6 +263,13 @@ export interface DrftSource {
|
|
|
211
263
|
* carry geometry with no scene at all — which is every baked model this format has ever held.
|
|
212
264
|
*/
|
|
213
265
|
readonly entities?: EntsScene;
|
|
266
|
+
/**
|
|
267
|
+
* The textures ahead of the geometry rather than after it. A model wants its outline before its
|
|
268
|
+
* pictures, so a viewer sees a shape early; a world whose every region wears the same texture
|
|
269
|
+
* arrays wants the pictures before its first region, or that region waits for the file's end.
|
|
270
|
+
* What it costs a model that asks: its first part arrives later by the size of its pictures.
|
|
271
|
+
*/
|
|
272
|
+
readonly texturesFirst?: boolean;
|
|
214
273
|
}
|
|
215
274
|
|
|
216
275
|
/**
|
|
@@ -315,8 +374,9 @@ function encodeString(value: string): Uint8Array {
|
|
|
315
374
|
|
|
316
375
|
function buildHead(
|
|
317
376
|
head: Partial<DrftHead> | undefined,
|
|
318
|
-
meshes: readonly MeshData[],
|
|
377
|
+
meshes: readonly (MeshData | DrftAssembly)[],
|
|
319
378
|
instances: readonly DrftInstanceGroup[] = [],
|
|
379
|
+
kit: ReadonlySet<number> = new Set(),
|
|
320
380
|
): PendingChunk {
|
|
321
381
|
/*
|
|
322
382
|
* Bounds are computed rather than taken on trust. A caller that gets them wrong would
|
|
@@ -331,8 +391,23 @@ function buildHead(
|
|
|
331
391
|
let maxZ = -Infinity;
|
|
332
392
|
const placed = new Map<number, Float32Array>();
|
|
333
393
|
for (const group of instances) placed.set(group.mesh, group.transforms);
|
|
394
|
+
const pieceAt = (o: number): MeshData => meshes[o] as MeshData;
|
|
334
395
|
for (let m = 0; m < meshes.length; m++) {
|
|
335
|
-
|
|
396
|
+
/* A kit piece is drawn only where it is copied, and its copies are counted with them. */
|
|
397
|
+
if (kit.has(m)) continue;
|
|
398
|
+
const slot = meshes[m] as MeshData | DrftAssembly;
|
|
399
|
+
if (isAssembly(slot)) {
|
|
400
|
+
const b = assemblyBounds(slot, pieceAt);
|
|
401
|
+
if (!((b[0] as number) <= (b[3] as number))) continue;
|
|
402
|
+
minX = Math.min(minX, b[0] as number);
|
|
403
|
+
minY = Math.min(minY, b[1] as number);
|
|
404
|
+
minZ = Math.min(minZ, b[2] as number);
|
|
405
|
+
maxX = Math.max(maxX, b[3] as number);
|
|
406
|
+
maxY = Math.max(maxY, b[4] as number);
|
|
407
|
+
maxZ = Math.max(maxZ, b[5] as number);
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
const mesh = slot;
|
|
336
411
|
const transforms = placed.get(m);
|
|
337
412
|
if (transforms === undefined) {
|
|
338
413
|
for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
|
|
@@ -446,6 +521,8 @@ function buildMesh(mesh: MeshData, code = CHUNK_MESH, flags = CHUNK_REQUIRED): P
|
|
|
446
521
|
these two bits are always set together and the reader may rely on it. */
|
|
447
522
|
if (mesh.joints !== undefined) attributes |= ATTR_JOINTS;
|
|
448
523
|
if (mesh.weights !== undefined) attributes |= ATTR_WEIGHTS;
|
|
524
|
+
if (mesh.layers !== undefined) attributes |= ATTR_LAYERS;
|
|
525
|
+
if (mesh.channel !== undefined) attributes |= ATTR_CHANNEL;
|
|
449
526
|
|
|
450
527
|
/* Order is frozen: mandatory arrays, then optional ones by ascending bit. */
|
|
451
528
|
const arrays: (Float32Array | Uint32Array)[] = [
|
|
@@ -465,6 +542,10 @@ function buildMesh(mesh: MeshData, code = CHUNK_MESH, flags = CHUNK_REQUIRED): P
|
|
|
465
542
|
container validated this pair for four minor versions while writing neither of them. */
|
|
466
543
|
if (mesh.joints !== undefined) arrays.push(mesh.joints);
|
|
467
544
|
if (mesh.weights !== undefined) arrays.push(mesh.weights);
|
|
545
|
+
/* Newest of all, 1.20, so last. See ATTR_LAYERS. */
|
|
546
|
+
if (mesh.layers !== undefined) arrays.push(mesh.layers);
|
|
547
|
+
/* And newer still, 1.23. See ATTR_CHANNEL. */
|
|
548
|
+
if (mesh.channel !== undefined) arrays.push(mesh.channel);
|
|
468
549
|
arrays.push(mesh.indices);
|
|
469
550
|
|
|
470
551
|
const PREFIX = 16;
|
|
@@ -628,7 +709,30 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
628
709
|
* same asset less its textures rather than a refusal. Moving it earlier in the file changes
|
|
629
710
|
* nothing about that, because a chunk is found through the table rather than by position.
|
|
630
711
|
*/
|
|
631
|
-
const
|
|
712
|
+
const kit = new Set(source.kit ?? []);
|
|
713
|
+
checkKit(source.kit ?? [], source.meshes.length);
|
|
714
|
+
source.meshes.forEach((slot, ordinal) => {
|
|
715
|
+
if (!isAssembly(slot)) return;
|
|
716
|
+
checkAssembly(slot, `mesh ${ordinal}`);
|
|
717
|
+
checkCopies(slot, ordinal, kit, (o) => {
|
|
718
|
+
const piece = source.meshes[o];
|
|
719
|
+
return piece === undefined || isAssembly(piece) ? undefined : piece;
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
for (const o of kit) {
|
|
723
|
+
if (isAssembly(source.meshes[o] as MeshData | DrftAssembly)) {
|
|
724
|
+
throw new DrftError(`mesh ${o} is a kit piece and an assembly; a piece is vertices`);
|
|
725
|
+
}
|
|
726
|
+
if ((source.instances ?? []).some((g) => g.mesh === o)) {
|
|
727
|
+
throw new DrftError(`mesh ${o} is a kit piece and is placed by INST`);
|
|
728
|
+
}
|
|
729
|
+
if ((source.regions ?? []).some((r) => regionNames(r, o))) {
|
|
730
|
+
throw new DrftError(
|
|
731
|
+
`mesh ${o} is a kit piece and a region's; a piece is drawn only as copies`,
|
|
732
|
+
);
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const chunks: PendingChunk[] = [buildHead(source.head, source.meshes, source.instances, kit)];
|
|
632
736
|
/*
|
|
633
737
|
* The outline first, ahead of even the paint. It is the one thing that puts a recognisable
|
|
634
738
|
* whole object on screen, it is a few hundred kilobytes against tens of megabytes, and the
|
|
@@ -677,6 +781,11 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
677
781
|
bytes: buildInstances(source.instances as readonly DrftInstanceGroup[]),
|
|
678
782
|
});
|
|
679
783
|
}
|
|
784
|
+
/* The kit ahead of the geometry, for the placements' reason: a streaming consumer must know a
|
|
785
|
+
mesh is a piece before it lands, or it has already drawn it at the origin. */
|
|
786
|
+
if (kit.size > 0) {
|
|
787
|
+
chunks.push({ code: CHUNK_KITS, flags: CHUNK_REQUIRED, bytes: buildKit(source.kit ?? []) });
|
|
788
|
+
}
|
|
680
789
|
/* Small, and ahead of the geometry, so a streaming consumer can place its lamps early. */
|
|
681
790
|
if ((source.lights?.length ?? 0) > 0) {
|
|
682
791
|
chunks.push({ code: CHUNK_LITE, flags: 0, bytes: buildLights(source.lights ?? []) });
|
|
@@ -707,7 +816,22 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
707
816
|
if (source.entities !== undefined) {
|
|
708
817
|
chunks.push({ code: CHUNK_ENTS, flags: 0, bytes: buildEnts(source.entities) });
|
|
709
818
|
}
|
|
819
|
+
if (source.texturesFirst === true) {
|
|
820
|
+
for (const texture of source.textures ?? []) chunks.push(buildTexture(texture));
|
|
821
|
+
}
|
|
822
|
+
const regionsBefore = planRegions(source.regions ?? [], source.meshes.length, (m) => {
|
|
823
|
+
const mesh = source.meshes[m];
|
|
824
|
+
return mesh !== undefined && isAssembly(mesh) ? mesh.pieces : undefined;
|
|
825
|
+
});
|
|
826
|
+
for (const group of source.instances ?? []) {
|
|
827
|
+
if ((source.regions ?? []).some((r) => regionNames(r, group.mesh))) {
|
|
828
|
+
throw new DrftError(`mesh ${group.mesh} is placed by INST and is also a region's`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
710
831
|
source.meshes.forEach((mesh, ordinal) => {
|
|
832
|
+
for (const region of regionsBefore[ordinal] ?? []) {
|
|
833
|
+
chunks.push({ code: CHUNK_REGN, flags: CHUNK_REQUIRED, bytes: buildRegion(region) });
|
|
834
|
+
}
|
|
711
835
|
/*
|
|
712
836
|
* **Its deltas immediately before it, not after.** A streaming consumer uploads a mesh the
|
|
713
837
|
* moment its chunk lands, so deltas arriving afterwards are deltas for geometry already on the
|
|
@@ -715,6 +839,10 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
715
839
|
* them in hand when it builds. The mesh ordinal in the payload still does the pairing, so a
|
|
716
840
|
* reader meeting them in another order is unaffected.
|
|
717
841
|
*/
|
|
842
|
+
if (isAssembly(mesh)) {
|
|
843
|
+
chunks.push({ code: CHUNK_MSHC, flags: CHUNK_REQUIRED, bytes: buildAssembly(mesh) });
|
|
844
|
+
return;
|
|
845
|
+
}
|
|
718
846
|
if (mesh.morphTargets !== undefined && mesh.morphTargetCount !== undefined) {
|
|
719
847
|
chunks.push({
|
|
720
848
|
code: CHUNK_MORP,
|
|
@@ -733,7 +861,15 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
733
861
|
chunks.push(buildMesh(mesh));
|
|
734
862
|
}
|
|
735
863
|
});
|
|
736
|
-
for (const
|
|
864
|
+
for (const region of regionsBefore[source.meshes.length] ?? []) {
|
|
865
|
+
chunks.push({ code: CHUNK_REGN, flags: CHUNK_REQUIRED, bytes: buildRegion(region) });
|
|
866
|
+
}
|
|
867
|
+
if (source.lightVolume !== undefined) {
|
|
868
|
+
chunks.push({ code: CHUNK_LVOL, flags: 0, bytes: buildLightVolume(source.lightVolume) });
|
|
869
|
+
}
|
|
870
|
+
if (source.texturesFirst !== true) {
|
|
871
|
+
for (const texture of source.textures ?? []) chunks.push(buildTexture(texture));
|
|
872
|
+
}
|
|
737
873
|
/* One `SUBS` for the file, and only where something was labelled — an empty chunk would cost
|
|
738
874
|
sixteen bytes to say nothing, and rule 2 already covers its absence. */
|
|
739
875
|
if ((source.substances?.length ?? 0) > 0) {
|
package/src/index.ts
CHANGED
|
@@ -70,6 +70,46 @@ export { buildInstances, readInstances } from './drftInstances.ts';
|
|
|
70
70
|
export { buildLights, readLights } from './drftLights.ts';
|
|
71
71
|
export type { DrftLight, DrftLightKind } from './drftLights.ts';
|
|
72
72
|
export type { DrftInstanceGroup } from './drftInstances.ts';
|
|
73
|
+
export {
|
|
74
|
+
REGION_MAX_LEVELS,
|
|
75
|
+
RegionLedger,
|
|
76
|
+
buildRegion,
|
|
77
|
+
planRegions,
|
|
78
|
+
readRegion,
|
|
79
|
+
} from './drftRegions.ts';
|
|
80
|
+
/* A mesh carried as painted copies of kit pieces, expanded where it is needed. The attribute bits
|
|
81
|
+
are a MESH's, which is what an assembly declares its optional arrays with. */
|
|
82
|
+
export {
|
|
83
|
+
ATTR_CHANNEL,
|
|
84
|
+
ATTR_EMISSIVE_COLOR,
|
|
85
|
+
ATTR_GRAIN,
|
|
86
|
+
ATTR_LAYERS,
|
|
87
|
+
ATTR_RELIEF,
|
|
88
|
+
ATTR_ROUGHNESS,
|
|
89
|
+
ATTR_SPECULAR,
|
|
90
|
+
ATTR_TANGENT,
|
|
91
|
+
ATTR_UVS,
|
|
92
|
+
} from './drftFormat.ts';
|
|
93
|
+
export {
|
|
94
|
+
ASSEMBLY_ATTRIBUTES,
|
|
95
|
+
COPY_MATRIX_FLOATS,
|
|
96
|
+
COPY_UV_FLOATS,
|
|
97
|
+
SURFACE,
|
|
98
|
+
SURFACE_FLOATS,
|
|
99
|
+
buildAssembly,
|
|
100
|
+
buildKit,
|
|
101
|
+
checkAssembly,
|
|
102
|
+
checkCopies,
|
|
103
|
+
isAssembly,
|
|
104
|
+
readAssembly,
|
|
105
|
+
readKit,
|
|
106
|
+
} from './drftAssembly.ts';
|
|
107
|
+
export type { DrftAssembly } from './drftAssembly.ts';
|
|
108
|
+
export { assemblyBounds, expandAssembly } from './assemble.ts';
|
|
109
|
+
export type { PieceLookup } from './assemble.ts';
|
|
110
|
+
export type { DrftRegion, DrftRegionCollision, DrftRegionLevel } from './drftRegions.ts';
|
|
111
|
+
export { buildLightVolume, readLightVolume } from './drftLightVolume.ts';
|
|
112
|
+
export type { DrftLightVolume } from './drftLightVolume.ts';
|
|
73
113
|
/*
|
|
74
114
|
* **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
|
|
75
115
|
* carries no semantics — this package is the container and does not know what a channel means —
|
package/src/meshData.ts
CHANGED
|
@@ -107,6 +107,25 @@ export interface MeshData {
|
|
|
107
107
|
* separates asphalt from plaster, and the pass states it: see `Renderer.setSurfaceRelief`.
|
|
108
108
|
*/
|
|
109
109
|
relief?: Float32Array;
|
|
110
|
+
/**
|
|
111
|
+
* Which image of a texture array each vertex's face wears: one whole number a vertex, 0 the first
|
|
112
|
+
* layer. Absent means layer 0, which is the only layer a plain texture has.
|
|
113
|
+
*
|
|
114
|
+
* **What it is for is a merged mesh wearing many images in one draw.** A block of forty buildings
|
|
115
|
+
* with forty facades is forty materials — forty draws, forty material changes — when each image
|
|
116
|
+
* is its own texture; with the facades as the layers of one array and each face naming its layer,
|
|
117
|
+
* it is one. `Renderer.createSurfaceTextureArray` builds the array, and every map of a material
|
|
118
|
+
* (albedo, normal, ORM, emissive) is read at the same layer, so an albedo and its emissive twin
|
|
119
|
+
* share an index.
|
|
120
|
+
*
|
|
121
|
+
* **It travels in the texture-coordinate attribute, as its third component**, because all
|
|
122
|
+
* sixteen of WebGL2's guaranteed vertex locations are spent in the instanced variant. So it needs
|
|
123
|
+
* `uvs` beside it and is refused without them: a layer with no coordinates addresses nothing.
|
|
124
|
+
*
|
|
125
|
+
* Whole numbers, checked, because the shader rounds to the nearest layer and a 1.5 would land on
|
|
126
|
+
* whichever side rounding favoured on that device.
|
|
127
|
+
*/
|
|
128
|
+
layers?: Float32Array;
|
|
110
129
|
/**
|
|
111
130
|
* Four floats per vertex, and the only attribute whose lanes mean four different things.
|
|
112
131
|
*
|
|
@@ -235,6 +254,25 @@ export function validateMeshData(data: MeshData): void {
|
|
|
235
254
|
check('grain', data.grain, 1);
|
|
236
255
|
check('relief', data.relief, 1);
|
|
237
256
|
check('channel', data.channel, 4);
|
|
257
|
+
check('layers', data.layers, 1);
|
|
258
|
+
if (data.layers !== undefined) {
|
|
259
|
+
if (data.uvs === undefined) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
'Mesh: layers without uvs — a texture-array layer travels as the third texture coordinate, ' +
|
|
262
|
+
'so a mesh that names layers has to supply the two it rides with',
|
|
263
|
+
);
|
|
264
|
+
}
|
|
265
|
+
const layers = data.layers;
|
|
266
|
+
for (let v = 0; v < layers.length; v++) {
|
|
267
|
+
const layer = layers[v] as number;
|
|
268
|
+
if (!(layer >= 0) || Math.floor(layer) !== layer) {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`Mesh: layer ${layer} at vertex ${v} — a texture-array layer is a whole number at or ` +
|
|
271
|
+
'above zero, and anything else lands on whichever layer the device rounds it to',
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
238
276
|
/*
|
|
239
277
|
* `tangents` was declared optional on 2026-08-22 and was not checked here until 2026-08-25, which
|
|
240
278
|
* made the widest optional attribute in the format the one a short buffer could reach a driver
|