@driftengine/drft 3.61.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.
Files changed (67) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +9 -0
  4. package/dist/animationData.d.ts +53 -0
  5. package/dist/animationData.js +13 -0
  6. package/dist/coarseFirst.d.ts +27 -0
  7. package/dist/coarseFirst.js +118 -0
  8. package/dist/drftColliders.d.ts +53 -0
  9. package/dist/drftColliders.js +137 -0
  10. package/dist/drftFormat.d.ts +454 -0
  11. package/dist/drftFormat.js +350 -0
  12. package/dist/drftRead.d.ts +121 -0
  13. package/dist/drftRead.js +487 -0
  14. package/dist/drftSkin.d.ts +40 -0
  15. package/dist/drftSkin.js +270 -0
  16. package/dist/drftStream.d.ts +170 -0
  17. package/dist/drftStream.js +315 -0
  18. package/dist/drftSubs.d.ts +18 -0
  19. package/dist/drftSubs.js +70 -0
  20. package/dist/drftWrite.d.ts +103 -0
  21. package/dist/drftWrite.js +478 -0
  22. package/dist/fixtures/v1-0.d.ts +9 -0
  23. package/dist/fixtures/v1-0.js +9 -0
  24. package/dist/fixtures/v1-1.d.ts +16 -0
  25. package/dist/fixtures/v1-1.js +16 -0
  26. package/dist/fixtures/v1-11.d.ts +14 -0
  27. package/dist/fixtures/v1-11.js +14 -0
  28. package/dist/fixtures/v1-2.d.ts +19 -0
  29. package/dist/fixtures/v1-2.js +19 -0
  30. package/dist/fixtures/v1-3.d.ts +18 -0
  31. package/dist/fixtures/v1-3.js +18 -0
  32. package/dist/fixtures/v1-4.d.ts +13 -0
  33. package/dist/fixtures/v1-4.js +13 -0
  34. package/dist/fixtures/v1-5.d.ts +14 -0
  35. package/dist/fixtures/v1-5.js +14 -0
  36. package/dist/fixtures/v1-6.d.ts +9 -0
  37. package/dist/fixtures/v1-6.js +9 -0
  38. package/dist/fixtures/v1-7.d.ts +9 -0
  39. package/dist/fixtures/v1-7.js +9 -0
  40. package/dist/fixtures/v1-9.d.ts +9 -0
  41. package/dist/fixtures/v1-9.js +9 -0
  42. package/dist/index.d.ts +26 -0
  43. package/dist/index.js +29 -0
  44. package/dist/meshData.d.ts +210 -0
  45. package/dist/meshData.js +105 -0
  46. package/package.json +57 -0
  47. package/src/animationData.ts +58 -0
  48. package/src/coarseFirst.ts +113 -0
  49. package/src/drftColliders.ts +153 -0
  50. package/src/drftFormat.ts +537 -0
  51. package/src/drftRead.ts +652 -0
  52. package/src/drftSkin.ts +326 -0
  53. package/src/drftStream.ts +436 -0
  54. package/src/drftSubs.ts +86 -0
  55. package/src/drftWrite.ts +613 -0
  56. package/src/fixtures/v1-0.ts +10 -0
  57. package/src/fixtures/v1-1.ts +17 -0
  58. package/src/fixtures/v1-11.ts +15 -0
  59. package/src/fixtures/v1-2.ts +20 -0
  60. package/src/fixtures/v1-3.ts +19 -0
  61. package/src/fixtures/v1-4.ts +14 -0
  62. package/src/fixtures/v1-5.ts +15 -0
  63. package/src/fixtures/v1-6.ts +10 -0
  64. package/src/fixtures/v1-7.ts +10 -0
  65. package/src/fixtures/v1-9.ts +10 -0
  66. package/src/index.ts +56 -0
  67. package/src/meshData.ts +301 -0
