@driftengine/drft 3.63.0 → 4.1.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/NOTICE +20 -0
- package/README.md +1 -1
- package/dist/drftFormat.d.ts +52 -1
- package/dist/drftFormat.js +59 -1
- package/dist/drftMeshlets.d.ts +52 -0
- package/dist/drftMeshlets.js +140 -0
- package/dist/drftRead.d.ts +47 -0
- package/dist/drftRead.js +37 -3
- package/dist/drftWrite.d.ts +48 -0
- package/dist/drftWrite.js +59 -3
- package/dist/dtex.d.ts +127 -0
- package/dist/dtex.js +324 -0
- package/dist/ents.d.ts +47 -0
- package/dist/ents.js +60 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +22 -0
- package/dist/navm.d.ts +58 -0
- package/dist/navm.js +105 -0
- package/dist/ngrf.d.ts +29 -0
- package/dist/ngrf.js +214 -0
- package/dist/nnet.d.ts +76 -0
- package/dist/nnet.js +194 -0
- package/dist/sdfv.d.ts +71 -0
- package/dist/sdfv.js +142 -0
- package/package.json +4 -2
- package/src/drftFormat.ts +66 -1
- package/src/drftMeshlets.ts +185 -0
- package/src/drftRead.ts +82 -2
- package/src/drftWrite.ts +112 -2
- package/src/dtex.ts +404 -0
- package/src/ents.ts +105 -0
- package/src/index.ts +43 -0
- package/src/navm.ts +171 -0
- package/src/ngrf.ts +299 -0
- package/src/nnet.ts +250 -0
- package/src/sdfv.ts +178 -0
package/src/nnet.ts
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `NNET` — the weights of the small networks the engine evaluates, in either precision.
|
|
3
|
+
*
|
|
4
|
+
* **One chunk for every network a file carries, each found by its role.** A role is a FourCC the
|
|
5
|
+
* consumer chooses — reconstruction's refinement tier, a capture stage's prior — so a file can carry
|
|
6
|
+
* several and a reader asks for the one it wants rather than counting. Two networks with one role
|
|
7
|
+
* would make that question ambiguous, so the writer and the reader both refuse them.
|
|
8
|
+
*
|
|
9
|
+
* **The shape is carried, and the weight count has to agree with it.** A network whose weights are
|
|
10
|
+
* all present and whose shape says they are laid out differently is the corruption that reads as a
|
|
11
|
+
* working file: every weight lands in some layer, the evaluator runs, and the output is wrong
|
|
12
|
+
* everywhere. The layout is `@driftengine/texture`'s `evalNetwork`: for each layer, the weight
|
|
13
|
+
* matrix row by row by output, then the biases. `nnetWeightCount` is that formula, and
|
|
14
|
+
* `@driftengine/assets` holds it equal to `networkWeightCount` — the two packages cannot import one
|
|
15
|
+
* another, so a test that can import both is what keeps them one convention.
|
|
16
|
+
*
|
|
17
|
+
* **Sixteen or thirty-two bits, per network.** Half precision is what a device with `shader-f16`
|
|
18
|
+
* runs, and the bits are stored as they will be uploaded rather than converted here — `halfWeights`
|
|
19
|
+
* in `@driftengine/texture` is the one place a float becomes half-precision bits.
|
|
20
|
+
*
|
|
21
|
+
* **No per-chunk version field**, matching `SDFV`, `MSHL`, `DTEX` and every other chunk here, and
|
|
22
|
+
* **additive**: a reader that does not know the code skips it by its length and loses only the
|
|
23
|
+
* networks, which it had nothing to evaluate them with anyway.
|
|
24
|
+
*
|
|
25
|
+
* ### Layout
|
|
26
|
+
*
|
|
27
|
+
* ```text
|
|
28
|
+
* u32 count
|
|
29
|
+
* count x NNET_ENTRY_BYTES:
|
|
30
|
+
* u32 role a FourCC naming what the network is for
|
|
31
|
+
* u32 inputs
|
|
32
|
+
* u32 outputs
|
|
33
|
+
* u32 hiddenCount at most NNET_MAX_HIDDEN
|
|
34
|
+
* u32 hidden[8] widths; the slots past hiddenCount are zero
|
|
35
|
+
* u32 precision 16 or 32
|
|
36
|
+
* u32 weightCount
|
|
37
|
+
* weights every network's weights back to back, in entry order: four bytes each at
|
|
38
|
+
* 32 bits, two at 16, each block padded to a four-byte boundary
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
41
|
+
* **The padding is what lets every block be viewed rather than copied.** An odd number of
|
|
42
|
+
* sixteen-bit weights ends two bytes short of a boundary, and a `Float32Array` cannot be taken at an
|
|
43
|
+
* offset that is not a multiple of four.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { DrftError, align, fourCC, fourCCName } from './drftFormat.ts';
|
|
47
|
+
|
|
48
|
+
/** Role, three counts, eight widths, precision and weight count. */
|
|
49
|
+
export const NNET_ENTRY_BYTES = 56;
|
|
50
|
+
|
|
51
|
+
/** Hidden layers the entry table has room for. */
|
|
52
|
+
export const NNET_MAX_HIDDEN = 8;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* The widest layer a network may declare.
|
|
56
|
+
*
|
|
57
|
+
* **A bound on the arithmetic, not on ambition.** A width is a `u32` read out of a file and two of
|
|
58
|
+
* them multiply into a weight count, so a corrupt pair asks for an allocation the size of the
|
|
59
|
+
* address space. 4,096 is far past anything the device evaluates — its widest is sixteen — and
|
|
60
|
+
* keeps every product of two widths comfortably inside the integers a double holds exactly.
|
|
61
|
+
*/
|
|
62
|
+
export const NNET_MAX_WIDTH = 4096;
|
|
63
|
+
|
|
64
|
+
export interface DrftNetwork {
|
|
65
|
+
/** Four printable characters naming what the network is for. */
|
|
66
|
+
readonly role: string;
|
|
67
|
+
readonly inputs: number;
|
|
68
|
+
/** Hidden layer widths, input side first. At most `NNET_MAX_HIDDEN`. */
|
|
69
|
+
readonly hidden: readonly number[];
|
|
70
|
+
readonly outputs: number;
|
|
71
|
+
/** Single-precision weights, or half-precision bits as the device uploads them. */
|
|
72
|
+
readonly weights: Float32Array | Uint16Array;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface DrftNnet {
|
|
76
|
+
readonly networks: readonly DrftNetwork[];
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** How many weights and biases a shape needs, in `evalNetwork`'s layout. */
|
|
80
|
+
export function nnetWeightCount(
|
|
81
|
+
inputs: number,
|
|
82
|
+
hidden: readonly number[],
|
|
83
|
+
outputs: number,
|
|
84
|
+
): number {
|
|
85
|
+
let total = 0;
|
|
86
|
+
let previous = inputs;
|
|
87
|
+
for (const width of hidden) {
|
|
88
|
+
total += previous * width + width;
|
|
89
|
+
previous = width;
|
|
90
|
+
}
|
|
91
|
+
return total + previous * outputs + outputs;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function describeShape(inputs: number, hidden: readonly number[], outputs: number): string {
|
|
95
|
+
return [inputs, ...hidden, outputs].join(' → ');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/* Four characters from space to tilde, which is what a FourCC a person chose looks like. */
|
|
99
|
+
function printableRole(role: string): boolean {
|
|
100
|
+
if (role.length !== 4) return false;
|
|
101
|
+
for (let i = 0; i < 4; i++) {
|
|
102
|
+
const code = role.charCodeAt(i);
|
|
103
|
+
if (code < 0x20 || code > 0x7e) return false;
|
|
104
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function refuseWidth(label: string, width: number): void {
|
|
109
|
+
if (!(width >= 1 && width <= NNET_MAX_WIDTH)) {
|
|
110
|
+
throw new DrftError(
|
|
111
|
+
`${label} declares a width of ${width}; a layer is between 1 and ${NNET_MAX_WIDTH} wide`,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** Write the chunk. Refuses anything a reader would have to guess about. */
|
|
117
|
+
export function buildNnet(nnet: DrftNnet): Uint8Array {
|
|
118
|
+
const roles = new Set<string>();
|
|
119
|
+
let weightBytes = 0;
|
|
120
|
+
for (const [at, network] of nnet.networks.entries()) {
|
|
121
|
+
const label = `NNET network ${at}`;
|
|
122
|
+
if (!printableRole(network.role)) {
|
|
123
|
+
throw new DrftError(
|
|
124
|
+
`${label} has the role ${JSON.stringify(network.role)}; a role is four printable characters`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
if (roles.has(network.role)) {
|
|
128
|
+
throw new DrftError(
|
|
129
|
+
`${label} repeats the role ${network.role}; a reader finds a network by it`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
roles.add(network.role);
|
|
133
|
+
if (network.hidden.length > NNET_MAX_HIDDEN) {
|
|
134
|
+
throw new DrftError(
|
|
135
|
+
`${label} has ${network.hidden.length} hidden layers and the table holds ${NNET_MAX_HIDDEN}`,
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
for (const width of [network.inputs, ...network.hidden, network.outputs]) {
|
|
139
|
+
refuseWidth(label, width);
|
|
140
|
+
}
|
|
141
|
+
const expected = nnetWeightCount(network.inputs, network.hidden, network.outputs);
|
|
142
|
+
if (network.weights.length !== expected) {
|
|
143
|
+
throw new DrftError(
|
|
144
|
+
`${label} (${network.role}) is ${describeShape(network.inputs, network.hidden, network.outputs)}, ` +
|
|
145
|
+
`which needs ${expected} weights, and carries ${network.weights.length}`,
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
weightBytes += align(network.weights.byteLength);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const table = 4 + nnet.networks.length * NNET_ENTRY_BYTES;
|
|
152
|
+
const bytes = new Uint8Array(table + weightBytes);
|
|
153
|
+
const view = new DataView(bytes.buffer);
|
|
154
|
+
view.setUint32(0, nnet.networks.length, true);
|
|
155
|
+
|
|
156
|
+
let at = 4;
|
|
157
|
+
let block = table;
|
|
158
|
+
for (const network of nnet.networks) {
|
|
159
|
+
view.setUint32(at, fourCC(network.role), true);
|
|
160
|
+
view.setUint32(at + 4, network.inputs, true);
|
|
161
|
+
view.setUint32(at + 8, network.outputs, true);
|
|
162
|
+
view.setUint32(at + 12, network.hidden.length, true);
|
|
163
|
+
for (const [layer, width] of network.hidden.entries()) {
|
|
164
|
+
view.setUint32(at + 16 + layer * 4, width, true);
|
|
165
|
+
}
|
|
166
|
+
const half = network.weights instanceof Uint16Array;
|
|
167
|
+
view.setUint32(at + 48, half ? 16 : 32, true);
|
|
168
|
+
view.setUint32(at + 52, network.weights.length, true);
|
|
169
|
+
bytes.set(
|
|
170
|
+
new Uint8Array(
|
|
171
|
+
network.weights.buffer,
|
|
172
|
+
network.weights.byteOffset,
|
|
173
|
+
network.weights.byteLength,
|
|
174
|
+
),
|
|
175
|
+
block,
|
|
176
|
+
);
|
|
177
|
+
at += NNET_ENTRY_BYTES;
|
|
178
|
+
block += align(network.weights.byteLength);
|
|
179
|
+
}
|
|
180
|
+
return bytes;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Read the chunk, refusing every shape a reader would otherwise have to trust. */
|
|
184
|
+
export function readNnet(buffer: ArrayBuffer, offset: number, byteLength: number): DrftNnet {
|
|
185
|
+
if (byteLength < 4) throw new DrftError('NNET is too short to hold its count');
|
|
186
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
187
|
+
const count = view.getUint32(0, true);
|
|
188
|
+
|
|
189
|
+
const table = 4 + count * NNET_ENTRY_BYTES;
|
|
190
|
+
/* The count first, before anything derived from it is trusted — `SDFV` says why. */
|
|
191
|
+
if (table > byteLength) {
|
|
192
|
+
throw new DrftError(
|
|
193
|
+
`NNET declares ${count} networks, which needs ${table} bytes of table, and the chunk has ` +
|
|
194
|
+
`${byteLength}`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const networks: DrftNetwork[] = [];
|
|
199
|
+
const roles = new Set<string>();
|
|
200
|
+
let block = table;
|
|
201
|
+
for (let i = 0; i < count; i++) {
|
|
202
|
+
const at = 4 + i * NNET_ENTRY_BYTES;
|
|
203
|
+
const role = fourCCName(view.getUint32(at, true));
|
|
204
|
+
const label = `NNET network ${i} (${role})`;
|
|
205
|
+
if (roles.has(role)) {
|
|
206
|
+
throw new DrftError(`${label} repeats a role; a reader finds a network by it`);
|
|
207
|
+
}
|
|
208
|
+
roles.add(role);
|
|
209
|
+
const inputs = view.getUint32(at + 4, true);
|
|
210
|
+
const outputs = view.getUint32(at + 8, true);
|
|
211
|
+
const hiddenCount = view.getUint32(at + 12, true);
|
|
212
|
+
if (hiddenCount > NNET_MAX_HIDDEN) {
|
|
213
|
+
throw new DrftError(
|
|
214
|
+
`${label} declares ${hiddenCount} hidden layers and the table holds ${NNET_MAX_HIDDEN}`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
const hidden: number[] = [];
|
|
218
|
+
for (let layer = 0; layer < hiddenCount; layer++) {
|
|
219
|
+
hidden.push(view.getUint32(at + 16 + layer * 4, true));
|
|
220
|
+
}
|
|
221
|
+
for (const width of [inputs, ...hidden, outputs]) refuseWidth(label, width);
|
|
222
|
+
|
|
223
|
+
const precision = view.getUint32(at + 48, true);
|
|
224
|
+
if (precision !== 16 && precision !== 32) {
|
|
225
|
+
throw new DrftError(`${label} declares ${precision}-bit weights; the layout holds 16 or 32`);
|
|
226
|
+
}
|
|
227
|
+
const weightCount = view.getUint32(at + 52, true);
|
|
228
|
+
const expected = nnetWeightCount(inputs, hidden, outputs);
|
|
229
|
+
if (weightCount !== expected) {
|
|
230
|
+
throw new DrftError(
|
|
231
|
+
`${label} carries ${weightCount} weights, and its shape ` +
|
|
232
|
+
`${describeShape(inputs, hidden, outputs)} needs ${expected}`,
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
const blockBytes = weightCount * (precision / 8);
|
|
236
|
+
if (block + blockBytes > byteLength) {
|
|
237
|
+
throw new DrftError(
|
|
238
|
+
`${label} carries ${blockBytes} bytes of weights, and the chunk has ` +
|
|
239
|
+
`${byteLength - block} left`,
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const weights =
|
|
243
|
+
precision === 32
|
|
244
|
+
? new Float32Array(buffer, offset + block, weightCount)
|
|
245
|
+
: new Uint16Array(buffer, offset + block, weightCount);
|
|
246
|
+
networks.push({ role, inputs, hidden, outputs, weights });
|
|
247
|
+
block += align(blockBytes);
|
|
248
|
+
}
|
|
249
|
+
return { networks };
|
|
250
|
+
}
|
package/src/sdfv.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `SDFV` — an object's signed distance field, so indirect light can be traced through it.
|
|
3
|
+
*
|
|
4
|
+
* **What it carries and why it is in the container at all.** A distance field is expensive to
|
|
5
|
+
* build — exact point-to-triangle distance from every grid point in a band around every triangle —
|
|
6
|
+
* and it depends on nothing but the geometry. `bakeObjectSdf` in `@driftengine/assets` produces
|
|
7
|
+
* one; `globalField.ts` in `@driftengine/core` composes them around the camera each frame. Neither
|
|
8
|
+
* end can do the other's job, and the container is what joins them.
|
|
9
|
+
*
|
|
10
|
+
* **One chunk for the file with the mesh ordinal carried per entry, which is `SUBS`'s lesson.**
|
|
11
|
+
* The chunk table's `index` is the ordinal *within a FourCC*, so a file where only the fourth mesh
|
|
12
|
+
* has a field would carry a chunk at index 0 — and a reader pairing by it would trace light through
|
|
13
|
+
* the wrong geometry. `MORP` found that first and paid for it.
|
|
14
|
+
*
|
|
15
|
+
* **No per-chunk version field**, matching `COLL`, `MSHL`, `DTEX` and every other chunk here: the
|
|
16
|
+
* file's own version covers the format and a second one would be a second thing to keep in step.
|
|
17
|
+
* The implementation plan asked this chunk to refuse a bad version of its own; the convention it
|
|
18
|
+
* would have broken is older than the plan and is argued in `drftMeshlets.ts`, so what refuses a
|
|
19
|
+
* version here is the container, as it does for every other chunk.
|
|
20
|
+
*
|
|
21
|
+
* **Additive, and that is what keeps the freeze in `FORMAT.md` intact.** A reader that does not
|
|
22
|
+
* know this code skips it by its length and loses only indirect light, which is the one thing it
|
|
23
|
+
* could not have traced anyway.
|
|
24
|
+
*
|
|
25
|
+
* ### Layout
|
|
26
|
+
*
|
|
27
|
+
* ```text
|
|
28
|
+
* u32 count
|
|
29
|
+
* count x SDFV_ENTRY_BYTES:
|
|
30
|
+
* u32 mesh the mesh ordinal this field belongs to
|
|
31
|
+
* u32 nx, ny, nz samples on each axis
|
|
32
|
+
* f32 bounds[6] minX, minY, minZ, maxX, maxY, maxZ
|
|
33
|
+
* f32 fields every entry's samples, back to back, in entry order
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* **The table is a fixed stride and the samples follow it**, rather than each entry carrying its
|
|
37
|
+
* own samples inline. Two reasons, and the second is the load-bearing one: a reader can walk the
|
|
38
|
+
* table to find one mesh's field without parsing every field before it, and every sample block
|
|
39
|
+
* starts at a four-byte boundary — `4 + count * 40` is divisible by four for any count — so a
|
|
40
|
+
* `Float32Array` view can be taken straight onto the buffer instead of copying. `drftMeshlets.ts`
|
|
41
|
+
* takes its views the same way and for the same reason.
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { DrftError, align } from './drftFormat.ts';
|
|
45
|
+
|
|
46
|
+
/** `u32` mesh, three `u32` dimensions and six `f32` bounds. A field's samples are elsewhere. */
|
|
47
|
+
export const SDFV_ENTRY_BYTES = 40;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The most samples one field may declare.
|
|
51
|
+
*
|
|
52
|
+
* **A bound on the count, not a bound on ambition.** The product of three `u32`s read out of a
|
|
53
|
+
* file overflows a double at 2^53 and is a plausible number long before that, so a corrupt entry
|
|
54
|
+
* asks for an allocation that either throws somewhere unhelpful or succeeds and eats the tab. A
|
|
55
|
+
* 512-cubed field is 537 MB at four bytes a sample, which is far past anything a per-object bake
|
|
56
|
+
* produces and is comfortably under where the arithmetic stops being trustworthy.
|
|
57
|
+
*/
|
|
58
|
+
export const SDFV_MAX_SAMPLES = 512 * 512 * 512;
|
|
59
|
+
|
|
60
|
+
export interface DrftSdfvEntry {
|
|
61
|
+
/** The mesh ordinal this field belongs to, as `MESH` numbers them. */
|
|
62
|
+
readonly mesh: number;
|
|
63
|
+
/** Samples on each axis. The spacing is the same on all three; see `bakeObjectSdf`. */
|
|
64
|
+
readonly dims: readonly [number, number, number];
|
|
65
|
+
/** The field's own extent, `[minX, minY, minZ, maxX, maxY, maxZ]`. Six floats. */
|
|
66
|
+
readonly bounds: Float32Array;
|
|
67
|
+
/** Distance in metres, negative inside. `x` fastest, then `y`, then `z`. */
|
|
68
|
+
readonly field: Float32Array;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface DrftSdfv {
|
|
72
|
+
readonly entries: readonly DrftSdfvEntry[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Write the chunk. Refuses anything a reader would have to guess about. */
|
|
76
|
+
export function buildSdfv(sdfv: DrftSdfv): Uint8Array {
|
|
77
|
+
let samples = 0;
|
|
78
|
+
for (const [at, entry] of sdfv.entries.entries()) {
|
|
79
|
+
const [nx, ny, nz] = entry.dims;
|
|
80
|
+
if (!(nx >= 1 && ny >= 1 && nz >= 1)) {
|
|
81
|
+
throw new DrftError(
|
|
82
|
+
`SDFV entry ${at} declares dimensions ${nx}x${ny}x${nz}; every dimension is at least one.`,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (entry.bounds.length !== 6) {
|
|
86
|
+
throw new DrftError(
|
|
87
|
+
`SDFV entry ${at} carries ${entry.bounds.length} bounds and the layout is six.`,
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
const expected = nx * ny * nz;
|
|
91
|
+
if (entry.field.length !== expected) {
|
|
92
|
+
throw new DrftError(
|
|
93
|
+
`SDFV entry ${at} is ${nx}x${ny}x${nz}, which is ${expected} samples, and carries ` +
|
|
94
|
+
`${entry.field.length}.`,
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
samples += expected;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const table = 4 + sdfv.entries.length * SDFV_ENTRY_BYTES;
|
|
101
|
+
const bytes = new Uint8Array(align(table + samples * 4));
|
|
102
|
+
const view = new DataView(bytes.buffer);
|
|
103
|
+
view.setUint32(0, sdfv.entries.length, true);
|
|
104
|
+
|
|
105
|
+
let at = 4;
|
|
106
|
+
let sample = table;
|
|
107
|
+
for (const entry of sdfv.entries) {
|
|
108
|
+
view.setUint32(at, entry.mesh, true);
|
|
109
|
+
view.setUint32(at + 4, entry.dims[0], true);
|
|
110
|
+
view.setUint32(at + 8, entry.dims[1], true);
|
|
111
|
+
view.setUint32(at + 12, entry.dims[2], true);
|
|
112
|
+
for (let axis = 0; axis < 6; axis++) {
|
|
113
|
+
view.setFloat32(at + 16 + axis * 4, entry.bounds[axis] as number, true);
|
|
114
|
+
}
|
|
115
|
+
at += SDFV_ENTRY_BYTES;
|
|
116
|
+
new Float32Array(bytes.buffer, sample, entry.field.length).set(entry.field);
|
|
117
|
+
sample += entry.field.length * 4;
|
|
118
|
+
}
|
|
119
|
+
return bytes;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Read the chunk, refusing every shape a reader would otherwise have to trust. */
|
|
123
|
+
export function readSdfv(buffer: ArrayBuffer, offset: number, byteLength: number): DrftSdfv {
|
|
124
|
+
if (byteLength < 4) throw new DrftError('SDFV is too short to hold its count');
|
|
125
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
126
|
+
const count = view.getUint32(0, true);
|
|
127
|
+
|
|
128
|
+
const table = 4 + count * SDFV_ENTRY_BYTES;
|
|
129
|
+
/*
|
|
130
|
+
* **The count is checked against the chunk before a single entry is read.** It is the first
|
|
131
|
+
* thing in the payload and everything after derives from it, so a corrupt one is what turns a
|
|
132
|
+
* short read into an allocation the size of the file's declared ambition.
|
|
133
|
+
*/
|
|
134
|
+
if (table > byteLength) {
|
|
135
|
+
throw new DrftError(
|
|
136
|
+
`SDFV declares ${count} fields, which needs ${table} bytes of table, and the chunk has ` +
|
|
137
|
+
`${byteLength}`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const entries: DrftSdfvEntry[] = [];
|
|
142
|
+
let sample = table;
|
|
143
|
+
for (let i = 0; i < count; i++) {
|
|
144
|
+
const at = 4 + i * SDFV_ENTRY_BYTES;
|
|
145
|
+
const mesh = view.getUint32(at, true);
|
|
146
|
+
const nx = view.getUint32(at + 4, true);
|
|
147
|
+
const ny = view.getUint32(at + 8, true);
|
|
148
|
+
const nz = view.getUint32(at + 12, true);
|
|
149
|
+
if (nx < 1 || ny < 1 || nz < 1) {
|
|
150
|
+
throw new DrftError(
|
|
151
|
+
`SDFV field ${i} declares dimensions ${nx}x${ny}x${nz}; a field with no samples has no ` +
|
|
152
|
+
'surface.',
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
const samples = nx * ny * nz;
|
|
156
|
+
if (samples > SDFV_MAX_SAMPLES) {
|
|
157
|
+
throw new DrftError(
|
|
158
|
+
`SDFV field ${i} declares ${samples} samples and the cap is ${SDFV_MAX_SAMPLES}`,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
if (sample + samples * 4 > byteLength) {
|
|
162
|
+
throw new DrftError(
|
|
163
|
+
`SDFV field ${i} is ${nx}x${ny}x${nz}, which is ${samples * 4} bytes, and the chunk has ` +
|
|
164
|
+
`${byteLength - sample} left`,
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
const bounds = new Float32Array(6);
|
|
168
|
+
for (let axis = 0; axis < 6; axis++) bounds[axis] = view.getFloat32(at + 16 + axis * 4, true);
|
|
169
|
+
entries.push({
|
|
170
|
+
mesh,
|
|
171
|
+
dims: [nx, ny, nz],
|
|
172
|
+
bounds,
|
|
173
|
+
field: new Float32Array(buffer, offset + sample, samples),
|
|
174
|
+
});
|
|
175
|
+
sample += samples * 4;
|
|
176
|
+
}
|
|
177
|
+
return { entries };
|
|
178
|
+
}
|