@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 CHANGED
@@ -7,3 +7,23 @@ This product includes software developed at Drift Technologies
7
7
  Licensed under the Apache License, Version 2.0. Section 4(d) of that licence
8
8
  requires this NOTICE to be reproduced in any derivative work you distribute,
9
9
  in the source, the documentation, or a display generated by the work.
10
+
11
+ Models run by DriftCapture
12
+ --------------------------
13
+ DriftCapture runs the models below. Their weights are not in this repository
14
+ and are not distributed with it: tools/capture-weights/ fetches each at the
15
+ revision its manifest pins, verifies it, and converts it, and a game that
16
+ ships one ships it under that model's own licence. Each model's definition in
17
+ this engine is written from its project's published code.
18
+
19
+ - Depth Anything 3 Small and Depth Anything 3 Base. Apache License 2.0.
20
+ Copyright (c) 2025 ByteDance Ltd. and/or its affiliates.
21
+ - Depth Anything V2 Small. Apache License 2.0. The University of Hong Kong
22
+ and TikTok; converted to the Transformers layout by The HuggingFace Team.
23
+ - MobileSAM. Apache License 2.0. Its image encoder is TinyViT, Copyright (c)
24
+ 2022 Microsoft; its prompt encoder and mask decoder are Segment Anything's,
25
+ Copyright (c) Meta Platforms, Inc. and affiliates.
26
+ - SAM 2.1 tiny. Apache License 2.0. Copyright (c) Meta Platforms, Inc. and
27
+ affiliates.
28
+ - OWLv2 base patch16. Apache License 2.0. Copyright 2023 Google AI and The
29
+ HuggingFace Team.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `.drft` container: read and write a baked scene. Declares no runtime dependency.
4
4
 
5
- **Cost: 5.0 KB gzipped.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
5
+ **Cost: 8.1 KB gzipped.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
7
7
 
8
8
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
@@ -20,7 +20,7 @@ export declare const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export declare const DRFT_VERSION_MINOR = 12;
23
+ export declare const DRFT_VERSION_MINOR = 17;
24
24
  /** Bytes before the chunk table. */
25
25
  export declare const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -169,6 +169,57 @@ export declare const CHUNK_MORP: number;
169
169
  * fire. See `drftSubs.ts` for the layout.
170
170
  */
171
171
  export declare const CHUNK_SUBS: number;
172
+ /**
173
+ * `MSHL`: a mesh's clusters and the level-of-detail graph over them.
174
+ *
175
+ * Additive, and additive is what keeps the freeze in `FORMAT.md` intact: a reader that does not
176
+ * know this code skips it by its length and loses only the GPU-driven path, which is the one thing
177
+ * it could not have drawn anyway.
178
+ */
179
+ export declare const CHUNK_MSHL: number;
180
+ /**
181
+ * `DTEX`: a material as a decode program over a latent, rather than as pictures.
182
+ *
183
+ * Additive on the same terms as `MSHL`: a reader that does not know this code skips it by its
184
+ * length and loses only the material, which it had no way to decode. See `dtex.ts` for the layout
185
+ * and for why the chunk validates containment rather than meaning.
186
+ */
187
+ export declare const CHUNK_DTEX: number;
188
+ /**
189
+ * `SDFV`: an object's signed distance field, for tracing indirect light through it.
190
+ *
191
+ * Additive on the same terms as `MSHL` and `DTEX`: a reader that does not know this code skips it
192
+ * by its length and loses only indirect light, which it had no way to trace anyway. See `sdfv.ts`
193
+ * for the layout, and for why it carries no version of its own.
194
+ */
195
+ export declare const CHUNK_SDFV: number;
196
+ /**
197
+ * `NNET`: the weights of the small networks the engine evaluates, found by role.
198
+ *
199
+ * Additive on the same terms as `SDFV`: a reader that does not know this code skips it by its
200
+ * length and loses only the networks, which it had nothing to evaluate them with. See `nnet.ts`.
201
+ */
202
+ export declare const CHUNK_NNET: number;
203
+ /**
204
+ * `NGRF`: a network as a graph of operators and its tensors, found by role — the form a transformer
205
+ * takes, which `NNET`'s perceptron table cannot. Additive on the same terms. See `ngrf.ts`.
206
+ */
207
+ export declare const CHUNK_NGRF: number;
208
+ /**
209
+ * `NAVM`: a polygon mesh a character can walk, with the placement it was built at.
210
+ *
211
+ * Additive on the same terms as `DTEX`: a reader that does not know this code skips it by its
212
+ * length and loses only the navigation, which it had nothing to walk with. See `navm.ts` for the
213
+ * layout, and for why the placement travels inside the chunk rather than beside it.
214
+ */
215
+ export declare const CHUNK_NAVM: number;
216
+ /**
217
+ * `ENTS`: the things in a scene, as the entity model wrote them.
218
+ *
219
+ * Additive on the same terms. See `ents.ts` for the layout, and for why a chunk in a binary
220
+ * container holds text.
221
+ */
222
+ export declare const CHUNK_ENTS: number;
172
223
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
173
224
  export declare const SPLAT_BLOCK_PREFIX = 40;
