@driftengine/drft 4.4.2 → 4.6.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/assemble.d.ts +7 -0
- package/dist/assemble.js +242 -0
- package/dist/drftAssembly.d.ts +62 -0
- package/dist/drftAssembly.js +267 -0
- package/dist/drftFormat.d.ts +58 -3
- package/dist/drftFormat.js +58 -4
- package/dist/drftLightVolume.d.ts +12 -0
- package/dist/drftLightVolume.js +172 -0
- package/dist/drftQuantised.js +4 -1
- package/dist/drftRead.d.ts +21 -1
- package/dist/drftRead.js +81 -4
- package/dist/drftRegions.d.ts +58 -0
- package/dist/drftRegions.js +313 -0
- package/dist/drftStream.d.ts +49 -1
- package/dist/drftStream.js +74 -5
- package/dist/drftWrite.d.ts +35 -1
- package/dist/drftWrite.js +103 -6
- package/dist/index.d.ts +9 -0
- package/dist/index.js +7 -0
- package/dist/meshData.d.ts +19 -0
- package/dist/meshData.js +15 -0
- package/package.json +4 -4
- package/src/assemble.ts +268 -0
- package/src/drftAssembly.ts +322 -0
- package/src/drftFormat.ts +74 -4
- package/src/drftLightVolume.ts +179 -0
- package/src/drftQuantised.ts +8 -1
- package/src/drftRead.ts +107 -4
- package/src/drftRegions.ts +350 -0
- package/src/drftStream.ts +112 -7
- package/src/drftWrite.ts +147 -5
- package/src/index.ts +40 -0
- package/src/meshData.ts +38 -0
package/dist/drftWrite.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
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_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_INST, CHUNK_LITE, CHUNK_MSHQ, 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';
|
|
13
|
+
import { ALIGNMENT, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_JOINTS, ATTR_RELIEF, ATTR_TANGENT, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_UVS, ATTR_WEIGHTS, ATTR_LAYERS, ATTR_CHANNEL, CHUNK_ENTRY_BYTES, CHUNK_HEAD, CHUNK_ANIM, CHUNK_DTEX, CHUNK_ENTS, CHUNK_NAVM, CHUNK_LODM, CHUNK_LODF, CHUNK_MORP, CHUNK_NODE, CHUNK_INST, CHUNK_REGN, CHUNK_LVOL, CHUNK_LITE, CHUNK_MSHQ, CHUNK_KITS, CHUNK_MSHC, 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
16
|
import { buildSdfv } from './sdfv.js';
|
|
@@ -21,8 +21,17 @@ import { buildNavm } from './navm.js';
|
|
|
21
21
|
import { buildNgrf } from './ngrf.js';
|
|
22
22
|
import { buildColliders } from './drftColliders.js';
|
|
23
23
|
import { buildInstances } from './drftInstances.js';
|
|
24
|
+
import { buildRegion, planRegions } from './drftRegions.js';
|
|
25
|
+
import { buildLightVolume } from './drftLightVolume.js';
|
|
26
|
+
/** Whether a region names a mesh, as a level or a prop. */
|
|
27
|
+
function regionNames(region, mesh) {
|
|
28
|
+
return (region.levels.some((level) => level.meshes.includes(mesh)) ||
|
|
29
|
+
region.instances.some((group) => group.mesh === mesh));
|
|
30
|
+
}
|
|
24
31
|
import { buildLights } from './drftLights.js';
|
|
25
32
|
import { encodeQuantisedMesh } from './drftQuantised.js';
|
|
33
|
+
import { assemblyBounds } from './assemble.js';
|
|
34
|
+
import { buildAssembly, buildKit, checkAssembly, checkCopies, checkKit, isAssembly, } from './drftAssembly.js';
|
|
26
35
|
/**
|
|
27
36
|
* The most blocks a capture is split into, and the fewest splats worth having in one.
|
|
28
37
|
*
|
|
@@ -104,7 +113,7 @@ function encodeString(value) {
|
|
|
104
113
|
bytes.set(text, 4);
|
|
105
114
|
return bytes;
|
|
106
115
|
}
|
|
107
|
-
function buildHead(head, meshes, instances = []) {
|
|
116
|
+
function buildHead(head, meshes, instances = [], kit = new Set()) {
|
|
108
117
|
/*
|
|
109
118
|
* Bounds are computed rather than taken on trust. A caller that gets them wrong would
|
|
110
119
|
* produce an asset that culls itself out of frame, which looks like a rendering bug and
|
|
@@ -119,8 +128,25 @@ function buildHead(head, meshes, instances = []) {
|
|
|
119
128
|
const placed = new Map();
|
|
120
129
|
for (const group of instances)
|
|
121
130
|
placed.set(group.mesh, group.transforms);
|
|
131
|
+
const pieceAt = (o) => meshes[o];
|
|
122
132
|
for (let m = 0; m < meshes.length; m++) {
|
|
123
|
-
|
|
133
|
+
/* A kit piece is drawn only where it is copied, and its copies are counted with them. */
|
|
134
|
+
if (kit.has(m))
|
|
135
|
+
continue;
|
|
136
|
+
const slot = meshes[m];
|
|
137
|
+
if (isAssembly(slot)) {
|
|
138
|
+
const b = assemblyBounds(slot, pieceAt);
|
|
139
|
+
if (!(b[0] <= b[3]))
|
|
140
|
+
continue;
|
|
141
|
+
minX = Math.min(minX, b[0]);
|
|
142
|
+
minY = Math.min(minY, b[1]);
|
|
143
|
+
minZ = Math.min(minZ, b[2]);
|
|
144
|
+
maxX = Math.max(maxX, b[3]);
|
|
145
|
+
maxY = Math.max(maxY, b[4]);
|
|
146
|
+
maxZ = Math.max(maxZ, b[5]);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const mesh = slot;
|
|
124
150
|
const transforms = placed.get(m);
|
|
125
151
|
if (transforms === undefined) {
|
|
126
152
|
for (let at = 0; at + 2 < mesh.positions.length; at += 3) {
|
|
@@ -250,6 +276,10 @@ function buildMesh(mesh, code = CHUNK_MESH, flags = CHUNK_REQUIRED) {
|
|
|
250
276
|
attributes |= ATTR_JOINTS;
|
|
251
277
|
if (mesh.weights !== undefined)
|
|
252
278
|
attributes |= ATTR_WEIGHTS;
|
|
279
|
+
if (mesh.layers !== undefined)
|
|
280
|
+
attributes |= ATTR_LAYERS;
|
|
281
|
+
if (mesh.channel !== undefined)
|
|
282
|
+
attributes |= ATTR_CHANNEL;
|
|
253
283
|
/* Order is frozen: mandatory arrays, then optional ones by ascending bit. */
|
|
254
284
|
const arrays = [
|
|
255
285
|
mesh.positions,
|
|
@@ -277,6 +307,12 @@ function buildMesh(mesh, code = CHUNK_MESH, flags = CHUNK_REQUIRED) {
|
|
|
277
307
|
arrays.push(mesh.joints);
|
|
278
308
|
if (mesh.weights !== undefined)
|
|
279
309
|
arrays.push(mesh.weights);
|
|
310
|
+
/* Newest of all, 1.20, so last. See ATTR_LAYERS. */
|
|
311
|
+
if (mesh.layers !== undefined)
|
|
312
|
+
arrays.push(mesh.layers);
|
|
313
|
+
/* And newer still, 1.23. See ATTR_CHANNEL. */
|
|
314
|
+
if (mesh.channel !== undefined)
|
|
315
|
+
arrays.push(mesh.channel);
|
|
280
316
|
arrays.push(mesh.indices);
|
|
281
317
|
const PREFIX = 16;
|
|
282
318
|
let total = PREFIX;
|
|
@@ -366,6 +402,12 @@ function buildMaterials(materials) {
|
|
|
366
402
|
view.setFloat32(at + 68, material.occlusionStrength, true);
|
|
367
403
|
view.setFloat32(at + 72, material.cutout, true);
|
|
368
404
|
view.setUint32(at + 76, (material.blend === true ? 1 : 0) | (material.doubleSided === true ? 2 : 0), true);
|
|
405
|
+
/* Glass, 1.19: after the flags word, so every earlier offset stays where readers expect it. */
|
|
406
|
+
view.setFloat32(at + 80, material.transmission ?? 0, true);
|
|
407
|
+
view.setFloat32(at + 84, material.frost ?? 0, true);
|
|
408
|
+
view.setFloat32(at + 88, material.tint?.[0] ?? 1, true);
|
|
409
|
+
view.setFloat32(at + 92, material.tint?.[1] ?? 1, true);
|
|
410
|
+
view.setFloat32(at + 96, material.tint?.[2] ?? 1, true);
|
|
369
411
|
at += MATERIAL_ENTRY_BYTES;
|
|
370
412
|
}
|
|
371
413
|
return { code: CHUNK_MATL, flags: 0, bytes };
|
|
@@ -416,7 +458,29 @@ export function writeDrft(source) {
|
|
|
416
458
|
* same asset less its textures rather than a refusal. Moving it earlier in the file changes
|
|
417
459
|
* nothing about that, because a chunk is found through the table rather than by position.
|
|
418
460
|
*/
|
|
419
|
-
const
|
|
461
|
+
const kit = new Set(source.kit ?? []);
|
|
462
|
+
checkKit(source.kit ?? [], source.meshes.length);
|
|
463
|
+
source.meshes.forEach((slot, ordinal) => {
|
|
464
|
+
if (!isAssembly(slot))
|
|
465
|
+
return;
|
|
466
|
+
checkAssembly(slot, `mesh ${ordinal}`);
|
|
467
|
+
checkCopies(slot, ordinal, kit, (o) => {
|
|
468
|
+
const piece = source.meshes[o];
|
|
469
|
+
return piece === undefined || isAssembly(piece) ? undefined : piece;
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
for (const o of kit) {
|
|
473
|
+
if (isAssembly(source.meshes[o])) {
|
|
474
|
+
throw new DrftError(`mesh ${o} is a kit piece and an assembly; a piece is vertices`);
|
|
475
|
+
}
|
|
476
|
+
if ((source.instances ?? []).some((g) => g.mesh === o)) {
|
|
477
|
+
throw new DrftError(`mesh ${o} is a kit piece and is placed by INST`);
|
|
478
|
+
}
|
|
479
|
+
if ((source.regions ?? []).some((r) => regionNames(r, o))) {
|
|
480
|
+
throw new DrftError(`mesh ${o} is a kit piece and a region's; a piece is drawn only as copies`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const chunks = [buildHead(source.head, source.meshes, source.instances, kit)];
|
|
420
484
|
/*
|
|
421
485
|
* The outline first, ahead of even the paint. It is the one thing that puts a recognisable
|
|
422
486
|
* whole object on screen, it is a few hundred kilobytes against tens of megabytes, and the
|
|
@@ -466,6 +530,11 @@ export function writeDrft(source) {
|
|
|
466
530
|
bytes: buildInstances(source.instances),
|
|
467
531
|
});
|
|
468
532
|
}
|
|
533
|
+
/* The kit ahead of the geometry, for the placements' reason: a streaming consumer must know a
|
|
534
|
+
mesh is a piece before it lands, or it has already drawn it at the origin. */
|
|
535
|
+
if (kit.size > 0) {
|
|
536
|
+
chunks.push({ code: CHUNK_KITS, flags: CHUNK_REQUIRED, bytes: buildKit(source.kit ?? []) });
|
|
537
|
+
}
|
|
469
538
|
/* Small, and ahead of the geometry, so a streaming consumer can place its lamps early. */
|
|
470
539
|
if ((source.lights?.length ?? 0) > 0) {
|
|
471
540
|
chunks.push({ code: CHUNK_LITE, flags: 0, bytes: buildLights(source.lights ?? []) });
|
|
@@ -494,7 +563,23 @@ export function writeDrft(source) {
|
|
|
494
563
|
if (source.entities !== undefined) {
|
|
495
564
|
chunks.push({ code: CHUNK_ENTS, flags: 0, bytes: buildEnts(source.entities) });
|
|
496
565
|
}
|
|
566
|
+
if (source.texturesFirst === true) {
|
|
567
|
+
for (const texture of source.textures ?? [])
|
|
568
|
+
chunks.push(buildTexture(texture));
|
|
569
|
+
}
|
|
570
|
+
const regionsBefore = planRegions(source.regions ?? [], source.meshes.length, (m) => {
|
|
571
|
+
const mesh = source.meshes[m];
|
|
572
|
+
return mesh !== undefined && isAssembly(mesh) ? mesh.pieces : undefined;
|
|
573
|
+
});
|
|
574
|
+
for (const group of source.instances ?? []) {
|
|
575
|
+
if ((source.regions ?? []).some((r) => regionNames(r, group.mesh))) {
|
|
576
|
+
throw new DrftError(`mesh ${group.mesh} is placed by INST and is also a region's`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
497
579
|
source.meshes.forEach((mesh, ordinal) => {
|
|
580
|
+
for (const region of regionsBefore[ordinal] ?? []) {
|
|
581
|
+
chunks.push({ code: CHUNK_REGN, flags: CHUNK_REQUIRED, bytes: buildRegion(region) });
|
|
582
|
+
}
|
|
498
583
|
/*
|
|
499
584
|
* **Its deltas immediately before it, not after.** A streaming consumer uploads a mesh the
|
|
500
585
|
* moment its chunk lands, so deltas arriving afterwards are deltas for geometry already on the
|
|
@@ -502,6 +587,10 @@ export function writeDrft(source) {
|
|
|
502
587
|
* them in hand when it builds. The mesh ordinal in the payload still does the pairing, so a
|
|
503
588
|
* reader meeting them in another order is unaffected.
|
|
504
589
|
*/
|
|
590
|
+
if (isAssembly(mesh)) {
|
|
591
|
+
chunks.push({ code: CHUNK_MSHC, flags: CHUNK_REQUIRED, bytes: buildAssembly(mesh) });
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
505
594
|
if (mesh.morphTargets !== undefined && mesh.morphTargetCount !== undefined) {
|
|
506
595
|
chunks.push({
|
|
507
596
|
code: CHUNK_MORP,
|
|
@@ -521,8 +610,16 @@ export function writeDrft(source) {
|
|
|
521
610
|
chunks.push(buildMesh(mesh));
|
|
522
611
|
}
|
|
523
612
|
});
|
|
524
|
-
for (const
|
|
525
|
-
chunks.push(
|
|
613
|
+
for (const region of regionsBefore[source.meshes.length] ?? []) {
|
|
614
|
+
chunks.push({ code: CHUNK_REGN, flags: CHUNK_REQUIRED, bytes: buildRegion(region) });
|
|
615
|
+
}
|
|
616
|
+
if (source.lightVolume !== undefined) {
|
|
617
|
+
chunks.push({ code: CHUNK_LVOL, flags: 0, bytes: buildLightVolume(source.lightVolume) });
|
|
618
|
+
}
|
|
619
|
+
if (source.texturesFirst !== true) {
|
|
620
|
+
for (const texture of source.textures ?? [])
|
|
621
|
+
chunks.push(buildTexture(texture));
|
|
622
|
+
}
|
|
526
623
|
/* One `SUBS` for the file, and only where something was labelled — an empty chunk would cost
|
|
527
624
|
sixteen bytes to say nothing, and rule 2 already covers its absence. */
|
|
528
625
|
if ((source.substances?.length ?? 0) > 0) {
|
package/dist/index.d.ts
CHANGED
|
@@ -24,6 +24,15 @@ export { buildInstances, readInstances } from './drftInstances.ts';
|
|
|
24
24
|
export { buildLights, readLights } from './drftLights.ts';
|
|
25
25
|
export type { DrftLight, DrftLightKind } from './drftLights.ts';
|
|
26
26
|
export type { DrftInstanceGroup } from './drftInstances.ts';
|
|
27
|
+
export { REGION_MAX_LEVELS, RegionLedger, buildRegion, planRegions, readRegion, } from './drftRegions.ts';
|
|
28
|
+
export { ATTR_CHANNEL, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_LAYERS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, } from './drftFormat.ts';
|
|
29
|
+
export { ASSEMBLY_ATTRIBUTES, COPY_MATRIX_FLOATS, COPY_UV_FLOATS, SURFACE, SURFACE_FLOATS, buildAssembly, buildKit, checkAssembly, checkCopies, isAssembly, readAssembly, readKit, } from './drftAssembly.ts';
|
|
30
|
+
export type { DrftAssembly } from './drftAssembly.ts';
|
|
31
|
+
export { assemblyBounds, expandAssembly } from './assemble.ts';
|
|
32
|
+
export type { PieceLookup } from './assemble.ts';
|
|
33
|
+
export type { DrftRegion, DrftRegionCollision, DrftRegionLevel } from './drftRegions.ts';
|
|
34
|
+
export { buildLightVolume, readLightVolume } from './drftLightVolume.ts';
|
|
35
|
+
export type { DrftLightVolume } from './drftLightVolume.ts';
|
|
27
36
|
export { buildDtex, readDtex, DTEX_ADDRESS_MODES, DTEX_MAX_TILES, DTEX_REGISTERS } from './dtex.ts';
|
|
28
37
|
export type { DtexEntry, DtexMaterial } from './dtex.ts';
|
|
29
38
|
export { buildNavm, readNavm } from './navm.ts';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,13 @@ export { MAX_COLLIDER_HULLS, MAX_COLLIDER_POINTS, buildColliders, readColliders,
|
|
|
23
23
|
/* Meshes drawn many times, one matrix a copy, as the baker finds merged copies again. */
|
|
24
24
|
export { buildInstances, readInstances } from './drftInstances.js';
|
|
25
25
|
export { buildLights, readLights } from './drftLights.js';
|
|
26
|
+
export { REGION_MAX_LEVELS, RegionLedger, buildRegion, planRegions, readRegion, } from './drftRegions.js';
|
|
27
|
+
/* A mesh carried as painted copies of kit pieces, expanded where it is needed. The attribute bits
|
|
28
|
+
are a MESH's, which is what an assembly declares its optional arrays with. */
|
|
29
|
+
export { ATTR_CHANNEL, ATTR_EMISSIVE_COLOR, ATTR_GRAIN, ATTR_LAYERS, ATTR_RELIEF, ATTR_ROUGHNESS, ATTR_SPECULAR, ATTR_TANGENT, ATTR_UVS, } from './drftFormat.js';
|
|
30
|
+
export { ASSEMBLY_ATTRIBUTES, COPY_MATRIX_FLOATS, COPY_UV_FLOATS, SURFACE, SURFACE_FLOATS, buildAssembly, buildKit, checkAssembly, checkCopies, isAssembly, readAssembly, readKit, } from './drftAssembly.js';
|
|
31
|
+
export { assemblyBounds, expandAssembly } from './assemble.js';
|
|
32
|
+
export { buildLightVolume, readLightVolume } from './drftLightVolume.js';
|
|
26
33
|
/*
|
|
27
34
|
* **`DTEX`: a material as a decode program over a latent, rather than as pictures.** The chunk
|
|
28
35
|
* carries no semantics — this package is the container and does not know what a channel means —
|
package/dist/meshData.d.ts
CHANGED
|
@@ -106,6 +106,25 @@ export interface MeshData {
|
|
|
106
106
|
* separates asphalt from plaster, and the pass states it: see `Renderer.setSurfaceRelief`.
|
|
107
107
|
*/
|
|
108
108
|
relief?: Float32Array;
|
|
109
|
+
/**
|
|
110
|
+
* Which image of a texture array each vertex's face wears: one whole number a vertex, 0 the first
|
|
111
|
+
* layer. Absent means layer 0, which is the only layer a plain texture has.
|
|
112
|
+
*
|
|
113
|
+
* **What it is for is a merged mesh wearing many images in one draw.** A block of forty buildings
|
|
114
|
+
* with forty facades is forty materials — forty draws, forty material changes — when each image
|
|
115
|
+
* is its own texture; with the facades as the layers of one array and each face naming its layer,
|
|
116
|
+
* it is one. `Renderer.createSurfaceTextureArray` builds the array, and every map of a material
|
|
117
|
+
* (albedo, normal, ORM, emissive) is read at the same layer, so an albedo and its emissive twin
|
|
118
|
+
* share an index.
|
|
119
|
+
*
|
|
120
|
+
* **It travels in the texture-coordinate attribute, as its third component**, because all
|
|
121
|
+
* sixteen of WebGL2's guaranteed vertex locations are spent in the instanced variant. So it needs
|
|
122
|
+
* `uvs` beside it and is refused without them: a layer with no coordinates addresses nothing.
|
|
123
|
+
*
|
|
124
|
+
* Whole numbers, checked, because the shader rounds to the nearest layer and a 1.5 would land on
|
|
125
|
+
* whichever side rounding favoured on that device.
|
|
126
|
+
*/
|
|
127
|
+
layers?: Float32Array;
|
|
109
128
|
/**
|
|
110
129
|
* Four floats per vertex, and the only attribute whose lanes mean four different things.
|
|
111
130
|
*
|
package/dist/meshData.js
CHANGED
|
@@ -50,6 +50,21 @@ export function validateMeshData(data) {
|
|
|
50
50
|
check('grain', data.grain, 1);
|
|
51
51
|
check('relief', data.relief, 1);
|
|
52
52
|
check('channel', data.channel, 4);
|
|
53
|
+
check('layers', data.layers, 1);
|
|
54
|
+
if (data.layers !== undefined) {
|
|
55
|
+
if (data.uvs === undefined) {
|
|
56
|
+
throw new Error('Mesh: layers without uvs — a texture-array layer travels as the third texture coordinate, ' +
|
|
57
|
+
'so a mesh that names layers has to supply the two it rides with');
|
|
58
|
+
}
|
|
59
|
+
const layers = data.layers;
|
|
60
|
+
for (let v = 0; v < layers.length; v++) {
|
|
61
|
+
const layer = layers[v];
|
|
62
|
+
if (!(layer >= 0) || Math.floor(layer) !== layer) {
|
|
63
|
+
throw new Error(`Mesh: layer ${layer} at vertex ${v} — a texture-array layer is a whole number at or ` +
|
|
64
|
+
'above zero, and anything else lands on whichever layer the device rounds it to');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
53
68
|
/*
|
|
54
69
|
* `tangents` was declared optional on 2026-08-22 and was not checked here until 2026-08-25, which
|
|
55
70
|
* made the widest optional attribute in the format the one a short buffer could reach a driver
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/drft",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "The .drft container: a streaming format for baked 3D scenes",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,9 +27,9 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@driftengine/core": "4.
|
|
31
|
-
"@driftengine/entities": "4.
|
|
32
|
-
"@driftengine/texture": "4.
|
|
30
|
+
"@driftengine/core": "4.6.0",
|
|
31
|
+
"@driftengine/entities": "4.6.0",
|
|
32
|
+
"@driftengine/texture": "4.6.0"
|
|
33
33
|
},
|
|
34
34
|
"author": "Drift Technologies",
|
|
35
35
|
"repository": {
|
package/src/assemble.ts
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An assembly expanded into the mesh it describes: every copy's piece through its matrix, painted
|
|
3
|
+
* by its surface, its texture coordinates stretched as the copy says.
|
|
4
|
+
*
|
|
5
|
+
* **Positions by the matrix, normals by its inverse transpose**, so a slope stretched along one
|
|
6
|
+
* axis stays perpendicular to itself rather than leaning with the stretch. **A copy that mirrors
|
|
7
|
+
* — a negative determinant — winds its triangles back** and flips its tangent's handedness, so its
|
|
8
|
+
* faces still wind counter-clockwise seen from outside and a mirrored texture still lights the
|
|
9
|
+
* right way up. Tangents follow the matrix, since a tangent is a direction along the surface.
|
|
10
|
+
*
|
|
11
|
+
* **A coordinate is stretched along the piece's own tangent**: u by `|uStretch ⊙ t|` for the
|
|
12
|
+
* piece's unit tangent t, v the same along its bitangent, then offset. That is what lets one unit
|
|
13
|
+
* box serve every box in a city: its face coordinates run 0..1, and a copy 13 m by 4 m carrying
|
|
14
|
+
* stretches of size over tile repeats its facade in metres on every face. What it gives up is a
|
|
15
|
+
* stretch that varies across one face — a cone's side, where the circumference shrinks up the
|
|
16
|
+
* slope — which comes out as the stretch at each vertex's own tangent, and exact only where the
|
|
17
|
+
* piece's coordinates are linear in its surface.
|
|
18
|
+
*
|
|
19
|
+
* One allocation per output array, sized by a first pass over the copies.
|
|
20
|
+
*/
|
|
21
|
+
import {
|
|
22
|
+
COPY_MATRIX_FLOATS,
|
|
23
|
+
COPY_UV_FLOATS,
|
|
24
|
+
SURFACE,
|
|
25
|
+
SURFACE_FLOATS,
|
|
26
|
+
determinant,
|
|
27
|
+
} from './drftAssembly.ts';
|
|
28
|
+
import type { DrftAssembly } from './drftAssembly.ts';
|
|
29
|
+
import {
|
|
30
|
+
ATTR_EMISSIVE_COLOR,
|
|
31
|
+
ATTR_GRAIN,
|
|
32
|
+
ATTR_LAYERS,
|
|
33
|
+
ATTR_RELIEF,
|
|
34
|
+
ATTR_ROUGHNESS,
|
|
35
|
+
ATTR_SPECULAR,
|
|
36
|
+
ATTR_TANGENT,
|
|
37
|
+
ATTR_UVS,
|
|
38
|
+
ATTR_CHANNEL,
|
|
39
|
+
DrftError,
|
|
40
|
+
} from './drftFormat.ts';
|
|
41
|
+
import type { MeshData } from './meshData.ts';
|
|
42
|
+
|
|
43
|
+
/** Finds a kit piece by mesh ordinal. */
|
|
44
|
+
export type PieceLookup = (ordinal: number) => MeshData;
|
|
45
|
+
|
|
46
|
+
function pieceOf(piece: PieceLookup, assembly: DrftAssembly, c: number): MeshData {
|
|
47
|
+
const mesh = piece(assembly.pieces[c] as number);
|
|
48
|
+
const textured = (assembly.attributes & ATTR_UVS) !== 0;
|
|
49
|
+
if (textured && (mesh.uvs === undefined || mesh.tangents === undefined)) {
|
|
50
|
+
throw new DrftError(
|
|
51
|
+
`piece ${assembly.pieces[c]} has no ${mesh.uvs === undefined ? 'texture coordinates' : 'tangents'}, and a textured assembly copies it`,
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if ((assembly.attributes & ATTR_TANGENT) !== 0 && mesh.tangents === undefined) {
|
|
55
|
+
throw new DrftError(
|
|
56
|
+
`piece ${assembly.pieces[c]} has no tangents, and the assembly carries them`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
if ((assembly.attributes & ATTR_CHANNEL) !== 0 && mesh.channel === undefined) {
|
|
60
|
+
throw new DrftError(`piece ${assembly.pieces[c]} has no channel, and the assembly sways`);
|
|
61
|
+
}
|
|
62
|
+
return mesh;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function expandAssembly(assembly: DrftAssembly, piece: PieceLookup): MeshData {
|
|
66
|
+
const { attributes, surfaces, surfaceOf, transforms, uv } = assembly;
|
|
67
|
+
const copies = assembly.pieces.length;
|
|
68
|
+
let vertices = 0;
|
|
69
|
+
let indexCount = 0;
|
|
70
|
+
for (let c = 0; c < copies; c++) {
|
|
71
|
+
const mesh = pieceOf(piece, assembly, c);
|
|
72
|
+
vertices += mesh.positions.length / 3;
|
|
73
|
+
indexCount += mesh.indices.length;
|
|
74
|
+
}
|
|
75
|
+
const has = (bit: number): boolean => (attributes & bit) !== 0;
|
|
76
|
+
const out: MeshData = {
|
|
77
|
+
positions: new Float32Array(vertices * 3),
|
|
78
|
+
normals: new Float32Array(vertices * 3),
|
|
79
|
+
colors: new Float32Array(vertices * 3),
|
|
80
|
+
emissive: new Float32Array(vertices),
|
|
81
|
+
indices: new Uint32Array(indexCount),
|
|
82
|
+
};
|
|
83
|
+
if (has(ATTR_SPECULAR)) out.specular = new Float32Array(vertices);
|
|
84
|
+
if (has(ATTR_UVS)) out.uvs = new Float32Array(vertices * 2);
|
|
85
|
+
if (has(ATTR_EMISSIVE_COLOR)) out.emissiveColor = new Float32Array(vertices * 3);
|
|
86
|
+
if (has(ATTR_ROUGHNESS)) out.roughness = new Float32Array(vertices);
|
|
87
|
+
if (has(ATTR_GRAIN)) out.grain = new Float32Array(vertices);
|
|
88
|
+
if (has(ATTR_RELIEF)) out.relief = new Float32Array(vertices);
|
|
89
|
+
if (has(ATTR_TANGENT)) out.tangents = new Float32Array(vertices * 4);
|
|
90
|
+
if (has(ATTR_LAYERS)) out.layers = new Float32Array(vertices);
|
|
91
|
+
if (has(ATTR_CHANNEL)) out.channel = new Float32Array(vertices * 4);
|
|
92
|
+
|
|
93
|
+
let v0 = 0;
|
|
94
|
+
let i0 = 0;
|
|
95
|
+
for (let c = 0; c < copies; c++) {
|
|
96
|
+
const mesh = pieceOf(piece, assembly, c);
|
|
97
|
+
const m = c * COPY_MATRIX_FLOATS;
|
|
98
|
+
const t = transforms;
|
|
99
|
+
const [a, b, cc, d, e, f, g, h, k] = [
|
|
100
|
+
t[m] as number,
|
|
101
|
+
t[m + 1] as number,
|
|
102
|
+
t[m + 2] as number,
|
|
103
|
+
t[m + 3] as number,
|
|
104
|
+
t[m + 4] as number,
|
|
105
|
+
t[m + 5] as number,
|
|
106
|
+
t[m + 6] as number,
|
|
107
|
+
t[m + 7] as number,
|
|
108
|
+
t[m + 8] as number,
|
|
109
|
+
];
|
|
110
|
+
const [tx, ty, tz] = [t[m + 9] as number, t[m + 10] as number, t[m + 11] as number];
|
|
111
|
+
const det = determinant(t, m);
|
|
112
|
+
/* The inverse transpose is the cofactor matrix over the determinant. With columns (a b cc),
|
|
113
|
+
(d e f), (g h k) the matrix's rows are (a d g), (b e h), (cc f k), and nRC is the cofactor of
|
|
114
|
+
row R, column C — not of C, R, which would be the plain inverse. */
|
|
115
|
+
const n00 = (e * k - f * h) / det;
|
|
116
|
+
const n01 = (cc * h - b * k) / det;
|
|
117
|
+
const n02 = (b * f - cc * e) / det;
|
|
118
|
+
const n10 = (f * g - d * k) / det;
|
|
119
|
+
const n11 = (a * k - cc * g) / det;
|
|
120
|
+
const n12 = (cc * d - a * f) / det;
|
|
121
|
+
const n20 = (d * h - e * g) / det;
|
|
122
|
+
const n21 = (b * g - a * h) / det;
|
|
123
|
+
const n22 = (a * e - b * d) / det;
|
|
124
|
+
const hand = det < 0 ? -1 : 1;
|
|
125
|
+
const s = (surfaceOf[c] as number) * SURFACE_FLOATS;
|
|
126
|
+
const u = c * COPY_UV_FLOATS;
|
|
127
|
+
const count = mesh.positions.length / 3;
|
|
128
|
+
const p = mesh.positions;
|
|
129
|
+
const n = mesh.normals;
|
|
130
|
+
const tan = mesh.tangents;
|
|
131
|
+
const tex = mesh.uvs;
|
|
132
|
+
for (let i = 0; i < count; i++) {
|
|
133
|
+
const o = v0 + i;
|
|
134
|
+
const px = p[i * 3] as number;
|
|
135
|
+
const py = p[i * 3 + 1] as number;
|
|
136
|
+
const pz = p[i * 3 + 2] as number;
|
|
137
|
+
out.positions[o * 3] = a * px + d * py + g * pz + tx;
|
|
138
|
+
out.positions[o * 3 + 1] = b * px + e * py + h * pz + ty;
|
|
139
|
+
out.positions[o * 3 + 2] = cc * px + f * py + k * pz + tz;
|
|
140
|
+
const nx = n[i * 3] as number;
|
|
141
|
+
const ny = n[i * 3 + 1] as number;
|
|
142
|
+
const nz = n[i * 3 + 2] as number;
|
|
143
|
+
let wx = n00 * nx + n01 * ny + n02 * nz;
|
|
144
|
+
let wy = n10 * nx + n11 * ny + n12 * nz;
|
|
145
|
+
let wz = n20 * nx + n21 * ny + n22 * nz;
|
|
146
|
+
const nl = Math.hypot(wx, wy, wz) || 1;
|
|
147
|
+
wx /= nl;
|
|
148
|
+
wy /= nl;
|
|
149
|
+
wz /= nl;
|
|
150
|
+
out.normals[o * 3] = wx;
|
|
151
|
+
out.normals[o * 3 + 1] = wy;
|
|
152
|
+
out.normals[o * 3 + 2] = wz;
|
|
153
|
+
out.colors[o * 3] = surfaces[s + SURFACE.color] as number;
|
|
154
|
+
out.colors[o * 3 + 1] = surfaces[s + SURFACE.color + 1] as number;
|
|
155
|
+
out.colors[o * 3 + 2] = surfaces[s + SURFACE.color + 2] as number;
|
|
156
|
+
out.emissive[o] = surfaces[s + SURFACE.emissive] as number;
|
|
157
|
+
if (out.specular) out.specular[o] = surfaces[s + SURFACE.specular] as number;
|
|
158
|
+
if (out.emissiveColor) {
|
|
159
|
+
out.emissiveColor[o * 3] = surfaces[s + SURFACE.emissiveColor] as number;
|
|
160
|
+
out.emissiveColor[o * 3 + 1] = surfaces[s + SURFACE.emissiveColor + 1] as number;
|
|
161
|
+
out.emissiveColor[o * 3 + 2] = surfaces[s + SURFACE.emissiveColor + 2] as number;
|
|
162
|
+
}
|
|
163
|
+
if (out.roughness) out.roughness[o] = surfaces[s + SURFACE.roughness] as number;
|
|
164
|
+
if (out.grain) out.grain[o] = surfaces[s + SURFACE.grain] as number;
|
|
165
|
+
if (out.relief) out.relief[o] = surfaces[s + SURFACE.relief] as number;
|
|
166
|
+
if (out.layers) out.layers[o] = surfaces[s + SURFACE.layer] as number;
|
|
167
|
+
/* Sway, sky and opacity are the piece's own: shape, not paint. */
|
|
168
|
+
if (out.channel && mesh.channel !== undefined) {
|
|
169
|
+
for (let lane = 0; lane < 4; lane++) {
|
|
170
|
+
out.channel[o * 4 + lane] = mesh.channel[i * 4 + lane] as number;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (tan !== undefined && (out.tangents !== undefined || out.uvs !== undefined)) {
|
|
174
|
+
const qx = tan[i * 4] as number;
|
|
175
|
+
const qy = tan[i * 4 + 1] as number;
|
|
176
|
+
const qz = tan[i * 4 + 2] as number;
|
|
177
|
+
const qw = tan[i * 4 + 3] as number;
|
|
178
|
+
if (out.tangents) {
|
|
179
|
+
let rx = a * qx + d * qy + g * qz;
|
|
180
|
+
let ry = b * qx + e * qy + h * qz;
|
|
181
|
+
let rz = cc * qx + f * qy + k * qz;
|
|
182
|
+
const rl = Math.hypot(rx, ry, rz) || 1;
|
|
183
|
+
rx /= rl;
|
|
184
|
+
ry /= rl;
|
|
185
|
+
rz /= rl;
|
|
186
|
+
out.tangents[o * 4] = rx;
|
|
187
|
+
out.tangents[o * 4 + 1] = ry;
|
|
188
|
+
out.tangents[o * 4 + 2] = rz;
|
|
189
|
+
out.tangents[o * 4 + 3] = qw * hand;
|
|
190
|
+
}
|
|
191
|
+
if (out.uvs && tex !== undefined) {
|
|
192
|
+
/* The piece's own directions for u and v: its tangent, and the bitangent n × t · w. */
|
|
193
|
+
const ql = Math.hypot(qx, qy, qz) || 1;
|
|
194
|
+
const bx = (ny * qz - nz * qy) * qw;
|
|
195
|
+
const by = (nz * qx - nx * qz) * qw;
|
|
196
|
+
const bz = (nx * qy - ny * qx) * qw;
|
|
197
|
+
const bl = Math.hypot(bx, by, bz) || 1;
|
|
198
|
+
const su = Math.hypot(
|
|
199
|
+
((uv[u] as number) * qx) / ql,
|
|
200
|
+
((uv[u + 1] as number) * qy) / ql,
|
|
201
|
+
((uv[u + 2] as number) * qz) / ql,
|
|
202
|
+
);
|
|
203
|
+
const sv = Math.hypot(
|
|
204
|
+
((uv[u + 3] as number) * bx) / bl,
|
|
205
|
+
((uv[u + 4] as number) * by) / bl,
|
|
206
|
+
((uv[u + 5] as number) * bz) / bl,
|
|
207
|
+
);
|
|
208
|
+
out.uvs[o * 2] = (tex[i * 2] as number) * su + (uv[u + 6] as number);
|
|
209
|
+
out.uvs[o * 2 + 1] = (tex[i * 2 + 1] as number) * sv + (uv[u + 7] as number);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const idx = mesh.indices;
|
|
214
|
+
for (let j = 0; j + 2 < idx.length; j += 3) {
|
|
215
|
+
out.indices[i0 + j] = v0 + (idx[j] as number);
|
|
216
|
+
/* A mirrored copy winds back, or its faces turn inside out. */
|
|
217
|
+
out.indices[i0 + j + 1] = v0 + (idx[hand < 0 ? j + 2 : j + 1] as number);
|
|
218
|
+
out.indices[i0 + j + 2] = v0 + (idx[hand < 0 ? j + 1 : j + 2] as number);
|
|
219
|
+
}
|
|
220
|
+
v0 += count;
|
|
221
|
+
i0 += idx.length;
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Min xyz then max xyz of every copy: its piece's box through its matrix, corner by corner. */
|
|
227
|
+
export function assemblyBounds(
|
|
228
|
+
assembly: DrftAssembly,
|
|
229
|
+
piece: PieceLookup,
|
|
230
|
+
out: Float32Array = new Float32Array(6),
|
|
231
|
+
): Float32Array {
|
|
232
|
+
out.set([Infinity, Infinity, Infinity, -Infinity, -Infinity, -Infinity]);
|
|
233
|
+
for (let c = 0; c < assembly.pieces.length; c++) {
|
|
234
|
+
const p = piece(assembly.pieces[c] as number).positions;
|
|
235
|
+
let lx = Infinity;
|
|
236
|
+
let ly = Infinity;
|
|
237
|
+
let lz = Infinity;
|
|
238
|
+
let hx = -Infinity;
|
|
239
|
+
let hy = -Infinity;
|
|
240
|
+
let hz = -Infinity;
|
|
241
|
+
for (let i = 0; i + 2 < p.length; i += 3) {
|
|
242
|
+
lx = Math.min(lx, p[i] as number);
|
|
243
|
+
ly = Math.min(ly, p[i + 1] as number);
|
|
244
|
+
lz = Math.min(lz, p[i + 2] as number);
|
|
245
|
+
hx = Math.max(hx, p[i] as number);
|
|
246
|
+
hy = Math.max(hy, p[i + 1] as number);
|
|
247
|
+
hz = Math.max(hz, p[i + 2] as number);
|
|
248
|
+
}
|
|
249
|
+
if (lx > hx) continue;
|
|
250
|
+
const m = c * COPY_MATRIX_FLOATS;
|
|
251
|
+
const t = assembly.transforms;
|
|
252
|
+
for (let corner = 0; corner < 8; corner++) {
|
|
253
|
+
const x = corner & 1 ? hx : lx;
|
|
254
|
+
const y = corner & 2 ? hy : ly;
|
|
255
|
+
const z = corner & 4 ? hz : lz;
|
|
256
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
257
|
+
const w =
|
|
258
|
+
(t[m + axis] as number) * x +
|
|
259
|
+
(t[m + 3 + axis] as number) * y +
|
|
260
|
+
(t[m + 6 + axis] as number) * z +
|
|
261
|
+
(t[m + 9 + axis] as number);
|
|
262
|
+
if (w < (out[axis] as number)) out[axis] = w;
|
|
263
|
+
if (w > (out[axis + 3] as number)) out[axis + 3] = w;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
return out;
|
|
268
|
+
}
|