@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.
- package/README.md +1 -1
- package/dist/drftFormat.d.ts +31 -1
- package/dist/drftFormat.js +17 -2
- package/dist/drftInstances.d.ts +9 -0
- package/dist/drftInstances.js +68 -0
- package/dist/drftLights.d.ts +21 -0
- package/dist/drftLights.js +108 -0
- package/dist/drftQuantised.d.ts +3 -0
- package/dist/drftQuantised.js +362 -0
- package/dist/drftRead.d.ts +9 -0
- package/dist/drftRead.js +22 -1
- package/dist/drftStream.d.ts +43 -1
- package/dist/drftStream.js +41 -6
- package/dist/drftWrite.d.ts +15 -0
- package/dist/drftWrite.js +110 -20
- package/dist/index.d.ts +7 -3
- package/dist/index.js +5 -2
- package/dist/meshData.js +9 -2
- package/dist/sdfv.d.ts +15 -0
- package/dist/sdfv.js +15 -0
- package/package.json +4 -4
- package/src/drftFormat.ts +36 -2
- package/src/drftInstances.ts +81 -0
- package/src/drftLights.ts +131 -0
- package/src/drftQuantised.ts +401 -0
- package/src/drftRead.ts +32 -0
- package/src/drftStream.ts +88 -4
- package/src/drftWrite.ts +125 -13
- package/src/index.ts +14 -3
- package/src/meshData.ts +9 -2
- package/src/sdfv.ts +16 -0
|
@@ -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 ===
|
|
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 =
|
|
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
|
}
|