@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.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  The `.drft` container: read and write a baked scene. Declares no runtime dependency.
4
4
 
5
- **Cost: 8.1 KB gzipped.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
5
+ **Cost: 9.7 KB gzipped.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
6
6
  3% — the number is a fact about the build rather than a claim in a document.
7
7
 
8
8
  Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
@@ -20,7 +20,7 @@ export declare const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export declare const DRFT_VERSION_MINOR = 17;
23
+ export declare const DRFT_VERSION_MINOR = 18;
24
24
  /** Bytes before the chunk table. */
25
25
  export declare const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -220,6 +220,17 @@ export declare const CHUNK_NAVM: number;
220
220
  * container holds text.
221
221
  */
222
222
  export declare const CHUNK_ENTS: number;
223
+ /**
224
+ * Meshes drawn many times, one matrix a copy. **Required**: see `drftInstances.ts` for why a reader
225
+ * that skipped it would be worse than one that refused. Added in 1.18.
226
+ */
227
+ export declare const CHUNK_INST: number;
228
+ /**
229
+ * A mesh in the bytes its data needs: constants, 16-bit ranges and octahedral directions, decoded on
230
+ * read into the `MeshData` a `MESH` gives. **Required**, and counted as a mesh in every ordinal.
231
+ * See `drftQuantised.ts`. Added in 1.18.
232
+ */
233
+ export declare const CHUNK_MSHQ: number;
223
234
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
224
235
  export declare const SPLAT_BLOCK_PREFIX = 40;
225
236
  /**
@@ -460,6 +471,23 @@ export interface DrftMaterial {
460
471
  * tested.
461
472
  */
462
473
  readonly cutout: number;
474
+ /**
475
+ * Whether the surface blends: its texture's alpha is coverage, times `opacity`. 1.18.
476
+ *
477
+ * **A flag, because `opacity` could not say it.** glTF's `BLEND` makes alpha the factor times the
478
+ * texture, and a factor of 1 left `opacity` at 1: every loader drew the material opaque and a
479
+ * leaf's shape, which is all in its texture's alpha, was never read. Absent means false, which is
480
+ * what every file before 1.18 meant.
481
+ */
482
+ readonly blend?: boolean;
483
+ /**
484
+ * Whether both faces of the surface are seen: bit 1 of the flags word, 1.18.
485
+ *
486
+ * glTF's `doubleSided`, and what a curtain, a flag and a leaf card are. A one-sided surface is
487
+ * culled from behind, so a curtain seen from the other side of its arch is a hole, and half a
488
+ * tree's cards are not drawn. Absent means false, which is how every surface before it was drawn.
489
+ */
490
+ readonly doubleSided?: boolean;
463
491
  }
464
492
  /** Floats in a `MATL` entry, then the signed texture indices that follow them. */
465
493
  export declare const MATERIAL_FLOATS = 15;
@@ -473,6 +501,8 @@ export declare const MATERIAL_FLOATS = 15;
473
501
  * them is speculative — only unbound, and the plans that bind them need no second stride bump.
474
502
  */
475
503
  export declare const MATERIAL_INDICES = 4;
504
+ /** A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. */
505
+ export declare const MATERIAL_FLAGS = 1;
476
506
  export declare const MATERIAL_ENTRY_BYTES: number;
477
507
  /** What `HEAD` says about the asset as a whole. */
