@driftengine/drft 4.3.0 → 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,401 @@
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 {
27
+ ATTR_EMISSIVE_COLOR,
28
+ ATTR_GRAIN,
29
+ ATTR_JOINTS,
30
+ ATTR_RELIEF,
31
+ ATTR_ROUGHNESS,
32
+ ATTR_SPECULAR,
33
+ ATTR_TANGENT,
34
+ ATTR_UVS,
35
+ ATTR_WEIGHTS,
36
+ DrftError,
37
+ align,
38
+ } from './drftFormat.ts';
39
+ import type { MeshData } from './meshData.ts';
40
+
41
+ const RAW = 0;
42
+ const CONSTANT = 1;
43
+ const RANGE16 = 2;
44
+ const OCTAHEDRAL = 3;
45
+ const OCTAHEDRAL_SIGNED = 4;
46
+
47
+ /** Header flag: the indices are 16-bit. */
48
+ const INDICES_16 = 1;
49
+
50
+ type Name =
51
+ | 'positions'
52
+ | 'normals'
53
+ | 'colors'
54
+ | 'emissive'
55
+ | 'specular'
56
+ | 'uvs'
57
+ | 'emissiveColor'
58
+ | 'roughness'
59
+ | 'grain'
60
+ | 'relief'
61
+ | 'tangents'
62
+ | 'joints'
63
+ | 'weights';
64
+
65
+ /** The frozen order `MESH` uses, with each optional array's bit: `FORMAT.md` §4.3. */
66
+ const ORDER: readonly {
67
+ name: Name;
68
+ width: number;
69
+ bit: number;
70
+ kind: 'value' | 'direction' | 'exact';
71
+ }[] = [
72
+ { name: 'positions', width: 3, bit: 0, kind: 'value' },
73
+ { name: 'normals', width: 3, bit: 0, kind: 'direction' },
74
+ { name: 'colors', width: 3, bit: 0, kind: 'value' },
75
+ { name: 'emissive', width: 1, bit: 0, kind: 'value' },
76
+ { name: 'specular', width: 1, bit: ATTR_SPECULAR, kind: 'value' },
77
+ { name: 'uvs', width: 2, bit: ATTR_UVS, kind: 'value' },
78
+ { name: 'emissiveColor', width: 3, bit: ATTR_EMISSIVE_COLOR, kind: 'value' },
79
+ { name: 'roughness', width: 1, bit: ATTR_ROUGHNESS, kind: 'value' },
80
+ { name: 'grain', width: 1, bit: ATTR_GRAIN, kind: 'value' },
81
+ { name: 'relief', width: 1, bit: ATTR_RELIEF, kind: 'value' },
82
+ { name: 'tangents', width: 4, bit: ATTR_TANGENT, kind: 'direction' },
83
+ { name: 'joints', width: 4, bit: ATTR_JOINTS, kind: 'exact' },
84
+ { name: 'weights', width: 4, bit: ATTR_WEIGHTS, kind: 'value' },
85
+ ];
86
+
87
+ /** A growing little-endian byte list, for the writer: offline, so it may allocate. */
88
+ class Bytes {
89
+ private data = new Uint8Array(1024);
90
+ private view = new DataView(this.data.buffer);
91
+ length = 0;
92
+ private room(n: number): void {
93
+ if (this.length + n <= this.data.length) return;
94
+ let size = this.data.length * 2;
95
+ while (size < this.length + n) size *= 2;
96
+ const next = new Uint8Array(size);
97
+ next.set(this.data.subarray(0, this.length));
98
+ this.data = next;
99
+ this.view = new DataView(next.buffer);
100
+ }
101
+ u8(v: number): void {
102
+ this.room(1);
103
+ this.view.setUint8(this.length, v);
104
+ this.length += 1;
105
+ }
106
+ u16(v: number): void {
107
+ this.room(2);
108
+ this.view.setUint16(this.length, v, true);
109
+ this.length += 2;
110
+ }
111
+ i16(v: number): void {
112
+ this.room(2);
113
+ this.view.setInt16(this.length, v, true);
114
+ this.length += 2;
115
+ }
116
+ u32(v: number): void {
117
+ this.room(4);
118
+ this.view.setUint32(this.length, v, true);
119
+ this.length += 4;
120
+ }
121
+ f32(v: number): void {
122
+ this.room(4);
123
+ this.view.setFloat32(this.length, v, true);
124
+ this.length += 4;
125
+ }
126
+ pad(): void {
127
+ while (this.length % 4 !== 0) this.u8(0);
128
+ }
129
+ finish(): Uint8Array {
130
+ this.pad();
131
+ return this.data.slice(0, align(this.length));
132
+ }
133
+ }
134
+
135
+ export function encodeQuantisedMesh(mesh: MeshData): Uint8Array {
136
+ const vertices = mesh.positions.length / 3;
137
+ let attributes = 0;
138
+ for (const entry of ORDER)
139
+ if (entry.bit !== 0 && mesh[entry.name] !== undefined) attributes |= entry.bit;
140
+ if (((attributes & ATTR_JOINTS) !== 0) !== ((attributes & ATTR_WEIGHTS) !== 0)) {
141
+ throw new DrftError('a mesh carries joints without weights, or weights without joints');
142
+ }
143
+ const narrow = vertices <= 65_536;
144
+ const out = new Bytes();
145
+ out.u32(vertices);
146
+ out.u32(mesh.indices.length);
147
+ out.u32(attributes);
148
+ out.u32(narrow ? INDICES_16 : 0);
149
+ for (const entry of ORDER) {
150
+ const values = mesh[entry.name];
151
+ if (values === undefined) continue;
152
+ writeAttribute(out, values, vertices, entry.width, entry.kind);
153
+ }
154
+ for (let i = 0; i < mesh.indices.length; i++) {
155
+ const index = mesh.indices[i] as number;
156
+ if (narrow) out.u16(index);
157
+ else out.u32(index);
158
+ }
159
+ return out.finish();
160
+ }
161
+
162
+ function writeAttribute(
163
+ out: Bytes,
164
+ values: Float32Array,
165
+ vertices: number,
166
+ width: number,
167
+ kind: 'value' | 'direction' | 'exact',
168
+ ): void {
169
+ const encoding = choose(values, vertices, width, kind);
170
+ out.u8(encoding);
171
+ out.u8(width);
172
+ out.u16(0);
173
+ if (encoding === CONSTANT) {
174
+ for (let c = 0; c < width; c++) out.f32(vertices === 0 ? 0 : (values[c] as number));
175
+ return;
176
+ }
177
+ if (encoding === RAW) {
178
+ for (let i = 0; i < vertices * width; i++) out.f32(values[i] as number);
179
+ return;
180
+ }
181
+ if (encoding === RANGE16) {
182
+ const lo = new Float64Array(width).fill(Infinity);
183
+ const hi = new Float64Array(width).fill(-Infinity);
184
+ for (let v = 0; v < vertices; v++) {
185
+ for (let c = 0; c < width; c++) {
186
+ const x = values[v * width + c] as number;
187
+ if (x < (lo[c] as number)) lo[c] = x;
188
+ if (x > (hi[c] as number)) hi[c] = x;
189
+ }
190
+ }
191
+ for (let c = 0; c < width; c++) out.f32(lo[c] as number);
192
+ for (let c = 0; c < width; c++) out.f32(hi[c] as number);
193
+ /* Quantised against the float32 the reader will see, so the reader's arithmetic is the same. */
194
+ const flo = Float32Array.from(lo);
195
+ const fhi = Float32Array.from(hi);
196
+ for (let v = 0; v < vertices; v++) {
197
+ for (let c = 0; c < width; c++) {
198
+ const span = (fhi[c] as number) - (flo[c] as number);
199
+ const x = values[v * width + c] as number;
200
+ out.u16(span > 0 ? Math.round(((x - (flo[c] as number)) / span) * 65535) : 0);
201
+ }
202
+ }
203
+ out.pad();
204
+ return;
205
+ }
206
+ /* Octahedral: two snorm16 a direction, and for a tangent a sign byte after them all. */
207
+ for (let v = 0; v < vertices; v++) {
208
+ const [u, w] = octEncode(
209
+ values[v * width] as number,
210
+ values[v * width + 1] as number,
211
+ values[v * width + 2] as number,
212
+ );
213
+ out.i16(u);
214
+ out.i16(w);
215
+ }
216
+ if (encoding === OCTAHEDRAL_SIGNED) {
217
+ for (let v = 0; v < vertices; v++) out.u8((values[v * 4 + 3] as number) < 0 ? 1 : 0);
218
+ out.pad();
219
+ }
220
+ }
221
+
222
+ function choose(values: Float32Array, vertices: number, width: number, kind: string): number {
223
+ if (kind === 'exact') return RAW;
224
+ let constant = true;
225
+ for (let v = 1; v < vertices && constant; v++) {
226
+ for (let c = 0; c < width; c++) {
227
+ if (values[v * width + c] !== values[c]) {
228
+ constant = false;
229
+ break;
230
+ }
231
+ }
232
+ }
233
+ if (constant) return CONSTANT;
234
+ if (kind === 'direction') {
235
+ /* Only unit directions, and for a tangent only a handedness of exactly ±1, take a code. */
236
+ for (let v = 0; v < vertices; v++) {
237
+ const x = values[v * width] as number;
238
+ const y = values[v * width + 1] as number;
239
+ const z = values[v * width + 2] as number;
240
+ if (Math.abs(Math.hypot(x, y, z) - 1) > 1e-3) return RAW;
241
+ if (width === 4 && Math.abs(values[v * 4 + 3] as number) !== 1) return RAW;
242
+ }
243
+ return width === 4 ? OCTAHEDRAL_SIGNED : OCTAHEDRAL;
244
+ }
245
+ return RANGE16;
246
+ }
247
+
248
+ const SNORM = 32767;
249
+
250
+ /**
251
+ * The octahedral code of a unit direction: the nearest of the four 16-bit neighbours of the exact
252
+ * projection, measured by decoding each: the "precise" variant, whose worst over 200,000 directions
253
+ * is 4.3e-5 rad. Rounding each coordinate alone does noticeably worse near the folds.
254
+ */
255
+ function octEncode(x: number, y: number, z: number): [number, number] {
256
+ const l1 = Math.abs(x) + Math.abs(y) + Math.abs(z);
257
+ let u = x / l1;
258
+ let w = y / l1;
259
+ if (z < 0) {
260
+ const pu = u;
261
+ u = (1 - Math.abs(w)) * (pu >= 0 ? 1 : -1);
262
+ w = (1 - Math.abs(pu)) * (w >= 0 ? 1 : -1);
263
+ }
264
+ const fu = Math.floor(u * SNORM);
265
+ const fw = Math.floor(w * SNORM);
266
+ let best: [number, number] = [fu, fw];
267
+ let bestDot = -2;
268
+ for (let du = 0; du <= 1; du++) {
269
+ for (let dw = 0; dw <= 1; dw++) {
270
+ const qu = Math.max(-SNORM, Math.min(SNORM, fu + du));
271
+ const qw = Math.max(-SNORM, Math.min(SNORM, fw + dw));
272
+ octDecode(qu, qw, OCT_SCRATCH, 0);
273
+ const dot =
274
+ (OCT_SCRATCH[0] as number) * x +
275
+ (OCT_SCRATCH[1] as number) * y +
276
+ (OCT_SCRATCH[2] as number) * z;
277
+ if (dot > bestDot) {
278
+ bestDot = dot;
279
+ best = [qu, qw];
280
+ }
281
+ }
282
+ }
283
+ return best;
284
+ }
285
+
286
+ /*
287
+ * Float64, because the choice is between codes a ten-thousandth of a radian apart and a float32 dot
288
+ * product near 1 cannot see differences below about 3e-4: its own rounding is that coarse.
289
+ */
290
+ const OCT_SCRATCH = new Float64Array(3);
291
+
292
+ function octDecode(qu: number, qw: number, out: Float32Array | Float64Array, at: number): void {
293
+ let x = Math.max(-1, qu / SNORM);
294
+ let y = Math.max(-1, qw / SNORM);
295
+ const z = 1 - Math.abs(x) - Math.abs(y);
296
+ if (z < 0) {
297
+ const px = x;
298
+ x = (1 - Math.abs(y)) * (px >= 0 ? 1 : -1);
299
+ y = (1 - Math.abs(px)) * (y >= 0 ? 1 : -1);
300
+ }
301
+ /*
302
+ * A square root rather than `Math.hypot`, which guards an overflow that three numbers bounded by
303
+ * one cannot reach and costs nearly three times as much for it: 0.7 s of a five-pack load was this
304
+ * function. The float32 a reader keeps agreed with `hypot`'s on every one of 263 million codes
305
+ * sampled across the whole lattice.
306
+ */
307
+ const len = Math.sqrt(x * x + y * y + z * z);
308
+ out[at] = x / len;
309
+ out[at + 1] = y / len;
310
+ out[at + 2] = z / len;
311
+ }
312
+
313
+ export function decodeQuantisedMesh(
314
+ buffer: ArrayBufferLike,
315
+ offset: number,
316
+ byteLength: number,
317
+ ): MeshData {
318
+ const view = new DataView(buffer, offset, byteLength);
319
+ if (byteLength < 16) throw new DrftError('MSHQ is shorter than its header');
320
+ const vertices = view.getUint32(0, true);
321
+ const indexCount = view.getUint32(4, true);
322
+ const attributes = view.getUint32(8, true);
323
+ const flags = view.getUint32(12, true);
324
+ let at = 16;
325
+ const need = (n: number, what: string): void => {
326
+ if (at + n > byteLength) throw new DrftError(`MSHQ ends inside ${what}`);
327
+ };
328
+ const out: Record<string, unknown> = {};
329
+ for (const entry of ORDER) {
330
+ if (entry.bit !== 0 && (attributes & entry.bit) === 0) continue;
331
+ need(4, entry.name);
332
+ const encoding = view.getUint8(at);
333
+ const width = view.getUint8(at + 1);
334
+ at += 4;
335
+ if (width !== entry.width) {
336
+ throw new DrftError(
337
+ `MSHQ ${entry.name} is ${width} wide, and the format says ${entry.width}`,
338
+ );
339
+ }
340
+ const values = new Float32Array(vertices * width);
341
+ if (encoding === CONSTANT) {
342
+ need(width * 4, entry.name);
343
+ for (let c = 0; c < width; c++) {
344
+ const x = view.getFloat32(at + c * 4, true);
345
+ for (let v = 0; v < vertices; v++) values[v * width + c] = x;
346
+ }
347
+ at += width * 4;
348
+ } else if (encoding === RAW) {
349
+ need(vertices * width * 4, entry.name);
350
+ for (let i = 0; i < vertices * width; i++) values[i] = view.getFloat32(at + i * 4, true);
351
+ at += vertices * width * 4;
352
+ } else if (encoding === RANGE16) {
353
+ need(width * 8 + vertices * width * 2, entry.name);
354
+ const lo = new Float32Array(width);
355
+ const hi = new Float32Array(width);
356
+ for (let c = 0; c < width; c++) lo[c] = view.getFloat32(at + c * 4, true);
357
+ for (let c = 0; c < width; c++) hi[c] = view.getFloat32(at + width * 4 + c * 4, true);
358
+ at += width * 8;
359
+ for (let v = 0; v < vertices; v++) {
360
+ for (let c = 0; c < width; c++) {
361
+ const q = view.getUint16(at + (v * width + c) * 2, true);
362
+ values[v * width + c] =
363
+ (lo[c] as number) + (q / 65535) * ((hi[c] as number) - (lo[c] as number));
364
+ }
365
+ }
366
+ at = align(at + vertices * width * 2);
367
+ } else if (encoding === OCTAHEDRAL || encoding === OCTAHEDRAL_SIGNED) {
368
+ need(vertices * 4, entry.name);
369
+ for (let v = 0; v < vertices; v++) {
370
+ octDecode(
371
+ view.getInt16(at + v * 4, true),
372
+ view.getInt16(at + v * 4 + 2, true),
373
+ values,
374
+ v * width,
375
+ );
376
+ }
377
+ at += vertices * 4;
378
+ if (encoding === OCTAHEDRAL_SIGNED) {
379
+ need(vertices, `${entry.name}' handedness`);
380
+ for (let v = 0; v < vertices; v++) values[v * 4 + 3] = view.getUint8(at + v) === 1 ? -1 : 1;
381
+ at = align(at + vertices);
382
+ }
383
+ } else {
384
+ throw new DrftError(
385
+ `MSHQ ${entry.name} has encoding ${encoding}, which this reader does not know`,
386
+ );
387
+ }
388
+ out[entry.name] = values;
389
+ }
390
+ const narrow = (flags & INDICES_16) !== 0;
391
+ need(indexCount * (narrow ? 2 : 4), 'its indices');
392
+ const indices = new Uint32Array(indexCount);
393
+ for (let i = 0; i < indexCount; i++) {
394
+ const index = narrow ? view.getUint16(at + i * 2, true) : view.getUint32(at + i * 4, true);
395
+ if (index >= vertices)
396
+ throw new DrftError(`MSHQ index ${i} names vertex ${index} of ${vertices}`);
397
+ indices[i] = index;
398
+ }
399
+ out['indices'] = indices;
400
+ return out as unknown as MeshData;
401
+ }
package/src/drftRead.ts CHANGED
@@ -54,6 +54,9 @@ import {
54
54
  fourCCName,
55
55
  CHUNK_ANIM,
56
56
  CHUNK_NODE,
57
+ CHUNK_INST,
58
+ CHUNK_LITE,
59
+ CHUNK_MSHQ,
57
60
  CHUNK_SKIN,
58
61
  CHUNK_MORP,
59
62
  CHUNK_NNET,
@@ -71,6 +74,11 @@ import { type NavPolyMesh, readNavm } from './navm.ts';
71
74
  import { type DrftNetwork, type DrftNnet, readNnet } from './nnet.ts';
72
75
  import { type DrftGraph, readNgrf } from './ngrf.ts';
73
76
  import { readColliders } from './drftColliders.ts';
77
+ import { readInstances } from './drftInstances.ts';
78
+ import type { DrftInstanceGroup } from './drftInstances.ts';
79
+ import { readLights } from './drftLights.ts';
80
+ import type { DrftLight } from './drftLights.ts';
81
+ import { decodeQuantisedMesh } from './drftQuantised.ts';
74
82
 
75
83
  /** An embedded image, still compressed. Decoding is the consumer's, through `createImageBitmap`. */
76
84
  export interface DrftTexture {
@@ -145,6 +153,13 @@ export interface DrftAsset {
145
153
  * behaves identically against a file with a hierarchy it ignores.
146
154
  */
147
155
  readonly nodes: readonly DrftNode[];
156
+ /**
157
+ * Meshes drawn many times, each group naming the mesh that holds one copy and a matrix a copy.
158
+ * Empty for a file that instances nothing, which is every file before 1.18.
159
+ */
160
+ readonly instances: readonly DrftInstanceGroup[];
161
+ /** The lights the scene was authored with, or empty. `LITE`, defined at 1.18. */
162
+ readonly lights: readonly DrftLight[];
148
163
  /** Skins, in the order their chunks appear. Empty for a file that deforms nothing. */
149
164
  readonly skins: readonly DrftSkin[];
150
165
  /** Clips, in the order their chunks appear. Empty for a file that animates nothing. */
@@ -365,6 +380,9 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
365
380
  let nnet: DrftNnet | null = null;
366
381
  let graphs: DrftGraph[] = [];
367
382
  let colliders: readonly Float32Array[] = [];
383
+ /* Read once every mesh is counted, because a group is checked against the mesh count. */
384
+ let instanceChunk: { offset: number; byteLength: number } | null = null;
385
+ let lights: DrftLight[] = [];
368
386
  const skipped: string[] = [];
369
387
  let head: DrftHead | null = null;
370
388
 
@@ -406,6 +424,9 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
406
424
 
407
425
  if (chunk.code === CHUNK_HEAD) head = readHead(buffer, chunk);
408
426
  else if (chunk.code === CHUNK_MESH) meshes.push(readMesh(buffer, chunk));
427
+ /* A mesh like any other, in the same ordinal sequence: see `drftQuantised.ts`. */
428
+ else if (chunk.code === CHUNK_MSHQ)
429
+ meshes.push(decodeQuantisedMesh(buffer, chunk.offset, chunk.byteLength));
409
430
  /* A level of detail is a mesh payload under another code, so it takes the same reader. */
410
431
  else if (chunk.code === CHUNK_LODM)
411
432
  levels.push({ level: chunk.index, mesh: readMesh(buffer, chunk) });
@@ -421,6 +442,8 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
421
442
  navigation = readNavm(buffer, chunk.offset, chunk.byteLength);
422
443
  else if (chunk.code === CHUNK_ENTS) entities = readEnts(buffer, chunk.offset, chunk.byteLength);
423
444
  else if (chunk.code === CHUNK_NODE) nodes = readNodes(buffer, chunk.offset, chunk.byteLength);
445
+ else if (chunk.code === CHUNK_INST) instanceChunk = chunk;
446
+ else if (chunk.code === CHUNK_LITE) lights = readLights(buffer, chunk.offset, chunk.byteLength);
424
447
  else if (chunk.code === CHUNK_SKIN)
425
448
  skins.push(readSkin(buffer, chunk.offset, chunk.byteLength));
426
449
  else if (chunk.code === CHUNK_ANIM)
@@ -517,6 +540,11 @@ export function readDrft(buffer: ArrayBuffer): DrftAsset {
517
540
  lods,
518
541
  levels: discreteLevels,
519
542
  nodes,
543
+ instances:
544
+ instanceChunk === null
545
+ ? []
546
+ : readInstances(buffer, instanceChunk.offset, instanceChunk.byteLength, meshes.length),
547
+ lights,
520
548
  skins,
521
549
  clips,
522
550
  materials,
@@ -726,6 +754,10 @@ export function readMaterials(buffer: ArrayBuffer, chunk: DrftChunk): DrftMateri
726
754
  occlusionStrength: has(68, 4) ? view.getFloat32(at + 68, true) : 0,
727
755
  /* Discard nothing, which is exactly what a file written before this field meant. */
728
756
  cutout: has(72, 4) ? view.getFloat32(at + 72, true) : 0,
757
+ /* Opaque unless its opacity says otherwise, which is what a file before 1.18 meant. */
758
+ blend: has(76, 4) ? (view.getUint32(at + 76, true) & 1) !== 0 : false,
759
+ /* One-sided unless it says otherwise, which is how every file before the bit was drawn. */
760
+ doubleSided: has(76, 4) ? (view.getUint32(at + 76, true) & 2) !== 0 : false,
729
761
  });
730
762
  }
731
763
  return out;
package/src/drftStream.ts CHANGED
@@ -18,6 +18,10 @@ import {
18
18
  fourCCName,
19
19
  CHUNK_ANIM,
20
20
  CHUNK_NODE,
21
+ CHUNK_INST,
22
+ CHUNK_LITE,
23
+ CHUNK_SDFV,
24
+ CHUNK_MSHQ,
21
25
  CHUNK_SKIN,
22
26
  CHUNK_MORP,
23
27
  } from './drftFormat.ts';
@@ -28,6 +32,13 @@ import type { AnimationClip, DrftSkin } from './animationData.ts';
28
32
  import type { DrftNode } from './drftSkin.ts';
29
33
  import type { DrftMorph } from './drftSkin.ts';
30
34
  import { readClip, readMorph, readNodes, readSkin } from './drftSkin.ts';
35
+ import { readInstances } from './drftInstances.ts';
36
+ import type { DrftInstanceGroup } from './drftInstances.ts';
37
+ import { readLights } from './drftLights.ts';
38
+ import { readSdfv } from './sdfv.ts';
39
+ import type { DrftSdfvEntry } from './sdfv.ts';
40
+ import type { DrftLight } from './drftLights.ts';
41
+ import { decodeQuantisedMesh } from './drftQuantised.ts';
31
42
 
32
43
  /**
33
44
  * What the file says is coming, known from the first few kilobytes.
@@ -101,6 +112,18 @@ export interface DrftStreamHandlers {
101
112
  * not useful — a child whose parent has not arrived cannot be placed.
102
113
  */
103
114
  readonly onNodes?: (nodes: readonly DrftNode[]) => void;
115
+ /**
116
+ * The asset's instance groups, once, when `INST` lands — ahead of the meshes, where the writer
117
+ * puts it, so a consumer knows a mesh is drawn many times before that mesh arrives.
118
+ */
119
+ readonly onInstances?: (groups: readonly DrftInstanceGroup[]) => void;
120
+ /** The lights the scene was authored with, once, when `LITE` lands. See `drftLights.ts`. */
121
+ readonly onLights?: (lights: readonly DrftLight[]) => void;
122
+ /**
123
+ * The distance fields indirect light is traced against, once, when `SDFV` lands. Each names the
124
+ * mesh it belongs to, or `SDFV_WHOLE_FILE` for one over all of the file's static geometry.
125
+ */
126
+ readonly onFields?: (fields: readonly DrftSdfvEntry[]) => void;
104
127
  /** One mesh's morph deltas, naming the mesh ordinal they belong to. */
105
128
  readonly onMorph?: (morph: DrftMorph) => void;
106
129
  /** One skin, as its `SKIN` chunk lands. Several arrive for an asset with several. */
@@ -327,7 +350,9 @@ export class DrftStream {
327
350
  * counted in `meshCount`, which is the number of parts — an outline is not a part, and
328
351
  * a readout saying "1 of 188 parts" would be counting the model twice.
329
352
  */
330
- if (chunk.code === CHUNK_MESH || chunk.code === CHUNK_LODM) meshBytes += chunk.byteLength;
353
+ if (chunk.code === CHUNK_MESH || chunk.code === CHUNK_MSHQ || chunk.code === CHUNK_LODM) {
354
+ meshBytes += chunk.byteLength;
355
+ }
331
356
  if (chunk.code === CHUNK_TEXS) textureBytes += chunk.byteLength;
332
357
  /* Its own weight rather than geometry's: a capture is not made of parts, so folding it into
333
358
  `meshBytes` would make a "parts" readout weigh something that is not one. */
@@ -346,7 +371,7 @@ export class DrftStream {
346
371
  this.manifestSent = true;
347
372
  this.handlers.onManifest?.({
348
373
  totalBytes: this.buffer.byteLength,
349
- meshCount: this.chunks.filter((c) => c.code === CHUNK_MESH).length,
374
+ meshCount: this.chunks.filter((c) => c.code === CHUNK_MESH || c.code === CHUNK_MSHQ).length,
350
375
  textureCount: this.chunks.filter((c) => c.code === CHUNK_TEXS).length,
351
376
  lodCount: this.chunks.filter((c) => c.code === CHUNK_LODM).length,
352
377
  splatBlockCount: this.chunks.filter((c) => c.code === CHUNK_SPLT).length,
@@ -369,8 +394,11 @@ export class DrftStream {
369
394
  if (chunk.code === CHUNK_HEAD) {
370
395
  this.head = readHead(this.buffer, chunk);
371
396
  this.handlers.onHead?.(this.head);
372
- } else if (chunk.code === CHUNK_MESH) {
373
- const mesh = readMesh(this.buffer, chunk);
397
+ } else if (chunk.code === CHUNK_MESH || chunk.code === CHUNK_MSHQ) {
398
+ const mesh =
399
+ chunk.code === CHUNK_MESH
400
+ ? readMesh(this.buffer, chunk)
401
+ : decodeQuantisedMesh(this.buffer, chunk.offset, chunk.byteLength);
374
402
  this.meshCount++;
375
403
  this.handlers.onMesh?.(mesh, this.meshOrdinal++);
376
404
  } else if (chunk.code === CHUNK_SPLT) {
@@ -388,6 +416,17 @@ export class DrftStream {
388
416
  this.handlers.onMorph?.(readMorph(this.buffer, chunk.offset, chunk.byteLength));
389
417
  } else if (chunk.code === CHUNK_NODE) {
390
418
  this.handlers.onNodes?.(readNodes(this.buffer, chunk.offset, chunk.byteLength));
419
+ } else if (chunk.code === CHUNK_INST) {
420
+ const meshes = this.chunks.filter(
421
+ (c) => c.code === CHUNK_MESH || c.code === CHUNK_MSHQ,
422
+ ).length;
423
+ this.handlers.onInstances?.(
424
+ readInstances(this.buffer, chunk.offset, chunk.byteLength, meshes),
425
+ );
426
+ } else if (chunk.code === CHUNK_LITE) {
427
+ this.handlers.onLights?.(readLights(this.buffer, chunk.offset, chunk.byteLength));
428
+ } else if (chunk.code === CHUNK_SDFV) {
429
+ this.handlers.onFields?.(readSdfv(this.buffer, chunk.offset, chunk.byteLength).entries);
391
430
  } else if (chunk.code === CHUNK_SKIN) {
392
431
  this.handlers.onSkin?.(readSkin(this.buffer, chunk.offset, chunk.byteLength), chunk.index);
393
432
  } else if (chunk.code === CHUNK_ANIM) {
@@ -408,6 +447,42 @@ export class DrftStream {
408
447
  }
409
448
  }
410
449
 
450
+ /**
451
+ * How long a stream may work before it gives way for a task, in milliseconds. The loader's own
452
+ * frame budget is six, and this is the same share of a 60 Hz frame for the same reason.
453
+ */
454
+ export const DEFAULT_STREAM_SLICE_MS = 6;
455
+
456
+ /**
457
+ * When a stream stops to let something else run, and how.
458
+ *
459
+ * **Why a stream has to stop at all.** Once a body's bytes are buffered, every `read` resolves as a
460
+ * microtask, so a loop that pushes what it reads decodes chunk after chunk inside one task and no
461
+ * frame can draw until the file is done. From a local server a five-file courtyard ran in
462
+ * stretches of 0.6 to 1.1 s and drew 117 frames in five seconds. From a slow network the reads
463
+ * wait, the loop yields there by itself, and this costs nothing.
464
+ *
465
+ * Both functions are capabilities, for the reason `FetchLike` is: a host with its own clock or its
466
+ * own scheduler supplies them. What it gives up is throughput when nothing else wants the thread —
467
+ * a task per slice, which a browser may clamp to four milliseconds once timers nest. What would
468
+ * make it wrong is a single chunk longer than the slice, which a slice cannot divide.
469
+ */
470
+ export interface DrftStreamPacing {
471
+ /** Work this long, then give way. `DEFAULT_STREAM_SLICE_MS` when absent. */
472
+ readonly sliceMs?: number;
473
+ /** A monotonic clock in milliseconds. `performance.now` when absent. */
474
+ readonly now?: () => number;
475
+ /** Resolve after other tasks have had a turn. A zero-delay timer when absent. */
476
+ readonly giveWay?: () => Promise<void>;
477
+ }
478
+
479
+ const monotonicNow = (): number => performance.now();
480
+ /* A timer rather than a microtask: a microtask would rejoin the task it is trying to leave. */
481
+ const nextTask = (): Promise<void> =>
482
+ new Promise<void>((resolve) => {
483
+ setTimeout(resolve, 0);
484
+ });
485
+
411
486
  /**
412
487
  * Read a `.drft` from a fetch response as it arrives.
413
488
  *
@@ -418,6 +493,7 @@ export class DrftStream {
418
493
  export async function streamDrft(
419
494
  response: Response,
420
495
  handlers: DrftStreamHandlers = {},
496
+ pacing: DrftStreamPacing = {},
421
497
  ): Promise<void> {
422
498
  if (!response.ok) {
423
499
  throw new DrftError(`fetching the asset returned ${response.status} ${response.statusText}`);
@@ -425,12 +501,20 @@ export async function streamDrft(
425
501
  const body = response.body;
426
502
  if (body === null) throw new DrftError('the response carried no body to stream');
427
503
 
504
+ const sliceMs = pacing.sliceMs ?? DEFAULT_STREAM_SLICE_MS;
505
+ const now = pacing.now ?? monotonicNow;
506
+ const giveWay = pacing.giveWay ?? nextTask;
428
507
  const stream = new DrftStream(handlers);
429
508
  const reader = body.getReader();
509
+ let sliceStarted = now();
430
510
  for (;;) {
431
511
  const next = await reader.read();
432
512
  if (next.done) break;
433
513
  if (next.value !== undefined) stream.push(next.value);
514
+ if (now() - sliceStarted >= sliceMs) {
515
+ await giveWay();
516
+ sliceStarted = now();
517
+ }
434
518
  }
435
519
  stream.end();
436
520
  }