174
225
  /**
@@ -20,7 +20,7 @@ export const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export const DRFT_VERSION_MINOR = 12;
23
+ export const DRFT_VERSION_MINOR = 17;
24
24
  /** Bytes before the chunk table. */
25
25
  export const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -180,6 +180,57 @@ export const CHUNK_MORP = fourCC('MORP');
180
180
  * fire. See `drftSubs.ts` for the layout.
181
181
  */
182
182
  export const CHUNK_SUBS = fourCC('SUBS');
183
+ /**
184
+ * `MSHL`: a mesh's clusters and the level-of-detail graph over them.
185
+ *
186
+ * Additive, and additive is what keeps the freeze in `FORMAT.md` intact: a reader that does not
187
+ * know this code skips it by its length and loses only the GPU-driven path, which is the one thing
188
+ * it could not have drawn anyway.
189
+ */
190
+ export const CHUNK_MSHL = fourCC('MSHL');
191
+ /**
192
+ * `DTEX`: a material as a decode program over a latent, rather than as pictures.
193
+ *
194
+ * Additive on the same terms as `MSHL`: a reader that does not know this code skips it by its
195
+ * length and loses only the material, which it had no way to decode. See `dtex.ts` for the layout
196
+ * and for why the chunk validates containment rather than meaning.
197
+ */
198
+ export const CHUNK_DTEX = fourCC('DTEX');
199
+ /**
200
+ * `SDFV`: an object's signed distance field, for tracing indirect light through it.
201
+ *
202
+ * Additive on the same terms as `MSHL` and `DTEX`: a reader that does not know this code skips it
203
+ * by its length and loses only indirect light, which it had no way to trace anyway. See `sdfv.ts`
204
+ * for the layout, and for why it carries no version of its own.
205
+ */
206
+ export const CHUNK_SDFV = fourCC('SDFV');
207
+ /**
208
+ * `NNET`: the weights of the small networks the engine evaluates, found by role.
209
+ *
210
+ * Additive on the same terms as `SDFV`: a reader that does not know this code skips it by its
211
+ * length and loses only the networks, which it had nothing to evaluate them with. See `nnet.ts`.
212
+ */
213
+ export const CHUNK_NNET = fourCC('NNET');
214
+ /**
215
+ * `NGRF`: a network as a graph of operators and its tensors, found by role — the form a transformer
216
+ * takes, which `NNET`'s perceptron table cannot. Additive on the same terms. See `ngrf.ts`.
217
+ */
218
+ export const CHUNK_NGRF = fourCC('NGRF');
219
+ /**
220
+ * `NAVM`: a polygon mesh a character can walk, with the placement it was built at.
221
+ *
222
+ * Additive on the same terms as `DTEX`: a reader that does not know this code skips it by its
223
+ * length and loses only the navigation, which it had nothing to walk with. See `navm.ts` for the
224
+ * layout, and for why the placement travels inside the chunk rather than beside it.
225
+ */
226
+ export const CHUNK_NAVM = fourCC('NAVM');
227
+ /**
228
+ * `ENTS`: the things in a scene, as the entity model wrote them.
229
+ *
230
+ * Additive on the same terms. See `ents.ts` for the layout, and for why a chunk in a binary
231
+ * container holds text.
232
+ */
233
+ export const CHUNK_ENTS = fourCC('ENTS');
183
234
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
184
235
  export const SPLAT_BLOCK_PREFIX = 40;