@@ -0,0 +1,537 @@
1
+ /**
2
+ * The `.drft` container: constants, layout and the compatibility rules.
3
+ *
4
+ * Shared by the reader and the writer so the two cannot disagree about a byte. Everything
5
+ * here is a fact about the format rather than about either side of it — if a number lives
6
+ * in this file, changing it changes the format.
7
+ *
8
+ * See docs/FORMAT.md for the specification this implements and the reasoning behind it.
9
+ */
10
+
11
+ /** `DRFT`, little-endian, as a u32. The first four bytes of every file. */
12
+ export const DRFT_MAGIC = 0x54465244;
13
+
14
+ /**
15
+ * The generation of the layout. Bumped only when a byte changes meaning.
16
+ *
17
+ * A file whose major exceeds the reader's is refused — that is the one direction
18
+ * compatibility is allowed to fail, and it fails loudly rather than by misreading.
19
+ */
20
+ export const DRFT_VERSION_MAJOR = 1;
21
+
22
+ /**
23
+ * Additive revisions within a generation: new optional chunks, new attribute bits, new
24
+ * enum values with a defined fallback. Never a changed meaning.
25
+ */
26
+ export const DRFT_VERSION_MINOR = 12;
27
+
28
+ /** Bytes before the chunk table. */
29
+ export const HEADER_BYTES = 32;
30
+ /** Bytes per chunk table entry. */
31
+ export const CHUNK_ENTRY_BYTES = 16;
32
+
33
+ /**
34
+ * Everything is 4-byte aligned so a reader can build a `Float32Array` view straight over
35
+ * the fetched buffer. A typed array cannot start at an arbitrary offset, so alignment is
36
+ * not tidiness here — it is the entire reason this format exists rather than glTF.
37
+ */
38
+ export const ALIGNMENT = 4;
39
+
40
+ /** Round up to the next aligned boundary. */
41
+ export function align(value: number): number {
42
+ return (value + (ALIGNMENT - 1)) & ~(ALIGNMENT - 1);
43
+ }
44
+
45
+ /** A four-character code as a u32, so chunk identity is one integer compare. */
46
+ export function fourCC(code: string): number {
47
+ if (code.length !== 4) throw new Error(`drft: a FourCC is four characters, got "${code}"`);
48
+ return (
49
+ (code.charCodeAt(0) & 0xff) |
50
+ ((code.charCodeAt(1) & 0xff) << 8) |
51
+ ((code.charCodeAt(2) & 0xff) << 16) |
52
+ ((code.charCodeAt(3) & 0xff) << 24)
53
+ );
54
+ }
55
+
56
+ /** Back to text, for error messages that name what was not understood. */
57
+ export function fourCCName(code: number): string {
58
+ return String.fromCharCode(
59
+ code & 0xff,
60
+ (code >> 8) & 0xff,
61
+ (code >> 16) & 0xff,
62
+ (code >> 24) & 0xff,
63
+ );
64
+ }
65
+
66
+ export const CHUNK_HEAD = fourCC('HEAD');
67
+ export const CHUNK_MESH = fourCC('MESH');
68
+ /**
69
+ * The asset's own hierarchy: parent index, TRS, mesh index and name. Optional.
70
+ *
71
+ * **Specified in v1 and implemented in 1.6**, which is worth stating rather than quietly fixing.
72
+ * This constant and its `KNOWN_CHUNKS` entry have existed since the format did, and docs/FORMAT.md
73
+ * §4.3 described the payload — while no writer emitted one and no reader consumed one, for the
74
+ * whole of v1. Rigid TRS animation is the first thing that needs a hierarchy, so the chunk is
75
+ * finally built, and to the layout §4.3 already promised rather than to a new one.
76
+ */
77
+ export const CHUNK_NODE = fourCC('NODE');
78
+ export const CHUNK_MATL = fourCC('MATL');
79
+ export const CHUNK_TEXS = fourCC('TEXS');
80
+ /**
81
+ * Baked colliders: a set of convex hulls, as the points whose hulls they are.
82
+ *
83
+ * **Claimed in v1 and defined in 1.12.** The code and the `KNOWN_CHUNKS` entry existed from the
84
+ * start while no writer emitted one and no reader consumed one — the same history `CHUNK_NODE` and
85
+ * `CHUNK_ANIM` had. `drftColliders.ts` carries the payload and the reason it is points rather than
86
+ * shapes.
87
+ */
88
+ export const CHUNK_COLL = fourCC('COLL');
89
+ /**
90
+ * A discrete level of detail, as a **complete nested `.drft`**. Added at 1.9.
91
+ *
92
+ * **Not `LODM`, and the difference is what the chunk exists for.** `LODM` is one merged,
93
+ * material-less outline standing in for a whole asset while it loads. A discrete level is a whole
94
+ * model in its own right — its own meshes, its own materials, its own hierarchy — which is what a
95
+ * source that ships four hand-authored levels actually has, and folding one into `LODM` would
96
+ * merge its parts into a single mesh and discard every material it carries.
97
+ *
98
+ * **A nested file rather than appended arrays, and that is what keeps it additive.** `MATL` is
99
+ * parallel to `MESH` by ordinal and `NODE.mesh` indexes `MESH`, so appending another level's
100
+ * meshes to the same arrays would leave an older reader drawing every level at once, on top of
101
+ * itself. Nesting means an older reader skips one unknown chunk under §4.4 rule 2 and opens the
102
+ * file as the full-detail model it also is, which is the degradation that rule promises.
103
+ *
104
+ * Read as bytes rather than parsed, so a consumer pays only for a level it decides to use.
105
+ */
106
+ export const CHUNK_LODF = fourCC('LODF');
107
+ export const CHUNK_LITE = fourCC('LITE');
108
+ /**
109
+ * A coarse whole-asset level of detail, added in 1.2. Its payload is a `MESH` payload.
110
+ *
111
+ * **A chunk of its own rather than a level field inside `MESH`**, which is what §4.6 of
112
+ * docs/FORMAT.md proposed and is the one part of that design that could not stand. Three
113
+ * things break if a coarse level arrives as a `MESH`, and every one of them is silent:
114
+ *
115
+ * - `MESH` is a *required* chunk kind, so rule 2's protection does not apply. A 1.1 reader
116
+ * would draw the coarse level as a real part, on top of the model it stands for.
117
+ * - `MATL` pairs with `MESH` **by ordinal** and both sides refuse a count mismatch, so a
118
+ * file with one extra `MESH` either fails to open or shifts every material by one.
119
+ * - The manifest counts `MESH` chunks, so "142 of 187 parts" would gain a part that is not
120
+ * one.
121
+ *
122
+ * Optional and skipped in silence, which is rule 2 doing exactly its job: a 1.1 reader opens
123
+ * a 1.2 file as the model without its outline, and the outline is not something a reader that
124
+ * has never heard of it should draw. The table's `index` is the **level ordinal, coarsest
125
+ * first**, which is the same "ordinal within a FourCC" every other chunk kind uses.
126
+ */
127
+ export const CHUNK_LODM = fourCC('LODM');
128
+
129
+ /**
130
+ * One block of a Gaussian splat capture, added in 1.5. Optional, and there are usually several.
131
+ *
132
+ * **Several chunks rather than one, and that is the whole point of putting splats in this
133
+ * container at all.** A chunk is reported when its last byte lands, so a single chunk holding a
134
+ * million splats arrives all at once and streaming buys nothing. Split into blocks whose contents
135
+ * are interleaved across the *whole* capture — see `coarseFirst.ts` — the first block that lands
136
+ * is a complete sparse capture rather than a finished corner of one, and the load opens on a
137
+ * recognisable place that densifies. The table's `index` is the block ordinal.
138
+ *
139
+ * Optional, so rule 2 of docs/FORMAT.md §4.4 applies exactly as it does to `LODM`: a 1.4 reader
140
+ * skips every block in silence and opens the file as whatever else it holds. That is the right
141
+ * degradation — a reader that has never heard of a splat cannot draw one, and a capture is not a
142
+ * mesh it could approximate.
143
+ *
144
+ * **The payload is opaque to this format.** Its per-splat record is `wordsPerSplat` `u32`s whose
145
+ * meaning belongs to whoever packed them, which is `@driftengine/splats`. That keeps the container
146
+ * free of the shader's texel layout and lets a later minor version widen the record — for
147
+ * spherical harmonics, say — without this file learning anything new.
148
+ */
149
+ export const CHUNK_SPLT = fourCC('SPLT');
150
+
151
+ /**
152
+ * A skin: joints, their parents and their inverse bind matrices. Optional, added in 1.6.
153
+ *
154
+ * Written **parents-first**, and the reader refuses any other order. A palette is resolved in
155
+ * index order and reads each joint's parent as it goes, so a nearly-sorted rig is wrong in one
156
+ * limb — which reads as a bad animation rather than as a bad file. The importer is the layer that
157
+ * sorts, because it is also the one that can remap every index naming a joint.
158
+ */
159
+ export const CHUNK_SKIN = fourCC('SKIN');
160
+
161
+ /**
162
+ * One animation clip. Optional, one chunk per clip, added in 1.6.
163
+ *
164
+ * **This FourCC was claimed and undefined for the whole of v1** — docs/FORMAT.md §4.3 listed it as
165
+ * "Reserved. Not in v1", and §4.4's own example of a free additive change was "a v1.4 file carrying
166
+ * `ANIM` opens in a v1.0 reader as a v1.0 file would". That example stops being hypothetical here,
167
+ * and it holds: the chunk is optional, so rule 2 skips it in silence.
168
+ *
169
+ * The table's `index` is the clip ordinal, which is the same "ordinal within a FourCC" every other
170
+ * repeated chunk kind uses.
171
+ */
172
+ export const CHUNK_ANIM = fourCC('ANIM');
173
+
174
+ /**
175
+ * One mesh's morph target deltas. Optional, one chunk per mesh that has any, added in 1.7.
176
+ *
177
+ * **A chunk of its own rather than more attribute bits in `MESH`**, and the reason is the one
178
+ * `CHUNK_LODM` gives about not being a `MESH`: an attribute covers every vertex once, and morph
179
+ * deltas cover every vertex once *per target*. There is no attribute bit that can express a length
180
+ * that scales with a count stored elsewhere, and a reader that guessed would read the wrong number
181
+ * of floats and hand back geometry rather than fail.
182
+ *
183
+ * The table's `index` is the **mesh ordinal** it belongs to, which is how a reader pairs them — the
184
+ * same "ordinal within a FourCC" `MATL` uses, except that `MATL` pairs by position and this pairs
185
+ * by an index a mesh may not have. A mesh with no targets simply has no chunk.
186
+ */
187
+ export const CHUNK_MORP = fourCC('MORP');
188
+
189
+ /**
190
+ * Which substance each material is made of. Optional, one chunk per file, added in 1.8.
191
+ *
192
+ * **`§16` of the chemistry design, and it exists so there is no code between an artist and a fire**:
193
+ * the baker reads `extras.substance` off a glTF material and writes it here, and a consumer hands
194
+ * the ids to `installChemistry().match`. Labelling the oak in Blender is the whole of what a
195
+ * consumer does to make a log burn like oak.
196
+ *
197
+ * **One chunk for the file, pairing by an ordinal in the payload.** `CHUNK_MORP`'s note records why:
198
+ * the table's `index` is the ordinal within a FourCC, so a file where only the fourth material is
199
+ * labelled would carry a chunk at index 0 and a reader pairing by it would set the wrong material on
200
+ * fire. See `drftSubs.ts` for the layout.
201
+ */
202
+ export const CHUNK_SUBS = fourCC('SUBS');
203
+
204
+ /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
205
+ export const SPLAT_BLOCK_PREFIX = 40;
206
+
207
+ /**
208
+ * Chunks this reader understands.
209
+ *
210
+ * The set matters because of rule 3 in docs/FORMAT.md §4.4: a *required* chunk that is not in
211
+ * here is a refusal. An optional one is skipped in silence, which is what makes adding a
212
+ * chunk in a minor version free.
213
+ */
214
+ export const KNOWN_CHUNKS: ReadonlySet<number> = new Set([
215
+ CHUNK_HEAD,
216
+ CHUNK_MESH,
217
+ CHUNK_NODE,
218
+ CHUNK_MATL,
219
+ CHUNK_TEXS,
220
+ CHUNK_COLL,
221
+ CHUNK_LITE,
222
+ CHUNK_LODM,
223
+ CHUNK_LODF,
224
+ CHUNK_SPLT,
225
+ CHUNK_SKIN,
226
+ CHUNK_ANIM,
227
+ CHUNK_MORP,
228
+ CHUNK_SUBS,
229
+ ]);
230
+
231
+ /** Chunk flags. */
232
+ export const CHUNK_REQUIRED = 1 << 0;
233
+
234
+ /** File flags. Bit 0 is set on every file this engine writes. */
235
+ export const FLAG_LITTLE_ENDIAN = 1 << 0;
236
+
237
+ /**
238
+ * Which optional attributes a `MESH` chunk carries.
239
+ *
240
+ * Positions, normals, colours and emissive are mandatory — every `MeshData` has them —
241
+ * so they take no bit. The four optional arrays do, in the order they appear in the
242
+ * payload, and the order is frozen: a new attribute takes the next free bit and appends
243
+ * to the payload, which is why an older reader can stop at the bits it knows.
244
+ */
245
+ export const ATTR_SPECULAR = 1 << 0;
246
+ export const ATTR_UVS = 1 << 1;
247
+ export const ATTR_EMISSIVE_COLOR = 1 << 2;
248
+ export const ATTR_ROUGHNESS = 1 << 3;
249
+ /**
250
+ * Added in 1.1, and the worked example of why the order is frozen rather than tidy.
251
+ *
252
+ * It takes the next free bit and appends to the end of the payload, so a 1.0 reader meets
253
+ * exactly the arrays it knows in exactly the places it expects them and stops before this
254
+ * one. The asset then draws without grain, which is what a reader that has never heard of
255
+ * grain should produce. Inserting it beside `roughness`, where it belongs conceptually,
256
+ * would have shifted every array after it and repainted the model with no error raised.
257
+ */
258
+ export const ATTR_GRAIN = 1 << 4;
259
+ /**
260
+ * Added in 1.3, and it is the same story as `grain` one bit along.
261
+ *
262
+ * `MeshData.relief` and `MeshBuilder.setRelief` shipped in engine 0.23.0 and the container was
263
+ * never taught the bit, so a mesh carrying microscopic relief went through `writeDrft` and came
264
+ * back without it. Everything else survived, which is what made it invisible: a round trip gave
265
+ * the right shape with its surface texture gone, and that reads as a lighting problem rather
266
+ * than as a format dropping an array. Found from outside by a test that writes every optional
267
+ * attribute and reports *all* the losses rather than the first.
268
+ *
269
+ * Next free bit and appended last, for the reason spelled out above: a 1.2 reader meets the
270
+ * arrays it knows where it expects them, stops before this one, and draws the asset smooth,
271
+ * which is what a reader that has never heard of relief should produce.
272
+ */
273
+ export const ATTR_RELIEF = 1 << 5;
274
+ /**
275
+ * Added in 1.4, and the first optional attribute wider than three floats a vertex.
276
+ *
277
+ * A tangent frame: `xyz` running with u, and the bitangent's handedness in `w`. Four floats
278
+ * rather than three because a UV layout can mirror, and a bitangent computed without the sign
279
+ * lights one side of a model inside out. See `generateTangents` for the derivation.
280
+ *
281
+ * Next free bit, appended last, exactly as `grain` and `relief` each explain at length: a 1.3
282
+ * reader meets the six arrays it knows in the six places it expects them, stops before this one,
283
+ * and draws the asset with no tangent frame — which is what a reader that has never heard of
284
+ * tangents should produce, and which is precisely what today's shaders do with one.
285
+ */
286
+ export const ATTR_TANGENT = 1 << 6;
287
+ /**
288
+ * Added in 1.11, and it is `relief` again with more at stake.
289
+ *
290
+ * **Which joints move a vertex, four of them, as indices into a skinning palette.** `MeshData`
291
+ * declared `joints` and `weights` from the day skinning shipped and `validateMeshData` checked
292
+ * both — four floats a vertex, and neither present without the other — and this format wrote
293
+ * neither, because `buildMesh` listed the six optional arrays it knew and there was no bit to
294
+ * list. The `SKIN` chunk beside it was correct the whole time, so a file carried a skeleton, its
295
+ * bind pose and its clips, pointing at vertices that recorded no influence on any of them.
296
+ *
297
+ * **The failure was silent, which is what makes it the worse kind.** A baker handed the writer a
298
+ * correctly-validated skinned mesh, got no error, and produced a file nothing could skin — and
299
+ * `Mesh.isSkinned` reads `data.joints`, so what came back drew in bind pose for ever. Confine
300
+ * wrote its own binary for one human body rather than use this, about seventy lines across a
301
+ * baker and a loader, and that is the cost this closes.
302
+ *
303
+ * Two bits rather than one, because they are two arrays in the payload and the frozen order is a
304
+ * property of arrays. They are still both-or-neither: `validateMeshData` refuses one without the
305
+ * other on the way out and again on the way in, so the pair cannot half-arrive.
306
+ *
307
+ * Next free bits, appended last, for the reason `grain`, `relief` and `tangents` each spell out:
308
+ * a 1.10 reader meets the seven arrays it knows in the seven places it expects them, stops before
309
+ * these, and draws the asset in bind pose — which is what a reader that has never heard of
310
+ * skinning should produce.
311
+ */
312
+ export const ATTR_JOINTS = 1 << 7;
313
+ /** How much each of `ATTR_JOINTS`' four influences moves the vertex. See it for the whole story. */
314
+ export const ATTR_WEIGHTS = 1 << 8;
315
+
316
+ /** Everything this version defines, so a reader can spot bits from a later writer. */
317
+ export const ATTR_KNOWN =
318
+ ATTR_SPECULAR |
319
+ ATTR_UVS |
320
+ ATTR_EMISSIVE_COLOR |
321
+ ATTR_ROUGHNESS |
322
+ ATTR_GRAIN |
323
+ ATTR_RELIEF |
324
+ ATTR_TANGENT |
325
+ ATTR_JOINTS |
326
+ ATTR_WEIGHTS;
327
+
328
+ /**
329
+ * How a `TEXS` payload is encoded.
330
+ *
331
+ * The compressed three are decoded by `createImageBitmap`, which every target browser has,
332
+ * so carrying them costs no dependency and no decoder of ours. `RAW` is uncompressed RGBA8
333
+ * for generated data and tests. `KTX2` is reserved rather than defined: it is the right
334
+ * long-term answer and it needs a transcoder of some 200 KB, so it becomes an additive
335
+ * minor version once a game needs it rather than a cost every asset pays now.
336
+ */
337
+ export const CODEC_PNG = 1;
338
+ export const CODEC_JPEG = 2;
339
+ export const CODEC_WEBP = 3;
340
+ export const CODEC_RAW = 4;
341
+
342
+ /** What a codec is called, for a message naming what was not understood. */
343
+ export function codecName(codec: number): string {
344
+ return codec === CODEC_PNG
345
+ ? 'PNG'
346
+ : codec === CODEC_JPEG
347
+ ? 'JPEG'
348
+ : codec === CODEC_WEBP
349
+ ? 'WEBP'
350
+ : codec === CODEC_RAW
351
+ ? 'RAW'
352
+ : `codec ${codec}`;
353
+ }
354
+
355
+ /**
356
+ * A capture, as the writer is handed it.
357
+ *
358
+ * **`positions` is read and never stored**, which is the one surprising field here. The writer
359
+ * needs them to decide the coarse-first block layout, and the reader gets them back out of the
360
+ * records — so writing them a second time would be twelve bytes a splat of duplicate in the one
361
+ * place this format is trying hardest to be small.
362
+ */
363
+ export interface DrftSplats {
364
+ readonly count: number;
365
+ /** Three per splat, in the capture's own space. Read for layout only. */
366
+ readonly positions: Float32Array;
367
+ /** `wordsPerSplat` `u32`s per splat, in `count` order. Opaque here. */
368
+ readonly records: Uint32Array;
369
+ readonly wordsPerSplat: number;
370
+ readonly boundsMin: Float32Array;
371
+ readonly boundsMax: Float32Array;
372
+ /** Coefficients the source carried and the packer did not read. Carried as a number. */
373
+ readonly sphericalHarmonics?: number;
374
+ /**
375
+ * How many blocks to split the capture into, or absent for a size this writer chooses.
376
+ *
377
+ * More blocks means a capture that opens sooner and a longer chunk table; each entry is
378
+ * sixteen bytes, so even thirty-two blocks is half a kilobyte against megabytes of records.
379
+ */
380
+ readonly blocks?: number;
381
+ }
382
+
383
+ /**
384
+ * One block of a capture, as the reader hands it back.
385
+ *
386
+ * Every block repeats the whole capture's count and bounds, which is twenty-eight bytes each and
387
+ * makes a block self-describing: a consumer that has only the first one can size its buffers and
388
+ * cull against the final extent, rather than waiting for a manifest it may already have passed.
389
+ */
390
+ export interface DrftSplatBlock {
391
+ /** Splats in this block. */
392
+ readonly count: number;
393
+ /** Splats in the whole capture, so a reader allocates once from the first block it meets. */
394
+ readonly totalCount: number;
395
+ readonly wordsPerSplat: number;
396
+ readonly sphericalHarmonics: number;
397
+ /** The whole capture's extent, not this block's. */
398
+ readonly boundsMin: Float32Array;
399
+ readonly boundsMax: Float32Array;
400
+ /** A view over the file's own bytes: `wordsPerSplat * count` words, no copy. */
401
+ readonly records: Uint32Array;
402
+ }
403
+
404
+ /**
405
+ * One surface, stored once per mesh rather than once per vertex.
406
+ *
407
+ * Parallel to the `MESH` chunks by ordinal: material *n* describes mesh *n*. A parallel
408
+ * array rather than a field inside `MESH` because a material is small and a mesh is
409
+ * megabytes, and the loader wants every material before it uploads anything, so it can
410
+ * create each texture once and share it between the meshes that name it.
411
+ */
412
+ export interface DrftMaterial {
413
+ /**
414
+ * What the source called this surface. Diagnostic, and more than diagnostic.
415
+ *
416
+ * A bought asset frequently carries **no material data worth the name** and yet names its
417
+ * materials perfectly well: the one this format was built against stores every surface as
418
+ * the same default grey with no transparency, while calling them `body`, `glass`,
419
+ * `chrome`, `tire_mat5` and `calipers`. The name is then the only thing in the file that
420
+ * says what a surface *is*, so it is carried rather than discarded, and a scene can dress
421
+ * an import without editing the import.
422
+ */
423
+ readonly name: string;
424
+ readonly color: readonly [number, number, number];
425
+ readonly specular: number;
426
+ readonly roughness: number;
427
+ readonly emissive: number;
428
+ readonly emissiveColor: readonly [number, number, number];
429
+ /** 1 is opaque. Below 1 the surface blends, and the loader draws it after the solid ones. */
430
+ readonly opacity: number;
431
+ /** Index into the asset's textures for the colour map, or -1 for an untextured surface. */
432
+ readonly albedo: number;
433
+ /**
434
+ * Index into the asset's textures for the surface-space normal map, or -1 for none.
435
+ *
436
+ * **Written by any baker that can derive it, and bound by nothing yet.** Normal maps ship in the
437
+ * renderer; carrying one through the container is the plan after ORM's. The field is defined
438
+ * here rather than appended later so that all four indices cost the format one minor version
439
+ * instead of three — see `MATERIAL_INDICES`.
440
+ */
441
+ readonly normalMap: number;
442
+ /** Occlusion in R, roughness in G, metallic in B, or -1 for none. glTF's packing. */
443
+ readonly ormMap: number;
444
+ /** Index into the asset's textures for the emissive map, or -1. Written, not yet bound. */
445
+ readonly emissiveMap: number;
446
+ /**
447
+ * glTF's `roughnessFactor` where an `ormMap` supplies the roughness, and 1 where it does not.
448
+ *
449
+ * **A factor that multiplies a texture is not a value**, which is the mistake this pair exists
450
+ * to stop repeating. `roughness` and `specular` above are the scalars a material states when it
451
+ * has no map; these are what the same numbers mean when it has one.
452
+ */
453
+ readonly roughnessScale: number;
454
+ /** glTF's `metallicFactor` where an `ormMap` supplies the metallic, and 1 where it does not. */
455
+ readonly metallicScale: number;
456
+ /**
457
+ * glTF's `occlusionTexture.strength`, or **0** where nothing establishes that the ORM map's R
458
+ * channel holds occlusion at all.
459
+ *
460
+ * glTF assigns G and B of a `metallicRoughnessTexture` and says nothing about R, so occlusion is
461
+ * only there when `occlusionTexture` names the same image — which is what the ORM convention is.
462
+ * Anywhere else this is 0, because reading R would be reading whatever the exporter left.
463
+ */
464
+ readonly occlusionStrength: number;
465
+ /**
466
+ * How much of the environment this surface mirrors, 0 to 1.
467
+ *
468
+ * Distinct from `specular`, which is the strength of a highlight from a *light*. A surface
469
+ * can take a sharp highlight and reflect nothing, which is most plastic, or reflect its
470
+ * surroundings strongly, which is what makes paint and chrome read as what they are.
471
+ */
472
+ readonly reflectivity: number;
473
+ /**
474
+ * Alpha below which a fragment is discarded, 0 for a surface that discards nothing.
475
+ *
476
+ * **A test, not an opacity, and the difference is the whole reason this field exists.** A cutout
477
+ * says which *texels* of a surface are there at all — the gaps in a grille, the space between
478
+ * leaves, the holes in a fence — and leaves everything it keeps at full strength. An opacity
479
+ * says how much of the light passes through the surface that is there. A format with only the
480
+ * second has to spend it on the first, and `SurfaceMaterial.cutout` had no way through the
481
+ * container until this: an alpha-tested surface arrived as a solid rectangle or as nothing.
482
+ *
483
+ * glTF states it exactly — `alphaMode: 'MASK'` with `alphaCutoff`, which defaults to 0.5 — and
484
+ * it is what a vehicle format's alpha-test reference is, on the materials that say they are
485
+ * tested.
486
+ */
487
+ readonly cutout: number;
488
+ }
489
+
490
+ /** Floats in a `MATL` entry, then the signed texture indices that follow them. */
491
+ export const MATERIAL_FLOATS = 15;
492
+ /**
493
+ * Albedo, normal, ORM and emissive.
494
+ *
495
+ * **Four at once rather than one per feature, and the arithmetic is the argument.** Each appended
496
+ * field is a writer change, a reader change, a `FORMAT.md` table edit and a forged-old-file test,
497
+ * so taking them one at a time would spend three minor versions arriving where this arrives in
498
+ * one. A glTF material names all four of its maps as plainly as it names the first, so none of
499
+ * them is speculative — only unbound, and the plans that bind them need no second stride bump.
500
+ */
501
+ export const MATERIAL_INDICES = 4;
502
+ export const MATERIAL_ENTRY_BYTES = MATERIAL_FLOATS * 4 + MATERIAL_INDICES * 4;
503
+
504
+ /** What `HEAD` says about the asset as a whole. */
505
+ export interface DrftHead {
506
+ /** The asset's own name, for diagnostics. Never load-bearing. */
507
+ readonly name: string;
508
+ /** What wrote it — tool and version — so a bad bake can be traced to its baker. */
509
+ readonly generator: string;
510
+ /** Metres per unit in the source. 1 for an asset already authored in metres. */
511
+ readonly unitScale: number;
512
+ /** Axis-aligned bounds of everything, in this file's units: min x/y/z then max x/y/z. */
513
+ readonly bounds: readonly number[];
514
+ }
515
+
516
+ /** A chunk as the table describes it, before its payload is touched. */
517
+ export interface DrftChunk {
518
+ readonly code: number;
519
+ readonly offset: number;
520
+ readonly byteLength: number;
521
+ readonly flags: number;
522
+ readonly index: number;
523
+ }
524
+
525
+ /**
526
+ * Thrown for any malformed or unreadable file, with the offset that failed.
527
+ *
528
+ * One error type rather than several, because every caller does the same thing with it:
529
+ * a `.drft` that cannot be read is an asset that cannot be shown, and the useful part is
530
+ * the message. Loading fails loudly at init; nothing partial is ever returned.
531
+ */
532
+ export class DrftError extends Error {
533
+ constructor(message: string) {
534
+ super(`drft: ${message}`);
535
+ this.name = 'DrftError';
536
+ }
537
+ }