@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
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Model readers for seven formats, and the streaming loader that uploads them.
|
|
4
4
|
|
|
5
|
-
**Cost:
|
|
5
|
+
**Cost: 22.5 KB gzipped on top of core.** Measured by `scripts/size-gate.test.mjs`, which fails if it drifts more than
|
|
6
6
|
3% — the number is a fact about the build rather than a claim in a document.
|
|
7
7
|
|
|
8
8
|
Part of [DriftEngine](../../README.md). See [`docs/ARCHITECTURE.md`](../../docs/ARCHITECTURE.md)
|
package/dist/drftLoader.d.ts
CHANGED
|
@@ -7,7 +7,9 @@ import type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
|
7
7
|
import { TextureSet } from './drftTextures.ts';
|
|
8
8
|
import type { DrawSurfaceOverride } from './drawKey.ts';
|
|
9
9
|
import type { AnimationClip, DrftLight, DrftMorph, DrftNode, DrftSkin } from '@driftengine/drft';
|
|
10
|
+
import type { DrftLightVolume, EntsScene, NavPolyMesh } from '@driftengine/drft';
|
|
10
11
|
import type { DrftLoadProgress, DrftPart } from './loadProgress.ts';
|
|
12
|
+
import type { LoadedRegion } from './regionStore.ts';
|
|
11
13
|
/**
|
|
12
14
|
* How a loaded model is placed: fitted to a size, or left exactly where its file puts it.
|
|
13
15
|
*
|
|
@@ -152,6 +154,23 @@ export interface DrftLoaderOptions {
|
|
|
152
154
|
* of the file — which is why it is an option a caller weighs rather than a default.
|
|
153
155
|
*/
|
|
154
156
|
readonly texturePreview?: number;
|
|
157
|
+
/**
|
|
158
|
+
* Take each image as it decodes, by the name its `TEXS` chunk gives it, and upload none: for a
|
|
159
|
+
* consumer that builds its own textures — a world packing every picture into a layer of an
|
|
160
|
+
* array — where a surface texture of each as well would spend the memory twice. On the same
|
|
161
|
+
* frame clock as an upload, one a frame; `null` for an image that did not decode. Materials in
|
|
162
|
+
* the file that name an image then draw untextured, which is the consumer's to answer.
|
|
163
|
+
*/
|
|
164
|
+
readonly onImage?: (name: string, image: ImageBitmap | null) => void;
|
|
165
|
+
/**
|
|
166
|
+
* Offered each mesh no region holds as it arrives, by its ordinal in the file; return true to
|
|
167
|
+
* take it, and it is neither uploaded as a part nor merged. For a consumer that draws some of a
|
|
168
|
+
* file's meshes its own way — a crowd of kinds it instances and moves every frame — which a part
|
|
169
|
+
* cannot be, since the merge bakes a part into a static group by the image it wears. Counted as
|
|
170
|
+
* arrived, so a loading bar still finishes. What it gives up: the loader's upload budget and
|
|
171
|
+
* fade, which a taken mesh leaves to the consumer.
|
|
172
|
+
*/
|
|
173
|
+
readonly onMesh?: (mesh: MeshData, ordinal: number) => boolean;
|
|
155
174
|
}
|
|
156
175
|
/**
|
|
157
176
|
* A `.drft` loaded progressively: the model builds up on screen instead of appearing.
|
|
@@ -185,6 +204,11 @@ export declare class DrftLoader {
|
|
|
185
204
|
private readonly revealed;
|
|
186
205
|
/** Arrived, not yet uploaded. Drained by `update` at the budget. */
|
|
187
206
|
private readonly queue;
|
|
207
|
+
/**
|
|
208
|
+
* Paged region meshes asked for and not yet up, drained after `queue` on the same clock: each is
|
|
209
|
+
* an expansion from the copies the file carried, then an upload.
|
|
210
|
+
*/
|
|
211
|
+
private pageQueue;
|
|
188
212
|
/** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
|
|
189
213
|
private readonly loadedInstances;
|
|
190
214
|
/**
|
|
@@ -253,6 +277,10 @@ export declare class DrftLoader {
|
|
|
253
277
|
*/
|
|
254
278
|
private loadedNodes;
|
|
255
279
|
private loadedLights;
|
|
280
|
+
private loadedColliders;
|
|
281
|
+
private loadedNavigation;
|
|
282
|
+
private loadedEntities;
|
|
283
|
+
private loadedLightVolume;
|
|
256
284
|
private loadedFields;
|
|
257
285
|
/** The fields placed, built once the fit and the fields are both known. See `fields`. */
|
|
258
286
|
private placedFields;
|
|
@@ -276,6 +304,8 @@ export declare class DrftLoader {
|
|
|
276
304
|
private merged;
|
|
277
305
|
private disposed;
|
|
278
306
|
private arrived;
|
|
307
|
+
/** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
|
|
308
|
+
private readonly regionStore;
|
|
279
309
|
constructor(renderer: RendererApi, options?: DrftLoaderOptions);
|
|
280
310
|
get progress(): DrftLoadProgress;
|
|
281
311
|
/**
|
|
@@ -306,6 +336,41 @@ export declare class DrftLoader {
|
|
|
306
336
|
* geometry. What a candela is in this scene's light units is the caller's decision.
|
|
307
337
|
*/
|
|
308
338
|
get lights(): readonly DrftLight[];
|
|
339
|
+
/**
|
|
340
|
+
* The convex hulls the asset collides as, in the file's own space and unfitted, or empty. Handed
|
|
341
|
+
* on for the caller to build shapes from, as `hullShape` takes them.
|
|
342
|
+
*/
|
|
343
|
+
get colliders(): readonly Float32Array[];
|
|
344
|
+
/** The asset's navigation mesh, unfitted, or null for a file carrying no `NAVM`. */
|
|
345
|
+
get navigation(): NavPolyMesh | null;
|
|
346
|
+
/**
|
|
347
|
+
* The asset's serialised entities, or null for a file carrying no `ENTS` — handed on as
|
|
348
|
+
* `deserializeWorld` takes them, because what an entity is belongs to the caller's schemas.
|
|
349
|
+
*/
|
|
350
|
+
get entities(): EntsScene | null;
|
|
351
|
+
/**
|
|
352
|
+
* A streamed world's regions by id, each filling as its meshes and props upload — `pending` is 0
|
|
353
|
+
* once one is whole. Their meshes are never among `parts`: a region's levels are alternatives,
|
|
354
|
+
* drawn one at a time through `HlodSet`, and its batches cull instance by instance.
|
|
355
|
+
*/
|
|
356
|
+
get regions(): ReadonlyMap<number, LoadedRegion>;
|
|
357
|
+
/** One mesh to draw, from the stream or expanded from an assembly, queued for upload. */
|
|
358
|
+
private take;
|
|
359
|
+
/**
|
|
360
|
+
* Bring a region's paged level up, or free it. A level that arrived as assemblies (`paged`) is
|
|
361
|
+
* held as the copies the file carried; asked for, each mesh is expanded and uploaded on the
|
|
362
|
+
* loader's clocked queue in later `update`s, and `resident` turns true when the last is up. Freed,
|
|
363
|
+
* its meshes go and the copies stay. A level that is not paged ignores this.
|
|
364
|
+
*
|
|
365
|
+
* The caller decides from what it will draw: page in the levels `HlodSet` is choosing or about
|
|
366
|
+
* to, draw a coarser one until the fine one is resident, and page out what the eye has left.
|
|
367
|
+
*/
|
|
368
|
+
pageRegion(id: number, level: number, resident: boolean): void;
|
|
369
|
+
/**
|
|
370
|
+
* A world's summed lights, unfitted, or null until `LVOL` lands and for a file carrying none.
|
|
371
|
+
* Handed straight to `createWorldLightField`, whose volume it is shaped as.
|
|
372
|
+
*/
|
|
373
|
+
get lightVolume(): DrftLightVolume | null;
|
|
309
374
|
/**
|
|
310
375
|
* The distance fields the file carries, each where it stands: at the loader's fit, and once a
|
|
311
376
|
* copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
|
|
@@ -422,10 +487,9 @@ export declare class DrftLoader {
|
|
|
422
487
|
/**
|
|
423
488
|
* A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
|
|
424
489
|
*
|
|
425
|
-
* **The fit is conjugated into each placement
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
* model's copies then land where its geometry does, and are not scaled twice.
|
|
490
|
+
* **The fit is conjugated into each placement** by `fitCopies`, which a region's props share: the
|
|
491
|
+
* prototype is uploaded with the fit already applied, so the copies land where its geometry does
|
|
492
|
+
* and are not scaled twice.
|
|
429
493
|
*/
|
|
430
494
|
private uploadInstanced;
|
|
431
495
|
private buildGroup;
|
package/dist/drftLoader.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** Loading a `.drft` into drawable parts progressively, without stalling the frame. */
|
|
2
|
-
import { MeshBuilder, concatMeshes,
|
|
2
|
+
import { MeshBuilder, concatMeshes, placeMesh } from '@driftengine/core';
|
|
3
3
|
import { placeFields } from './fieldPlacement.js';
|
|
4
4
|
import { TextureSet, textureColorSpaces } from './drftTextures.js';
|
|
5
5
|
import { drawKeyOf, resolveDrawGrouping } from './drawKey.js';
|
|
6
|
-
import { streamDrft, CODEC_JPEG, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError, } from '@driftengine/drft';
|
|
6
|
+
import { streamDrft, CODEC_JPEG, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError, expandAssembly, } from '@driftengine/drft';
|
|
7
|
+
import { RegionStore, fitCopies } from './regionStore.js';
|
|
7
8
|
import { DEFAULT_REVEAL_SEC, DEFAULT_UPLOAD_MS_PER_FRAME, DEFAULT_UPLOADS_PER_FRAME, isDocumentResponse, mayBeginMore, } from './uploadBudget.js';
|
|
8
9
|
/**
|
|
9
10
|
* A `.drft` loaded progressively: the model builds up on screen instead of appearing.
|
|
@@ -37,6 +38,11 @@ export class DrftLoader {
|
|
|
37
38
|
revealed = [];
|
|
38
39
|
/** Arrived, not yet uploaded. Drained by `update` at the budget. */
|
|
39
40
|
queue = [];
|
|
41
|
+
/**
|
|
42
|
+
* Paged region meshes asked for and not yet up, drained after `queue` on the same clock: each is
|
|
43
|
+
* an expansion from the copies the file carried, then an upload.
|
|
44
|
+
*/
|
|
45
|
+
pageQueue = [];
|
|
40
46
|
/** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
|
|
41
47
|
loadedInstances = new Map();
|
|
42
48
|
/**
|
|
@@ -105,6 +111,10 @@ export class DrftLoader {
|
|
|
105
111
|
*/
|
|
106
112
|
loadedNodes = [];
|
|
107
113
|
loadedLights = [];
|
|
114
|
+
loadedColliders = [];
|
|
115
|
+
loadedNavigation = null;
|
|
116
|
+
loadedEntities = null;
|
|
117
|
+
loadedLightVolume = null;
|
|
108
118
|
loadedFields = [];
|
|
109
119
|
/** The fields placed, built once the fit and the fields are both known. See `fields`. */
|
|
110
120
|
placedFields = null;
|
|
@@ -137,9 +147,12 @@ export class DrftLoader {
|
|
|
137
147
|
merged = false;
|
|
138
148
|
disposed = false;
|
|
139
149
|
arrived = 0;
|
|
150
|
+
/** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
|
|
151
|
+
regionStore;
|
|
140
152
|
constructor(renderer, options = {}) {
|
|
141
153
|
this.renderer = renderer;
|
|
142
154
|
this.options = options;
|
|
155
|
+
this.regionStore = new RegionStore(renderer, () => this.fit);
|
|
143
156
|
}
|
|
144
157
|
get progress() {
|
|
145
158
|
return this.state;
|
|
@@ -178,6 +191,91 @@ export class DrftLoader {
|
|
|
178
191
|
get lights() {
|
|
179
192
|
return this.loadedLights;
|
|
180
193
|
}
|
|
194
|
+
/**
|
|
195
|
+
* The convex hulls the asset collides as, in the file's own space and unfitted, or empty. Handed
|
|
196
|
+
* on for the caller to build shapes from, as `hullShape` takes them.
|
|
197
|
+
*/
|
|
198
|
+
get colliders() {
|
|
199
|
+
return this.loadedColliders;
|
|
200
|
+
}
|
|
201
|
+
/** The asset's navigation mesh, unfitted, or null for a file carrying no `NAVM`. */
|
|
202
|
+
get navigation() {
|
|
203
|
+
return this.loadedNavigation;
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* The asset's serialised entities, or null for a file carrying no `ENTS` — handed on as
|
|
207
|
+
* `deserializeWorld` takes them, because what an entity is belongs to the caller's schemas.
|
|
208
|
+
*/
|
|
209
|
+
get entities() {
|
|
210
|
+
return this.loadedEntities;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* A streamed world's regions by id, each filling as its meshes and props upload — `pending` is 0
|
|
214
|
+
* once one is whole. Their meshes are never among `parts`: a region's levels are alternatives,
|
|
215
|
+
* drawn one at a time through `HlodSet`, and its batches cull instance by instance.
|
|
216
|
+
*/
|
|
217
|
+
get regions() {
|
|
218
|
+
return this.regionStore.regions;
|
|
219
|
+
}
|
|
220
|
+
/** One mesh to draw, from the stream or expanded from an assembly, queued for upload. */
|
|
221
|
+
take(mesh, ordinal) {
|
|
222
|
+
if (this.disposed)
|
|
223
|
+
return;
|
|
224
|
+
/* A region's own meshes are known by now: a region is written ahead of what it introduces. */
|
|
225
|
+
if (!this.regionStore.owns(ordinal) && this.options.onMesh?.(mesh, ordinal) === true) {
|
|
226
|
+
this.arrived++;
|
|
227
|
+
this.set({ phase: 'geometry', partsDone: this.arrived });
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
const material = this.materials[ordinal];
|
|
231
|
+
/*
|
|
232
|
+
* Deltas onto the mesh they name, before it is queued for upload. The container writes a
|
|
233
|
+
* `MORP` immediately ahead of its `MESH` so this is in hand; a file that puts them the
|
|
234
|
+
* other way round still opens, and that mesh simply does not morph — a defined
|
|
235
|
+
* degradation rather than a wrong picture.
|
|
236
|
+
*/
|
|
237
|
+
const morph = this.loadedMorphs.find((entry) => entry.mesh === ordinal);
|
|
238
|
+
const withMorph = morph === undefined
|
|
239
|
+
? mesh
|
|
240
|
+
: { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
|
|
241
|
+
this.queue.push({
|
|
242
|
+
mesh: withMorph,
|
|
243
|
+
ordinal,
|
|
244
|
+
material,
|
|
245
|
+
instances: this.loadedInstances.get(ordinal) ?? null,
|
|
246
|
+
});
|
|
247
|
+
this.arrived++;
|
|
248
|
+
this.set({ phase: 'geometry', partsDone: this.arrived });
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Bring a region's paged level up, or free it. A level that arrived as assemblies (`paged`) is
|
|
252
|
+
* held as the copies the file carried; asked for, each mesh is expanded and uploaded on the
|
|
253
|
+
* loader's clocked queue in later `update`s, and `resident` turns true when the last is up. Freed,
|
|
254
|
+
* its meshes go and the copies stay. A level that is not paged ignores this.
|
|
255
|
+
*
|
|
256
|
+
* The caller decides from what it will draw: page in the levels `HlodSet` is choosing or about
|
|
257
|
+
* to, draw a coarser one until the fine one is resident, and page out what the eye has left.
|
|
258
|
+
*/
|
|
259
|
+
pageRegion(id, level, resident) {
|
|
260
|
+
const changes = this.regionStore.page(id, level, resident);
|
|
261
|
+
if (resident) {
|
|
262
|
+
for (const { ordinal, expand } of changes) {
|
|
263
|
+
this.pageQueue.push({ ordinal, material: this.materials[ordinal], expand });
|
|
264
|
+
}
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (changes.length === 0)
|
|
268
|
+
return;
|
|
269
|
+
const released = new Set(changes.map((c) => c.ordinal));
|
|
270
|
+
this.pageQueue = this.pageQueue.filter((item) => !released.has(item.ordinal));
|
|
271
|
+
}
|
|
272
|
+
/**
|
|
273
|
+
* A world's summed lights, unfitted, or null until `LVOL` lands and for a file carrying none.
|
|
274
|
+
* Handed straight to `createWorldLightField`, whose volume it is shaped as.
|
|
275
|
+
*/
|
|
276
|
+
get lightVolume() {
|
|
277
|
+
return this.loadedLightVolume;
|
|
278
|
+
}
|
|
181
279
|
/**
|
|
182
280
|
* The distance fields the file carries, each where it stands: at the loader's fit, and once a
|
|
183
281
|
* copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
|
|
@@ -349,6 +447,15 @@ export class DrftLoader {
|
|
|
349
447
|
onLights: (lights) => {
|
|
350
448
|
this.loadedLights = lights;
|
|
351
449
|
},
|
|
450
|
+
onColliders: (hulls) => {
|
|
451
|
+
this.loadedColliders = hulls;
|
|
452
|
+
},
|
|
453
|
+
onNavigation: (navigation) => {
|
|
454
|
+
this.loadedNavigation = navigation;
|
|
455
|
+
},
|
|
456
|
+
onEntities: (entities) => {
|
|
457
|
+
this.loadedEntities = entities;
|
|
458
|
+
},
|
|
352
459
|
onFields: (fields) => {
|
|
353
460
|
this.loadedFields = fields;
|
|
354
461
|
this.placedFields = null;
|
|
@@ -357,6 +464,14 @@ export class DrftLoader {
|
|
|
357
464
|
for (const group of groups)
|
|
358
465
|
this.loadedInstances.set(group.mesh, group.transforms);
|
|
359
466
|
},
|
|
467
|
+
onLightVolume: (volume) => {
|
|
468
|
+
this.loadedLightVolume = volume;
|
|
469
|
+
},
|
|
470
|
+
onRegion: (region) => {
|
|
471
|
+
if (this.disposed)
|
|
472
|
+
return;
|
|
473
|
+
this.regionStore.admit(region);
|
|
474
|
+
},
|
|
360
475
|
onSkin: (skin) => {
|
|
361
476
|
this.loadedSkins.push(skin);
|
|
362
477
|
},
|
|
@@ -422,27 +537,23 @@ export class DrftLoader {
|
|
|
422
537
|
this.materials = list;
|
|
423
538
|
this.set({ phase: 'materials' });
|
|
424
539
|
},
|
|
425
|
-
|
|
540
|
+
onKit: (pieces) => {
|
|
541
|
+
/* A piece is drawn only as copies: it is not a part, and the count said it was. */
|
|
542
|
+
this.set({ partsTotal: Math.max(0, this.state.partsTotal - pieces.length) });
|
|
543
|
+
},
|
|
544
|
+
onAssembly: (assembly, ordinal, piece) => {
|
|
426
545
|
if (this.disposed)
|
|
427
546
|
return;
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
: { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
|
|
439
|
-
this.queue.push({
|
|
440
|
-
mesh: withMorph,
|
|
441
|
-
material,
|
|
442
|
-
instances: this.loadedInstances.get(ordinal) ?? null,
|
|
443
|
-
});
|
|
444
|
-
this.arrived++;
|
|
445
|
-
this.set({ phase: 'geometry', partsDone: this.arrived });
|
|
547
|
+
if (this.regionStore.isLevelMesh(ordinal)) {
|
|
548
|
+
this.regionStore.hold(ordinal, assembly, piece);
|
|
549
|
+
this.arrived++;
|
|
550
|
+
this.set({ phase: 'geometry', partsDone: this.arrived });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
this.take(expandAssembly(assembly, piece), ordinal);
|
|
554
|
+
},
|
|
555
|
+
onMesh: (mesh, ordinal) => {
|
|
556
|
+
this.take(mesh, ordinal);
|
|
446
557
|
},
|
|
447
558
|
onTexture: (texture, ordinal) => {
|
|
448
559
|
if (this.disposed)
|
|
@@ -572,10 +683,28 @@ export class DrftLoader {
|
|
|
572
683
|
const next = this.queue.shift();
|
|
573
684
|
if (next === undefined)
|
|
574
685
|
break;
|
|
575
|
-
if (this.uploadOne(next.mesh, next.material, next.instances))
|
|
686
|
+
if (this.uploadOne(next.mesh, next.material, next.instances, next.ordinal))
|
|
576
687
|
changed = true;
|
|
577
688
|
begun++;
|
|
578
689
|
}
|
|
690
|
+
/* Paged region meshes, after the stream's own: an expansion from the copies, then an upload. */
|
|
691
|
+
while (this.pageQueue.length > 0 &&
|
|
692
|
+
mayBeginMore(begun, performance.now() - startedMs, msBudget)) {
|
|
693
|
+
const next = this.pageQueue.shift();
|
|
694
|
+
if (next === undefined)
|
|
695
|
+
break;
|
|
696
|
+
if (this.uploadOne(next.expand(), next.material, null, next.ordinal))
|
|
697
|
+
changed = true;
|
|
698
|
+
begun++;
|
|
699
|
+
}
|
|
700
|
+
/* A region's prop batches, on the same clock: each is an allocation and an upload. Drained
|
|
701
|
+
before the merge below can begin, which is what lets `ready` not wait on them separately. */
|
|
702
|
+
while (this.regionStore.busy &&
|
|
703
|
+
mayBeginMore(begun, performance.now() - startedMs, msBudget) &&
|
|
704
|
+
this.regionStore.buildNext()) {
|
|
705
|
+
changed = true;
|
|
706
|
+
begun++;
|
|
707
|
+
}
|
|
579
708
|
/* One image a frame, and only into a frame that has room for it. A 2048 square decodes
|
|
580
709
|
off-thread and then uploads with its mips, which is the single most expensive thing in a
|
|
581
710
|
load and lands right after the geometry — on top of the parts, until this was clocked. */
|
|
@@ -583,7 +712,11 @@ export class DrftLoader {
|
|
|
583
712
|
mayBeginMore(begun, performance.now() - startedMs, msBudget)) {
|
|
584
713
|
const ordinal = this.imageQueue.shift();
|
|
585
714
|
if (ordinal !== undefined) {
|
|
586
|
-
this.
|
|
715
|
+
const take = this.options.onImage;
|
|
716
|
+
if (take === undefined)
|
|
717
|
+
this.rebuildTextures(ordinal);
|
|
718
|
+
else
|
|
719
|
+
take(this.chunks[ordinal]?.name ?? '', this.images[ordinal] ?? null);
|
|
587
720
|
/*
|
|
588
721
|
* Counted here, once, and only for the image itself.
|
|
589
722
|
*
|
|
@@ -655,6 +788,7 @@ export class DrftLoader {
|
|
|
655
788
|
if (this.disposed)
|
|
656
789
|
return;
|
|
657
790
|
this.disposed = true;
|
|
791
|
+
this.regionStore.dispose();
|
|
658
792
|
/*
|
|
659
793
|
* Everything this class ever made, each disposed once.
|
|
660
794
|
*
|
|
@@ -760,7 +894,7 @@ export class DrftLoader {
|
|
|
760
894
|
return true;
|
|
761
895
|
}
|
|
762
896
|
/** One part, transformed, dressed and uploaded. Returns whether it landed. */
|
|
763
|
-
uploadOne(mesh, material, instances) {
|
|
897
|
+
uploadOne(mesh, material, instances, ordinal) {
|
|
764
898
|
const fit = this.fit;
|
|
765
899
|
if (fit === null)
|
|
766
900
|
return false;
|
|
@@ -779,6 +913,37 @@ export class DrftLoader {
|
|
|
779
913
|
*/
|
|
780
914
|
const g = resolveDrawGrouping(material, override);
|
|
781
915
|
const { albedo, orm, normal, emissive, opacity, reflectivity, roughnessScale, metallicScale, occlusionStrength, cutout, blend, doubleSided, } = g;
|
|
916
|
+
/*
|
|
917
|
+
* A region's mesh — a level or a prop's prototype — is one part of its own, never grouped: the
|
|
918
|
+
* merge would weld a region's levels together and one region to the next. See `regionStore.ts`.
|
|
919
|
+
*/
|
|
920
|
+
if (this.regionStore.owns(ordinal)) {
|
|
921
|
+
try {
|
|
922
|
+
this.regionStore.arrived(ordinal, {
|
|
923
|
+
mesh: this.renderer.createMesh(placed),
|
|
924
|
+
albedo,
|
|
925
|
+
orm,
|
|
926
|
+
normal,
|
|
927
|
+
emissive,
|
|
928
|
+
opacity,
|
|
929
|
+
reflectivity,
|
|
930
|
+
roughnessScale,
|
|
931
|
+
metallicScale,
|
|
932
|
+
occlusionStrength,
|
|
933
|
+
cutout,
|
|
934
|
+
blend,
|
|
935
|
+
doubleSided,
|
|
936
|
+
glass: glassOf(g),
|
|
937
|
+
reveal: 1,
|
|
938
|
+
instances: null,
|
|
939
|
+
});
|
|
940
|
+
return true;
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
console.warn(`DrftLoader: region mesh ${ordinal} would not upload and was skipped`, error);
|
|
944
|
+
return false;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
782
947
|
/*
|
|
783
948
|
* Added to its merged group here, while there is a budget governing how much of this happens
|
|
784
949
|
* in one frame.
|
|
@@ -883,42 +1048,15 @@ export class DrftLoader {
|
|
|
883
1048
|
/**
|
|
884
1049
|
* A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
|
|
885
1050
|
*
|
|
886
|
-
* **The fit is conjugated into each placement
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
* model's copies then land where its geometry does, and are not scaled twice.
|
|
1051
|
+
* **The fit is conjugated into each placement** by `fitCopies`, which a region's props share: the
|
|
1052
|
+
* prototype is uploaded with the fit already applied, so the copies land where its geometry does
|
|
1053
|
+
* and are not scaled twice.
|
|
890
1054
|
*/
|
|
891
1055
|
uploadInstanced(prototype, instances, fit, surface) {
|
|
892
1056
|
try {
|
|
893
1057
|
const uploaded = this.renderer.createMesh(prototype);
|
|
894
|
-
const
|
|
895
|
-
const batch = this.renderer.createInstanced(uploaded, count);
|
|
896
|
-
const data = createMeshInstances(count);
|
|
897
|
-
const s = fit.scale;
|
|
898
|
-
for (let k = 0; k < count; k++) {
|
|
899
|
-
const at = k * 16;
|
|
900
|
-
for (let i = 0; i < 12; i++)
|
|
901
|
-
data.models[at + i] = instances[at + i];
|
|
902
|
-
const ux = instances[at + 12];
|
|
903
|
-
const uy = instances[at + 13];
|
|
904
|
-
const uz = instances[at + 14];
|
|
905
|
-
/* R·t, with R the copy's own 3×3 read column-major. */
|
|
906
|
-
const rtx = instances[at] * fit.x +
|
|
907
|
-
instances[at + 4] * fit.y +
|
|
908
|
-
instances[at + 8] * fit.z;
|
|
909
|
-
const rty = instances[at + 1] * fit.x +
|
|
910
|
-
instances[at + 5] * fit.y +
|
|
911
|
-
instances[at + 9] * fit.z;
|
|
912
|
-
const rtz = instances[at + 2] * fit.x +
|
|
913
|
-
instances[at + 6] * fit.y +
|
|
914
|
-
instances[at + 10] * fit.z;
|
|
915
|
-
data.models[at + 12] = fit.x - rtx + s * ux;
|
|
916
|
-
data.models[at + 13] = fit.y - rty + s * uy;
|
|
917
|
-
data.models[at + 14] = fit.z - rtz + s * uz;
|
|
918
|
-
data.models[at + 15] = 1;
|
|
919
|
-
}
|
|
920
|
-
data.tints.fill(1);
|
|
921
|
-
data.count = count;
|
|
1058
|
+
const data = fitCopies(instances, fit);
|
|
1059
|
+
const batch = this.renderer.createInstanced(uploaded, data.count);
|
|
922
1060
|
this.renderer.uploadInstanced(batch, data);
|
|
923
1061
|
const part = { mesh: uploaded, ...surface, reveal: 0, instances: { batch, data } };
|
|
924
1062
|
this.revealed.push(part);
|
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';
|
|
@@ -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
|
+
}
|