185
236
  /**
@@ -204,6 +255,13 @@ export const KNOWN_CHUNKS = new Set([
204
255
  CHUNK_ANIM,
205
256
  CHUNK_MORP,
206
257
  CHUNK_SUBS,
258
+ CHUNK_MSHL,
259
+ CHUNK_DTEX,
260
+ CHUNK_SDFV,
261
+ CHUNK_NNET,
262
+ CHUNK_NGRF,
263
+ CHUNK_NAVM,
264
+ CHUNK_ENTS,
207
265
  ]);
208
266
  /** Chunk flags. */
209
267
  export const CHUNK_REQUIRED = 1 << 0;
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `MSHL`: the clusters a mesh draws as, and the level-of-detail graph over them.
3
+ *
4
+ * **One chunk per level, not one for the whole graph.** A level is a complete alternative mesh —
5
+ * its own positions, its own indices, its own clusters — so folding every level into one chunk
6
+ * would mean one payload whose parts are only meaningful together. Separate chunks let a loader
7
+ * stream the coarsest level first and refine, which is the same argument `coarseFirst.ts` already
8
+ * makes for splats.
9
+ *
10
+ * ```
11
+ * u32 clusterCount
12
+ * u32 indexTotal
13
+ * u32 triangleOffsets[clusterCount] first triangle of each cluster
14
+ * u32 triangleCounts[clusterCount]
15
+ * f32 boundsCentre[clusterCount * 3]
16
+ * f32 boundsRadius[clusterCount]
17
+ * f32 coneAxis[clusterCount * 3] average face normal, normalised
18
+ * f32 coneCutoff[clusterCount] minimum dot of any face against the axis
19
+ * f32 ownError[clusterCount] this cluster's geometric error
20
+ * f32 parentError[clusterCount] the error of what replaces it one level up
21
+ * u32 indices[indexTotal]
22
+ * ```
23
+ *
24
+ * **No per-chunk version field**, matching `COLL` and every other chunk here: the file's own
25
+ * version covers the format and a second one would be a second thing to keep in step.
26
+ *
27
+ * **`parentError` may be infinite**, which is how the coarsest level says it has no parent. A
28
+ * float32 carries that exactly, so a reader needs no sentinel and a comparison against it needs
29
+ * no special case.
30
+ */
31
+ /** How many clusters one level may carry. Well past what a streaming budget permits. */
32
+ export declare const MAX_CLUSTERS: number;
33
+ export interface MeshletLevel {
34
+ triangleOffsets: Uint32Array;
35
+ triangleCounts: Uint32Array;
36
+ boundsCentre: Float32Array;
37
+ boundsRadius: Float32Array;
38
+ coneAxis: Float32Array;
39
+ coneCutoff: Float32Array;
40
+ ownError: Float32Array;
41
+ parentError: Float32Array;
42
+ indices: Uint32Array;
43
+ count: number;
44
+ }
45
+ export declare function buildMeshlets(level: MeshletLevel): Uint8Array;
46
+ /**
47
+ * The level an `MSHL` chunk carries, as views over the fetched buffer.
48
+ *
49
+ * Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
50
+ * object holding them.
51
+ */
52
+ export declare function readMeshlets(buffer: ArrayBuffer, offset: number, byteLength: number): MeshletLevel;
@@ -0,0 +1,140 @@
1
+ import { DrftError, align } from './drftFormat.js';
2
+ /**
3
+ * `MSHL`: the clusters a mesh draws as, and the level-of-detail graph over them.
4
+ *
5
+ * **One chunk per level, not one for the whole graph.** A level is a complete alternative mesh —
6
+ * its own positions, its own indices, its own clusters — so folding every level into one chunk
7
+ * would mean one payload whose parts are only meaningful together. Separate chunks let a loader
8
+ * stream the coarsest level first and refine, which is the same argument `coarseFirst.ts` already
9
+ * makes for splats.
10
+ *
11
+ * ```
12
+ * u32 clusterCount
13
+ * u32 indexTotal
14
+ * u32 triangleOffsets[clusterCount] first triangle of each cluster
15
+ * u32 triangleCounts[clusterCount]
16
+ * f32 boundsCentre[clusterCount * 3]
17
+ * f32 boundsRadius[clusterCount]
18
+ * f32 coneAxis[clusterCount * 3] average face normal, normalised
19
+ * f32 coneCutoff[clusterCount] minimum dot of any face against the axis
20
+ * f32 ownError[clusterCount] this cluster's geometric error
21
+ * f32 parentError[clusterCount] the error of what replaces it one level up
22
+ * u32 indices[indexTotal]
23
+ * ```
24
+ *
25
+ * **No per-chunk version field**, matching `COLL` and every other chunk here: the file's own
26
+ * version covers the format and a second one would be a second thing to keep in step.
27
+ *
28
+ * **`parentError` may be infinite**, which is how the coarsest level says it has no parent. A
29
+ * float32 carries that exactly, so a reader needs no sentinel and a comparison against it needs
30
+ * no special case.
31
+ */
32
+ /** How many clusters one level may carry. Well past what a streaming budget permits. */
33
+ export const MAX_CLUSTERS = 1 << 20;
34
+ const HEADER = 8;
35
+ function layoutOf(clusterCount, indexTotal) {
36
+ const triangleOffsets = HEADER;
37
+ const triangleCounts = triangleOffsets + clusterCount * 4;
38
+ const boundsCentre = triangleCounts + clusterCount * 4;
39
+ const boundsRadius = boundsCentre + clusterCount * 12;
40
+ const coneAxis = boundsRadius + clusterCount * 4;
41
+ const coneCutoff = coneAxis + clusterCount * 12;
42
+ const ownError = coneCutoff + clusterCount * 4;
43
+ const parentError = ownError + clusterCount * 4;
44
+ const indices = parentError + clusterCount * 4;
45
+ return {
46
+ triangleOffsets,
47
+ triangleCounts,
48
+ boundsCentre,
49
+ boundsRadius,
50
+ coneAxis,
51
+ coneCutoff,
52
+ ownError,
53
+ parentError,
54
+ indices,
55
+ size: indices + indexTotal * 4,
56
+ };
57
+ }
58
+ export function buildMeshlets(level) {
59
+ const { count } = level;
60
+ if (count > MAX_CLUSTERS) {
61
+ throw new DrftError(`${count} clusters exceeds the ${MAX_CLUSTERS} cap`);
62
+ }
63
+ if (level.triangleOffsets.length < count || level.triangleCounts.length < count) {
64
+ throw new DrftError('MSHL was given fewer cluster offsets or counts than clusters');
65
+ }
66
+ if (level.indices.length % 3 !== 0) {
67
+ throw new DrftError(`MSHL was given ${level.indices.length} indices, which is not whole triangles`);
68
+ }
69
+ /*
70
+ * Every cluster's triangles are checked to lie inside the index array before anything is
71
+ * written. A cluster running past the end would otherwise be a subarray of arbitrary geometry on
72
+ * the way back in, which is the same failure `COLL` checks its starts table for.
73
+ */
74
+ const triangles = level.indices.length / 3;
75
+ for (let c = 0; c < count; c += 1) {
76
+ const at = level.triangleOffsets[c];
77
+ const n = level.triangleCounts[c];
78
+ if (at + n > triangles) {
79
+ throw new DrftError(`MSHL cluster ${c} runs past the ${triangles} triangles it was given`);
80
+ }
81
+ }
82
+ const layout = layoutOf(count, level.indices.length);
83
+ const bytes = new Uint8Array(align(layout.size));
84
+ const view = new DataView(bytes.buffer);
85
+ view.setUint32(0, count, true);
86
+ view.setUint32(4, level.indices.length, true);
87
+ new Uint32Array(bytes.buffer, layout.triangleOffsets, count).set(level.triangleOffsets.subarray(0, count));
88
+ new Uint32Array(bytes.buffer, layout.triangleCounts, count).set(level.triangleCounts.subarray(0, count));
89
+ new Float32Array(bytes.buffer, layout.boundsCentre, count * 3).set(level.boundsCentre.subarray(0, count * 3));
90
+ new Float32Array(bytes.buffer, layout.boundsRadius, count).set(level.boundsRadius.subarray(0, count));
91
+ new Float32Array(bytes.buffer, layout.coneAxis, count * 3).set(level.coneAxis.subarray(0, count * 3));
92
+ new Float32Array(bytes.buffer, layout.coneCutoff, count).set(level.coneCutoff.subarray(0, count));
93
+ new Float32Array(bytes.buffer, layout.ownError, count).set(level.ownError.subarray(0, count));
94
+ new Float32Array(bytes.buffer, layout.parentError, count).set(level.parentError.subarray(0, count));
95
+ new Uint32Array(bytes.buffer, layout.indices, level.indices.length).set(level.indices);
96
+ return bytes;
97
+ }
98
+ /**
99
+ * The level an `MSHL` chunk carries, as views over the fetched buffer.
100
+ *
101
+ * Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
102
+ * object holding them.
103
+ */
104
+ export function readMeshlets(buffer, offset, byteLength) {
105
+ if (byteLength < HEADER)
106
+ throw new DrftError('MSHL is too short to hold its counts');
107
+ const view = new DataView(buffer, offset, byteLength);
108
+ const count = view.getUint32(0, true);
109
+ const indexTotal = view.getUint32(4, true);
110
+ if (count > MAX_CLUSTERS) {
111
+ throw new DrftError(`MSHL claims ${count} clusters, over the ${MAX_CLUSTERS} cap`);
112
+ }
113
+ if (indexTotal % 3 !== 0) {
114
+ throw new DrftError(`MSHL claims ${indexTotal} indices, which is not whole triangles`);
115
+ }
116
+ const layout = layoutOf(count, indexTotal);
117
+ if (byteLength < layout.size) {
118
+ throw new DrftError(`MSHL claims ${count} clusters and ${indexTotal} indices, which runs past the chunk`);
119
+ }
120
+ const triangleOffsets = new Uint32Array(buffer, offset + layout.triangleOffsets, count);
121
+ const triangleCounts = new Uint32Array(buffer, offset + layout.triangleCounts, count);
122
+ const triangles = indexTotal / 3;
123
+ for (let c = 0; c < count; c += 1) {
124
+ if (triangleOffsets[c] + triangleCounts[c] > triangles) {
125
+ throw new DrftError(`MSHL cluster ${c} runs past the ${triangles} triangles the chunk holds`);
126
+ }
127
+ }
128
+ return {
129
+ triangleOffsets,
130
+ triangleCounts,
131
+ boundsCentre: new Float32Array(buffer, offset + layout.boundsCentre, count * 3),
132
+ boundsRadius: new Float32Array(buffer, offset + layout.boundsRadius, count),
133
+ coneAxis: new Float32Array(buffer, offset + layout.coneAxis, count * 3),
134
+ coneCutoff: new Float32Array(buffer, offset + layout.coneCutoff, count),
135
+ ownError: new Float32Array(buffer, offset + layout.ownError, count),
136
+ parentError: new Float32Array(buffer, offset + layout.parentError, count),
137
+ indices: new Uint32Array(buffer, offset + layout.indices, indexTotal),
138
+ count,
139
+ };
140
+ }
@@ -18,6 +18,12 @@ import type { AnimationClip, DrftSkin } from './animationData.ts';
18
18
  import type { DrftNode } from './drftSkin.ts';
