@driftengine/assets 4.5.0 → 4.6.1
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/drftLoader.d.ts +68 -4
- package/dist/drftLoader.js +194 -56
- package/dist/index.d.ts +1 -0
- 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/drftLoader.ts +223 -57
- package/src/index.ts +1 -0
- package/src/regionStore.ts +333 -0
- package/src/repeats.ts +1 -0
- package/src/weld.ts +6 -0
|
@@ -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.1",
|
|
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.1",
|
|
31
|
+
"@driftengine/texture": "4.6.1"
|
|
32
32
|
},
|
|
33
33
|
"peerDependencies": {
|
|
34
|
-
"@driftengine/core": "4.
|
|
34
|
+
"@driftengine/core": "4.6.1"
|
|
35
35
|
},
|
|
36
36
|
"author": "Drift Technologies",
|
|
37
37
|
"repository": {
|