@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.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +9 -0
- package/dist/animationData.d.ts +53 -0
- package/dist/animationData.js +13 -0
- package/dist/coarseFirst.d.ts +27 -0
- package/dist/coarseFirst.js +118 -0
- package/dist/drftColliders.d.ts +53 -0
- package/dist/drftColliders.js +137 -0
- package/dist/drftFormat.d.ts +454 -0
- package/dist/drftFormat.js +350 -0
- package/dist/drftRead.d.ts +121 -0
- package/dist/drftRead.js +487 -0
- package/dist/drftSkin.d.ts +40 -0
- package/dist/drftSkin.js +270 -0
- package/dist/drftStream.d.ts +170 -0
- package/dist/drftStream.js +315 -0
- package/dist/drftSubs.d.ts +18 -0
- package/dist/drftSubs.js +70 -0
- package/dist/drftWrite.d.ts +103 -0
- package/dist/drftWrite.js +478 -0
- package/dist/fixtures/v1-0.d.ts +9 -0
- package/dist/fixtures/v1-0.js +9 -0
- package/dist/fixtures/v1-1.d.ts +16 -0
- package/dist/fixtures/v1-1.js +16 -0
- package/dist/fixtures/v1-11.d.ts +14 -0
- package/dist/fixtures/v1-11.js +14 -0
- package/dist/fixtures/v1-2.d.ts +19 -0
- package/dist/fixtures/v1-2.js +19 -0
- package/dist/fixtures/v1-3.d.ts +18 -0
- package/dist/fixtures/v1-3.js +18 -0
- package/dist/fixtures/v1-4.d.ts +13 -0
- package/dist/fixtures/v1-4.js +13 -0
- package/dist/fixtures/v1-5.d.ts +14 -0
- package/dist/fixtures/v1-5.js +14 -0
- package/dist/fixtures/v1-6.d.ts +9 -0
- package/dist/fixtures/v1-6.js +9 -0
- package/dist/fixtures/v1-7.d.ts +9 -0
- package/dist/fixtures/v1-7.js +9 -0
- package/dist/fixtures/v1-9.d.ts +9 -0
- package/dist/fixtures/v1-9.js +9 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.js +29 -0
- package/dist/meshData.d.ts +210 -0
- package/dist/meshData.js +105 -0
- package/package.json +57 -0
- package/src/animationData.ts +58 -0
- package/src/coarseFirst.ts +113 -0
- package/src/drftColliders.ts +153 -0
- package/src/drftFormat.ts +537 -0
- package/src/drftRead.ts +652 -0
- package/src/drftSkin.ts +326 -0
- package/src/drftStream.ts +436 -0
- package/src/drftSubs.ts +86 -0
- package/src/drftWrite.ts +613 -0
- package/src/fixtures/v1-0.ts +10 -0
- package/src/fixtures/v1-1.ts +17 -0
- package/src/fixtures/v1-11.ts +15 -0
- package/src/fixtures/v1-2.ts +20 -0
- package/src/fixtures/v1-3.ts +19 -0
- package/src/fixtures/v1-4.ts +14 -0
- package/src/fixtures/v1-5.ts +15 -0
- package/src/fixtures/v1-6.ts +10 -0
- package/src/fixtures/v1-7.ts +10 -0
- package/src/fixtures/v1-9.ts +10 -0
- package/src/index.ts +56 -0
- 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
|
+
}
|