19
19
  import type { MeshData } from './meshData.ts';
20
20
  import type { DrftChunk, DrftHead, DrftMaterial, DrftSplatBlock } from './drftFormat.ts';
21
+ import { type DrftSdfvEntry } from './sdfv.ts';
22
+ import { type DtexEntry } from './dtex.ts';
23
+ import { type EntsScene } from './ents.ts';
24
+ import { type NavPolyMesh } from './navm.ts';
25
+ import { type DrftNetwork } from './nnet.ts';
26
+ import { type DrftGraph } from './ngrf.ts';
21
27
  /** An embedded image, still compressed. Decoding is the consumer's, through `createImageBitmap`. */
22
28
  export interface DrftTexture {
23
29
  /** What the source called this image. Empty for a file written before names existed. */
@@ -65,6 +71,24 @@ export interface DrftAsset {
65
71
  * file's coarse-first order, so a consumer that draws a prefix of them draws a sparse whole.
66
72
  */
67
73
  readonly splats: DrftSplatBlock | null;
74
+ /**
75
+ * A decode program per material, or empty for a file that carries none.
76
+ *
77
+ * Each says which `MATL` entry it belongs to. **A reader that does not know this chunk skips it
78
+ * by its length** and loses only the material it could not have decoded, which is what keeps the
79
+ * format's freeze intact.
80
+ */
81
+ readonly dtex: readonly DtexEntry[];
82
+ /**
83
+ * A way across the scene, or `null` for a file that carries none.
84
+ *
85
+ * **`null` rather than an empty mesh**, because *nobody built one* and *there is nowhere to walk*
86
+ * are different facts, and a consumer that cannot tell them apart cannot decide whether to build
87
+ * one itself.
88
+ */
89
+ readonly navigation: NavPolyMesh | null;
90
+ /** The things in the scene, or `null` for a file that is an asset rather than a scene. */
91
+ readonly entities: EntsScene | null;
68
92
  /**
69
93
  * The asset's own hierarchy, or empty for a file carrying no `NODE`.
70
94
  *
@@ -89,6 +113,29 @@ export interface DrftAsset {
89
113
  readonly material: number;
90
114
  readonly substance: string;
91
115
  }[];
116
+ /**
117
+ * The signed distance field of each mesh that carries one, or empty for a file with none.
118
+ *
119
+ * The `SDFV` chunk, added at 1.13. Views over the fetched buffer, so a file that carries fields
120
+ * for geometry a consumer never traces through costs one view apiece and no copy. Hand them to
121
+ * `composeGlobalField`, which is what turns per-object fields into a world around the camera.
122
+ */
123
+ readonly fields: readonly DrftSdfvEntry[];
124
+ /**
125
+ * The networks the file carries, each with the role a consumer asks for it by, or empty.
126
+ *
127
+ * The `NNET` chunk, added at 1.14. Weights are views over the fetched buffer, in the precision
128
+ * they were written in — half-precision bits stay bits, because the device uploads them as such.
129
+ */
130
+ readonly networks: readonly DrftNetwork[];
131
+ /**
132
+ * The graphs the file carries — networks built from operators — each with its role, or empty.
133
+ *
134
+ * The `NGRF` chunk, added at 1.15. Tensors are views over the fetched buffer where its alignment
135
+ * allows, in the precision they were written in. `@driftengine/texture` validates one before it
136
+ * runs, naming any operator it lacks.
137
+ */
138
+ readonly graphs: readonly DrftGraph[];
92
139
  /**
93
140
  * The convex hulls this asset collides as, or empty for a file carrying no `COLL`.
94
141
  *
package/dist/drftRead.js CHANGED
@@ -16,8 +16,14 @@
16
16
  */
17
17
  import { readClip, readMorph, readNodes, readSkin } from './drftSkin.js';
18
18
  import { validateMeshData } from './meshData.js';
19
- import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_LODF, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MESH, CHUNK_REQUIRED, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, CHUNK_MATL, CHUNK_TEXS, KNOWN_CHUNKS, MATERIAL_ENTRY_BYTES, align, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_SKIN, CHUNK_MORP, CHUNK_SUBS, CHUNK_COLL, } from './drftFormat.js';
19
+ import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_LODF, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MESH, CHUNK_REQUIRED, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, CHUNK_MATL, CHUNK_TEXS, KNOWN_CHUNKS, MATERIAL_ENTRY_BYTES, align, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_SKIN, CHUNK_MORP, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_COLL, } from './drftFormat.js';
20
20
  import { readSubs } from './drftSubs.js';
