@driftengine/drft 3.63.0 → 4.0.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/NOTICE +20 -0
- package/README.md +1 -1
- package/dist/drftFormat.d.ts +52 -1
- package/dist/drftFormat.js +59 -1
- package/dist/drftMeshlets.d.ts +52 -0
- package/dist/drftMeshlets.js +140 -0
- package/dist/drftRead.d.ts +47 -0
- package/dist/drftRead.js +37 -3
- package/dist/drftWrite.d.ts +48 -0
- package/dist/drftWrite.js +59 -3
- package/dist/dtex.d.ts +127 -0
- package/dist/dtex.js +324 -0
- package/dist/ents.d.ts +47 -0
- package/dist/ents.js +60 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/navm.d.ts +58 -0
- package/dist/navm.js +105 -0
- package/dist/ngrf.d.ts +29 -0
- package/dist/ngrf.js +214 -0
- package/dist/nnet.d.ts +76 -0
- package/dist/nnet.js +194 -0
- package/dist/sdfv.d.ts +71 -0
- package/dist/sdfv.js +142 -0
- package/package.json +4 -2
- package/src/drftFormat.ts +66 -1
- package/src/drftMeshlets.ts +185 -0
- package/src/drftRead.ts +82 -2
- package/src/drftWrite.ts +112 -2
- package/src/dtex.ts +404 -0
- package/src/ents.ts +105 -0
- package/src/index.ts +43 -0
- package/src/navm.ts +171 -0
- package/src/ngrf.ts +299 -0
- package/src/nnet.ts +250 -0
- package/src/sdfv.ts +178 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { DrftError, align } from './drftFormat.ts';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* `MSHL`: the clusters a mesh draws as, and the level-of-detail graph over them.
|
|
5
|
+
*
|
|
6
|
+
* **One chunk per level, not one for the whole graph.** A level is a complete alternative mesh —
|
|
7
|
+
* its own positions, its own indices, its own clusters — so folding every level into one chunk
|
|
8
|
+
* would mean one payload whose parts are only meaningful together. Separate chunks let a loader
|
|
9
|
+
* stream the coarsest level first and refine, which is the same argument `coarseFirst.ts` already
|
|
10
|
+
* makes for splats.
|
|
11
|
+
*
|
|
12
|
+
* ```
|
|
13
|
+
* u32 clusterCount
|
|
14
|
+
* u32 indexTotal
|
|
15
|
+
* u32 triangleOffsets[clusterCount] first triangle of each cluster
|
|
16
|
+
* u32 triangleCounts[clusterCount]
|
|
17
|
+
* f32 boundsCentre[clusterCount * 3]
|
|
18
|
+
* f32 boundsRadius[clusterCount]
|
|
19
|
+
* f32 coneAxis[clusterCount * 3] average face normal, normalised
|
|
20
|
+
* f32 coneCutoff[clusterCount] minimum dot of any face against the axis
|
|
21
|
+
* f32 ownError[clusterCount] this cluster's geometric error
|
|
22
|
+
* f32 parentError[clusterCount] the error of what replaces it one level up
|
|
23
|
+
* u32 indices[indexTotal]
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* **No per-chunk version field**, matching `COLL` and every other chunk here: the file's own
|
|
27
|
+
* version covers the format and a second one would be a second thing to keep in step.
|
|
28
|
+
*
|
|
29
|
+
* **`parentError` may be infinite**, which is how the coarsest level says it has no parent. A
|
|
30
|
+
* float32 carries that exactly, so a reader needs no sentinel and a comparison against it needs
|
|
31
|
+
* no special case.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/** How many clusters one level may carry. Well past what a streaming budget permits. */
|
|
35
|
+
export const MAX_CLUSTERS = 1 << 20;
|
|
36
|
+
|
|
37
|
+
export interface MeshletLevel {
|
|
38
|
+
triangleOffsets: Uint32Array;
|
|
39
|
+
triangleCounts: Uint32Array;
|
|
40
|
+
boundsCentre: Float32Array;
|
|
41
|
+
boundsRadius: Float32Array;
|
|
42
|
+
coneAxis: Float32Array;
|
|
43
|
+
coneCutoff: Float32Array;
|
|
44
|
+
ownError: Float32Array;
|
|
45
|
+
parentError: Float32Array;
|
|
46
|
+
indices: Uint32Array;
|
|
47
|
+
count: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const HEADER = 8;
|
|
51
|
+
|
|
52
|
+
function layoutOf(clusterCount: number, indexTotal: number) {
|
|
53
|
+
const triangleOffsets = HEADER;
|
|
54
|
+
const triangleCounts = triangleOffsets + clusterCount * 4;
|
|
55
|
+
const boundsCentre = triangleCounts + clusterCount * 4;
|
|
56
|
+
const boundsRadius = boundsCentre + clusterCount * 12;
|
|
57
|
+
const coneAxis = boundsRadius + clusterCount * 4;
|
|
58
|
+
const coneCutoff = coneAxis + clusterCount * 12;
|
|
59
|
+
const ownError = coneCutoff + clusterCount * 4;
|
|
60
|
+
const parentError = ownError + clusterCount * 4;
|
|
61
|
+
const indices = parentError + clusterCount * 4;
|
|
62
|
+
return {
|
|
63
|
+
triangleOffsets,
|
|
64
|
+
triangleCounts,
|
|
65
|
+
boundsCentre,
|
|
66
|
+
boundsRadius,
|
|
67
|
+
coneAxis,
|
|
68
|
+
coneCutoff,
|
|
69
|
+
ownError,
|
|
70
|
+
parentError,
|
|
71
|
+
indices,
|
|
72
|
+
size: indices + indexTotal * 4,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function buildMeshlets(level: MeshletLevel): Uint8Array {
|
|
77
|
+
const { count } = level;
|
|
78
|
+
if (count > MAX_CLUSTERS) {
|
|
79
|
+
throw new DrftError(`${count} clusters exceeds the ${MAX_CLUSTERS} cap`);
|
|
80
|
+
}
|
|
81
|
+
if (level.triangleOffsets.length < count || level.triangleCounts.length < count) {
|
|
82
|
+
throw new DrftError('MSHL was given fewer cluster offsets or counts than clusters');
|
|
83
|
+
}
|
|
84
|
+
if (level.indices.length % 3 !== 0) {
|
|
85
|
+
throw new DrftError(
|
|
86
|
+
`MSHL was given ${level.indices.length} indices, which is not whole triangles`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/*
|
|
91
|
+
* Every cluster's triangles are checked to lie inside the index array before anything is
|
|
92
|
+
* written. A cluster running past the end would otherwise be a subarray of arbitrary geometry on
|
|
93
|
+
* the way back in, which is the same failure `COLL` checks its starts table for.
|
|
94
|
+
*/
|
|
95
|
+
const triangles = level.indices.length / 3;
|
|
96
|
+
for (let c = 0; c < count; c += 1) {
|
|
97
|
+
const at = level.triangleOffsets[c] as number;
|
|
98
|
+
const n = level.triangleCounts[c] as number;
|
|
99
|
+
if (at + n > triangles) {
|
|
100
|
+
throw new DrftError(`MSHL cluster ${c} runs past the ${triangles} triangles it was given`);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const layout = layoutOf(count, level.indices.length);
|
|
105
|
+
const bytes = new Uint8Array(align(layout.size));
|
|
106
|
+
const view = new DataView(bytes.buffer);
|
|
107
|
+
view.setUint32(0, count, true);
|
|
108
|
+
view.setUint32(4, level.indices.length, true);
|
|
109
|
+
|
|
110
|
+
new Uint32Array(bytes.buffer, layout.triangleOffsets, count).set(
|
|
111
|
+
level.triangleOffsets.subarray(0, count),
|
|
112
|
+
);
|
|
113
|
+
new Uint32Array(bytes.buffer, layout.triangleCounts, count).set(
|
|
114
|
+
level.triangleCounts.subarray(0, count),
|
|
115
|
+
);
|
|
116
|
+
new Float32Array(bytes.buffer, layout.boundsCentre, count * 3).set(
|
|
117
|
+
level.boundsCentre.subarray(0, count * 3),
|
|
118
|
+
);
|
|
119
|
+
new Float32Array(bytes.buffer, layout.boundsRadius, count).set(
|
|
120
|
+
level.boundsRadius.subarray(0, count),
|
|
121
|
+
);
|
|
122
|
+
new Float32Array(bytes.buffer, layout.coneAxis, count * 3).set(
|
|
123
|
+
level.coneAxis.subarray(0, count * 3),
|
|
124
|
+
);
|
|
125
|
+
new Float32Array(bytes.buffer, layout.coneCutoff, count).set(level.coneCutoff.subarray(0, count));
|
|
126
|
+
new Float32Array(bytes.buffer, layout.ownError, count).set(level.ownError.subarray(0, count));
|
|
127
|
+
new Float32Array(bytes.buffer, layout.parentError, count).set(
|
|
128
|
+
level.parentError.subarray(0, count),
|
|
129
|
+
);
|
|
130
|
+
new Uint32Array(bytes.buffer, layout.indices, level.indices.length).set(level.indices);
|
|
131
|
+
return bytes;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The level an `MSHL` chunk carries, as views over the fetched buffer.
|
|
136
|
+
*
|
|
137
|
+
* Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
|
|
138
|
+
* object holding them.
|
|
139
|
+
*/
|
|
140
|
+
export function readMeshlets(
|
|
141
|
+
buffer: ArrayBuffer,
|
|
142
|
+
offset: number,
|
|
143
|
+
byteLength: number,
|
|
144
|
+
): MeshletLevel {
|
|
145
|
+
if (byteLength < HEADER) throw new DrftError('MSHL is too short to hold its counts');
|
|
146
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
147
|
+
const count = view.getUint32(0, true);
|
|
148
|
+
const indexTotal = view.getUint32(4, true);
|
|
149
|
+
|
|
150
|
+
if (count > MAX_CLUSTERS) {
|
|
151
|
+
throw new DrftError(`MSHL claims ${count} clusters, over the ${MAX_CLUSTERS} cap`);
|
|
152
|
+
}
|
|
153
|
+
if (indexTotal % 3 !== 0) {
|
|
154
|
+
throw new DrftError(`MSHL claims ${indexTotal} indices, which is not whole triangles`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const layout = layoutOf(count, indexTotal);
|
|
158
|
+
if (byteLength < layout.size) {
|
|
159
|
+
throw new DrftError(
|
|
160
|
+
`MSHL claims ${count} clusters and ${indexTotal} indices, which runs past the chunk`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const triangleOffsets = new Uint32Array(buffer, offset + layout.triangleOffsets, count);
|
|
165
|
+
const triangleCounts = new Uint32Array(buffer, offset + layout.triangleCounts, count);
|
|
166
|
+
const triangles = indexTotal / 3;
|
|
167
|
+
for (let c = 0; c < count; c += 1) {
|
|
168
|
+
if ((triangleOffsets[c] as number) + (triangleCounts[c] as number) > triangles) {
|
|
169
|
+
throw new DrftError(`MSHL cluster ${c} runs past the ${triangles} triangles the chunk holds`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
triangleOffsets,
|
|
175
|
+
triangleCounts,
|
|
176
|
+
boundsCentre: new Float32Array(buffer, offset + layout.boundsCentre, count * 3),
|
|
177
|
+
boundsRadius: new Float32Array(buffer, offset + layout.boundsRadius, count),
|
|
178
|
+
coneAxis: new Float32Array(buffer, offset + layout.coneAxis, count * 3),
|
|
179
|
+
coneCutoff: new Float32Array(buffer, offset + layout.coneCutoff, count),
|
|
180
|
+
ownError: new Float32Array(buffer, offset + layout.ownError, count),
|
|
181
|
+
parentError: new Float32Array(buffer, offset + layout.parentError, count),
|
|
182
|
+
indices: new Uint32Array(buffer, offset + layout.indices, indexTotal),
|
|
183
|
+
count,
|
|
184
|
+
};
|
|
185
|
+
}
|
package/src/drftRead.ts
CHANGED
|
@@ -35,6 +35,9 @@ import {
|
|
|
35
35
|
CHUNK_HEAD,
|
|
36
36
|
CHUNK_LODM,
|
|
37
37
|
CHUNK_LODF,
|
|
38
|
+
CHUNK_DTEX,
|
|
39
|
+
CHUNK_ENTS,
|
|
40
|
+
CHUNK_NAVM,
|
|
38
41
|
CHUNK_SPLT,
|
|
39
42
|
SPLAT_BLOCK_PREFIX,
|
|
40
43
|
CHUNK_MESH,
|
|
@@ -53,11 +56,20 @@ import {
|
|
|
53
56
|
CHUNK_NODE,
|
|
54
57
|
CHUNK_SKIN,
|
|
55
58
|
CHUNK_MORP,
|
|
59
|
+
CHUNK_NNET,
|
|
60
|
+
CHUNK_NGRF,
|
|
61
|
+
CHUNK_SDFV,
|
|
56
62
|
CHUNK_SUBS,
|
|
57
63
|
CHUNK_COLL,
|
|
58
64
|
} from './drftFormat.ts';
|
|
59
65
|
import type { DrftChunk, DrftHead, DrftMaterial, DrftSplatBlock } from './drftFormat.ts';
|
|
60
66
|
import { type DrftSubs, readSubs } from './drftSubs.ts';
|
|
67
|
+
import { type DrftSdfv, type DrftSdfvEntry, readSdfv } from './sdfv.ts';
|
|
68
|
+
import { type DtexEntry, readDtex } from './dtex.ts';
|
|
69
|
+
import { type EntsScene, readEnts } from './ents.ts';
|
|
70
|
+
import { type NavPolyMesh, readNavm } from './navm.ts';
|
|
71
|
+
import { type DrftNetwork, type DrftNnet, readNnet } from './nnet.ts';
|
|
72
|
+
import { type DrftGraph, readNgrf } from './ngrf.ts';
|
|
61
73
|
import { readColliders } from './drftColliders.ts';
|
|
62
74
|
|
|
63
75
|
/** An embedded image, still compressed. Decoding is the consumer's, through `createImageBitmap`. */
|
|
@@ -108,6 +120,24 @@ export interface DrftAsset {
|
|
|
108
120
|
* file's coarse-first order, so a consumer that draws a prefix of them draws a sparse whole.
|
|
109
121
|
*/
|
|
110
122
|
readonly splats: DrftSplatBlock | null;
|
|
123
|
+
/**
|
|
124
|
+
* A decode program per material, or empty for a file that carries none.
|
|
125
|
+
*
|
|
126
|
+
* Each says which `MATL` entry it belongs to. **A reader that does not know this chunk skips it
|
|
127
|
+
* by its length** and loses only the material it could not have decoded, which is what keeps the
|
|
128
|
+
* format's freeze intact.
|
|
129
|
+
*/
|
|
130
|
+
readonly dtex: readonly DtexEntry[];
|
|
131
|
+
/**
|
|
132
|
+
* A way across the scene, or `null` for a file that carries none.
|
|
133
|
+
*
|
|
134
|
+
* **`null` rather than an empty mesh**, because *nobody built one* and *there is nowhere to walk*
|
|
135
|
+
* are different facts, and a consumer that cannot tell them apart cannot decide whether to build
|
|
136
|
+
* one itself.
|
|
137
|
+
*/
|
|
138
|
+
readonly navigation: NavPolyMesh | null;
|
|
139
|
+
/** The things in the scene, or `null` for a file that is an asset rather than a scene. */
|
|
140
|
+
readonly entities: EntsScene | null;
|
|
111
141
|
/**
|
|
112
142
|
* The asset's own hierarchy, or empty for a file carrying no `NODE`.
|
|
113
143
|
*
|
|
@@ -129,6 +159,29 @@ export interface DrftAsset {
|
|
|
129
159
|
* **exactly** and reports what it could not, rather than guessing.
|
|
130
160
|
*/
|
|
131
161
|
readonly substances: readonly { readonly material: number; readonly substance: string }[];
|
|
162
|
+
/**
|
|
163
|
+
* The signed distance field of each mesh that carries one, or empty for a file with none.
|
|
164
|
+
*
|
|
165
|
+
* The `SDFV` chunk, added at 1.13. Views over the fetched buffer, so a file that carries fields
|
|
166
|
+
* for geometry a consumer never traces through costs one view apiece and no copy. Hand them to
|
|
167
|
+
* `composeGlobalField`, which is what turns per-object fields into a world around the camera.
|
|
168
|
+
*/
|
|
169
|
+
readonly fields: readonly DrftSdfvEntry[];
|
|
170
|
+
/**
|
|
171
|
+
* The networks the file carries, each with the role a consumer asks for it by, or empty.
|
|
172
|
+
*
|
|
173
|
+
* The `NNET` chunk, added at 1.14. Weights are views over the fetched buffer, in the precision
|
|
174
|
+
* they were written in — half-precision bits stay bits, because the device uploads them as such.
|
|
175
|
+
*/
|
|
176
|
+
readonly networks: readonly DrftNetwork[];
|
|
177
|
+
/**
|
|
178
|
+
* The graphs the file carries — networks built from operators — each with its role, or empty.
|
|
179
|
+
*
|
|
180
|
+
* The `NGRF` chunk, added at 1.15. Tensors are views over the fetched buffer where its alignment
|
|
181
|
+
* allows, in the precision they were written in. `@driftengine/texture` validates one before it
|
|
182
|
+
* runs, naming any operator it lacks.
|
|
183
|
+
*/
|
|
184
|
+
readonly graphs: readonly DrftGraph[];
|
|
132
185
|
/**
|
|
133
186
|
* The convex hulls this asset collides as, or empty for a file carrying no `COLL`.
|
|
134
187
|
*
|
|
@@ -300,11 +353,17 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
300
353
|
const textures: DrftTexture[] = [];
|
|
301
354
|
let materials: DrftMaterial[] = [];
|
|
302
355
|
const splatBlocks: { block: DrftSplatBlock; at: number }[] = [];
|
|
356
|
+
const dtex: DtexEntry[] = [];
|
|
357
|
+
let navigation: NavPolyMesh | null = null;
|
|
358
|
+
let entities: EntsScene | null = null;
|
|
303
359
|
let nodes: DrftNode[] = [];
|
|
304
360
|
const skins: DrftSkin[] = [];
|
|
305
361
|
const clips: AnimationClip[] = [];
|
|
306
362
|
const morphs: DrftMorph[] = [];
|
|
307
363
|
let subs: DrftSubs | null = null;
|
|
364
|
+
let fields: DrftSdfv | null = null;
|
|
365
|
+
let nnet: DrftNnet | null = null;
|
|
366
|
+
let graphs: DrftGraph[] = [];
|
|
308
367
|
let colliders: readonly Float32Array[] = [];
|
|
309
368
|
const skipped: string[] = [];
|
|
310
369
|
let head: DrftHead | null = null;
|
|
@@ -357,6 +416,10 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
357
416
|
else if (chunk.code === CHUNK_MATL) materials = readMaterials(buffer, chunk);
|
|
358
417
|
else if (chunk.code === CHUNK_SPLT)
|
|
359
418
|
splatBlocks.push({ block: readSplatBlock(buffer, chunk), at: chunk.index });
|
|
419
|
+
else if (chunk.code === CHUNK_DTEX) dtex.push(readDtex(buffer, chunk.offset, chunk.byteLength));
|
|
420
|
+
else if (chunk.code === CHUNK_NAVM)
|
|
421
|
+
navigation = readNavm(buffer, chunk.offset, chunk.byteLength);
|
|
422
|
+
else if (chunk.code === CHUNK_ENTS) entities = readEnts(buffer, chunk.offset, chunk.byteLength);
|
|
360
423
|
else if (chunk.code === CHUNK_NODE) nodes = readNodes(buffer, chunk.offset, chunk.byteLength);
|
|
361
424
|
else if (chunk.code === CHUNK_SKIN)
|
|
362
425
|
skins.push(readSkin(buffer, chunk.offset, chunk.byteLength));
|
|
@@ -365,6 +428,9 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
365
428
|
else if (chunk.code === CHUNK_MORP)
|
|
366
429
|
morphs.push(readMorph(buffer, chunk.offset, chunk.byteLength));
|
|
367
430
|
else if (chunk.code === CHUNK_SUBS) subs = readSubs(buffer, chunk.offset, chunk.byteLength);
|
|
431
|
+
else if (chunk.code === CHUNK_SDFV) fields = readSdfv(buffer, chunk.offset, chunk.byteLength);
|
|
432
|
+
else if (chunk.code === CHUNK_NNET) nnet = readNnet(buffer, chunk.offset, chunk.byteLength);
|
|
433
|
+
else if (chunk.code === CHUNK_NGRF) graphs = readNgrf(buffer, chunk.offset, chunk.byteLength);
|
|
368
434
|
else if (chunk.code === CHUNK_COLL)
|
|
369
435
|
colliders = readColliders(buffer, chunk.offset, chunk.byteLength);
|
|
370
436
|
/* Every other known chunk is defined but not yet carried; see docs/FORMAT.md phase table. */
|
|
@@ -377,8 +443,16 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
377
443
|
* for the absence of a chunk kind it never needed would be the format telling a consumer what
|
|
378
444
|
* its scene may be made of.
|
|
379
445
|
*/
|
|
380
|
-
|
|
381
|
-
|
|
446
|
+
/* And networks, since 1.15: a converted model is weights and nothing else. */
|
|
447
|
+
if (
|
|
448
|
+
meshes.length === 0 &&
|
|
449
|
+
splatBlocks.length === 0 &&
|
|
450
|
+
(nnet?.networks.length ?? 0) === 0 &&
|
|
451
|
+
graphs.length === 0
|
|
452
|
+
) {
|
|
453
|
+
throw new DrftError(
|
|
454
|
+
'no MESH, SPLT, NNET or NGRF chunk — an asset with no geometry, capture or network',
|
|
455
|
+
);
|
|
382
456
|
}
|
|
383
457
|
|
|
384
458
|
/*
|
|
@@ -448,7 +522,13 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
|
|
|
448
522
|
materials,
|
|
449
523
|
textures,
|
|
450
524
|
substances: subs?.entries ?? [],
|
|
525
|
+
fields: fields?.entries ?? [],
|
|
526
|
+
networks: nnet?.networks ?? [],
|
|
527
|
+
graphs,
|
|
451
528
|
colliders,
|
|
529
|
+
dtex,
|
|
530
|
+
navigation,
|
|
531
|
+
entities,
|
|
452
532
|
splats: joinSplatBlocks(splatBlocks),
|
|
453
533
|
skipped,
|
|
454
534
|
versionMajor,
|
package/src/drftWrite.ts
CHANGED
|
@@ -28,6 +28,9 @@ import {
|
|
|
28
28
|
CHUNK_ENTRY_BYTES,
|
|
29
29
|
CHUNK_HEAD,
|
|
30
30
|
CHUNK_ANIM,
|
|
31
|
+
CHUNK_DTEX,
|
|
32
|
+
CHUNK_ENTS,
|
|
33
|
+
CHUNK_NAVM,
|
|
31
34
|
CHUNK_LODM,
|
|
32
35
|
CHUNK_LODF,
|
|
33
36
|
CHUNK_MORP,
|
|
@@ -39,6 +42,9 @@ import {
|
|
|
39
42
|
CHUNK_MESH,
|
|
40
43
|
CHUNK_REQUIRED,
|
|
41
44
|
CHUNK_COLL,
|
|
45
|
+
CHUNK_NNET,
|
|
46
|
+
CHUNK_NGRF,
|
|
47
|
+
CHUNK_SDFV,
|
|
42
48
|
CHUNK_SUBS,
|
|
43
49
|
CHUNK_TEXS,
|
|
44
50
|
MATERIAL_ENTRY_BYTES,
|
|
@@ -53,6 +59,12 @@ import {
|
|
|
53
59
|
import type { DrftHead, DrftMaterial, DrftSplats } from './drftFormat.ts';
|
|
54
60
|
import { coarseFirstOrder } from './coarseFirst.ts';
|
|
55
61
|
import { type DrftSubstanceEntry, buildSubs } from './drftSubs.ts';
|
|
62
|
+
import { type DrftSdfvEntry, buildSdfv } from './sdfv.ts';
|
|
63
|
+
import { type DrftNetwork, buildNnet } from './nnet.ts';
|
|
64
|
+
import { type DtexEntry, buildDtex } from './dtex.ts';
|
|
65
|
+
import { type EntsScene, buildEnts } from './ents.ts';
|
|
66
|
+
import { type NavPolyMesh, buildNavm } from './navm.ts';
|
|
67
|
+
import { type DrftGraph, buildNgrf } from './ngrf.ts';
|
|
56
68
|
import { buildColliders } from './drftColliders.ts';
|
|
57
69
|
|
|
58
70
|
/** An image to embed, already compressed, with what its own header said about it. */
|
|
@@ -91,6 +103,25 @@ export interface DrftSource {
|
|
|
91
103
|
* than by position, so a file labelling only its fourth material says so.
|
|
92
104
|
*/
|
|
93
105
|
readonly substances?: readonly DrftSubstanceEntry[];
|
|
106
|
+
/**
|
|
107
|
+
* The signed distance field of each mesh that has one, or absent for a file carrying none.
|
|
108
|
+
*
|
|
109
|
+
* Written as one `SDFV` chunk at 1.13. It pairs by the mesh ordinal carried per entry rather
|
|
110
|
+
* than by position, so a file with a field for only its fourth mesh says so — `SUBS`'s rule,
|
|
111
|
+
* and `MORP` is the chunk that found out why it is a rule.
|
|
112
|
+
*/
|
|
113
|
+
readonly fields?: readonly DrftSdfvEntry[];
|
|
114
|
+
/**
|
|
115
|
+
* The networks this file carries, or absent for a file carrying none.
|
|
116
|
+
*
|
|
117
|
+
* Written as one `NNET` chunk at 1.14, each network found by its role rather than by position.
|
|
118
|
+
*/
|
|
119
|
+
readonly networks?: readonly DrftNetwork[];
|
|
120
|
+
/**
|
|
121
|
+
* The graphs this file carries — networks built from operators rather than dense layers — or
|
|
122
|
+
* absent for a file carrying none. Written as one `NGRF` chunk at 1.15, each found by its role.
|
|
123
|
+
*/
|
|
124
|
+
readonly graphs?: readonly DrftGraph[];
|
|
94
125
|
/**
|
|
95
126
|
* The convex hulls this asset collides as, or absent for a file that carries none.
|
|
96
127
|
*
|
|
@@ -136,6 +167,29 @@ export interface DrftSource {
|
|
|
136
167
|
* `CHUNK_SPLT` for why it is several chunks rather than one.
|
|
137
168
|
*/
|
|
138
169
|
readonly splats?: DrftSplats;
|
|
170
|
+
/**
|
|
171
|
+
* A decode program per material, or absent for a file whose materials are ordinary.
|
|
172
|
+
*
|
|
173
|
+
* One `DTEX` chunk each, **paired to a `MATL` entry by the index inside the chunk** rather than
|
|
174
|
+
* by the order they appear. A file may carry one for some of its materials and not others — a
|
|
175
|
+
* scene where one surface came from a capture and the rest were authored — and a reader pairing
|
|
176
|
+
* by position would hand the wrong material the wrong texture with nothing to fail on.
|
|
177
|
+
*/
|
|
178
|
+
readonly dtex?: readonly DtexEntry[];
|
|
179
|
+
/**
|
|
180
|
+
* A way across the scene, or absent for a file nobody navigates.
|
|
181
|
+
*
|
|
182
|
+
* The mesh carries the origin and cell size it was built at, because the numbers in it are cell
|
|
183
|
+
* indices and a reader without those has a mesh in the wrong units at the wrong place.
|
|
184
|
+
*/
|
|
185
|
+
readonly navigation?: NavPolyMesh;
|
|
186
|
+
/**
|
|
187
|
+
* The things in the scene, as `serializeWorld` wrote them.
|
|
188
|
+
*
|
|
189
|
+
* A *scene*, not an asset: this is what a capture proposes or an editor saves, and a file may
|
|
190
|
+
* carry geometry with no scene at all — which is every baked model this format has ever held.
|
|
191
|
+
*/
|
|
192
|
+
readonly entities?: EntsScene;
|
|
139
193
|
}
|
|
140
194
|
|
|
141
195
|
/**
|
|
@@ -442,8 +496,20 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
442
496
|
* ordinary asset and the reader accepts it, so refusing to write one would be the two halves of
|
|
443
497
|
* the format disagreeing about what a file may contain.
|
|
444
498
|
*/
|
|
445
|
-
|
|
446
|
-
|
|
499
|
+
/*
|
|
500
|
+
* **And networks, since 1.15**, on the same argument: a converted model is weights and nothing
|
|
501
|
+
* else, and refusing it for the absence of geometry would be the format telling a consumer what
|
|
502
|
+
* an asset may be made of.
|
|
503
|
+
*/
|
|
504
|
+
if (
|
|
505
|
+
source.meshes.length === 0 &&
|
|
506
|
+
(source.splats === undefined || source.splats.count === 0) &&
|
|
507
|
+
(source.networks?.length ?? 0) === 0 &&
|
|
508
|
+
(source.graphs?.length ?? 0) === 0
|
|
509
|
+
) {
|
|
510
|
+
throw new DrftError(
|
|
511
|
+
'an asset must contain at least one mesh, a capture with splats in it, or a network',
|
|
512
|
+
);
|
|
447
513
|
}
|
|
448
514
|
|
|
449
515
|
const materials = source.materials;
|
|
@@ -508,6 +574,26 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
508
574
|
for (const clip of source.clips ?? []) {
|
|
509
575
|
chunks.push({ code: CHUNK_ANIM, flags: 0, bytes: buildClip(clip) });
|
|
510
576
|
}
|
|
577
|
+
for (const entry of source.dtex ?? []) {
|
|
578
|
+
/*
|
|
579
|
+
* **Refused here rather than at the reader**, because a decode program for a material the file
|
|
580
|
+
* does not carry is a bake that went wrong, and the writer is where the author can still be
|
|
581
|
+
* told. A reader meeting one has nothing useful to do with it.
|
|
582
|
+
*/
|
|
583
|
+
if (entry.material >= (source.materials?.length ?? 0)) {
|
|
584
|
+
throw new DrftError(
|
|
585
|
+
`drft: DTEX names material ${entry.material} and the file carries ` +
|
|
586
|
+
`${source.materials?.length ?? 0}`,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
chunks.push({ code: CHUNK_DTEX, flags: 0, bytes: buildDtex(entry) });
|
|
590
|
+
}
|
|
591
|
+
if (source.navigation !== undefined) {
|
|
592
|
+
chunks.push({ code: CHUNK_NAVM, flags: 0, bytes: buildNavm(source.navigation) });
|
|
593
|
+
}
|
|
594
|
+
if (source.entities !== undefined) {
|
|
595
|
+
chunks.push({ code: CHUNK_ENTS, flags: 0, bytes: buildEnts(source.entities) });
|
|
596
|
+
}
|
|
511
597
|
source.meshes.forEach((mesh, ordinal) => {
|
|
512
598
|
/*
|
|
513
599
|
* **Its deltas immediately before it, not after.** A streaming consumer uploads a mesh the
|
|
@@ -539,6 +625,30 @@ export function writeDrft(source: DrftSource): ArrayBuffer {
|
|
|
539
625
|
bytes: buildSubs({ entries: source.substances as readonly DrftSubstanceEntry[] }),
|
|
540
626
|
});
|
|
541
627
|
}
|
|
628
|
+
/* And one `SDFV`, on exactly the same terms. */
|
|
629
|
+
if ((source.fields?.length ?? 0) > 0) {
|
|
630
|
+
chunks.push({
|
|
631
|
+
code: CHUNK_SDFV,
|
|
632
|
+
flags: 0,
|
|
633
|
+
bytes: buildSdfv({ entries: source.fields as readonly DrftSdfvEntry[] }),
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
/* And one `NNET`, on the same terms again. */
|
|
637
|
+
if ((source.networks?.length ?? 0) > 0) {
|
|
638
|
+
chunks.push({
|
|
639
|
+
code: CHUNK_NNET,
|
|
640
|
+
flags: 0,
|
|
641
|
+
bytes: buildNnet({ networks: source.networks as readonly DrftNetwork[] }),
|
|
642
|
+
});
|
|
643
|
+
}
|
|
644
|
+
/* And one `NGRF`, for the networks a perceptron's table cannot describe. */
|
|
645
|
+
if ((source.graphs?.length ?? 0) > 0) {
|
|
646
|
+
chunks.push({
|
|
647
|
+
code: CHUNK_NGRF,
|
|
648
|
+
flags: 0,
|
|
649
|
+
bytes: buildNgrf(source.graphs as readonly DrftGraph[]),
|
|
650
|
+
});
|
|
651
|
+
}
|
|
542
652
|
|
|
543
653
|
/*
|
|
544
654
|
* `COLL` last among the small chunks, because collision is the one thing a viewer never needs and
|