478
508
  export interface DrftHead {
@@ -20,7 +20,7 @@ export const DRFT_VERSION_MAJOR = 1;
20
20
  * Additive revisions within a generation: new optional chunks, new attribute bits, new
21
21
  * enum values with a defined fallback. Never a changed meaning.
22
22
  */
23
- export const DRFT_VERSION_MINOR = 17;
23
+ export const DRFT_VERSION_MINOR = 18;
24
24
  /** Bytes before the chunk table. */
25
25
  export const HEADER_BYTES = 32;
26
26
  /** Bytes per chunk table entry. */
@@ -231,6 +231,17 @@ export const CHUNK_NAVM = fourCC('NAVM');
231
231
  * container holds text.
232
232
  */
233
233
  export const CHUNK_ENTS = fourCC('ENTS');
234
+ /**
235
+ * Meshes drawn many times, one matrix a copy. **Required**: see `drftInstances.ts` for why a reader
236
+ * that skipped it would be worse than one that refused. Added in 1.18.
237
+ */
238
+ export const CHUNK_INST = fourCC('INST');
239
+ /**
240
+ * A mesh in the bytes its data needs: constants, 16-bit ranges and octahedral directions, decoded on
241
+ * read into the `MeshData` a `MESH` gives. **Required**, and counted as a mesh in every ordinal.
242
+ * See `drftQuantised.ts`. Added in 1.18.
243
+ */
244
+ export const CHUNK_MSHQ = fourCC('MSHQ');
234
245
  /** Bytes before the records in a `SPLT` payload. See `DrftSplatBlock` for the fields. */
235
246
  export const SPLAT_BLOCK_PREFIX = 40;
236
247
  /**
@@ -262,6 +273,8 @@ export const KNOWN_CHUNKS = new Set([
262
273
  CHUNK_NGRF,
263
274
  CHUNK_NAVM,
264
275
  CHUNK_ENTS,
276
+ CHUNK_INST,
277
+ CHUNK_MSHQ,
265
278
  ]);
266
279
  /** Chunk flags. */
267
280
  export const CHUNK_REQUIRED = 1 << 0;
@@ -392,7 +405,9 @@ export const MATERIAL_FLOATS = 15;
392
405
  * them is speculative — only unbound, and the plans that bind them need no second stride bump.
393
406
  */
394
407
  export const MATERIAL_INDICES = 4;
395
- export const MATERIAL_ENTRY_BYTES = MATERIAL_FLOATS * 4 + MATERIAL_INDICES * 4;
408
+ /** A `u32` of flags after them, since 1.18: bit 0 is `blend`, bit 1 is `doubleSided`. */
409
+ export const MATERIAL_FLAGS = 1;
410
+ export const MATERIAL_ENTRY_BYTES = MATERIAL_FLOATS * 4 + MATERIAL_INDICES * 4 + MATERIAL_FLAGS * 4;
396
411
  /**
397
412
  * Thrown for any malformed or unreadable file, with the offset that failed.
398
413
  *
@@ -0,0 +1,9 @@
1
+ export interface DrftInstanceGroup {
2
+ /** The ordinal of the `MESH` this group draws, which holds one copy. */
3
+ readonly mesh: number;
4
+ /** Sixteen floats a copy, column-major, taking the mesh to where that copy stands. */
5
+ readonly transforms: Float32Array;
6
+ }
7
+ export declare function buildInstances(groups: readonly DrftInstanceGroup[]): Uint8Array;
8
+ /** The groups in an `INST` payload, each checked against the file's mesh count. */
9
+ export declare function readInstances(buffer: ArrayBufferLike, offset: number, byteLength: number, meshCount: number): DrftInstanceGroup[];
@@ -0,0 +1,68 @@
1
+ /**
2
+ * `INST`: meshes drawn many times, each copy placed by a column-major 4×4 matrix.
3
+ *
4
+ * **Required, because a reader that skipped it would draw one copy where there are thousands.** The
5
+ * mesh an instance group names holds one prototype, so an old reader that did not know the chunk
6
+ * would open a scene of ten thousand candles as a single candle at the origin and say nothing. A
7
+ * refusal naming `INST` is the honest answer. What that gives up is opening such a file at all in a
8
+ * reader before 1.18.
9
+ *
10
+ * Layout, little-endian:
11
+ * - u32 group count;
12
+ * - per group: u32 mesh ordinal, u32 instance count, then that many 16-float matrices.
13
+ *
14
+ * A mesh appears in at most one group.
15
+ */
16
+ import { DrftError, align } from './drftFormat.js';
17
+ export function buildInstances(groups) {
18
+ const seen = new Set();
19
+ let floats = 0;
20
+ for (const group of groups) {
21
+ if (group.transforms.length === 0 || group.transforms.length % 16 !== 0) {
22
+ throw new DrftError(`instance group for mesh ${group.mesh} has ${group.transforms.length} floats, which is not whole matrices`);
23
+ }
24
+ if (seen.has(group.mesh))
25
+ throw new DrftError(`mesh ${group.mesh} is instanced twice`);
26
+ seen.add(group.mesh);
27
+ floats += group.transforms.length;
28
+ }
29
+ const bytes = new Uint8Array(align(4 + groups.length * 8 + floats * 4));
30
+ const view = new DataView(bytes.buffer);
31
+ view.setUint32(0, groups.length, true);
32
+ let at = 4;
33
+ for (const group of groups) {
34
+ view.setUint32(at, group.mesh, true);
35
+ view.setUint32(at + 4, group.transforms.length / 16, true);
36
+ at += 8;
37
+ for (let i = 0; i < group.transforms.length; i++) {
38
+ view.setFloat32(at, group.transforms[i], true);
39
+ at += 4;
40
+ }
41
+ }
42
+ return bytes;
43
+ }
44
+ /** The groups in an `INST` payload, each checked against the file's mesh count. */
45
+ export function readInstances(buffer, offset, byteLength, meshCount) {
46
+ const view = new DataView(buffer, offset, byteLength);
47
+ const count = view.getUint32(0, true);
48
+ const out = [];
49
+ let at = 4;
50
+ for (let g = 0; g < count; g++) {
51
+ if (at + 8 > byteLength)
52
+ throw new DrftError('INST ends inside a group header');
53
+ const mesh = view.getUint32(at, true);
54
+ const copies = view.getUint32(at + 4, true);
55
+ at += 8;
56
+ if (mesh >= meshCount) {
57
+ throw new DrftError(`INST places mesh ${mesh}, and the file carries ${meshCount} meshes`);
58
+ }
59
+ if (at + copies * 64 > byteLength)
60
+ throw new DrftError(`INST ends inside mesh ${mesh}'s placements`);
61
+ const transforms = new Float32Array(copies * 16);
62
+ for (let i = 0; i < transforms.length; i++)
63
+ transforms[i] = view.getFloat32(at + i * 4, true);
64
+ at += copies * 64;
65
+ out.push({ mesh, transforms });
66
+ }
67
+ return out;
68
+ }
@@ -0,0 +1,21 @@
1
+ export type DrftLightKind = 'point' | 'spot' | 'directional';
2
+ export interface DrftLight {
3
+ readonly kind: DrftLightKind;
4
+ /** The node's name in the source, or empty. */
5
+ readonly name: string;
6
+ /** World space, metres. */
7
+ readonly position: readonly [number, number, number];
8
+ /** Where a spot or a directional light points, world space and unit length. glTF's local −z. */
9
+ readonly direction: readonly [number, number, number];
10
+ /** Linear, 0 to 1 a channel. */
11
+ readonly color: readonly [number, number, number];
12
+ /** Candela for a point or a spot, lux for a directional light. */
13
+ readonly intensity: number;
14
+ /** Metres at which the source's author cut the light off, or 0 for no cut. */
15
+ readonly range: number;
16
+ readonly innerConeRad: number;
17
+ readonly outerConeRad: number;
18
+ }
19
+ export declare function buildLights(lights: readonly DrftLight[]): Uint8Array;
20
+ /** The lights in a `LITE` payload. */
21
+ export declare function readLights(buffer: ArrayBufferLike, offset: number, byteLength: number): DrftLight[];
@@ -0,0 +1,108 @@
1
+ /**
2
+ * `LITE`: the lights a scene was authored with, in glTF's own terms.
3
+ *
4
+ * **The source's numbers, unconverted.** Intensity is candela for a point or a spot and lux for a
5
+ * directional light, as `KHR_lights_punctual` states it, because what a candela is in a consumer's
6
+ * units is that consumer's exposure decision and no two agree. The chunk says where the lamps are and
7
+ * what their author said about them. Deciding how bright they draw is the consumer's job.
8
+ *
9
+ * **Optional.** A reader that skips it loses the lights and nothing else; they were only ever the
10
+ * consumer's to place. What that gives up is a file whose lights are essential saying so, which
11
+ * none is: a scene without its lamps is darker, not wrong.
12
+ *
13
+ * Layout, little-endian:
14
+ * - u32 light count, u32 name count;
15
+ * - the names, each a u32 byte length and UTF-8, padded to four bytes. **Each distinct name is
16
+ * written once**: a pack of ten thousand candles names every one "Instance";
17
+ * - per light, 64 bytes: u32 kind (0 point, 1 spot, 2 directional), u32 name index, position,
18
+ * direction, colour (three f32 each), intensity, range (0 is unbounded), inner and outer cone
19
+ * in radians, and one reserved f32.
20
+ */
21
+ import { DrftError, align } from './drftFormat.js';
22
+ const KINDS = ['point', 'spot', 'directional'];
23
+ const LIGHT_BYTES = 64;
24
+ const encoder = new TextEncoder();
25
+ const decoder = new TextDecoder();
26
+ export function buildLights(lights) {
27
+ const names = [];
28
+ const nameIndex = new Map();
29
+ for (const light of lights) {
30
+ if (!nameIndex.has(light.name)) {
31
+ nameIndex.set(light.name, names.length);
32
+ names.push(light.name);
33
+ }
34
+ }
35
+ const encoded = names.map((name) => encoder.encode(name));
36
+ let nameBytes = 0;
37
+ for (const text of encoded)
38
+ nameBytes += align(4 + text.length);
39
+ const bytes = new Uint8Array(8 + nameBytes + lights.length * LIGHT_BYTES);
40
+ const view = new DataView(bytes.buffer);
41
+ view.setUint32(0, lights.length, true);
42
+ view.setUint32(4, names.length, true);
43
+ let at = 8;
44
+ for (const text of encoded) {
45
+ view.setUint32(at, text.length, true);
46
+ bytes.set(text, at + 4);
47
+ at += align(4 + text.length);
48
+ }
49
+ for (const light of lights) {
50
+ view.setUint32(at, KINDS.indexOf(light.kind), true);
51
+ view.setUint32(at + 4, nameIndex.get(light.name) ?? 0, true);
52
+ const floats = [
53
+ ...light.position,
54
+ ...light.direction,
55
+ ...light.color,
56
+ light.intensity,
57
+ light.range,
58
+ light.innerConeRad,
59
+ light.outerConeRad,
60
+ ];
61
+ for (let i = 0; i < floats.length; i++)
62
+ view.setFloat32(at + 8 + i * 4, floats[i], true);
63
+ at += LIGHT_BYTES;
64
+ }
65
+ return bytes;
66
+ }
67
+ /** The lights in a `LITE` payload. */
68
+ export function readLights(buffer, offset, byteLength) {
69
+ const view = new DataView(buffer, offset, byteLength);
70
+ if (byteLength < 8)
71
+ throw new DrftError('LITE is shorter than its own header');
72
+ const count = view.getUint32(0, true);
73
+ const nameCount = view.getUint32(4, true);
74
+ const names = [];
75
+ let at = 8;
76
+ for (let n = 0; n < nameCount; n++) {
77
+ if (at + 4 > byteLength)
78
+ throw new DrftError('LITE ends inside its names');
79
+ const length = view.getUint32(at, true);
80
+ if (at + 4 + length > byteLength)
81
+ throw new DrftError('a LITE name runs past the chunk');
82
+ names.push(decoder.decode(new Uint8Array(buffer, offset + at + 4, length)));
83
+ at += align(4 + length);
84
+ }
85
+ if (at + count * LIGHT_BYTES > byteLength) {
86
+ throw new DrftError(`LITE says ${count} lights and ends inside them`);
87
+ }
88
+ const f = (o) => view.getFloat32(o, true);
89
+ const out = [];
90
+ for (let i = 0; i < count; i++) {
91
+ const kind = KINDS[view.getUint32(at, true)];
92
+ if (kind === undefined)
93
+ throw new DrftError(`LITE light ${i} is of a kind this reader does not know`);
94
+ out.push({
95
+ kind,
96
+ name: names[view.getUint32(at + 4, true)] ?? '',
97
+ position: [f(at + 8), f(at + 12), f(at + 16)],
98
+ direction: [f(at + 20), f(at + 24), f(at + 28)],
99
+ color: [f(at + 32), f(at + 36), f(at + 40)],
100
+ intensity: f(at + 44),
101
+ range: f(at + 48),
102
+ innerConeRad: f(at + 52),
103
+ outerConeRad: f(at + 56),
104
+ });
105
+ at += LIGHT_BYTES;
106
+ }
107
+ return out;
108
+ }
@@ -0,0 +1,3 @@
1
+ import type { MeshData } from './meshData.ts';
2
+ export declare function encodeQuantisedMesh(mesh: MeshData): Uint8Array;
3
+ export declare function decodeQuantisedMesh(buffer: ArrayBufferLike, offset: number, byteLength: number): MeshData;