@driftengine/drft 4.2.1 → 4.4.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/drftFormat.d.ts +31 -1
- package/dist/drftFormat.js +17 -2
- package/dist/drftInstances.d.ts +9 -0
- package/dist/drftInstances.js +68 -0
- package/dist/drftLights.d.ts +21 -0
- package/dist/drftLights.js +108 -0
- package/dist/drftQuantised.d.ts +3 -0
- package/dist/drftQuantised.js +362 -0
- package/dist/drftRead.d.ts +9 -0
- package/dist/drftRead.js +22 -1
- package/dist/drftStream.d.ts +43 -1
- package/dist/drftStream.js +41 -6
- package/dist/drftWrite.d.ts +15 -0
- package/dist/drftWrite.js +110 -20
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -2
- package/dist/meshData.js +9 -2
- package/dist/sdfv.d.ts +15 -0
- package/dist/sdfv.js +15 -0
- package/package.json +4 -4
- package/src/drftFormat.ts +36 -2
- package/src/drftInstances.ts +81 -0
- package/src/drftLights.ts +131 -0
- package/src/drftQuantised.ts +401 -0
- package/src/drftRead.ts +32 -0
- package/src/drftStream.ts +88 -4
- package/src/drftWrite.ts +125 -13
- package/src/index.ts +14 -3
- package/src/meshData.ts +9 -2
- package/src/sdfv.ts +16 -0
package/src/drftWrite.ts
CHANGED
|
@@ -35,6 +35,9 @@ import {
|
|
|
35
35
|
CHUNK_LODF,
|
|
36
36
|
CHUNK_MORP,
|
|
37
37
|
CHUNK_NODE,
|
|
38
|
+
CHUNK_INST,
|
|
39
|
+
CHUNK_LITE,
|
|
40
|
+
CHUNK_MSHQ,
|
|
38
41
|
CHUNK_SKIN,
|
|
39
42
|
CHUNK_SPLT,
|
|
40
43
|
SPLAT_BLOCK_PREFIX,
|
|
@@ -66,6 +69,11 @@ import { type EntsScene, buildEnts } from './ents.ts';
|
|
|
66
69
|
import { type NavPolyMesh, buildNavm } from './navm.ts';
|
|
67
70
|
import { type DrftGraph, buildNgrf } from './ngrf.ts';
|
|
68
71
|
import { buildColliders } from './drftColliders.ts';
|
|
72
|
+
import { buildInstances } from './drftInstances.ts';
|
|
73
|
+
import type { DrftInstanceGroup } from './drftInstances.ts';
|
|
74
|
+
import { buildLights } from './drftLights.ts';
|
|
75
|
+
import type { DrftLight } from './drftLights.ts';
|
|
76
|
+
import { encodeQuantisedMesh } from './drftQuantised.ts';
|
|
69
77
|
|
|
70
78
|
/** An image to embed, already compressed, with what its own header said about it. */
|
|
71
79
|
export interface DrftTextureSource {
|
|
@@ -155,6 +163,19 @@ export interface DrftSource {
|
|
|
155
163
|
* the meshes exactly as it always did, which is why this is additive.
|
|
156
164
|
*/
|
|
157
165
|
readonly nodes?: readonly DrftNode[];
|
|
166
|
+
/**
|
|
167
|
+
* Meshes drawn many times: one `INST` chunk, written ahead of the geometry and required. The mesh
|
|
168
|
+
* a group names holds one copy; see `drftInstances.ts`.
|
|
169
|
+
*/
|
|
170
|
+
readonly instances?: readonly DrftInstanceGroup[];
|
|
171
|
+
/** The lights the scene was authored with: one `LITE` chunk, optional. See `drftLights.ts`. */
|
|
172
|
+
readonly lights?: readonly DrftLight[];
|
|
173
|
+
/**
|
|
174
|
+
* Write each mesh as `MSHQ` rather than `MESH`: constants, 16-bit ranges and octahedral
|
|
175
|
+
* directions, about a third of the bytes, within the bounds `drftQuantised.ts` states. Off unless
|
|
176
|
+
* asked, so a caller writing a file gets the bytes it always got; the baker asks by default.
|
|
177
|
+
*/
|
|
178
|
+
readonly quantise?: boolean;
|
|
158
179
|
/** Skins, one `SKIN` chunk each, or absent for an asset that deforms nothing. */
|
|
159
180
|
readonly skins?: readonly DrftSkin[];
|
|
160
181
|
/** Clips, one `ANIM` chunk each, or absent for an asset that animates nothing. */
|
|
@@ -292,7 +313,11 @@ function encodeString(value: string): Uint8Array {
|
|
|
292
313
|
return bytes;
|
|
293
314
|
}
|
|
294
315
|
|
|
295
|
-
function buildHead(
|
|
316
|
+
function buildHead(
|
|
317
|
+
head: Partial<DrftHead> | undefined,
|
|
318
|
+
meshes: readonly MeshData[],
|
|
319
|
+
instances: readonly DrftInstanceGroup[] = [],
|
|
320
|
+
): PendingChunk {
|
|
296
321
|
/*
|
|
297
322
|
* Bounds are computed rather than taken on trust. A caller that gets them wrong would
|
|
298
323
|
* produce an asset that culls itself out of frame, which looks like a rendering bug and
|
|
@@ -304,17 +329,72 @@ function buildHead(head: Partial<DrftHead> | undefined, meshes: readonly MeshDat
|
|
|
304
329
|
let maxX = -Infinity;
|
|
305
330
|
let maxY = -Infinity;
|
|
306
331
|
let maxZ = -Infinity;
|
|
307
|
-
|
|
332
|
+
const placed = new Map<number, Float32Array>();
|
|
333
|
+
for (const group of instances) placed.set(group.mesh, group.transforms);
|
|
334
|
+
for (let m = 0; m < meshes.length; m++) {
|
|
335
|
+
const mesh = meshes[m] as MeshData;
|
|
336
|
+
const transforms = placed.get(m);
|
|
337
|
+
if (transforms === undefined) {
|
|
338
|
+
for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
|
|
339
|
+
const x = mesh.positions[at] as number;
|
|
340
|
+
const y = mesh.positions[at + 1] as number;
|
|
341
|
+
const z = mesh.positions[at + 2] as number;
|
|
342
|
+
if (x < minX) minX = x;
|
|
343
|
+
if (y < minY) minY = y;
|
|
344
|
+
if (z < minZ) minZ = z;
|
|
345
|
+
if (x > maxX) maxX = x;
|
|
346
|
+
if (y > maxY) maxY = y;
|
|
347
|
+
if (z > maxZ) maxZ = z;
|
|
348
|
+
}
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
/*
|
|
352
|
+
* **An instanced mesh's bounds are every copy's**, or a file of ten thousand candles reports one
|
|
353
|
+
* candle's box and a consumer fits and culls it as one candle. Each copy's box is the prototype's
|
|
354
|
+
* eight corners through its matrix: conservative for a copy that turns, exact for one that moves.
|
|
355
|
+
*/
|
|
356
|
+
let lx = Infinity;
|
|
357
|
+
let ly = Infinity;
|
|
358
|
+
let lz = Infinity;
|
|
359
|
+
let hx = -Infinity;
|
|
360
|
+
let hy = -Infinity;
|
|
361
|
+
let hz = -Infinity;
|
|
308
362
|
for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
363
|
+
lx = Math.min(lx, mesh.positions[at] as number);
|
|
364
|
+
ly = Math.min(ly, mesh.positions[at + 1] as number);
|
|
365
|
+
lz = Math.min(lz, mesh.positions[at + 2] as number);
|
|
366
|
+
hx = Math.max(hx, mesh.positions[at] as number);
|
|
367
|
+
hy = Math.max(hy, mesh.positions[at + 1] as number);
|
|
368
|
+
hz = Math.max(hz, mesh.positions[at + 2] as number);
|
|
369
|
+
}
|
|
370
|
+
if (!Number.isFinite(lx)) continue;
|
|
371
|
+
for (let k = 0; k + 15 < transforms.length; k += 16) {
|
|
372
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
373
|
+
const cx = corner & 1 ? hx : lx;
|
|
374
|
+
const cy = corner & 2 ? hy : ly;
|
|
375
|
+
const cz = corner & 4 ? hz : lz;
|
|
376
|
+
const x =
|
|
377
|
+
(transforms[k] as number) * cx +
|
|
378
|
+
(transforms[k + 4] as number) * cy +
|
|
379
|
+
(transforms[k + 8] as number) * cz +
|
|
380
|
+
(transforms[k + 12] as number);
|
|
381
|
+
const y =
|
|
382
|
+
(transforms[k + 1] as number) * cx +
|
|
383
|
+
(transforms[k + 5] as number) * cy +
|
|
384
|
+
(transforms[k + 9] as number) * cz +
|
|
385
|
+
(transforms[k + 13] as number);
|
|
386
|
+
const z =
|
|
387
|
+
(transforms[k + 2] as number) * cx +
|
|
388
|
+
(transforms[k + 6] as number) * cy +
|
|
389
|
+
(transforms[k + 10] as number) * cz +
|
|
390
|
+
(transforms[k + 14] as number);
|
|
391
|
+
if (x < minX) minX = x;
|
|
392
|
+
if (y < minY) minY = y;
|
|
393
|
+
if (z < minZ) minZ = z;
|
|
394
|
+
if (x > maxX) maxX = x;
|
|
395
|
+
if (y > maxY) maxY = y;
|
|
396
|
+
if (z > maxZ) maxZ = z;
|
|
397
|
+
}
|
|
318
398
|
}
|
|
319
399
|
}
|
|
320
400
|
if (!Number.isFinite(minX)) {
|
|
@@ -478,6 +558,11 @@ function buildMaterials(materials: readonly DrftMaterial[]): PendingChunk {
|
|
|
478
558
|
view.setFloat32(at + 64, material.metallicScale, true);
|
|
479
559
|
view.setFloat32(at + 68, material.occlusionStrength, true);
|
|
480
560
|
view.setFloat32(at + 72, material.cutout, true);
|
|
561
|
+
view.setUint32(
|
|
562
|
+
at + 76,
|
|
563
|
+
(material.blend === true ? 1 : 0) | (material.doubleSided === true ? 2 : 0),
|
|
564
|
+
true,
|
|
565
|
+
);
|
|
481
566
|
at += MATERIAL_ENTRY_BYTES;
|
|
482
567
|
}
|
|
483
568
|
return { code: CHUNK_MATL, flags: 0, bytes };
|
|
@@ -537,7 +622,7 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
537
622
|
* same asset less its textures rather than a refusal. Moving it earlier in the file changes
|
|
538
623
|
* nothing about that, because a chunk is found through the table rather than by position.
|
|
539
624
|
*/
|
|
540
|
-
const chunks: PendingChunk[] = [buildHead(source.head, source.meshes)];
|
|
625
|
+
const chunks: PendingChunk[] = [buildHead(source.head, source.meshes, source.instances)];
|
|
541
626
|
/*
|
|
542
627
|
* The outline first, ahead of even the paint. It is the one thing that puts a recognisable
|
|
543
628
|
* whole object on screen, it is a few hundred kilobytes against tens of megabytes, and the
|
|
@@ -568,6 +653,28 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
568
653
|
for (const node of source.nodes === undefined ? [] : [source.nodes]) {
|
|
569
654
|
chunks.push({ code: CHUNK_NODE, flags: 0, bytes: buildNodes(node) });
|
|
570
655
|
}
|
|
656
|
+
/*
|
|
657
|
+
* The placements ahead of the meshes they place, for the rig's reason: a streaming consumer must
|
|
658
|
+
* know a mesh is drawn ten thousand times before it lands, or it has already made it one part.
|
|
659
|
+
*/
|
|
660
|
+
if ((source.instances?.length ?? 0) > 0) {
|
|
661
|
+
for (const group of source.instances ?? []) {
|
|
662
|
+
if (group.mesh >= source.meshes.length) {
|
|
663
|
+
throw new DrftError(
|
|
664
|
+
`an instance group places mesh ${group.mesh}, and the asset has ${source.meshes.length}`,
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
chunks.push({
|
|
669
|
+
code: CHUNK_INST,
|
|
670
|
+
flags: CHUNK_REQUIRED,
|
|
671
|
+
bytes: buildInstances(source.instances as readonly DrftInstanceGroup[]),
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
/* Small, and ahead of the geometry, so a streaming consumer can place its lamps early. */
|
|
675
|
+
if ((source.lights?.length ?? 0) > 0) {
|
|
676
|
+
chunks.push({ code: CHUNK_LITE, flags: 0, bytes: buildLights(source.lights ?? []) });
|
|
677
|
+
}
|
|
571
678
|
for (const skin of source.skins ?? []) {
|
|
572
679
|
chunks.push({ code: CHUNK_SKIN, flags: 0, bytes: buildSkin(skin) });
|
|
573
680
|
}
|
|
@@ -613,7 +720,12 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
613
720
|
}),
|
|
614
721
|
});
|
|
615
722
|
}
|
|
616
|
-
|
|
723
|
+
if (source.quantise === true) {
|
|
724
|
+
validateMeshData(mesh);
|
|
725
|
+
chunks.push({ code: CHUNK_MSHQ, flags: CHUNK_REQUIRED, bytes: encodeQuantisedMesh(mesh) });
|
|
726
|
+
} else {
|
|
727
|
+
chunks.push(buildMesh(mesh));
|
|
728
|
+
}
|
|
617
729
|
});
|
|
618
730
|
for (const texture of source.textures ?? []) chunks.push(buildTexture(texture));
|
|
619
731
|
/* One `SUBS` for the file, and only where something was labelled — an empty chunk would cost
|
package/src/index.ts
CHANGED
|
@@ -17,7 +17,13 @@ export type { DrftMorph, DrftNode } from './drftSkin.ts';
|
|
|
17
17
|
export type { DrftSubs, DrftSubstanceEntry } from './drftSubs.ts';
|
|
18
18
|
export { buildSubs, readSubs } from './drftSubs.ts';
|
|
19
19
|
export type { DrftSdfv, DrftSdfvEntry } from './sdfv.ts';
|
|
20
|
-
export {
|
|
20
|
+
export {
|
|
21
|
+
buildSdfv,
|
|
22
|
+
readSdfv,
|
|
23
|
+
SDFV_ENTRY_BYTES,
|
|
24
|
+
SDFV_MAX_SAMPLES,
|
|
25
|
+
SDFV_WHOLE_FILE,
|
|
26
|
+
} from './sdfv.ts';
|
|
21
27
|
export {
|
|
22
28
|
NNET_ENTRY_BYTES,
|
|
23
29
|
NNET_MAX_HIDDEN,
|
|
@@ -59,6 +65,11 @@ export {
|
|
|
59
65
|
buildColliders,
|
|
60
66
|
readColliders,
|
|
61
67
|
} from './drftColliders.ts';
|
|
68
|
+
/* Meshes drawn many times, one matrix a copy, as the baker finds merged copies again. */
|
|
69
|
+
export { buildInstances, readInstances } from './drftInstances.ts';
|
|
70
|
+
export { buildLights, readLights } from './drftLights.ts';
|
|
71
|
+
export type { DrftLight, DrftLightKind } from './drftLights.ts';
|
|
72
|
+
export type { DrftInstanceGroup } from './drftInstances.ts';
|
|
62
73
|
/*
|
|
63
74
|
* **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
|
|
64
75
|
* carries no semantics — this package is the container and does not know what a channel means —
|
|
@@ -83,8 +94,8 @@ export type { EntsScene } from './ents.ts';
|
|
|
83
94
|
export { readDrft } from './drftRead.ts';
|
|
84
95
|
export type { DrftAsset, DrftTexture } from './drftRead.ts';
|
|
85
96
|
/* Reading one while it is still arriving, for a scene that would rather show a model build. */
|
|
86
|
-
export { DrftStream, streamDrft } from './drftStream.ts';
|
|
87
|
-
export type { DrftManifest, DrftStreamHandlers } from './drftStream.ts';
|
|
97
|
+
export { DEFAULT_STREAM_SLICE_MS, DrftStream, streamDrft } from './drftStream.ts';
|
|
98
|
+
export type { DrftManifest, DrftStreamHandlers, DrftStreamPacing } from './drftStream.ts';
|
|
88
99
|
export { writeDrft } from './drftWrite.ts';
|
|
89
100
|
export type { DrftSource, DrftTextureSource } from './drftWrite.ts';
|
|
90
101
|
export { DrftError, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR } from './drftFormat.ts';
|
package/src/meshData.ts
CHANGED
|
@@ -288,9 +288,16 @@ export function validateMeshData(data: MeshData): void {
|
|
|
288
288
|
* An index past the end is the same hazard from the other side, and the same latitude
|
|
289
289
|
* applies: a driver may clamp it or refuse the draw. Scanned once at construction, which
|
|
290
290
|
* is a cost paid when a mesh is built rather than when it is drawn.
|
|
291
|
+
*
|
|
292
|
+
* **The array is read once, outside the loop.** A mesh arrives in as many object shapes as there
|
|
293
|
+
* are combinations of optional fields, so `data.indices` is a megamorphic load, and one inside the
|
|
294
|
+
* loop is a lookup per index the compiler cannot hoist: 0.8 s of a five-pack load, five times what
|
|
295
|
+
* the same scan costs over a local.
|
|
291
296
|
*/
|
|
292
|
-
|
|
293
|
-
|
|
297
|
+
const indices = data.indices;
|
|
298
|
+
const count = indices.length;
|
|
299
|
+
for (let at = 0; at < count; at++) {
|
|
300
|
+
const index = indices[at] as number;
|
|
294
301
|
if (index >= vertices) {
|
|
295
302
|
throw new Error(
|
|
296
303
|
`Mesh: index ${at} refers to vertex ${index}, but the mesh has ${vertices}. ` +
|
package/src/sdfv.ts
CHANGED
|
@@ -46,6 +46,22 @@ import { DrftError, align } from './drftFormat.ts';
|
|
|
46
46
|
/** `u32` mesh, three `u32` dimensions and six `f32` bounds. A field's samples are elsewhere. */
|
|
47
47
|
export const SDFV_ENTRY_BYTES = 40;
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* The mesh ordinal that means **the file's static geometry as a whole**, in the file's own space.
|
|
51
|
+
*
|
|
52
|
+
* **One field for a scene rather than one per mesh, because a scene is where per-mesh fields
|
|
53
|
+
* fail.** A bought courtyard exports its walls as a few meshes spanning the whole of it, so a field
|
|
54
|
+
* of thirty-two samples along each mesh's longest side is 1.2 m voxels on the walls that matter and
|
|
55
|
+
* 12.6 MB across four hundred meshes; one field over all of them at 25 cm is 3 MB and resolves a
|
|
56
|
+
* column. A reader places it with the file's own transform and nothing per mesh.
|
|
57
|
+
*
|
|
58
|
+
* **Additive, and it had to be checked rather than assumed**: no reader has ever compared an
|
|
59
|
+
* entry's ordinal with the mesh count, so a reader that predates this value reads the entry and
|
|
60
|
+
* pairs it with no mesh, which loses only the traced light it could not have used. What would make
|
|
61
|
+
* it wrong is a file whose static geometry moves, which is a file with more than one field.
|
|
62
|
+
*/
|
|
63
|
+
export const SDFV_WHOLE_FILE = 0xffffffff;
|
|
64
|
+
|
|
49
65
|
/**
|
|
50
66
|
* The most samples one field may declare.
|
|
51
67
|
*
|