@driftengine/assets 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/drawKey.d.ts +17 -1
- package/dist/drawKey.js +9 -3
- package/dist/drftLoader.d.ts +68 -4
- package/dist/drftLoader.js +205 -56
- package/dist/gltf.d.ts +22 -3
- package/dist/gltf.js +36 -1
- package/dist/index.d.ts +1 -0
- package/dist/loadProgress.d.ts +8 -1
- package/dist/regionStore.d.ts +94 -0
- package/dist/regionStore.js +272 -0
- package/dist/repeats.js +1 -0
- package/dist/weld.js +6 -0
- package/package.json +4 -4
- package/src/drawKey.ts +26 -3
- package/src/drftLoader.ts +238 -59
- package/src/gltf.ts +68 -5
- package/src/index.ts +1 -0
- package/src/loadProgress.ts +8 -1
- package/src/regionStore.ts +333 -0
- package/src/repeats.ts +1 -0
- package/src/weld.ts +6 -0
package/dist/gltf.js
CHANGED
|
@@ -428,6 +428,7 @@ function materialOf(material) {
|
|
|
428
428
|
* A separate occlusion image is dropped rather than loaded into a second unit. One map, one
|
|
429
429
|
* unit, and `textureUnitBudget.test.ts` says the next unit belongs to emissive.
|
|
430
430
|
*/
|
|
431
|
+
const glass = glassOf(material, modulated ? undefined : roughness);
|
|
431
432
|
const occlusion = material?.occlusionTexture;
|
|
432
433
|
const occlusionStrength = modulated && occlusion !== undefined && occlusion.index === ormMap
|
|
433
434
|
? (occlusion.strength ?? 1)
|
|
@@ -447,7 +448,7 @@ function materialOf(material) {
|
|
|
447
448
|
* cannot say the material blends, and a leaf whose shape is all in its texture's alpha drew as a
|
|
448
449
|
* solid card. So it is said.
|
|
449
450
|
*/
|
|
450
|
-
blend: blends,
|
|
451
|
+
blend: blends || glass.transmission > 0,
|
|
451
452
|
/* Seen from both faces: a curtain, a leaf card. The specification's default is one face. */
|
|
452
453
|
doubleSided: material?.doubleSided === true,
|
|
453
454
|
/* Only in MASK mode: the specification says `alphaCutoff` has no meaning in the other two. */
|
|
@@ -463,8 +464,42 @@ function materialOf(material) {
|
|
|
463
464
|
metallicScale: modulated ? (pbr?.metallicFactor ?? 1) : 1,
|
|
464
465
|
occlusionStrength,
|
|
465
466
|
albedo: pbr?.baseColorTexture?.index ?? -1,
|
|
467
|
+
transmission: glass.transmission,
|
|
468
|
+
frost: glass.frost,
|
|
469
|
+
tint: glass.tint,
|
|
466
470
|
};
|
|
467
471
|
}
|
|
472
|
+
const unit = (value, fallback) => Math.min(1, Math.max(0, value ?? fallback));
|
|
473
|
+
/**
|
|
474
|
+
* What a material's extensions say about letting light through, as this engine's glass.
|
|
475
|
+
*
|
|
476
|
+
* **Rough glass is frosted glass.** `KHR_materials_transmission` says its roughness blurs what is
|
|
477
|
+
* seen through the surface, and that blur is exactly what `frost` is, so a transmissive material's
|
|
478
|
+
* roughness raises its frost and a diffusely transmitting share raises it too — whichever says
|
|
479
|
+
* more. Only transmission makes a material glass: a surface stating diffuse transmission alone is a
|
|
480
|
+
* leaf or a sheet of paper, which this engine's glass would draw see-through and let every shadow
|
|
481
|
+
* pass, so it stays the opaque surface it was. What that gives up is a paper shade stated by that
|
|
482
|
+
* extension alone, which arrives opaque; a per-material override is the door for it.
|
|
483
|
+
*
|
|
484
|
+
* `roughness` is `undefined` where a map carries the roughness per texel: the factor then only
|
|
485
|
+
* scales the map, so it says nothing about how rough the glass is, and frost comes from the
|
|
486
|
+
* diffuse share alone rather than from a number the file did not state.
|
|
487
|
+
*/
|
|
488
|
+
function glassOf(material, roughness) {
|
|
489
|
+
const ext = material?.extensions;
|
|
490
|
+
const transmission = unit(ext?.KHR_materials_transmission?.transmissionFactor, 0);
|
|
491
|
+
if (transmission <= 0)
|
|
492
|
+
return { transmission: 0, frost: 0, tint: [1, 1, 1] };
|
|
493
|
+
const diffuse = ext?.KHR_materials_diffuse_transmission;
|
|
494
|
+
const frost = Math.max(unit(diffuse?.diffuseTransmissionFactor, 0), unit(roughness, 0));
|
|
495
|
+
const colour = diffuse?.diffuseTransmissionColorFactor ?? ext?.KHR_materials_volume?.attenuationColor;
|
|
496
|
+
const tint = [
|
|
497
|
+
unit(colour?.[0], 1),
|
|
498
|
+
unit(colour?.[1], 1),
|
|
499
|
+
unit(colour?.[2], 1),
|
|
500
|
+
];
|
|
501
|
+
return { transmission, frost, tint };
|
|
502
|
+
}
|
|
468
503
|
/** The required extensions this reader implements, or follows far enough to fail loudly. */
|
|
469
504
|
const KNOWN_REQUIRED = new Set([
|
|
470
505
|
'KHR_materials_pbrSpecularGlossiness',
|
package/dist/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
|
|
|
13
13
|
export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
14
14
|
export type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
|
|
15
15
|
export type { DrftLoadPhase, DrftLoadProgress, DrftPart } from './loadProgress.ts';
|
|
16
|
+
export type { LoadedRegion, LoadedRegionLevel } from './regionStore.ts';
|
|
16
17
|
export type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
17
18
|
export { TextureSet, textureColorSpaces } from './drftTextures.ts';
|
|
18
19
|
export { assetCandidates, basenameOf } from './assetPath.ts';
|
package/dist/loadProgress.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { MeshHandle, InstancedHandle, MeshInstances } from '@driftengine/core';
|
|
1
|
+
import type { GlassOptions, MeshHandle, InstancedHandle, MeshInstances } from '@driftengine/core';
|
|
2
2
|
/**
|
|
3
3
|
* The vocabulary of a load in flight: which phase it is in, how far it has got, and what a
|
|
4
4
|
* finished part is.
|
|
@@ -120,6 +120,13 @@ export interface DrftPart {
|
|
|
120
120
|
readonly blend: boolean;
|
|
121
121
|
/** Whether both faces are seen: glTF's `doubleSided`. Drawn without culling, each face lit as the front. */
|
|
122
122
|
readonly doubleSided: boolean;
|
|
123
|
+
/**
|
|
124
|
+
* The glass this part is, from its file or from `DrftLoaderOptions.surface`, or null for a part
|
|
125
|
+
* that lets no light through. A glass part is also `blend`, so a caller drawing blended parts
|
|
126
|
+
* translucent passes this as the draw's `glass` and every pane arrives as the file or the
|
|
127
|
+
* override said. One object per merged group, shared by its parts, so it is not rebuilt per draw.
|
|
128
|
+
*/
|
|
129
|
+
readonly glass: GlassOptions | null;
|
|
123
130
|
/**
|
|
124
131
|
* The copies this part is drawn as, or null for a part drawn once.
|
|
125
132
|
*
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { MeshInstances, RendererApi } from '@driftengine/core';
|
|
2
|
+
import type { DrftAssembly, DrftRegion, DrftRegionCollision, PieceLookup } from '@driftengine/drft';
|
|
3
|
+
import type { MeshData } from '@driftengine/drft';
|
|
4
|
+
import type { DrftPart } from './loadProgress.ts';
|
|
5
|
+
/** How the loader placed the model: `p · scale + (x, y, z)`. */
|
|
6
|
+
export interface LoaderFit {
|
|
7
|
+
readonly scale: number;
|
|
8
|
+
readonly x: number;
|
|
9
|
+
readonly y: number;
|
|
10
|
+
readonly z: number;
|
|
11
|
+
}
|
|
12
|
+
export interface LoadedRegionLevel {
|
|
13
|
+
/** How far, in metres of the file, this level may stand from the real surface. */
|
|
14
|
+
readonly error: number;
|
|
15
|
+
/** One part a mesh, filled as they upload. */
|
|
16
|
+
readonly parts: DrftPart[];
|
|
17
|
+
/** Whether any of its meshes arrived as assemblies, kept small until paged in. */
|
|
18
|
+
paged: boolean;
|
|
19
|
+
/** Whether every mesh is up: always, for a level that is not paged. */
|
|
20
|
+
resident: boolean;
|
|
21
|
+
}
|
|
22
|
+
export interface LoadedRegion {
|
|
23
|
+
readonly id: number;
|
|
24
|
+
/** Min xyz then max xyz, placed. */
|
|
25
|
+
readonly bounds: Float32Array;
|
|
26
|
+
/** Finest first. `HlodSet.add` takes the errors, scaled by the fit. */
|
|
27
|
+
readonly levels: readonly LoadedRegionLevel[];
|
|
28
|
+
/** Its props, one instanced part a prototype, culled per instance. */
|
|
29
|
+
readonly batches: DrftPart[];
|
|
30
|
+
/**
|
|
31
|
+
* Which of the region's groups in the file each batch is, index for index. Batches are built as
|
|
32
|
+
* their prototypes arrive, not in the file's order, so a caller pairing something of its own with
|
|
33
|
+
* each group — a material, a blend — pairs through this, never by position.
|
|
34
|
+
*/
|
|
35
|
+
readonly batchGroups: number[];
|
|
36
|
+
/** Six floats a box, placed, for `addOccluder`. */
|
|
37
|
+
readonly occluders: Float32Array;
|
|
38
|
+
/** What it collides as, placed, or null. */
|
|
39
|
+
readonly collision: DrftRegionCollision | null;
|
|
40
|
+
/** Meshes and batches still to come; 0 once every level and prop is up. */
|
|
41
|
+
pending: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Copies of a fitted prototype, each matrix conjugated by the fit: M becomes F·M·F⁻¹, the same turn
|
|
45
|
+
* and a translation of t − R·t + s·u for the fit's scale s and offset t and the copy's own
|
|
46
|
+
* translation u. The prototype already carries the fit, so the copies land where its geometry does
|
|
47
|
+
* and are not scaled twice.
|
|
48
|
+
*/
|
|
49
|
+
export declare function fitCopies(transforms: Float32Array, fit: LoaderFit): MeshInstances;
|
|
50
|
+
export declare class RegionStore {
|
|
51
|
+
private readonly renderer;
|
|
52
|
+
private readonly fit;
|
|
53
|
+
readonly regions: Map<number, LoadedRegion>;
|
|
54
|
+
/** Level meshes by ordinal: whose, and which level. */
|
|
55
|
+
private readonly levelOwners;
|
|
56
|
+
/** Prototypes that have uploaded, by ordinal. */
|
|
57
|
+
private readonly prototypes;
|
|
58
|
+
/** Groups whose prototype has not uploaded yet, by its ordinal. */
|
|
59
|
+
private readonly waiting;
|
|
60
|
+
private readonly jobs;
|
|
61
|
+
private readonly made;
|
|
62
|
+
/** Assemblies held small, by ordinal, and each paged level's ordinals. */
|
|
63
|
+
private readonly compact;
|
|
64
|
+
private readonly levelCompact;
|
|
65
|
+
/** Paged meshes asked for and not yet up, and those that are. */
|
|
66
|
+
private readonly requested;
|
|
67
|
+
private readonly pagedParts;
|
|
68
|
+
constructor(renderer: RendererApi, fit: () => LoaderFit | null);
|
|
69
|
+
/** Whether a mesh is a region's, as a level or a prop, and so is not a part of the model. */
|
|
70
|
+
owns(ordinal: number): boolean;
|
|
71
|
+
/** Whether batches are queued. */
|
|
72
|
+
get busy(): boolean;
|
|
73
|
+
/** A region's chunk has landed, ahead of the meshes it introduces. */
|
|
74
|
+
admit(region: DrftRegion): void;
|
|
75
|
+
/** Whether a mesh is one of a region's levels. */
|
|
76
|
+
isLevelMesh(ordinal: number): boolean;
|
|
77
|
+
/** A level's assembly has arrived: held small, its level paged and not yet up. */
|
|
78
|
+
hold(ordinal: number, assembly: DrftAssembly, piece: PieceLookup): void;
|
|
79
|
+
/**
|
|
80
|
+
* Ask for a paged level's meshes, or free them. Coming up, returns each mesh still to upload with
|
|
81
|
+
* a way to expand it, for the loader's queue; going down, frees what is up and returns the
|
|
82
|
+
* ordinals released, so the loader can drop any still queued.
|
|
83
|
+
*/
|
|
84
|
+
page(id: number, level: number, resident: boolean): {
|
|
85
|
+
ordinal: number;
|
|
86
|
+
expand: () => MeshData;
|
|
87
|
+
}[];
|
|
88
|
+
private release;
|
|
89
|
+
/** A region's mesh has uploaded, as the part the loader would have made of it. */
|
|
90
|
+
arrived(ordinal: number, part: DrftPart): void;
|
|
91
|
+
/** Build one queued batch inside the caller's budget; false when none was queued. */
|
|
92
|
+
buildNext(): boolean;
|
|
93
|
+
dispose(): void;
|
|
94
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The regions of a streamed world as they arrive: each one's meshes at every level, its prop
|
|
3
|
+
* batches, its occluders and its collision, kept apart from the loader's parts and never merged.
|
|
4
|
+
*
|
|
5
|
+
* **Apart, because a region's levels are alternatives rather than pieces.** The loader merges its
|
|
6
|
+
* parts by material into one mesh a group, which is right for one model and wrong here twice over:
|
|
7
|
+
* it would weld a district's finest level to its coarsest, drawing both, and weld one district to
|
|
8
|
+
* the next, so none could be dropped when the eye leaves. So a region's meshes upload on the
|
|
9
|
+
* loader's clocked queue like any part and land here instead, one part a mesh, a list a level.
|
|
10
|
+
*
|
|
11
|
+
* **A prop's prototype arrives once and serves every region that places it.** A region naming a
|
|
12
|
+
* prototype already up gets its batch at the next budgeted build; one naming a prototype still to
|
|
13
|
+
* come waits for it. Either way the batch is built inside a frame's budget, never from the stream's
|
|
14
|
+
* callback, and culls instance by instance (`createInstanced`'s `cull`).
|
|
15
|
+
*
|
|
16
|
+
* **A level that arrives as assemblies is paged** (`MSHC`, `drftAssembly.ts`). A city's finest
|
|
17
|
+
* level is millions of triangles, more than any device keeps resident, so such a level is held as
|
|
18
|
+
* the copies the file carried — tens of kilobytes a district — and comes up only when the caller
|
|
19
|
+
* pages it in, expanded and uploaded on the same clocked queue; paged out, its meshes are freed and
|
|
20
|
+
* the copies stay. What it costs is a region that is not yet up when the eye arrives: the caller
|
|
21
|
+
* draws a coarser level until `resident` says the fine one is there, and pages ahead of the eye.
|
|
22
|
+
*
|
|
23
|
+
* Everything is in the loader's placement: a world loads with `{ fit: 'none' }`, where that is the
|
|
24
|
+
* file's own space.
|
|
25
|
+
*/
|
|
26
|
+
import { createMeshInstances } from '@driftengine/core';
|
|
27
|
+
import { expandAssembly } from '@driftengine/drft';
|
|
28
|
+
/**
|
|
29
|
+
* Copies of a fitted prototype, each matrix conjugated by the fit: M becomes F·M·F⁻¹, the same turn
|
|
30
|
+
* and a translation of t − R·t + s·u for the fit's scale s and offset t and the copy's own
|
|
31
|
+
* translation u. The prototype already carries the fit, so the copies land where its geometry does
|
|
32
|
+
* and are not scaled twice.
|
|
33
|
+
*/
|
|
34
|
+
export function fitCopies(transforms, fit) {
|
|
35
|
+
const count = transforms.length / 16;
|
|
36
|
+
const data = createMeshInstances(count);
|
|
37
|
+
for (let k = 0; k < count; k++) {
|
|
38
|
+
const at = k * 16;
|
|
39
|
+
for (let i = 0; i < 12; i++)
|
|
40
|
+
data.models[at + i] = transforms[at + i];
|
|
41
|
+
/* R·t, with R the copy's own 3×3 read column-major. */
|
|
42
|
+
const rtx = transforms[at] * fit.x +
|
|
43
|
+
transforms[at + 4] * fit.y +
|
|
44
|
+
transforms[at + 8] * fit.z;
|
|
45
|
+
const rty = transforms[at + 1] * fit.x +
|
|
46
|
+
transforms[at + 5] * fit.y +
|
|
47
|
+
transforms[at + 9] * fit.z;
|
|
48
|
+
const rtz = transforms[at + 2] * fit.x +
|
|
49
|
+
transforms[at + 6] * fit.y +
|
|
50
|
+
transforms[at + 10] * fit.z;
|
|
51
|
+
data.models[at + 12] = fit.x - rtx + fit.scale * transforms[at + 12];
|
|
52
|
+
data.models[at + 13] = fit.y - rty + fit.scale * transforms[at + 13];
|
|
53
|
+
data.models[at + 14] = fit.z - rtz + fit.scale * transforms[at + 14];
|
|
54
|
+
data.models[at + 15] = 1;
|
|
55
|
+
}
|
|
56
|
+
data.tints.fill(1);
|
|
57
|
+
data.count = count;
|
|
58
|
+
return data;
|
|
59
|
+
}
|
|
60
|
+
/** Points packed xyz, placed by the fit into a new array. */
|
|
61
|
+
function placed(points, fit) {
|
|
62
|
+
const out = new Float32Array(points.length);
|
|
63
|
+
for (let i = 0; i < points.length; i += 3) {
|
|
64
|
+
out[i] = points[i] * fit.scale + fit.x;
|
|
65
|
+
out[i + 1] = points[i + 1] * fit.scale + fit.y;
|
|
66
|
+
out[i + 2] = points[i + 2] * fit.scale + fit.z;
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
export class RegionStore {
|
|
71
|
+
renderer;
|
|
72
|
+
fit;
|
|
73
|
+
regions = new Map();
|
|
74
|
+
/** Level meshes by ordinal: whose, and which level. */
|
|
75
|
+
levelOwners = new Map();
|
|
76
|
+
/** Prototypes that have uploaded, by ordinal. */
|
|
77
|
+
prototypes = new Map();
|
|
78
|
+
/** Groups whose prototype has not uploaded yet, by its ordinal. */
|
|
79
|
+
waiting = new Map();
|
|
80
|
+
jobs = [];
|
|
81
|
+
made = [];
|
|
82
|
+
/** Assemblies held small, by ordinal, and each paged level's ordinals. */
|
|
83
|
+
compact = new Map();
|
|
84
|
+
levelCompact = new Map();
|
|
85
|
+
/** Paged meshes asked for and not yet up, and those that are. */
|
|
86
|
+
requested = new Set();
|
|
87
|
+
pagedParts = new Map();
|
|
88
|
+
constructor(renderer, fit) {
|
|
89
|
+
this.renderer = renderer;
|
|
90
|
+
this.fit = fit;
|
|
91
|
+
}
|
|
92
|
+
/** Whether a mesh is a region's, as a level or a prop, and so is not a part of the model. */
|
|
93
|
+
owns(ordinal) {
|
|
94
|
+
return (this.levelOwners.has(ordinal) || this.prototypes.has(ordinal) || this.waiting.has(ordinal));
|
|
95
|
+
}
|
|
96
|
+
/** Whether batches are queued. */
|
|
97
|
+
get busy() {
|
|
98
|
+
return this.jobs.length > 0;
|
|
99
|
+
}
|
|
100
|
+
/** A region's chunk has landed, ahead of the meshes it introduces. */
|
|
101
|
+
admit(region) {
|
|
102
|
+
const fit = this.fit() ?? { scale: 1, x: 0, y: 0, z: 0 };
|
|
103
|
+
const loaded = {
|
|
104
|
+
id: region.id,
|
|
105
|
+
bounds: new Float32Array([
|
|
106
|
+
...placed([region.bounds[0], region.bounds[1], region.bounds[2]], fit),
|
|
107
|
+
...placed([region.bounds[3], region.bounds[4], region.bounds[5]], fit),
|
|
108
|
+
]),
|
|
109
|
+
levels: region.levels.map((level) => ({
|
|
110
|
+
error: level.error * fit.scale,
|
|
111
|
+
parts: [],
|
|
112
|
+
paged: false,
|
|
113
|
+
resident: true,
|
|
114
|
+
})),
|
|
115
|
+
batches: [],
|
|
116
|
+
batchGroups: [],
|
|
117
|
+
occluders: placed(region.occluders, fit),
|
|
118
|
+
collision: region.collision === null
|
|
119
|
+
? null
|
|
120
|
+
: {
|
|
121
|
+
positions: placed(region.collision.positions, fit),
|
|
122
|
+
indices: region.collision.indices,
|
|
123
|
+
},
|
|
124
|
+
pending: 0,
|
|
125
|
+
};
|
|
126
|
+
region.levels.forEach((level, index) => {
|
|
127
|
+
for (const mesh of level.meshes) {
|
|
128
|
+
this.levelOwners.set(mesh, { region: loaded, level: index });
|
|
129
|
+
loaded.pending++;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
region.instances.forEach((group, index) => {
|
|
133
|
+
loaded.pending++;
|
|
134
|
+
const prototype = this.prototypes.get(group.mesh);
|
|
135
|
+
if (prototype !== undefined) {
|
|
136
|
+
this.jobs.push({ region: loaded, prototype, transforms: group.transforms, group: index });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
const list = this.waiting.get(group.mesh) ?? [];
|
|
140
|
+
list.push({ region: loaded, transforms: group.transforms, group: index });
|
|
141
|
+
this.waiting.set(group.mesh, list);
|
|
142
|
+
});
|
|
143
|
+
this.regions.set(region.id, loaded);
|
|
144
|
+
}
|
|
145
|
+
/** Whether a mesh is one of a region's levels. */
|
|
146
|
+
isLevelMesh(ordinal) {
|
|
147
|
+
return this.levelOwners.has(ordinal);
|
|
148
|
+
}
|
|
149
|
+
/** A level's assembly has arrived: held small, its level paged and not yet up. */
|
|
150
|
+
hold(ordinal, assembly, piece) {
|
|
151
|
+
const owner = this.levelOwners.get(ordinal);
|
|
152
|
+
const level = owner?.region.levels[owner.level];
|
|
153
|
+
if (owner === undefined || level === undefined)
|
|
154
|
+
return;
|
|
155
|
+
this.compact.set(ordinal, { assembly, piece });
|
|
156
|
+
const list = this.levelCompact.get(level) ?? [];
|
|
157
|
+
list.push(ordinal);
|
|
158
|
+
this.levelCompact.set(level, list);
|
|
159
|
+
level.paged = true;
|
|
160
|
+
level.resident = false;
|
|
161
|
+
owner.region.pending--;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Ask for a paged level's meshes, or free them. Coming up, returns each mesh still to upload with
|
|
165
|
+
* a way to expand it, for the loader's queue; going down, frees what is up and returns the
|
|
166
|
+
* ordinals released, so the loader can drop any still queued.
|
|
167
|
+
*/
|
|
168
|
+
page(id, level, resident) {
|
|
169
|
+
const lv = this.regions.get(id)?.levels[level];
|
|
170
|
+
const ordinals = lv === undefined ? undefined : this.levelCompact.get(lv);
|
|
171
|
+
if (lv === undefined || ordinals === undefined)
|
|
172
|
+
return [];
|
|
173
|
+
const out = [];
|
|
174
|
+
for (const ordinal of ordinals) {
|
|
175
|
+
const held = this.compact.get(ordinal);
|
|
176
|
+
if (held === undefined)
|
|
177
|
+
continue;
|
|
178
|
+
if (resident) {
|
|
179
|
+
if (this.pagedParts.has(ordinal) || this.requested.has(ordinal))
|
|
180
|
+
continue;
|
|
181
|
+
this.requested.add(ordinal);
|
|
182
|
+
out.push({ ordinal, expand: () => expandAssembly(held.assembly, held.piece) });
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
this.requested.delete(ordinal);
|
|
186
|
+
const part = this.pagedParts.get(ordinal);
|
|
187
|
+
out.push({ ordinal, expand: () => expandAssembly(held.assembly, held.piece) });
|
|
188
|
+
if (part === undefined)
|
|
189
|
+
continue;
|
|
190
|
+
this.pagedParts.delete(ordinal);
|
|
191
|
+
this.release(part);
|
|
192
|
+
const at = lv.parts.indexOf(part);
|
|
193
|
+
if (at >= 0)
|
|
194
|
+
lv.parts.splice(at, 1);
|
|
195
|
+
}
|
|
196
|
+
if (!resident)
|
|
197
|
+
lv.resident = false;
|
|
198
|
+
return out;
|
|
199
|
+
}
|
|
200
|
+
release(part) {
|
|
201
|
+
const at = this.made.indexOf(part.mesh);
|
|
202
|
+
if (at >= 0)
|
|
203
|
+
this.made.splice(at, 1);
|
|
204
|
+
this.renderer.disposeMesh(part.mesh);
|
|
205
|
+
}
|
|
206
|
+
/** A region's mesh has uploaded, as the part the loader would have made of it. */
|
|
207
|
+
arrived(ordinal, part) {
|
|
208
|
+
if (this.compact.has(ordinal)) {
|
|
209
|
+
/* Paged out again while it was on its way up: nothing wants it. */
|
|
210
|
+
if (!this.requested.delete(ordinal)) {
|
|
211
|
+
this.renderer.disposeMesh(part.mesh);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
this.made.push(part.mesh);
|
|
215
|
+
this.pagedParts.set(ordinal, part);
|
|
216
|
+
const owner = this.levelOwners.get(ordinal);
|
|
217
|
+
const level = owner?.region.levels[owner.level];
|
|
218
|
+
if (level === undefined)
|
|
219
|
+
return;
|
|
220
|
+
level.parts.push(part);
|
|
221
|
+
level.resident = (this.levelCompact.get(level) ?? []).every((o) => this.pagedParts.has(o));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
this.made.push(part.mesh);
|
|
225
|
+
const owner = this.levelOwners.get(ordinal);
|
|
226
|
+
if (owner !== undefined) {
|
|
227
|
+
owner.region.levels[owner.level]?.parts.push(part);
|
|
228
|
+
owner.region.pending--;
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
this.prototypes.set(ordinal, part);
|
|
232
|
+
for (const group of this.waiting.get(ordinal) ?? []) {
|
|
233
|
+
this.jobs.push({
|
|
234
|
+
region: group.region,
|
|
235
|
+
prototype: part,
|
|
236
|
+
transforms: group.transforms,
|
|
237
|
+
group: group.group,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
this.waiting.delete(ordinal);
|
|
241
|
+
}
|
|
242
|
+
/** Build one queued batch inside the caller's budget; false when none was queued. */
|
|
243
|
+
buildNext() {
|
|
244
|
+
const job = this.jobs.shift();
|
|
245
|
+
if (job === undefined)
|
|
246
|
+
return false;
|
|
247
|
+
const data = fitCopies(job.transforms, this.fit() ?? { scale: 1, x: 0, y: 0, z: 0 });
|
|
248
|
+
try {
|
|
249
|
+
const batch = this.renderer.createInstanced(job.prototype.mesh, data.count, { cull: true });
|
|
250
|
+
this.renderer.uploadInstanced(batch, data);
|
|
251
|
+
job.region.batches.push({ ...job.prototype, reveal: 1, instances: { batch, data } });
|
|
252
|
+
job.region.batchGroups.push(job.group);
|
|
253
|
+
}
|
|
254
|
+
catch (error) {
|
|
255
|
+
console.warn(`DrftLoader: region ${job.region.id}'s props would not upload`, error);
|
|
256
|
+
}
|
|
257
|
+
job.region.pending--;
|
|
258
|
+
return true;
|
|
259
|
+
}
|
|
260
|
+
dispose() {
|
|
261
|
+
for (const region of this.regions.values()) {
|
|
262
|
+
for (const part of region.batches) {
|
|
263
|
+
if (part.instances !== null)
|
|
264
|
+
this.renderer.disposeInstanced(part.instances.batch);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
for (const mesh of this.made)
|
|
268
|
+
this.renderer.disposeMesh(mesh);
|
|
269
|
+
this.made.length = 0;
|
|
270
|
+
this.regions.clear();
|
|
271
|
+
}
|
|
272
|
+
}
|
package/dist/repeats.js
CHANGED
package/dist/weld.js
CHANGED
|
@@ -59,6 +59,12 @@ const ATTRIBUTES = {
|
|
|
59
59
|
* tip merged into the trunk it grows from would stop moving.
|
|
60
60
|
*/
|
|
61
61
|
channel: 4,
|
|
62
|
+
/*
|
|
63
|
+
* The texture-array layer. In the key for the same reason as the channel: two vertices at one
|
|
64
|
+
* position on two faces wearing different images are two vertices, and welding them would put one
|
|
65
|
+
* face on the other's picture.
|
|
66
|
+
*/
|
|
67
|
+
layers: 1,
|
|
62
68
|
};
|
|
63
69
|
const NAMES = Object.keys(ATTRIBUTES);
|
|
64
70
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@driftengine/assets",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.6.0",
|
|
4
4
|
"description": "Model readers and the streaming loader for the .drft container",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -27,11 +27,11 @@
|
|
|
27
27
|
],
|
|
28
28
|
"sideEffects": false,
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@driftengine/drft": "4.
|
|
31
|
-
"@driftengine/texture": "4.
|
|
30
|
+
"@driftengine/drft": "4.6.0",
|
|
31
|
+
"@driftengine/texture": "4.6.0"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@driftengine/core": "4.
|
|
34
|
+
"@driftengine/core": "4.6.0"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
37
37
|
"repository": {
|
package/src/drawKey.ts
CHANGED
|
@@ -25,6 +25,15 @@ export interface DrawSurfaceOverride {
|
|
|
25
25
|
* gallery and read as a black blotch. 0 says dielectric.
|
|
26
26
|
*/
|
|
27
27
|
readonly metallicScale?: number;
|
|
28
|
+
/**
|
|
29
|
+
* Glass, in place of what the file says: the share of light that passes, how milky it is, and
|
|
30
|
+
* the colour it takes (`GlassOptions` in the renderer). For a model whose panes arrive as an
|
|
31
|
+
* opaque dark material, because its format or its exporter had no word for glass. Transmission
|
|
32
|
+
* above zero draws the part blended.
|
|
33
|
+
*/
|
|
34
|
+
readonly transmission?: number;
|
|
35
|
+
readonly frost?: number;
|
|
36
|
+
readonly tint?: readonly [number, number, number];
|
|
28
37
|
}
|
|
29
38
|
|
|
30
39
|
/**
|
|
@@ -50,8 +59,17 @@ export interface DrawGrouping {
|
|
|
50
59
|
/** Whether both faces are drawn and lit. A merge of a two-sided part and a one-sided one would cull
|
|
51
60
|
* one of them or unhide the back of the other, so it is part of the key. */
|
|
52
61
|
readonly doubleSided: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Glass. Zero transmission is not glass, and then frost is 0 and the tint white whatever the
|
|
64
|
+
* file or the override said, so a stray value on an opaque surface never splits a draw.
|
|
65
|
+
*/
|
|
66
|
+
readonly transmission: number;
|
|
67
|
+
readonly frost: number;
|
|
68
|
+
readonly tint: readonly [number, number, number];
|
|
53
69
|
}
|
|
54
70
|
|
|
71
|
+
const CLEAR: readonly [number, number, number] = [1, 1, 1];
|
|
72
|
+
|
|
55
73
|
/**
|
|
56
74
|
* Resolve what a part will actually be drawn with.
|
|
57
75
|
*
|
|
@@ -60,7 +78,7 @@ export interface DrawGrouping {
|
|
|
60
78
|
* 0, which is the identity that lets a file saying nothing about a property draw the same as one
|
|
61
79
|
* that did not have the property to say.
|
|
62
80
|
*
|
|
63
|
-
* The override wins over the material for the
|
|
81
|
+
* The override wins over the material for the fields it may carry, because a consumer dressing
|
|
64
82
|
* a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
|
|
65
83
|
* a fact about the *image* a material wears, so nothing outside the file has anything to say
|
|
66
84
|
* about it.
|
|
@@ -69,6 +87,8 @@ export function resolveDrawGrouping(
|
|
|
69
87
|
material: DrftMaterial | undefined,
|
|
70
88
|
override?: DrawSurfaceOverride,
|
|
71
89
|
): DrawGrouping {
|
|
90
|
+
const transmission = Math.max(0, override?.transmission ?? material?.transmission ?? 0);
|
|
91
|
+
const glass = transmission > 0;
|
|
72
92
|
return {
|
|
73
93
|
albedo: material?.albedo ?? -1,
|
|
74
94
|
orm: material?.ormMap ?? -1,
|
|
@@ -80,8 +100,11 @@ export function resolveDrawGrouping(
|
|
|
80
100
|
metallicScale: override?.metallicScale ?? material?.metallicScale ?? 1,
|
|
81
101
|
occlusionStrength: material?.occlusionStrength ?? 0,
|
|
82
102
|
cutout: material?.cutout ?? 0,
|
|
83
|
-
blend: material?.blend === true,
|
|
103
|
+
blend: material?.blend === true || glass,
|
|
84
104
|
doubleSided: material?.doubleSided === true,
|
|
105
|
+
transmission,
|
|
106
|
+
frost: glass ? (override?.frost ?? material?.frost ?? 0) : 0,
|
|
107
|
+
tint: glass ? (override?.tint ?? material?.tint ?? CLEAR) : CLEAR,
|
|
85
108
|
};
|
|
86
109
|
}
|
|
87
110
|
|
|
@@ -101,5 +124,5 @@ export function drawKeyOf(
|
|
|
101
124
|
override?: DrawSurfaceOverride,
|
|
102
125
|
): string {
|
|
103
126
|
const g = resolveDrawGrouping(material, override);
|
|
104
|
-
return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}`;
|
|
127
|
+
return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}:${g.transmission}:${g.frost}:${g.tint[0]},${g.tint[1]},${g.tint[2]}`;
|
|
105
128
|
}
|