21
+ import { readSdfv } from './sdfv.js';
22
+ import { readDtex } from './dtex.js';
23
+ import { readEnts } from './ents.js';
24
+ import { readNavm } from './navm.js';
25
+ import { readNnet } from './nnet.js';
26
+ import { readNgrf } from './ngrf.js';
21
27
  import { readColliders } from './drftColliders.js';
22
28
  const decoder = new TextDecoder();
23
29
  function readString(view, buffer, at, limit) {
@@ -153,11 +159,17 @@ export function readDrft(buffer) {
153
159
  const textures = [];
154
160
  let materials = [];
155
161
  const splatBlocks = [];
162
+ const dtex = [];
163
+ let navigation = null;
164
+ let entities = null;
156
165
  let nodes = [];
157
166
  const skins = [];
158
167
  const clips = [];
159
168
  const morphs = [];
160
169
  let subs = null;
170
+ let fields = null;
171
+ let nnet = null;
172
+ let graphs = [];
161
173
  let colliders = [];
162
174
  const skipped = [];
163
175
  let head = null;
@@ -207,6 +219,12 @@ export function readDrft(buffer) {
207
219
  materials = readMaterials(buffer, chunk);
208
220
  else if (chunk.code === CHUNK_SPLT)
209
221
  splatBlocks.push({ block: readSplatBlock(buffer, chunk), at: chunk.index });
222
+ else if (chunk.code === CHUNK_DTEX)
223
+ dtex.push(readDtex(buffer, chunk.offset, chunk.byteLength));
224
+ else if (chunk.code === CHUNK_NAVM)
225
+ navigation = readNavm(buffer, chunk.offset, chunk.byteLength);
226
+ else if (chunk.code === CHUNK_ENTS)
227
+ entities = readEnts(buffer, chunk.offset, chunk.byteLength);
210
228
  else if (chunk.code === CHUNK_NODE)
211
229
  nodes = readNodes(buffer, chunk.offset, chunk.byteLength);
212
230
  else if (chunk.code === CHUNK_SKIN)
@@ -217,6 +235,12 @@ export function readDrft(buffer) {
217
235
  morphs.push(readMorph(buffer, chunk.offset, chunk.byteLength));
218
236
  else if (chunk.code === CHUNK_SUBS)
219
237
  subs = readSubs(buffer, chunk.offset, chunk.byteLength);
238
+ else if (chunk.code === CHUNK_SDFV)
239
+ fields = readSdfv(buffer, chunk.offset, chunk.byteLength);
240
+ else if (chunk.code === CHUNK_NNET)
241
+ nnet = readNnet(buffer, chunk.offset, chunk.byteLength);
242
+ else if (chunk.code === CHUNK_NGRF)
243
+ graphs = readNgrf(buffer, chunk.offset, chunk.byteLength);
220
244
  else if (chunk.code === CHUNK_COLL)
221
245
  colliders = readColliders(buffer, chunk.offset, chunk.byteLength);
222
246
  /* Every other known chunk is defined but not yet carried; see docs/FORMAT.md phase table. */
@@ -229,8 +253,12 @@ export function readDrft(buffer) {
229
253
  * for the absence of a chunk kind it never needed would be the format telling a consumer what
230
254
  * its scene may be made of.
231
255
  */
232
- if (meshes.length === 0 && splatBlocks.length === 0) {
233
- throw new DrftError('no MESH and no SPLT chunk — an asset with neither geometry nor a capture');
256
+ /* And networks, since 1.15: a converted model is weights and nothing else. */
257
+ if (meshes.length === 0 &&
258
+ splatBlocks.length === 0 &&
259
+ (nnet?.networks.length ?? 0) === 0 &&
260
+ graphs.length === 0) {
261
+ throw new DrftError('no MESH, SPLT, NNET or NGRF chunk — an asset with no geometry, capture or network');
234
262
  }
235
263
  /*
236
264
  * Checked here rather than trusted, because the pairing is by ordinal and a mismatch is
@@ -292,7 +320,13 @@ export function readDrft(buffer) {
292
320
  materials,
293
321
  textures,
294
322
  substances: subs?.entries ?? [],
323
+ fields: fields?.entries ?? [],
324
+ networks: nnet?.networks ?? [],
325
+ graphs,
295
326
  colliders,
327
+ dtex,
328
+ navigation,
329
+ entities,
296
330
  splats: joinSplatBlocks(splatBlocks),
297
331
  skipped,
298
332
  versionMajor,
@@ -13,6 +13,12 @@ import type { DrftNode } from './drftSkin.ts';
13
13
  import type { MeshData } from './meshData.ts';
14
14
  import type { DrftHead, DrftMaterial, DrftSplats } from './drftFormat.ts';
15
15
  import { type DrftSubstanceEntry } from './drftSubs.ts';
16
+ import { type DrftSdfvEntry } from './sdfv.ts';
17
+ import { type DrftNetwork } from './nnet.ts';
18
+ import { type DtexEntry } from './dtex.ts';
19
+ import { type EntsScene } from './ents.ts';
20
+ import { type NavPolyMesh } from './navm.ts';
21
+ import { type DrftGraph } from './ngrf.ts';
16
22
  /** An image to embed, already compressed, with what its own header said about it. */
17
23
  export interface DrftTextureSource {
18
24
  /**
@@ -48,6 +54,25 @@ export interface DrftSource {
48
54
  * than by position, so a file labelling only its fourth material says so.
49
55
  */
50
56
  readonly substances?: readonly DrftSubstanceEntry[];
57
+ /**
58
+ * The signed distance field of each mesh that has one, or absent for a file carrying none.
59
+ *
60
+ * Written as one `SDFV` chunk at 1.13. It pairs by the mesh ordinal carried per entry rather
61
+ * than by position, so a file with a field for only its fourth mesh says so — `SUBS`'s rule,
62
+ * and `MORP` is the chunk that found out why it is a rule.
63
+ */
64
+ readonly fields?: readonly DrftSdfvEntry[];
65
+ /**
66
+ * The networks this file carries, or absent for a file carrying none.
67
+ *
68
+ * Written as one `NNET` chunk at 1.14, each network found by its role rather than by position.
69
+ */
70
+ readonly networks?: readonly DrftNetwork[];
71
+ /**
72
+ * The graphs this file carries — networks built from operators rather than dense layers — or
73
+ * absent for a file carrying none. Written as one `NGRF` chunk at 1.15, each found by its role.
74
+ */
75
+ readonly graphs?: readonly DrftGraph[];
51
76
  /**
52
77
  * The convex hulls this asset collides as, or absent for a file that carries none.
53
78
  *
@@ -93,6 +118,29 @@ export interface DrftSource {
93
118
  * `CHUNK_SPLT` for why it is several chunks rather than one.
94
119
  */
95
120
  readonly splats?: DrftSplats;
121
+ /**
122
+ * A decode program per material, or absent for a file whose materials are ordinary.
123
+ *
124
+ * One `DTEX` chunk each, **paired to a `MATL` entry by the index inside the chunk** rather than
125
+ * by the order they appear. A file may carry one for some of its materials and not others — a
126
+ * scene where one surface came from a capture and the rest were authored — and a reader pairing
127
+ * by position would hand the wrong material the wrong texture with nothing to fail on.
128
+ */
129
+ readonly dtex?: readonly DtexEntry[];
130
+ /**
131
+ * A way across the scene, or absent for a file nobody navigates.
132
+ *
133
+ * The mesh carries the origin and cell size it was built at, because the numbers in it are cell
134
+ * indices and a reader without those has a mesh in the wrong units at the wrong place.
135
+ */
136
+ readonly navigation?: NavPolyMesh;
137
+ /**
138
+ * The things in the scene, as `serializeWorld` wrote them.
139
+ *
140
+ * A *scene*, not an asset: this is what a capture proposes or an editor saves, and a file may
141
+ * carry geometry with no scene at all — which is every baked model this format has ever held.
142
+ */
143
+ readonly entities?: EntsScene;
96
144
  }
97
145
  /**
98
146
  * Bake a `.drft` into one `ArrayBuffer`.