@driftengine/drft 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,362 @@
1
+ /**
2
+ * `MSHQ`: a mesh stored in the bytes its data needs, decoded on read into the same `MeshData` a
3
+ * `MESH` gives. Added in 1.18.
4
+ *
5
+ * **Why a second mesh chunk rather than a flag in `MESH`.** `MESH`'s reserved word must be ignored
6
+ * by readers (§4.4 rule 6), so a flag there would let an older reader read 16-bit numbers as floats
7
+ * and draw noise without a word. A new FourCC, marked required, makes that reader refuse by name.
8
+ *
9
+ * **One encoding per attribute, chosen from what the data allows:**
10
+ * - constant, when every vertex carries one value: a bought scene's colour and emissive, 16 bytes a
11
+ * vertex of nothing;
12
+ * - 16 bits over the attribute's own range, for positions, UVs, scalars and weights: half a step of
13
+ * error, 0.27 mm across 36 m;
14
+ * - octahedral in two 16-bit numbers, for unit normals, and for unit tangents with the handedness
15
+ * in a sign byte: within 5e-5 rad, 0.003°, measured over the sphere;
16
+ * - raw floats, where exactness is the contract (joints) or where a "direction" is not unit length
17
+ * and an octahedral code would bend it into one: data already malformed is carried as it came.
18
+ *
19
+ * Indices are 16-bit when the mesh has at most 65,536 vertices.
20
+ *
21
+ * What it gives up is exactness: a quantised mesh is within its bound of the source, not equal to it,
22
+ * and it is no longer a zero-copy view of the file. What would make that wrong is a consumer that
23
+ * compares vertices bit for bit, or one that cannot afford the decode's copy; `writeDrft`'s
24
+ * `quantise: false` writes `MESH` as before.
25
+ */
26
+ import { ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, ATTR_WEIGHTS, DrftError, align, } from './drftFormat.js';
27
+ const RAW = 0;
28
+ const CONSTANT = 1;
29
+ const RANGE16 = 2;
30
+ const OCTAHEDRAL = 3;
31
+ const OCTAHEDRAL_SIGNED = 4;
32
+ /** Header flag: the indices are 16-bit. */
33
+ const INDICES_16 = 1;
34
+ /** The frozen order `MESH` uses, with each optional array's bit: `FORMAT.md` §4.3. */
35
+ const ORDER = [
36
+ { name: 'positions', width: 3, bit: 0, kind: 'value' },
37
+ { name: 'normals', width: 3, bit: 0, kind: 'direction' },
38
+ { name: 'colors', width: 3, bit: 0, kind: 'value' },
39
+ { name: 'emissive', width: 1, bit: 0, kind: 'value' },
40
+ { name: 'specular', width: 1, bit: ATTR_SPECULAR, kind: 'value' },
41
+ { name: 'uvs', width: 2, bit: ATTR_UVS, kind: 'value' },
42
+ { name: 'emissiveColor', width: 3, bit: ATTR_EMISSIVE_COLOR, kind: 'value' },
43
+ { name: 'roughness', width: 1, bit: ATTR_ROUGHNESS, kind: 'value' },
44
+ { name: 'grain', width: 1, bit: ATTR_GRAIN, kind: 'value' },
45
+ { name: 'relief', width: 1, bit: ATTR_RELIEF, kind: 'value' },
46
+ { name: 'tangents', width: 4, bit: ATTR_TANGENT, kind: 'direction' },
47
+ { name: 'joints', width: 4, bit: ATTR_JOINTS, kind: 'exact' },
48
+ { name: 'weights', width: 4, bit: ATTR_WEIGHTS, kind: 'value' },
49
+ ];
50
+ /** A growing little-endian byte list, for the writer: offline, so it may allocate. */
51
+ class Bytes {
52
+ data = new Uint8Array(1024);
53
+ view = new DataView(this.data.buffer);
54
+ length = 0;
55
+ room(n) {
56
+ if (this.length + n <= this.data.length)
57
+ return;
58
+ let size = this.data.length * 2;
59
+ while (size < this.length + n)
60
+ size *= 2;
61
+ const next = new Uint8Array(size);
62
+ next.set(this.data.subarray(0, this.length));
63
+ this.data = next;
64
+ this.view = new DataView(next.buffer);
65
+ }
66
+ u8(v) {
67
+ this.room(1);
68
+ this.view.setUint8(this.length, v);
69
+ this.length += 1;
70
+ }
71
+ u16(v) {
72
+ this.room(2);
73
+ this.view.setUint16(this.length, v, true);
74
+ this.length += 2;
75
+ }
76
+ i16(v) {
77
+ this.room(2);
78
+ this.view.setInt16(this.length, v, true);
79
+ this.length += 2;
80
+ }
81
+ u32(v) {
82
+ this.room(4);
83
+ this.view.setUint32(this.length, v, true);
84
+ this.length += 4;
85
+ }
86
+ f32(v) {
87
+ this.room(4);
88
+ this.view.setFloat32(this.length, v, true);
89
+ this.length += 4;
90
+ }
91
+ pad() {
92
+ while (this.length % 4 !== 0)
93
+ this.u8(0);
94
+ }
95
+ finish() {
96
+ this.pad();
97
+ return this.data.slice(0, align(this.length));
98
+ }
99
+ }
100
+ export function encodeQuantisedMesh(mesh) {
101
+ const vertices = mesh.positions.length / 3;
102
+ let attributes = 0;
103
+ for (const entry of ORDER)
104
+ if (entry.bit !== 0 && mesh[entry.name] !== undefined)
105
+ attributes |= entry.bit;
106
+ if (((attributes & ATTR_JOINTS) !== 0) !== ((attributes & ATTR_WEIGHTS) !== 0)) {
107
+ throw new DrftError('a mesh carries joints without weights, or weights without joints');
108
+ }
109
+ const narrow = vertices <= 65_536;
110
+ const out = new Bytes();
111
+ out.u32(vertices);
112
+ out.u32(mesh.indices.length);
113
+ out.u32(attributes);
114
+ out.u32(narrow ? INDICES_16 : 0);
115
+ for (const entry of ORDER) {
116
+ const values = mesh[entry.name];
117
+ if (values === undefined)
118
+ continue;
119
+ writeAttribute(out, values, vertices, entry.width, entry.kind);
120
+ }
121
+ for (let i = 0; i < mesh.indices.length; i++) {
122
+ const index = mesh.indices[i];
123
+ if (narrow)
124
+ out.u16(index);
125
+ else
126
+ out.u32(index);
127
+ }
128
+ return out.finish();
129
+ }
130
+ function writeAttribute(out, values, vertices, width, kind) {
131
+ const encoding = choose(values, vertices, width, kind);
132
+ out.u8(encoding);
133
+ out.u8(width);
134
+ out.u16(0);
135
+ if (encoding === CONSTANT) {
136
+ for (let c = 0; c < width; c++)
137
+ out.f32(vertices === 0 ? 0 : values[c]);
138
+ return;
139
+ }
140
+ if (encoding === RAW) {
141
+ for (let i = 0; i < vertices * width; i++)
142
+ out.f32(values[i]);
143
+ return;
144
+ }
145
+ if (encoding === RANGE16) {
146
+ const lo = new Float64Array(width).fill(Infinity);
147
+ const hi = new Float64Array(width).fill(-Infinity);
148
+ for (let v = 0; v < vertices; v++) {
149
+ for (let c = 0; c < width; c++) {
150
+ const x = values[v * width + c];
151
+ if (x < lo[c])
152
+ lo[c] = x;
153
+ if (x > hi[c])
154
+ hi[c] = x;
155
+ }
156
+ }
157
+ for (let c = 0; c < width; c++)
158
+ out.f32(lo[c]);
159
+ for (let c = 0; c < width; c++)
160
+ out.f32(hi[c]);
161
+ /* Quantised against the float32 the reader will see, so the reader's arithmetic is the same. */
162
+ const flo = Float32Array.from(lo);
163
+ const fhi = Float32Array.from(hi);
164
+ for (let v = 0; v < vertices; v++) {
165
+ for (let c = 0; c < width; c++) {
166
+ const span = fhi[c] - flo[c];
167
+ const x = values[v * width + c];
168
+ out.u16(span > 0 ? Math.round(((x - flo[c]) / span) * 65535) : 0);
169
+ }
170
+ }
171
+ out.pad();
172
+ return;
173
+ }
174
+ /* Octahedral: two snorm16 a direction, and for a tangent a sign byte after them all. */
175
+ for (let v = 0; v < vertices; v++) {
176
+ const [u, w] = octEncode(values[v * width], values[v * width + 1], values[v * width + 2]);
177
+ out.i16(u);
178
+ out.i16(w);
179
+ }
180
+ if (encoding === OCTAHEDRAL_SIGNED) {
181
+ for (let v = 0; v < vertices; v++)
182
+ out.u8(values[v * 4 + 3] < 0 ? 1 : 0);
183
+ out.pad();
184
+ }
185
+ }
186
+ function choose(values, vertices, width, kind) {
187
+ if (kind === 'exact')
188
+ return RAW;
189
+ let constant = true;
190
+ for (let v = 1; v < vertices && constant; v++) {
191
+ for (let c = 0; c < width; c++) {
192
+ if (values[v * width + c] !== values[c]) {
193
+ constant = false;
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ if (constant)
199
+ return CONSTANT;
200
+ if (kind === 'direction') {
201
+ /* Only unit directions, and for a tangent only a handedness of exactly ±1, take a code. */
202
+ for (let v = 0; v < vertices; v++) {
203
+ const x = values[v * width];
204
+ const y = values[v * width + 1];
205
+ const z = values[v * width + 2];
206
+ if (Math.abs(Math.hypot(x, y, z) - 1) > 1e-3)
207
+ return RAW;
208
+ if (width === 4 && Math.abs(values[v * 4 + 3]) !== 1)
209
+ return RAW;
210
+ }
211
+ return width === 4 ? OCTAHEDRAL_SIGNED : OCTAHEDRAL;
212
+ }
213
+ return RANGE16;
214
+ }
215
+ const SNORM = 32767;
216
+ /**
217
+ * The octahedral code of a unit direction: the nearest of the four 16-bit neighbours of the exact
218
+ * projection, measured by decoding each: the "precise" variant, whose worst over 200,000 directions
219
+ * is 4.3e-5 rad. Rounding each coordinate alone does noticeably worse near the folds.
220
+ */
221
+ function octEncode(x, y, z) {
222
+ const l1 = Math.abs(x) + Math.abs(y) + Math.abs(z);
223
+ let u = x / l1;
224
+ let w = y / l1;
225
+ if (z < 0) {
226
+ const pu = u;
227
+ u = (1 - Math.abs(w)) * (pu >= 0 ? 1 : -1);
228
+ w = (1 - Math.abs(pu)) * (w >= 0 ? 1 : -1);
229
+ }
230
+ const fu = Math.floor(u * SNORM);
231
+ const fw = Math.floor(w * SNORM);
232
+ let best = [fu, fw];
233
+ let bestDot = -2;
234
+ for (let du = 0; du <= 1; du++) {
235
+ for (let dw = 0; dw <= 1; dw++) {
236
+ const qu = Math.max(-SNORM, Math.min(SNORM, fu + du));
237
+ const qw = Math.max(-SNORM, Math.min(SNORM, fw + dw));
238
+ octDecode(qu, qw, OCT_SCRATCH, 0);
239
+ const dot = OCT_SCRATCH[0] * x +
240
+ OCT_SCRATCH[1] * y +
241
+ OCT_SCRATCH[2] * z;
242
+ if (dot > bestDot) {
243
+ bestDot = dot;
244
+ best = [qu, qw];
245
+ }
246
+ }
247
+ }
248
+ return best;
249
+ }
250
+ /*
251
+ * Float64, because the choice is between codes a ten-thousandth of a radian apart and a float32 dot
252
+ * product near 1 cannot see differences below about 3e-4: its own rounding is that coarse.
253
+ */
254
+ const OCT_SCRATCH = new Float64Array(3);
255
+ function octDecode(qu, qw, out, at) {
256
+ let x = Math.max(-1, qu / SNORM);
257
+ let y = Math.max(-1, qw / SNORM);
258
+ const z = 1 - Math.abs(x) - Math.abs(y);
259
+ if (z < 0) {
260
+ const px = x;
261
+ x = (1 - Math.abs(y)) * (px >= 0 ? 1 : -1);
262
+ y = (1 - Math.abs(px)) * (y >= 0 ? 1 : -1);
263
+ }
264
+ /*
265
+ * A square root rather than `Math.hypot`, which guards an overflow that three numbers bounded by
266
+ * one cannot reach and costs nearly three times as much for it: 0.7 s of a five-pack load was this
267
+ * function. The float32 a reader keeps agreed with `hypot`'s on every one of 263 million codes
268
+ * sampled across the whole lattice.
269
+ */
270
+ const len = Math.sqrt(x * x + y * y + z * z);
271
+ out[at] = x / len;
272
+ out[at + 1] = y / len;
273
+ out[at + 2] = z / len;
274
+ }
275
+ export function decodeQuantisedMesh(buffer, offset, byteLength) {
276
+ const view = new DataView(buffer, offset, byteLength);
277
+ if (byteLength < 16)
278
+ throw new DrftError('MSHQ is shorter than its header');
279
+ const vertices = view.getUint32(0, true);
280
+ const indexCount = view.getUint32(4, true);
281
+ const attributes = view.getUint32(8, true);
282
+ const flags = view.getUint32(12, true);
283
+ let at = 16;
284
+ const need = (n, what) => {
285
+ if (at + n > byteLength)
286
+ throw new DrftError(`MSHQ ends inside ${what}`);
287
+ };
288
+ const out = {};
289
+ for (const entry of ORDER) {
290
+ if (entry.bit !== 0 && (attributes & entry.bit) === 0)
291
+ continue;
292
+ need(4, entry.name);
293
+ const encoding = view.getUint8(at);
294
+ const width = view.getUint8(at + 1);
295
+ at += 4;
296
+ if (width !== entry.width) {
297
+ throw new DrftError(`MSHQ ${entry.name} is ${width} wide, and the format says ${entry.width}`);
298
+ }
299
+ const values = new Float32Array(vertices * width);
300
+ if (encoding === CONSTANT) {
301
+ need(width * 4, entry.name);
302
+ for (let c = 0; c < width; c++) {
303
+ const x = view.getFloat32(at + c * 4, true);
304
+ for (let v = 0; v < vertices; v++)
305
+ values[v * width + c] = x;
306
+ }
307
+ at += width * 4;
308
+ }
309
+ else if (encoding === RAW) {
310
+ need(vertices * width * 4, entry.name);
311
+ for (let i = 0; i < vertices * width; i++)
312
+ values[i] = view.getFloat32(at + i * 4, true);
313
+ at += vertices * width * 4;
314
+ }
315
+ else if (encoding === RANGE16) {
316
+ need(width * 8 + vertices * width * 2, entry.name);
317
+ const lo = new Float32Array(width);
318
+ const hi = new Float32Array(width);
319
+ for (let c = 0; c < width; c++)
320
+ lo[c] = view.getFloat32(at + c * 4, true);
321
+ for (let c = 0; c < width; c++)
322
+ hi[c] = view.getFloat32(at + width * 4 + c * 4, true);
323
+ at += width * 8;
324
+ for (let v = 0; v < vertices; v++) {
325
+ for (let c = 0; c < width; c++) {
326
+ const q = view.getUint16(at + (v * width + c) * 2, true);
327
+ values[v * width + c] =
328
+ lo[c] + (q / 65535) * (hi[c] - lo[c]);
329
+ }
330
+ }
331
+ at = align(at + vertices * width * 2);
332
+ }
333
+ else if (encoding === OCTAHEDRAL || encoding === OCTAHEDRAL_SIGNED) {
334
+ need(vertices * 4, entry.name);
335
+ for (let v = 0; v < vertices; v++) {
336
+ octDecode(view.getInt16(at + v * 4, true), view.getInt16(at + v * 4 + 2, true), values, v * width);
337
+ }
338
+ at += vertices * 4;
339
+ if (encoding === OCTAHEDRAL_SIGNED) {
340
+ need(vertices, `${entry.name}' handedness`);
341
+ for (let v = 0; v < vertices; v++)
342
+ values[v * 4 + 3] = view.getUint8(at + v) === 1 ? -1 : 1;
343
+ at = align(at + vertices);
344
+ }
345
+ }
346
+ else {
347
+ throw new DrftError(`MSHQ ${entry.name} has encoding ${encoding}, which this reader does not know`);
348
+ }
349
+ out[entry.name] = values;
350
+ }
351
+ const narrow = (flags & INDICES_16) !== 0;
352
+ need(indexCount * (narrow ? 2 : 4), 'its indices');
353
+ const indices = new Uint32Array(indexCount);
354
+ for (let i = 0; i < indexCount; i++) {
355
+ const index = narrow ? view.getUint16(at + i * 2, true) : view.getUint32(at + i * 4, true);
356
+ if (index >= vertices)
357
+ throw new DrftError(`MSHQ index ${i} names vertex ${index} of ${vertices}`);
358
+ indices[i] = index;
359
+ }
360
+ out['indices'] = indices;
361
+ return out;
362
+ }
@@ -24,6 +24,8 @@ import { type EntsScene } from './ents.ts';
24
24
  import { type NavPolyMesh } from './navm.ts';
25
25
  import { type DrftNetwork } from './nnet.ts';
26
26
  import { type DrftGraph } from './ngrf.ts';
27
+ import type { DrftInstanceGroup } from './drftInstances.ts';
28
+ import type { DrftLight } from './drftLights.ts';
27
29
  /** An embedded image, still compressed. Decoding is the consumer's, through `createImageBitmap`. */
28
30
  export interface DrftTexture {
29
31
  /** What the source called this image. Empty for a file written before names existed. */
@@ -96,6 +98,13 @@ export interface DrftAsset {
96
98
  * behaves identically against a file with a hierarchy it ignores.
97
99
  */
98
100
  readonly nodes: readonly DrftNode[];
101
+ /**
102
+ * Meshes drawn many times, each group naming the mesh that holds one copy and a matrix a copy.
103
+ * Empty for a file that instances nothing, which is every file before 1.18.
104
+ */
105
+ readonly instances: readonly DrftInstanceGroup[];
106
+ /** The lights the scene was authored with, or empty. `LITE`, defined at 1.18. */
107
+ readonly lights: readonly DrftLight[];
99
108
  /** Skins, in the order their chunks appear. Empty for a file that deforms nothing. */
100
109
  readonly skins: readonly DrftSkin[];
101
110
  /** Clips, in the order their chunks appear. Empty for a file that animates nothing. */
package/dist/drftRead.js CHANGED
@@ -16,7 +16,7 @@
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_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';
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_INST, CHUNK_LITE, CHUNK_MSHQ, 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
21
  import { readSdfv } from './sdfv.js';
22
22
  import { readDtex } from './dtex.js';
@@ -25,6 +25,9 @@ import { readNavm } from './navm.js';
25
25
  import { readNnet } from './nnet.js';
26
26
  import { readNgrf } from './ngrf.js';
27
27
  import { readColliders } from './drftColliders.js';
28
+ import { readInstances } from './drftInstances.js';
29
+ import { readLights } from './drftLights.js';
30
+ import { decodeQuantisedMesh } from './drftQuantised.js';
28
31
  const decoder = new TextDecoder();
29
32
  function readString(view, buffer, at, limit) {
30
33
  if (at + 4 > limit)
@@ -171,6 +174,9 @@ export function readDrft(buffer) {
171
174
  let nnet = null;
172
175
  let graphs = [];
173
176
  let colliders = [];
177
+ /* Read once every mesh is counted, because a group is checked against the mesh count. */
178
+ let instanceChunk = null;
179
+ let lights = [];
174
180
  const skipped = [];
175
181
  let head = null;
176
182
  for (let i = 0; i < chunkCount; i++) {
@@ -206,6 +212,9 @@ export function readDrft(buffer) {
206
212
  head = readHead(buffer, chunk);
207
213
  else if (chunk.code === CHUNK_MESH)
208
214
  meshes.push(readMesh(buffer, chunk));
215
+ /* A mesh like any other, in the same ordinal sequence: see `drftQuantised.ts`. */
216
+ else if (chunk.code === CHUNK_MSHQ)
217
+ meshes.push(decodeQuantisedMesh(buffer, chunk.offset, chunk.byteLength));
209
218
  /* A level of detail is a mesh payload under another code, so it takes the same reader. */
210
219
  else if (chunk.code === CHUNK_LODM)
211
220
  levels.push({ level: chunk.index, mesh: readMesh(buffer, chunk) });
@@ -227,6 +236,10 @@ export function readDrft(buffer) {
227
236
  entities = readEnts(buffer, chunk.offset, chunk.byteLength);
228
237
  else if (chunk.code === CHUNK_NODE)
229
238
  nodes = readNodes(buffer, chunk.offset, chunk.byteLength);
239
+ else if (chunk.code === CHUNK_INST)
240
+ instanceChunk = chunk;
241
+ else if (chunk.code === CHUNK_LITE)
242
+ lights = readLights(buffer, chunk.offset, chunk.byteLength);
230
243
  else if (chunk.code === CHUNK_SKIN)
231
244
  skins.push(readSkin(buffer, chunk.offset, chunk.byteLength));
232
245
  else if (chunk.code === CHUNK_ANIM)
@@ -315,6 +328,10 @@ export function readDrft(buffer) {
315
328
  lods,
316
329
  levels: discreteLevels,
317
330
  nodes,
331
+ instances: instanceChunk === null
332
+ ? []
333
+ : readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length),
334
+ lights,
318
335
  skins,
319
336
  clips,
320
337
  materials,
@@ -515,6 +532,10 @@ export function readMaterials(buffer, chunk) {
515
532
  occlusionStrength: has(68, 4) ? view.getFloat32(at + 68, true) : 0,
516
533
  /* Discard nothing, which is exactly what a file written before this field meant. */
517
534
  cutout: has(72, 4) ? view.getFloat32(at + 72, true) : 0,
535
+ /* Opaque unless its opacity says otherwise, which is what a file before 1.18 meant. */
536
+ blend: has(76, 4) ? (view.getUint32(at + 76, true) & 1) !== 0 : false,
537
+ /* One-sided unless it says otherwise, which is how every file before the bit was drawn. */
538
+ doubleSided: has(76, 4) ? (view.getUint32(at + 76, true) & 2) !== 0 : false,
518
539
  });
519
540
  }
520
541
  return out;
@@ -5,6 +5,9 @@ import type { DrftTexture } from './drftRead.ts';
5
5
  import type { AnimationClip, DrftSkin } from './animationData.ts';
6
6
  import type { DrftNode } from './drftSkin.ts';
7
7
  import type { DrftMorph } from './drftSkin.ts';
8
+ import type { DrftInstanceGroup } from './drftInstances.ts';
9
+ import type { DrftSdfvEntry } from './sdfv.ts';
10
+ import type { DrftLight } from './drftLights.ts';
8
11
  /**
9
12
  * What the file says is coming, known from the first few kilobytes.
10
13
  *
@@ -76,6 +79,18 @@ export interface DrftStreamHandlers {
76
79
  * not useful — a child whose parent has not arrived cannot be placed.
77
80
  */
78
81
  readonly onNodes?: (nodes: readonly DrftNode[]) => void;
82
+ /**
83
+ * The asset's instance groups, once, when `INST` lands — ahead of the meshes, where the writer
84
+ * puts it, so a consumer knows a mesh is drawn many times before that mesh arrives.
85
+ */
86
+ readonly onInstances?: (groups: readonly DrftInstanceGroup[]) => void;
87
+ /** The lights the scene was authored with, once, when `LITE` lands. See `drftLights.ts`. */
88
+ readonly onLights?: (lights: readonly DrftLight[]) => void;
89
+ /**
90
+ * The distance fields indirect light is traced against, once, when `SDFV` lands. Each names the
91
+ * mesh it belongs to, or `SDFV_WHOLE_FILE` for one over all of the file's static geometry.
92
+ */
93
+ readonly onFields?: (fields: readonly DrftSdfvEntry[]) => void;
79
94
  /** One mesh's morph deltas, naming the mesh ordinal they belong to. */
80
95
  readonly onMorph?: (morph: DrftMorph) => void;
81
96
  /** One skin, as its `SKIN` chunk lands. Several arrive for an asset with several. */
@@ -160,6 +175,33 @@ export declare class DrftStream {
160
175
  /** Report every chunk whose last byte has now landed, in file order. */
161
176
  private settleChunks;
162
177
  }
178
+ /**
179
+ * How long a stream may work before it gives way for a task, in milliseconds. The loader's own
180
+ * frame budget is six, and this is the same share of a 60 Hz frame for the same reason.
181
+ */
182
+ export declare const DEFAULT_STREAM_SLICE_MS = 6;
183
+ /**
184
+ * When a stream stops to let something else run, and how.
185
+ *
186
+ * **Why a stream has to stop at all.** Once a body's bytes are buffered, every `read` resolves as a
187
+ * microtask, so a loop that pushes what it reads decodes chunk after chunk inside one task and no
188
+ * frame can draw until the file is done. From a local server a five-file courtyard ran in
189
+ * stretches of 0.6 to 1.1 s and drew 117 frames in five seconds. From a slow network the reads
190
+ * wait, the loop yields there by itself, and this costs nothing.
191
+ *
192
+ * Both functions are capabilities, for the reason `FetchLike` is: a host with its own clock or its
193
+ * own scheduler supplies them. What it gives up is throughput when nothing else wants the thread —
194
+ * a task per slice, which a browser may clamp to four milliseconds once timers nest. What would
195
+ * make it wrong is a single chunk longer than the slice, which a slice cannot divide.
196
+ */
197
+ export interface DrftStreamPacing {
198
+ /** Work this long, then give way. `DEFAULT_STREAM_SLICE_MS` when absent. */
199
+ readonly sliceMs?: number;
200
+ /** A monotonic clock in milliseconds. `performance.now` when absent. */
201
+ readonly now?: () => number;
202
+ /** Resolve after other tasks have had a turn. A zero-delay timer when absent. */
203
+ readonly giveWay?: () => Promise<void>;
204
+ }
163
205
  /**
164
206
  * Read a `.drft` from a fetch response as it arrives.
165
207
  *
@@ -167,4 +209,4 @@ export declare class DrftStream {
167
209
  * and the model improves as the bytes land. A response with no body — an error page, a
168
210
  * cache miss served as a redirect — throws rather than resolving to an empty asset.
169
211
  */
170
- export declare function streamDrft(response: Response, handlers?: DrftStreamHandlers): Promise<void>;
212
+ export declare function streamDrft(response: Response, handlers?: DrftStreamHandlers, pacing?: DrftStreamPacing): Promise<void>;
@@ -1,7 +1,11 @@
1
1
  /** Reading a `.drft` while it is still arriving, chunk by chunk. */
2
- import { CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_SPLT, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_TEXS, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, KNOWN_CHUNKS, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_SKIN, CHUNK_MORP, } from './drftFormat.js';
2
+ import { CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_LODM, CHUNK_SPLT, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_TEXS, DRFT_MAGIC, DRFT_VERSION_MAJOR, DrftError, HEADER_BYTES, KNOWN_CHUNKS, fourCCName, CHUNK_ANIM, CHUNK_NODE, CHUNK_INST, CHUNK_LITE, CHUNK_SDFV, CHUNK_MSHQ, CHUNK_SKIN, CHUNK_MORP, } from './drftFormat.js';
3
3
  import { readHead, readMaterials, readMesh, readSplatBlock, readTexture } from './drftRead.js';
4
4
  import { readClip, readMorph, readNodes, readSkin } from './drftSkin.js';
5
+ import { readInstances } from './drftInstances.js';
6
+ import { readLights } from './drftLights.js';
7
+ import { readSdfv } from './sdfv.js';
8
+ import { decodeQuantisedMesh } from './drftQuantised.js';
5
9
  /**
6
10
  * An incremental `.drft` reader: feed it bytes, it reports each chunk as that chunk completes.
7
11
  *
@@ -196,8 +200,9 @@ export class DrftStream {
196
200
  * counted in `meshCount`, which is the number of parts — an outline is not a part, and
197
201
  * a readout saying "1 of 188 parts" would be counting the model twice.
198
202
  */
199
- if (chunk.code === CHUNK_MESH || chunk.code === CHUNK_LODM)
203
+ if (chunk.code === CHUNK_MESH || chunk.code === CHUNK_MSHQ || chunk.code === CHUNK_LODM) {
200
204
  meshBytes += chunk.byteLength;
205
+ }
201
206
  if (chunk.code === CHUNK_TEXS)
202
207
  textureBytes += chunk.byteLength;
203
208
  /* Its own weight rather than geometry's: a capture is not made of parts, so folding it into
@@ -216,7 +221,7 @@ export class DrftStream {
216
221
  this.manifestSent = true;
217
222
  this.handlers.onManifest?.({
218
223
  totalBytes: this.buffer.byteLength,
219
- meshCount: this.chunks.filter((c) => c.code === CHUNK_MESH).length,
224
+ meshCount: this.chunks.filter((c) => c.code === CHUNK_MESH || c.code === CHUNK_MSHQ).length,
220
225
  textureCount: this.chunks.filter((c) => c.code === CHUNK_TEXS).length,
221
226
  lodCount: this.chunks.filter((c) => c.code === CHUNK_LODM).length,
222
227
  splatBlockCount: this.chunks.filter((c) => c.code === CHUNK_SPLT).length,
@@ -242,8 +247,10 @@ export class DrftStream {
242
247
  this.head = readHead(this.buffer, chunk);
243
248
  this.handlers.onHead?.(this.head);
244
249
  }
245
- else if (chunk.code === CHUNK_MESH) {
246
- const mesh = readMesh(this.buffer, chunk);
250
+ else if (chunk.code === CHUNK_MESH || chunk.code === CHUNK_MSHQ) {
251
+ const mesh = chunk.code === CHUNK_MESH
252
+ ? readMesh(this.buffer, chunk)
253
+ : decodeQuantisedMesh(this.buffer, chunk.offset, chunk.byteLength);
247
254
  this.meshCount++;
248
255
  this.handlers.onMesh?.(mesh, this.meshOrdinal++);
249
256
  }
@@ -265,6 +272,16 @@ export class DrftStream {
265
272
  else if (chunk.code === CHUNK_NODE) {
266
273
  this.handlers.onNodes?.(readNodes(this.buffer, chunk.offset, chunk.byteLength));
267
274
  }
275
+ else if (chunk.code === CHUNK_INST) {
276
+ const meshes = this.chunks.filter((c) => c.code === CHUNK_MESH || c.code === CHUNK_MSHQ).length;
277
+ this.handlers.onInstances?.(readInstances(this.buffer, chunk.offset, chunk.byteLength, meshes));
278
+ }
279
+ else if (chunk.code === CHUNK_LITE) {
280
+ this.handlers.onLights?.(readLights(this.buffer, chunk.offset, chunk.byteLength));
281
+ }
282
+ else if (chunk.code === CHUNK_SDFV) {
283
+ this.handlers.onFields?.(readSdfv(this.buffer, chunk.offset, chunk.byteLength).entries);
284
+ }
268
285
  else if (chunk.code === CHUNK_SKIN) {
269
286
  this.handlers.onSkin?.(readSkin(this.buffer, chunk.offset, chunk.byteLength), chunk.index);
270
287
  }
@@ -288,6 +305,16 @@ export class DrftStream {
288
305
  }
289
306
  }
290
307
  }
308
+ /**
309
+ * How long a stream may work before it gives way for a task, in milliseconds. The loader's own
310
+ * frame budget is six, and this is the same share of a 60 Hz frame for the same reason.
311
+ */
312
+ export const DEFAULT_STREAM_SLICE_MS = 6;
313
+ const monotonicNow = () => performance.now();
314
+ /* A timer rather than a microtask: a microtask would rejoin the task it is trying to leave. */
315
+ const nextTask = () => new Promise((resolve) => {
316
+ setTimeout(resolve, 0);
317
+ });
291
318
  /**
292
319
  * Read a `.drft` from a fetch response as it arrives.
293
320
  *
@@ -295,21 +322,29 @@ export class DrftStream {
295
322
  * and the model improves as the bytes land. A response with no body — an error page, a
296
323
  * cache miss served as a redirect — throws rather than resolving to an empty asset.
297
324
  */
298
- export async function streamDrft(response, handlers = {}) {
325
+ export async function streamDrft(response, handlers = {}, pacing = {}) {
299
326
  if (!response.ok) {
300
327
  throw new DrftError(`fetching the asset returned ${response.status} ${response.statusText}`);
301
328
  }
302
329
  const body = response.body;
303
330
  if (body === null)
304
331
  throw new DrftError('the response carried no body to stream');
332
+ const sliceMs = pacing.sliceMs ?? DEFAULT_STREAM_SLICE_MS;
333
+ const now = pacing.now ?? monotonicNow;
334
+ const giveWay = pacing.giveWay ?? nextTask;
305
335
  const stream = new DrftStream(handlers);
306
336
  const reader = body.getReader();
337
+ let sliceStarted = now();
307
338
  for (;;) {
308
339
  const next = await reader.read();
309
340
  if (next.done)
310
341
  break;
311
342
  if (next.value !== undefined)
312
343
  stream.push(next.value);
344
+ if (now() - sliceStarted >= sliceMs) {
345
+ await giveWay();
346
+ sliceStarted = now();
347
+ }
313
348
  }
314
349
  stream.end();
315
350
  }
@@ -19,6 +19,8 @@ import { type DtexEntry } from './dtex.ts';
19
19
  import { type EntsScene } from './ents.ts';
20
20
  import { type NavPolyMesh } from './navm.ts';
21
21
  import { type DrftGraph } from './ngrf.ts';
22
+ import type { DrftInstanceGroup } from './drftInstances.ts';
23
+ import type { DrftLight } from './drftLights.ts';
22
24
  /** An image to embed, already compressed, with what its own header said about it. */
23
25
  export interface DrftTextureSource {
24
26
  /**
@@ -106,6 +108,19 @@ export interface DrftSource {
106
108
  * the meshes exactly as it always did, which is why this is additive.
107
109
  */
108
110
  readonly nodes?: readonly DrftNode[];
111
+ /**
112
+ * Meshes drawn many times: one `INST` chunk, written ahead of the geometry and required. The mesh
113
+ * a group names holds one copy; see `drftInstances.ts`.
114
+ */
115
+ readonly instances?: readonly DrftInstanceGroup[];
116
+ /** The lights the scene was authored with: one `LITE` chunk, optional. See `drftLights.ts`. */
117
+ readonly lights?: readonly DrftLight[];
118
+ /**
119
+ * Write each mesh as `MSHQ` rather than `MESH`: constants, 16-bit ranges and octahedral
120
+ * directions, about a third of the bytes, within the bounds `drftQuantised.ts` states. Off unless
121
+ * asked, so a caller writing a file gets the bytes it always got; the baker asks by default.
122
+ */
123
+ readonly quantise?: boolean;
109
124
  /** Skins, one `SKIN` chunk each, or absent for an asset that deforms nothing. */
110
125
  readonly skins?: readonly DrftSkin[];
111
126
  /** Clips, one `ANIM` chunk each, or absent for an asset that animates nothing. */