@driftengine/drft 3.62.0 → 4.0.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/dist/drftWrite.js
CHANGED
|
@@ -10,9 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import { buildClip, buildMorph, buildNodes, buildSkin } from './drftSkin.js';
|
|
12
12
|
import { validateMeshData } from './meshData.js';
|
|
13
|
-
import { ALIGNMENT, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_ANIM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_SKIN, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_COLL, CHUNK_SUBS, CHUNK_TEXS, MATERIAL_ENTRY_BYTES, DRFT_MAGIC, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR, DrftError, FLAG_LITTLE_ENDIAN, HEADER_BYTES, align, } from './drftFormat.js';
|
|
13
|
+
import { ALIGNMENT, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_ANIM, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_SKIN, CHUNK_SPLT, SPLAT_BLOCK_PREFIX, CHUNK_MATL, CHUNK_MESH, CHUNK_REQUIRED, CHUNK_COLL, CHUNK_NNET, CHUNK_NGRF, CHUNK_SDFV, CHUNK_SUBS, CHUNK_TEXS, MATERIAL_ENTRY_BYTES, DRFT_MAGIC, DRFT_VERSION_MAJOR, DRFT_VERSION_MINOR, DrftError, FLAG_LITTLE_ENDIAN, HEADER_BYTES, align, } from './drftFormat.js';
|
|
14
14
|
import { coarseFirstOrder } from './coarseFirst.js';
|
|
15
15
|
import { buildSubs } from './drftSubs.js';
|
|
16
|
+
import { buildSdfv } from './sdfv.js';
|
|
17
|
+
import { buildNnet } from './nnet.js';
|
|
18
|
+
import { buildDtex } from './dtex.js';
|
|
19
|
+
import { buildEnts } from './ents.js';
|
|
20
|
+
import { buildNavm } from './navm.js';
|
|
21
|
+
import { buildNgrf } from './ngrf.js';
|
|
16
22
|
import { buildColliders } from './drftColliders.js';
|
|
17
23
|
/**
|
|
18
24
|
* The most blocks a capture is split into, and the fewest splats worth having in one.
|
|
@@ -313,8 +319,16 @@ export function writeDrft(source) {
|
|
|
313
319
|
* ordinary asset and the reader accepts it, so refusing to write one would be the two halves of
|
|
314
320
|
* the format disagreeing about what a file may contain.
|
|
315
321
|
*/
|
|
316
|
-
|
|
317
|
-
|
|
322
|
+
/*
|
|
323
|
+
* **And networks, since 1.15**, on the same argument: a converted model is weights and nothing
|
|
324
|
+
* else, and refusing it for the absence of geometry would be the format telling a consumer what
|
|
325
|
+
* an asset may be made of.
|
|
326
|
+
*/
|
|
327
|
+
if (source.meshes.length === 0 &&
|
|
328
|
+
(source.splats === undefined || source.splats.count === 0) &&
|
|
329
|
+
(source.networks?.length ?? 0) === 0 &&
|
|
330
|
+
(source.graphs?.length ?? 0) === 0) {
|
|
331
|
+
throw new DrftError('an asset must contain at least one mesh, a capture with splats in it, or a network');
|
|
318
332
|
}
|
|
319
333
|
const materials = source.materials;
|
|
320
334
|
if (materials !== undefined && materials.length !== source.meshes.length) {
|
|
@@ -378,6 +392,24 @@ export function writeDrft(source) {
|
|
|
378
392
|
for (const clip of source.clips ?? []) {
|
|
379
393
|
chunks.push({ code: CHUNK_ANIM, flags: 0, bytes: buildClip(clip) });
|
|
380
394
|
}
|
|
395
|
+
for (const entry of source.dtex ?? []) {
|
|
396
|
+
/*
|
|
397
|
+
* **Refused here rather than at the reader**, because a decode program for a material the file
|
|
398
|
+
* does not carry is a bake that went wrong, and the writer is where the author can still be
|
|
399
|
+
* told. A reader meeting one has nothing useful to do with it.
|
|
400
|
+
*/
|
|
401
|
+
if (entry.material >= (source.materials?.length ?? 0)) {
|
|
402
|
+
throw new DrftError(`drft: DTEX names material ${entry.material} and the file carries ` +
|
|
403
|
+
`${source.materials?.length ?? 0}`);
|
|
404
|
+
}
|
|
405
|
+
chunks.push({ code: CHUNK_DTEX, flags: 0, bytes: buildDtex(entry) });
|
|
406
|
+
}
|
|
407
|
+
if (source.navigation !== undefined) {
|
|
408
|
+
chunks.push({ code: CHUNK_NAVM, flags: 0, bytes: buildNavm(source.navigation) });
|
|
409
|
+
}
|
|
410
|
+
if (source.entities !== undefined) {
|
|
411
|
+
chunks.push({ code: CHUNK_ENTS, flags: 0, bytes: buildEnts(source.entities) });
|
|
412
|
+
}
|
|
381
413
|
source.meshes.forEach((mesh, ordinal) => {
|
|
382
414
|
/*
|
|
383
415
|
* **Its deltas immediately before it, not after.** A streaming consumer uploads a mesh the
|
|
@@ -410,6 +442,30 @@ export function writeDrft(source) {
|
|
|
410
442
|
bytes: buildSubs({ entries: source.substances }),
|
|
411
443
|
});
|
|
412
444
|
}
|
|
445
|
+
/* And one `SDFV`, on exactly the same terms. */
|
|
446
|
+
if ((source.fields?.length ?? 0) > 0) {
|
|
447
|
+
chunks.push({
|
|
448
|
+
code: CHUNK_SDFV,
|
|
449
|
+
flags: 0,
|
|
450
|
+
bytes: buildSdfv({ entries: source.fields }),
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
/* And one `NNET`, on the same terms again. */
|
|
454
|
+
if ((source.networks?.length ?? 0) > 0) {
|
|
455
|
+
chunks.push({
|
|
456
|
+
code: CHUNK_NNET,
|
|
457
|
+
flags: 0,
|
|
458
|
+
bytes: buildNnet({ networks: source.networks }),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/* And one `NGRF`, for the networks a perceptron's table cannot describe. */
|
|
462
|
+
if ((source.graphs?.length ?? 0) > 0) {
|
|
463
|
+
chunks.push({
|
|
464
|
+
code: CHUNK_NGRF,
|
|
465
|
+
flags: 0,
|
|
466
|
+
bytes: buildNgrf(source.graphs),
|
|
467
|
+
});
|
|
468
|
+
}
|
|
413
469
|
/*
|
|
414
470
|
* `COLL` last among the small chunks, because collision is the one thing a viewer never needs and
|
|
415
471
|
* a consumer that simulates has already waited for the whole file by the time it builds a world.
|
package/dist/dtex.d.ts
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `DTEX`: a material as a decode program over a latent, rather than as pictures.
|
|
3
|
+
*
|
|
4
|
+
* **The chunk carries no semantics and that is deliberate.** `@driftengine/drft` is the container
|
|
5
|
+
* and has no dependencies — it does not know what a `DecodeGraph` means, only what shape it is. So
|
|
6
|
+
* the types here are plain arrays that `@driftengine/texture`'s own types satisfy structurally,
|
|
7
|
+
* the same arrangement `Terrain` has with `Heightfield` across the physics boundary: a rule that
|
|
8
|
+
* cannot be shared as code is still shared as a test.
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* u32 material which MATL entry this decodes
|
|
12
|
+
* u32 latentWidth
|
|
13
|
+
* u32 latentHeight
|
|
14
|
+
* u32 latentComponents
|
|
15
|
+
* u32 channelCount
|
|
16
|
+
* u32 nodeCount decode graph nodes, four words each
|
|
17
|
+
* u32 resultRegister
|
|
18
|
+
* u32 addressMode 0 and 1 lattice, 2 and 3 texel-centre; clamp, then wrap
|
|
19
|
+
* u32 networkInputs
|
|
20
|
+
* u32 networkOutputs
|
|
21
|
+
* u32 hiddenCount
|
|
22
|
+
* u32 weightCount
|
|
23
|
+
* u32 tileCount
|
|
24
|
+
* u32 tileSize texels on a tile's edge, or 0 for a payload that is one image
|
|
25
|
+
* u32 hidden[hiddenCount]
|
|
26
|
+
* u32 channels[channelCount] (semanticIndex << 4) | component, the packing REMAP_CHANNEL uses
|
|
27
|
+
* u32 nodes[nodeCount * 4]
|
|
28
|
+
* u32 tileOffset[tileCount] byte offset into the latent payload
|
|
29
|
+
* u32 tileLength[tileCount]
|
|
30
|
+
* u32 tileHash[tileCount * 2] `hashTile`'s 64 bits, high word first
|
|
31
|
+
* f32 weights[weightCount]
|
|
32
|
+
* u8 latent[...] padded to the container's alignment
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* **No per-chunk version field, which is where this departs from the plan.** The plan's layout
|
|
36
|
+
* opened with a header and a version; every chunk in this container already answers to the file's
|
|
37
|
+
* own version, and `MSHL` states the reason in one line — a second version is a second thing to
|
|
38
|
+
* keep in step, and the one that drifts is the one nobody reads.
|
|
39
|
+
*
|
|
40
|
+
* **The tile table is the streaming unit, and until 2026-09-20 it could not be.** It was a flat
|
|
41
|
+
* list of offsets into a row-major image with a 32-bit hash that nothing wrote — and a tile of a
|
|
42
|
+
* row-major image is not contiguous, so no offset and length could name one. Three things make it
|
|
43
|
+
* real, and they are one decision rather than three:
|
|
44
|
+
*
|
|
45
|
+
* - **`tileSize` is in the header**, so a reader can work out the grid. Without it the table is a
|
|
46
|
+
* list of byte ranges nobody can address by tile.
|
|
47
|
+
* - **The payload is tile-major when there are tiles**: each tile's texels run together, in the
|
|
48
|
+
* grid's own order — row-major across, then down — which is exactly the order and the extent
|
|
49
|
+
* `@driftengine/texture`'s `latentTileGrid` cuts and `tilesForView` asks for. `tileCount` of 0
|
|
50
|
+
* means the payload is one row-major image, which is what every material small enough not to
|
|
51
|
+
* stream still is.
|
|
52
|
+
* - **The hash is `hashTile`'s 64 bits**, the same number residency addresses a tile by, so a tile
|
|
53
|
+
* that arrived for one material is already resident for every other material sharing it. Thirty
|
|
54
|
+
* two bits is about a one in ten thousand collision over sixty thousand tiles, and a collision
|
|
55
|
+
* here is one surface wearing another's texture.
|
|
56
|
+
*
|
|
57
|
+
* **What it gives up**: a reader that wants the whole image must put the tiles back together, which
|
|
58
|
+
* is a copy the row-major form does not need. That is why `tileCount` of 0 stays a legal chunk
|
|
59
|
+
* rather than being tidied away. **What would make it wrong** is a mip chain, which this chunk does
|
|
60
|
+
* not carry — `latentTileGrid` cuts one level per mip and a `DTEX` has one level, so the table is
|
|
61
|
+
* that level's tiles and nothing is implied about the others.
|
|
62
|
+
*
|
|
63
|
+
* **What is validated here is containment, not meaning.** A tile whose bytes run past the payload
|
|
64
|
+
* and a result register no node ever writes are refused, because those make a reader hand back
|
|
65
|
+
* arbitrary memory or an uninitialised value. Whether the program is one an interpreter can *run*
|
|
66
|
+
* is `validateDecodeGraph`'s question, in the package that knows what an operation is.
|
|
67
|
+
*
|
|
68
|
+
* Additive, and additive is what keeps `FORMAT.md`'s freeze intact: a reader that does not know
|
|
69
|
+
* this code skips it by its length and loses only the material, which it could not have decoded.
|
|
70
|
+
*/
|
|
71
|
+
/** Registers the decode vocabulary has. Mirrors `MAX_REGISTERS`; asserted equal by a test. */
|
|
72
|
+
export declare const DTEX_REGISTERS = 16;
|
|
73
|
+
/** Address modes the decode vocabulary has. Mirrors `ADDRESS_MODE_COUNT`; asserted equal by a test. */
|
|
74
|
+
export declare const DTEX_ADDRESS_MODES = 4;
|
|
75
|
+
/** How many tiles one material may carry. Well past what a streaming budget permits. */
|
|
76
|
+
export declare const DTEX_MAX_TILES: number;
|
|
77
|
+
/** A material's decode program, and which `MATL` entry it belongs to. */
|
|
78
|
+
export interface DtexEntry {
|
|
79
|
+
/** The index of the `MATL` entry this decodes. */
|
|
80
|
+
readonly material: number;
|
|
81
|
+
readonly texture: DtexMaterial;
|
|
82
|
+
}
|
|
83
|
+
export interface DtexMaterial {
|
|
84
|
+
latentWidth: number;
|
|
85
|
+
latentHeight: number;
|
|
86
|
+
latentComponents: number;
|
|
87
|
+
/** `(semanticIndex << 4) | component` per channel. */
|
|
88
|
+
channels: Uint32Array;
|
|
89
|
+
/** Four words a node: op, a, b, out. */
|
|
90
|
+
nodes: Uint32Array;
|
|
91
|
+
resultRegister: number;
|
|
92
|
+
addressMode: number;
|
|
93
|
+
networkInputs: number;
|
|
94
|
+
networkOutputs: number;
|
|
95
|
+
hidden: Uint32Array;
|
|
96
|
+
weights: Float32Array;
|
|
97
|
+
tileOffset: Uint32Array;
|
|
98
|
+
tileLength: Uint32Array;
|
|
99
|
+
/** Two words a tile, high first, as `hashTile` splits its 64 bits. */
|
|
100
|
+
tileHash: Uint32Array;
|
|
101
|
+
/**
|
|
102
|
+
* Texels on a tile's edge, or 0 where the payload is one row-major image.
|
|
103
|
+
*
|
|
104
|
+
* A grid rather than a count, because what streams is a rectangle of texels and a reader has to
|
|
105
|
+
* be able to say which one it is looking at.
|
|
106
|
+
*/
|
|
107
|
+
tileSize: number;
|
|
108
|
+
latent: Uint8Array;
|
|
109
|
+
}
|
|
110
|
+
/** How many tiles across a latent of `width` texels is cut into at this tile size. */
|
|
111
|
+
export declare function dtexTilesAcross(width: number, tileSize: number): number;
|
|
112
|
+
/**
|
|
113
|
+
* How many tiles the grid holds, which is what the table's length has to be.
|
|
114
|
+
*
|
|
115
|
+
* **The order is the grid's own**: across a row, then down, the same order
|
|
116
|
+
* `@driftengine/texture`'s `latentTileGrid` cuts in — so a reader with a tile coordinate reads
|
|
117
|
+
* `ty * dtexTilesAcross(width, tileSize) + tx` and gets the entry for it.
|
|
118
|
+
*/
|
|
119
|
+
export declare function dtexTileCount(width: number, height: number, tileSize: number): number;
|
|
120
|
+
export declare function buildDtex(entry: DtexEntry): Uint8Array;
|
|
121
|
+
/**
|
|
122
|
+
* The material a `DTEX` chunk carries, as views over the fetched buffer.
|
|
123
|
+
*
|
|
124
|
+
* Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
|
|
125
|
+
* object holding them.
|
|
126
|
+
*/
|
|
127
|
+
export declare function readDtex(buffer: ArrayBuffer, offset: number, byteLength: number): DtexEntry;
|
package/dist/dtex.js
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import { DrftError, align } from './drftFormat.js';
|
|
2
|
+
/**
|
|
3
|
+
* `DTEX`: a material as a decode program over a latent, rather than as pictures.
|
|
4
|
+
*
|
|
5
|
+
* **The chunk carries no semantics and that is deliberate.** `@driftengine/drft` is the container
|
|
6
|
+
* and has no dependencies — it does not know what a `DecodeGraph` means, only what shape it is. So
|
|
7
|
+
* the types here are plain arrays that `@driftengine/texture`'s own types satisfy structurally,
|
|
8
|
+
* the same arrangement `Terrain` has with `Heightfield` across the physics boundary: a rule that
|
|
9
|
+
* cannot be shared as code is still shared as a test.
|
|
10
|
+
*
|
|
11
|
+
* ```
|
|
12
|
+
* u32 material which MATL entry this decodes
|
|
13
|
+
* u32 latentWidth
|
|
14
|
+
* u32 latentHeight
|
|
15
|
+
* u32 latentComponents
|
|
16
|
+
* u32 channelCount
|
|
17
|
+
* u32 nodeCount decode graph nodes, four words each
|
|
18
|
+
* u32 resultRegister
|
|
19
|
+
* u32 addressMode 0 and 1 lattice, 2 and 3 texel-centre; clamp, then wrap
|
|
20
|
+
* u32 networkInputs
|
|
21
|
+
* u32 networkOutputs
|
|
22
|
+
* u32 hiddenCount
|
|
23
|
+
* u32 weightCount
|
|
24
|
+
* u32 tileCount
|
|
25
|
+
* u32 tileSize texels on a tile's edge, or 0 for a payload that is one image
|
|
26
|
+
* u32 hidden[hiddenCount]
|
|
27
|
+
* u32 channels[channelCount] (semanticIndex << 4) | component, the packing REMAP_CHANNEL uses
|
|
28
|
+
* u32 nodes[nodeCount * 4]
|
|
29
|
+
* u32 tileOffset[tileCount] byte offset into the latent payload
|
|
30
|
+
* u32 tileLength[tileCount]
|
|
31
|
+
* u32 tileHash[tileCount * 2] `hashTile`'s 64 bits, high word first
|
|
32
|
+
* f32 weights[weightCount]
|
|
33
|
+
* u8 latent[...] padded to the container's alignment
|
|
34
|
+
* ```
|
|
35
|
+
*
|
|
36
|
+
* **No per-chunk version field, which is where this departs from the plan.** The plan's layout
|
|
37
|
+
* opened with a header and a version; every chunk in this container already answers to the file's
|
|
38
|
+
* own version, and `MSHL` states the reason in one line — a second version is a second thing to
|
|
39
|
+
* keep in step, and the one that drifts is the one nobody reads.
|
|
40
|
+
*
|
|
41
|
+
* **The tile table is the streaming unit, and until 2026-09-20 it could not be.** It was a flat
|
|
42
|
+
* list of offsets into a row-major image with a 32-bit hash that nothing wrote — and a tile of a
|
|
43
|
+
* row-major image is not contiguous, so no offset and length could name one. Three things make it
|
|
44
|
+
* real, and they are one decision rather than three:
|
|
45
|
+
*
|
|
46
|
+
* - **`tileSize` is in the header**, so a reader can work out the grid. Without it the table is a
|
|
47
|
+
* list of byte ranges nobody can address by tile.
|
|
48
|
+
* - **The payload is tile-major when there are tiles**: each tile's texels run together, in the
|
|
49
|
+
* grid's own order — row-major across, then down — which is exactly the order and the extent
|
|
50
|
+
* `@driftengine/texture`'s `latentTileGrid` cuts and `tilesForView` asks for. `tileCount` of 0
|
|
51
|
+
* means the payload is one row-major image, which is what every material small enough not to
|
|
52
|
+
* stream still is.
|
|
53
|
+
* - **The hash is `hashTile`'s 64 bits**, the same number residency addresses a tile by, so a tile
|
|
54
|
+
* that arrived for one material is already resident for every other material sharing it. Thirty
|
|
55
|
+
* two bits is about a one in ten thousand collision over sixty thousand tiles, and a collision
|
|
56
|
+
* here is one surface wearing another's texture.
|
|
57
|
+
*
|
|
58
|
+
* **What it gives up**: a reader that wants the whole image must put the tiles back together, which
|
|
59
|
+
* is a copy the row-major form does not need. That is why `tileCount` of 0 stays a legal chunk
|
|
60
|
+
* rather than being tidied away. **What would make it wrong** is a mip chain, which this chunk does
|
|
61
|
+
* not carry — `latentTileGrid` cuts one level per mip and a `DTEX` has one level, so the table is
|
|
62
|
+
* that level's tiles and nothing is implied about the others.
|
|
63
|
+
*
|
|
64
|
+
* **What is validated here is containment, not meaning.** A tile whose bytes run past the payload
|
|
65
|
+
* and a result register no node ever writes are refused, because those make a reader hand back
|
|
66
|
+
* arbitrary memory or an uninitialised value. Whether the program is one an interpreter can *run*
|
|
67
|
+
* is `validateDecodeGraph`'s question, in the package that knows what an operation is.
|
|
68
|
+
*
|
|
69
|
+
* Additive, and additive is what keeps `FORMAT.md`'s freeze intact: a reader that does not know
|
|
70
|
+
* this code skips it by its length and loses only the material, which it could not have decoded.
|
|
71
|
+
*/
|
|
72
|
+
/** Registers the decode vocabulary has. Mirrors `MAX_REGISTERS`; asserted equal by a test. */
|
|
73
|
+
export const DTEX_REGISTERS = 16;
|
|
74
|
+
/** Address modes the decode vocabulary has. Mirrors `ADDRESS_MODE_COUNT`; asserted equal by a test. */
|
|
75
|
+
export const DTEX_ADDRESS_MODES = 4;
|
|
76
|
+
/** How many tiles one material may carry. Well past what a streaming budget permits. */
|
|
77
|
+
export const DTEX_MAX_TILES = 1 << 16;
|
|
78
|
+
/*
|
|
79
|
+
* Fourteen: the thirteen the layout above lists after `material`, and `material` itself.
|
|
80
|
+
*
|
|
81
|
+
* **The pairing is in the chunk rather than in the order the chunks appear**, because a file may
|
|
82
|
+
* carry a decode program for some of its materials and not others — a scene where one surface came
|
|
83
|
+
* from a capture and the rest were authored — and a reader that paired by position would give the
|
|
84
|
+
* wrong material the wrong texture without anything failing.
|
|
85
|
+
*/
|
|
86
|
+
const HEADER_WORDS = 14;
|
|
87
|
+
const HEADER = HEADER_WORDS * 4;
|
|
88
|
+
function layoutOf(material) {
|
|
89
|
+
const hiddenCount = material.hidden.length;
|
|
90
|
+
const channelCount = material.channels.length;
|
|
91
|
+
const nodeWords = material.nodes.length;
|
|
92
|
+
const tiles = material.tileOffset.length;
|
|
93
|
+
let at = HEADER;
|
|
94
|
+
const hidden = at;
|
|
95
|
+
at += hiddenCount * 4;
|
|
96
|
+
const channels = at;
|
|
97
|
+
at += channelCount * 4;
|
|
98
|
+
const nodes = at;
|
|
99
|
+
at += nodeWords * 4;
|
|
100
|
+
const tileOffset = at;
|
|
101
|
+
at += tiles * 4;
|
|
102
|
+
const tileLength = at;
|
|
103
|
+
at += tiles * 4;
|
|
104
|
+
const tileHash = at;
|
|
105
|
+
/* Two words a tile: `hashTile` is 64 bits and JavaScript has no integer that wide. */
|
|
106
|
+
at += tiles * 8;
|
|
107
|
+
const weights = at;
|
|
108
|
+
at += material.weights.length * 4;
|
|
109
|
+
const latent = at;
|
|
110
|
+
at += material.latent.length;
|
|
111
|
+
return { hidden, channels, nodes, tileOffset, tileLength, tileHash, weights, latent, size: at };
|
|
112
|
+
}
|
|
113
|
+
/** How many tiles across a latent of `width` texels is cut into at this tile size. */
|
|
114
|
+
export function dtexTilesAcross(width, tileSize) {
|
|
115
|
+
return tileSize > 0 ? Math.ceil(width / tileSize) : 0;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* How many tiles the grid holds, which is what the table's length has to be.
|
|
119
|
+
*
|
|
120
|
+
* **The order is the grid's own**: across a row, then down, the same order
|
|
121
|
+
* `@driftengine/texture`'s `latentTileGrid` cuts in — so a reader with a tile coordinate reads
|
|
122
|
+
* `ty * dtexTilesAcross(width, tileSize) + tx` and gets the entry for it.
|
|
123
|
+
*/
|
|
124
|
+
export function dtexTileCount(width, height, tileSize) {
|
|
125
|
+
return dtexTilesAcross(width, tileSize) * dtexTilesAcross(height, tileSize);
|
|
126
|
+
}
|
|
127
|
+
/** A tile table has to be the grid, or nothing can be addressed by where it is. */
|
|
128
|
+
function refuseStrayGrid(material, tiles) {
|
|
129
|
+
const { tileSize, latentWidth, latentHeight } = material;
|
|
130
|
+
if (tiles === 0) {
|
|
131
|
+
if (tileSize !== 0) {
|
|
132
|
+
throw new DrftError(`DTEX declares a tile edge of ${tileSize} texels and carries no tiles`);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!Number.isInteger(tileSize) || tileSize < 1) {
|
|
137
|
+
throw new DrftError(`DTEX carries ${tiles} tiles and a tile edge of ${tileSize} texels`);
|
|
138
|
+
}
|
|
139
|
+
const wanted = dtexTileCount(latentWidth, latentHeight, tileSize);
|
|
140
|
+
if (tiles !== wanted) {
|
|
141
|
+
throw new DrftError(`DTEX carries ${tiles} tiles where a ${latentWidth} by ${latentHeight} latent cut at ` +
|
|
142
|
+
`${tileSize} is ${wanted}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
export function buildDtex(entry) {
|
|
146
|
+
const { texture: material } = entry;
|
|
147
|
+
if (!Number.isInteger(entry.material) || entry.material < 0) {
|
|
148
|
+
throw new DrftError(`DTEX names material ${entry.material}, which is not an index`);
|
|
149
|
+
}
|
|
150
|
+
const tiles = material.tileOffset.length;
|
|
151
|
+
if (tiles > DTEX_MAX_TILES) {
|
|
152
|
+
throw new DrftError(`DTEX carries ${tiles} tiles, past the ${DTEX_MAX_TILES} cap`);
|
|
153
|
+
}
|
|
154
|
+
if (material.tileLength.length !== tiles || material.tileHash.length !== tiles * 2) {
|
|
155
|
+
throw new DrftError('DTEX was given a tile table whose three columns are different lengths');
|
|
156
|
+
}
|
|
157
|
+
refuseStrayGrid(material, tiles);
|
|
158
|
+
if (material.nodes.length === 0 || material.nodes.length % 4 !== 0) {
|
|
159
|
+
throw new DrftError(`DTEX was given ${material.nodes.length} node words, which is not whole nodes`);
|
|
160
|
+
}
|
|
161
|
+
if (material.latentComponents < 1 || material.latentComponents > 4) {
|
|
162
|
+
throw new DrftError(`DTEX latent has ${material.latentComponents} components, not one to four`);
|
|
163
|
+
}
|
|
164
|
+
/*
|
|
165
|
+
* **Only where the payload is one image.** With tiles it is tile-major and two identical tiles
|
|
166
|
+
* share one run, so a chunk whose payload is *smaller* than its grid is a chunk that deduplicated
|
|
167
|
+
* — which is the point. What holds there is the per-tile containment check below.
|
|
168
|
+
*/
|
|
169
|
+
if (tiles === 0 &&
|
|
170
|
+
material.latent.length <
|
|
171
|
+
material.latentWidth * material.latentHeight * material.latentComponents) {
|
|
172
|
+
throw new DrftError('DTEX latent payload is smaller than the grid it declares');
|
|
173
|
+
}
|
|
174
|
+
/*
|
|
175
|
+
* Every tile is checked to lie inside the payload before anything is written. A tile running
|
|
176
|
+
* past the end is a subarray of arbitrary memory on the way back in, which is exactly what
|
|
177
|
+
* `MSHL` checks its cluster table for.
|
|
178
|
+
*/
|
|
179
|
+
for (let t = 0; t < tiles; t += 1) {
|
|
180
|
+
const start = material.tileOffset[t];
|
|
181
|
+
const length = material.tileLength[t];
|
|
182
|
+
if (start + length > material.latent.length) {
|
|
183
|
+
throw new DrftError(`DTEX tile ${t} runs past the ${material.latent.length}-byte payload`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/* A result register nothing writes hands a reader whatever that register happened to hold. */
|
|
187
|
+
let written = false;
|
|
188
|
+
for (let n = 0; n * 4 < material.nodes.length; n += 1) {
|
|
189
|
+
const out = material.nodes[n * 4 + 3];
|
|
190
|
+
if (out >= DTEX_REGISTERS) {
|
|
191
|
+
throw new DrftError(`DTEX node ${n} writes register ${out}, past the ${DTEX_REGISTERS} there are`);
|
|
192
|
+
}
|
|
193
|
+
if (out === material.resultRegister)
|
|
194
|
+
written = true;
|
|
195
|
+
}
|
|
196
|
+
if (!written) {
|
|
197
|
+
throw new DrftError(`DTEX names register ${material.resultRegister} as its result, and no node writes it`);
|
|
198
|
+
}
|
|
199
|
+
const layout = layoutOf(material);
|
|
200
|
+
const bytes = new Uint8Array(align(layout.size));
|
|
201
|
+
const view = new DataView(bytes.buffer);
|
|
202
|
+
view.setUint32(0, entry.material, true);
|
|
203
|
+
view.setUint32(4, material.latentWidth, true);
|
|
204
|
+
view.setUint32(8, material.latentHeight, true);
|
|
205
|
+
view.setUint32(12, material.latentComponents, true);
|
|
206
|
+
view.setUint32(16, material.channels.length, true);
|
|
207
|
+
view.setUint32(20, material.nodes.length / 4, true);
|
|
208
|
+
view.setUint32(24, material.resultRegister, true);
|
|
209
|
+
view.setUint32(28, material.addressMode, true);
|
|
210
|
+
view.setUint32(32, material.networkInputs, true);
|
|
211
|
+
view.setUint32(36, material.networkOutputs, true);
|
|
212
|
+
view.setUint32(40, material.hidden.length, true);
|
|
213
|
+
view.setUint32(44, material.weights.length, true);
|
|
214
|
+
view.setUint32(48, tiles, true);
|
|
215
|
+
view.setUint32(52, material.tileSize, true);
|
|
216
|
+
new Uint32Array(bytes.buffer, layout.hidden, material.hidden.length).set(material.hidden);
|
|
217
|
+
new Uint32Array(bytes.buffer, layout.channels, material.channels.length).set(material.channels);
|
|
218
|
+
new Uint32Array(bytes.buffer, layout.nodes, material.nodes.length).set(material.nodes);
|
|
219
|
+
new Uint32Array(bytes.buffer, layout.tileOffset, tiles).set(material.tileOffset);
|
|
220
|
+
new Uint32Array(bytes.buffer, layout.tileLength, tiles).set(material.tileLength);
|
|
221
|
+
new Uint32Array(bytes.buffer, layout.tileHash, tiles * 2).set(material.tileHash);
|
|
222
|
+
new Float32Array(bytes.buffer, layout.weights, material.weights.length).set(material.weights);
|
|
223
|
+
bytes.set(material.latent, layout.latent);
|
|
224
|
+
return bytes;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* The material a `DTEX` chunk carries, as views over the fetched buffer.
|
|
228
|
+
*
|
|
229
|
+
* Views rather than copies, like every other chunk here, so a load allocates nothing beyond the
|
|
230
|
+
* object holding them.
|
|
231
|
+
*/
|
|
232
|
+
export function readDtex(buffer, offset, byteLength) {
|
|
233
|
+
if (byteLength < HEADER)
|
|
234
|
+
throw new DrftError('DTEX is too short to hold its header');
|
|
235
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
236
|
+
const materialIndex = view.getUint32(0, true);
|
|
237
|
+
const latentWidth = view.getUint32(4, true);
|
|
238
|
+
const latentHeight = view.getUint32(8, true);
|
|
239
|
+
const latentComponents = view.getUint32(12, true);
|
|
240
|
+
const channelCount = view.getUint32(16, true);
|
|
241
|
+
const nodeCount = view.getUint32(20, true);
|
|
242
|
+
const resultRegister = view.getUint32(24, true);
|
|
243
|
+
const addressMode = view.getUint32(28, true);
|
|
244
|
+
const networkInputs = view.getUint32(32, true);
|
|
245
|
+
const networkOutputs = view.getUint32(36, true);
|
|
246
|
+
const hiddenCount = view.getUint32(40, true);
|
|
247
|
+
const weightCount = view.getUint32(44, true);
|
|
248
|
+
const tiles = view.getUint32(48, true);
|
|
249
|
+
const tileSize = view.getUint32(52, true);
|
|
250
|
+
if (nodeCount === 0)
|
|
251
|
+
throw new DrftError('DTEX carries no decode program');
|
|
252
|
+
if (latentComponents < 1 || latentComponents > 4) {
|
|
253
|
+
throw new DrftError(`DTEX latent declares ${latentComponents} components, not one to four`);
|
|
254
|
+
}
|
|
255
|
+
if (addressMode >= DTEX_ADDRESS_MODES) {
|
|
256
|
+
throw new DrftError(`DTEX declares address mode ${String(addressMode)}; this reader knows 0 to ${String(DTEX_ADDRESS_MODES - 1)}`);
|
|
257
|
+
}
|
|
258
|
+
const shape = {
|
|
259
|
+
latentWidth,
|
|
260
|
+
latentHeight,
|
|
261
|
+
latentComponents,
|
|
262
|
+
channels: new Uint32Array(channelCount),
|
|
263
|
+
nodes: new Uint32Array(nodeCount * 4),
|
|
264
|
+
resultRegister,
|
|
265
|
+
addressMode,
|
|
266
|
+
networkInputs,
|
|
267
|
+
networkOutputs,
|
|
268
|
+
hidden: new Uint32Array(hiddenCount),
|
|
269
|
+
weights: new Float32Array(weightCount),
|
|
270
|
+
tileOffset: new Uint32Array(tiles),
|
|
271
|
+
tileLength: new Uint32Array(tiles),
|
|
272
|
+
tileHash: new Uint32Array(tiles * 2),
|
|
273
|
+
tileSize,
|
|
274
|
+
latent: new Uint8Array(0),
|
|
275
|
+
};
|
|
276
|
+
const layout = layoutOf(shape);
|
|
277
|
+
const latentBytes = byteLength - layout.latent;
|
|
278
|
+
if (latentBytes < 0) {
|
|
279
|
+
throw new DrftError(`DTEX declares ${String(layout.latent)} bytes of tables in a ${String(byteLength)}-byte chunk`);
|
|
280
|
+
}
|
|
281
|
+
const base = offset;
|
|
282
|
+
const material = {
|
|
283
|
+
latentWidth,
|
|
284
|
+
latentHeight,
|
|
285
|
+
latentComponents,
|
|
286
|
+
channels: new Uint32Array(buffer, base + layout.channels, channelCount),
|
|
287
|
+
nodes: new Uint32Array(buffer, base + layout.nodes, nodeCount * 4),
|
|
288
|
+
resultRegister,
|
|
289
|
+
addressMode,
|
|
290
|
+
networkInputs,
|
|
291
|
+
networkOutputs,
|
|
292
|
+
hidden: new Uint32Array(buffer, base + layout.hidden, hiddenCount),
|
|
293
|
+
weights: new Float32Array(buffer, base + layout.weights, weightCount),
|
|
294
|
+
tileOffset: new Uint32Array(buffer, base + layout.tileOffset, tiles),
|
|
295
|
+
tileLength: new Uint32Array(buffer, base + layout.tileLength, tiles),
|
|
296
|
+
tileHash: new Uint32Array(buffer, base + layout.tileHash, tiles * 2),
|
|
297
|
+
tileSize,
|
|
298
|
+
latent: new Uint8Array(buffer, base + layout.latent, latentBytes),
|
|
299
|
+
};
|
|
300
|
+
refuseStrayGrid(material, tiles);
|
|
301
|
+
if (tiles === 0 && latentBytes < latentWidth * latentHeight * latentComponents) {
|
|
302
|
+
throw new DrftError('DTEX latent payload is smaller than the grid it declares');
|
|
303
|
+
}
|
|
304
|
+
for (let t = 0; t < tiles; t += 1) {
|
|
305
|
+
const start = material.tileOffset[t];
|
|
306
|
+
const length = material.tileLength[t];
|
|
307
|
+
if (start + length > latentBytes) {
|
|
308
|
+
throw new DrftError(`DTEX tile ${t} runs past the ${String(latentBytes)}-byte payload`);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
let written = false;
|
|
312
|
+
for (let n = 0; n < nodeCount; n += 1) {
|
|
313
|
+
const out = material.nodes[n * 4 + 3];
|
|
314
|
+
if (out >= DTEX_REGISTERS) {
|
|
315
|
+
throw new DrftError(`DTEX node ${n} writes register ${out}, past the ${DTEX_REGISTERS} there are`);
|
|
316
|
+
}
|
|
317
|
+
if (out === resultRegister)
|
|
318
|
+
written = true;
|
|
319
|
+
}
|
|
320
|
+
if (!written) {
|
|
321
|
+
throw new DrftError(`DTEX names register ${resultRegister} as its result, and no node writes it`);
|
|
322
|
+
}
|
|
323
|
+
return { material: materialIndex, texture: material };
|
|
324
|
+
}
|
package/dist/ents.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `ENTS`: the things in a scene, as the entity model wrote them.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* u32 byteLength of the text that follows
|
|
6
|
+
* u8 scene[byteLength] UTF-8 JSON, padded to the container's alignment
|
|
7
|
+
* ```
|
|
8
|
+
*
|
|
9
|
+
* **Text, in a binary container, on purpose.** Every other chunk here holds numbers whose meaning
|
|
10
|
+
* this package knows — positions, indices, weights. A scene holds a *consumer's* component fields,
|
|
11
|
+
* whose types are declared in that consumer's own code and in its `.drs` files. A binary encoding
|
|
12
|
+
* for them would be an encoding of somebody else's type system, extended every time they add a
|
|
13
|
+
* field, and wrong in the direction that reads as working: a field the container did not know how
|
|
14
|
+
* to write would silently not be there. What `serializeWorld` produces is already a value; this
|
|
15
|
+
* carries it unchanged.
|
|
16
|
+
*
|
|
17
|
+
* **The scene's own version is inside it and is not repeated.** `MSHL` states the reason in one
|
|
18
|
+
* line — a second version is a second thing to keep in step, and the one that drifts is the one
|
|
19
|
+
* nobody reads.
|
|
20
|
+
*
|
|
21
|
+
* **What is checked is that this is a scene, not that it is a scene anybody can load.** Whether a
|
|
22
|
+
* component named here exists, and whether its schema still matches, is `deserializeWorld`'s
|
|
23
|
+
* question and it answers in words. Refusing text that is not a scene at all belongs here, so a
|
|
24
|
+
* consumer meets a sentence rather than `undefined.entities` three calls later.
|
|
25
|
+
*
|
|
26
|
+
* Additive on the same terms as `DTEX`: a reader that does not know this code skips it by its
|
|
27
|
+
* length and loses only the scene, which it had no entity model to load into anyway.
|
|
28
|
+
*/
|
|
29
|
+
/** A world as data. `@driftengine/entities`' `SerializedScene` is one of these. */
|
|
30
|
+
export interface EntsScene {
|
|
31
|
+
readonly version: number;
|
|
32
|
+
/** What each component type looked like when this was written. */
|
|
33
|
+
readonly schemas: Readonly<Record<string, unknown>>;
|
|
34
|
+
readonly entities: readonly {
|
|
35
|
+
/** Component name → field id → value. */
|
|
36
|
+
readonly components: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
37
|
+
}[];
|
|
38
|
+
}
|
|
39
|
+
export declare function buildEnts(scene: EntsScene): Uint8Array;
|
|
40
|
+
/**
|
|
41
|
+
* The scene an `ENTS` chunk carries.
|
|
42
|
+
*
|
|
43
|
+
* **A copy rather than a view, and it is the only chunk here that has to be.** Text is not a typed
|
|
44
|
+
* array over the buffer; parsing it is what makes it a scene. A consumer that loads one is about
|
|
45
|
+
* to create every entity in it anyway, so the copy is not the cost that matters.
|
|
46
|
+
*/
|
|
47
|
+
export declare function readEnts(buffer: ArrayBuffer, offset: number, byteLength: number): EntsScene;
|
package/dist/ents.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { DrftError, align } from './drftFormat.js';
|
|
2
|
+
const HEADER = 4;
|
|
3
|
+
const encoder = new TextEncoder();
|
|
4
|
+
const decoder = new TextDecoder();
|
|
5
|
+
export function buildEnts(scene) {
|
|
6
|
+
refuseNonScene(scene);
|
|
7
|
+
const text = encoder.encode(JSON.stringify(scene));
|
|
8
|
+
const bytes = new Uint8Array(align(HEADER + text.length));
|
|
9
|
+
new DataView(bytes.buffer).setUint32(0, text.length, true);
|
|
10
|
+
bytes.set(text, HEADER);
|
|
11
|
+
return bytes;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The scene an `ENTS` chunk carries.
|
|
15
|
+
*
|
|
16
|
+
* **A copy rather than a view, and it is the only chunk here that has to be.** Text is not a typed
|
|
17
|
+
* array over the buffer; parsing it is what makes it a scene. A consumer that loads one is about
|
|
18
|
+
* to create every entity in it anyway, so the copy is not the cost that matters.
|
|
19
|
+
*/
|
|
20
|
+
export function readEnts(buffer, offset, byteLength) {
|
|
21
|
+
if (byteLength < HEADER)
|
|
22
|
+
throw new DrftError('ENTS is too short to hold its header');
|
|
23
|
+
const view = new DataView(buffer, offset, byteLength);
|
|
24
|
+
const length = view.getUint32(0, true);
|
|
25
|
+
if (HEADER + length > byteLength) {
|
|
26
|
+
throw new DrftError(`ENTS declares ${String(length)} bytes of scene in a ${String(byteLength)}-byte chunk`);
|
|
27
|
+
}
|
|
28
|
+
const text = decoder.decode(new Uint8Array(buffer, offset + HEADER, length));
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
throw new DrftError(`ENTS does not hold a scene: ${error.message}`);
|
|
35
|
+
}
|
|
36
|
+
refuseNonScene(parsed);
|
|
37
|
+
return parsed;
|
|
38
|
+
}
|
|
39
|
+
/** The shape a scene has, checked on both sides so neither end can write one the other refuses. */
|
|
40
|
+
function refuseNonScene(scene) {
|
|
41
|
+
if (typeof scene !== 'object' || scene === null) {
|
|
42
|
+
throw new DrftError('ENTS holds no scene at all');
|
|
43
|
+
}
|
|
44
|
+
const held = scene;
|
|
45
|
+
if (!Number.isInteger(held.version) || held.version < 1) {
|
|
46
|
+
throw new DrftError(`ENTS holds a scene at version ${String(held.version)}`);
|
|
47
|
+
}
|
|
48
|
+
if (typeof held.schemas !== 'object' || held.schemas === null) {
|
|
49
|
+
throw new DrftError('ENTS holds a scene with no schemas, so nothing in it could be matched up');
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(held.entities)) {
|
|
52
|
+
throw new DrftError('ENTS holds a scene whose entities are not a list');
|
|
53
|
+
}
|
|
54
|
+
for (let at = 0; at < held.entities.length; at += 1) {
|
|
55
|
+
const entity = held.entities[at];
|
|
56
|
+
if (typeof entity !== 'object' || entity === null || typeof entity.components !== 'object') {
|
|
57
|
+
throw new DrftError(`ENTS holds an entity at ${at} with no components`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|