@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/src/drftLoader.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  import type { MeshData } from '@driftengine/drft';
4
4
  import type { MeshHandle, RendererApi } from '@driftengine/core';
5
5
  import type { GlassOptions, SurfaceTextureHandle } from '@driftengine/core';
6
- import { MeshBuilder, concatMeshes, createMeshInstances, placeMesh } from '@driftengine/core';
6
+ import { MeshBuilder, concatMeshes, placeMesh } from '@driftengine/core';
7
7
  import type { DrftMaterial } from '@driftengine/drft';
8
8
  import type { DrftSdfvEntry } from '@driftengine/drft';
9
9
  import { placeFields } from './fieldPlacement.ts';
@@ -19,9 +19,13 @@ import {
19
19
  CODEC_RAW,
20
20
  CODEC_WEBP,
21
21
  DrftError,
22
+ expandAssembly,
22
23
  } from '@driftengine/drft';
23
24
  import type { AnimationClip, DrftLight, DrftMorph, DrftNode, DrftSkin } from '@driftengine/drft';
25
+ import type { DrftLightVolume, EntsScene, NavPolyMesh } from '@driftengine/drft';
24
26
  import type { DrftLoadProgress, DrftPart } from './loadProgress.ts';
27
+ import { RegionStore, fitCopies } from './regionStore.ts';
28
+ import type { LoadedRegion } from './regionStore.ts';
25
29
  import {
26
30
  DEFAULT_REVEAL_SEC,
27
31
  DEFAULT_UPLOAD_MS_PER_FRAME,
@@ -178,6 +182,23 @@ export interface DrftLoaderOptions {
178
182
  * of the file — which is why it is an option a caller weighs rather than a default.
179
183
  */
180
184
  readonly texturePreview?: number;
185
+ /**
186
+ * Take each image as it decodes, by the name its `TEXS` chunk gives it, and upload none: for a
187
+ * consumer that builds its own textures — a world packing every picture into a layer of an
188
+ * array — where a surface texture of each as well would spend the memory twice. On the same
189
+ * frame clock as an upload, one a frame; `null` for an image that did not decode. Materials in
190
+ * the file that name an image then draw untextured, which is the consumer's to answer.
191
+ */
192
+ readonly onImage?: (name: string, image: ImageBitmap | null) => void;
193
+ /**
194
+ * Offered each mesh no region holds as it arrives, by its ordinal in the file; return true to
195
+ * take it, and it is neither uploaded as a part nor merged. For a consumer that draws some of a
196
+ * file's meshes its own way — a crowd of kinds it instances and moves every frame — which a part
197
+ * cannot be, since the merge bakes a part into a static group by the image it wears. Counted as
198
+ * arrived, so a loading bar still finishes. What it gives up: the loader's upload budget and
199
+ * fade, which a taken mesh leaves to the consumer.
200
+ */
201
+ readonly onMesh?: (mesh: MeshData, ordinal: number) => boolean;
181
202
  }
182
203
 
183
204
  /**
@@ -213,10 +234,20 @@ export class DrftLoader {
213
234
  /** Arrived, not yet uploaded. Drained by `update` at the budget. */
214
235
  private readonly queue: {
215
236
  mesh: MeshData;
237
+ ordinal: number;
216
238
  material: DrftMaterial | undefined;
217
239
  /** Sixteen floats a copy when the file draws this mesh many times, else null. */
218
240
  instances: Float32Array | null;
219
241
  }[] = [];
242
+ /**
243
+ * Paged region meshes asked for and not yet up, drained after `queue` on the same clock: each is
244
+ * an expansion from the copies the file carried, then an upload.
245
+ */
246
+ private pageQueue: {
247
+ ordinal: number;
248
+ material: DrftMaterial | undefined;
249
+ expand: () => MeshData;
250
+ }[] = [];
220
251
  /** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
221
252
  private readonly loadedInstances = new Map<number, Float32Array>();
222
253
  /**
@@ -305,6 +336,10 @@ export class DrftLoader {
305
336
  */
306
337
  private loadedNodes: readonly DrftNode[] = [];
307
338
  private loadedLights: readonly DrftLight[] = [];
339
+ private loadedColliders: readonly Float32Array[] = [];
340
+ private loadedNavigation: NavPolyMesh | null = null;
341
+ private loadedEntities: EntsScene | null = null;
342
+ private loadedLightVolume: DrftLightVolume | null = null;
308
343
  private loadedFields: readonly DrftSdfvEntry[] = [];
309
344
  /** The fields placed, built once the fit and the fields are both known. See `fields`. */
310
345
  private placedFields: readonly DrftFieldPlacement[] | null = null;
@@ -337,10 +372,13 @@ export class DrftLoader {
337
372
  private merged = false;
338
373
  private disposed = false;
339
374
  private arrived = 0;
375
+ /** A streamed world's regions, kept apart from the parts and never merged. See `regionStore.ts`. */
376
+ private readonly regionStore: RegionStore;
340
377
 
341
378
  constructor(renderer: RendererApi, options: DrftLoaderOptions = {}) {
342
379
  this.renderer = renderer;
343
380
  this.options = options;
381
+ this.regionStore = new RegionStore(renderer, () => this.fit);
344
382
  }
345
383
 
346
384
  get progress(): DrftLoadProgress {
@@ -384,6 +422,97 @@ export class DrftLoader {
384
422
  return this.loadedLights;
385
423
  }
386
424
 
425
+ /**
426
+ * The convex hulls the asset collides as, in the file's own space and unfitted, or empty. Handed
427
+ * on for the caller to build shapes from, as `hullShape` takes them.
428
+ */
429
+ get colliders(): readonly Float32Array[] {
430
+ return this.loadedColliders;
431
+ }
432
+
433
+ /** The asset's navigation mesh, unfitted, or null for a file carrying no `NAVM`. */
434
+ get navigation(): NavPolyMesh | null {
435
+ return this.loadedNavigation;
436
+ }
437
+
438
+ /**
439
+ * The asset's serialised entities, or null for a file carrying no `ENTS` — handed on as
440
+ * `deserializeWorld` takes them, because what an entity is belongs to the caller's schemas.
441
+ */
442
+ get entities(): EntsScene | null {
443
+ return this.loadedEntities;
444
+ }
445
+
446
+ /**
447
+ * A streamed world's regions by id, each filling as its meshes and props upload — `pending` is 0
448
+ * once one is whole. Their meshes are never among `parts`: a region's levels are alternatives,
449
+ * drawn one at a time through `HlodSet`, and its batches cull instance by instance.
450
+ */
451
+ get regions(): ReadonlyMap<number, LoadedRegion> {
452
+ return this.regionStore.regions;
453
+ }
454
+
455
+ /** One mesh to draw, from the stream or expanded from an assembly, queued for upload. */
456
+ private take(mesh: MeshData, ordinal: number): void {
457
+ if (this.disposed) return;
458
+ /* A region's own meshes are known by now: a region is written ahead of what it introduces. */
459
+ if (!this.regionStore.owns(ordinal) && this.options.onMesh?.(mesh, ordinal) === true) {
460
+ this.arrived++;
461
+ this.set({ phase: 'geometry', partsDone: this.arrived });
462
+ return;
463
+ }
464
+ const material = this.materials[ordinal];
465
+ /*
466
+ * Deltas onto the mesh they name, before it is queued for upload. The container writes a
467
+ * `MORP` immediately ahead of its `MESH` so this is in hand; a file that puts them the
468
+ * other way round still opens, and that mesh simply does not morph — a defined
469
+ * degradation rather than a wrong picture.
470
+ */
471
+ const morph = this.loadedMorphs.find((entry) => entry.mesh === ordinal);
472
+ const withMorph =
473
+ morph === undefined
474
+ ? mesh
475
+ : { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
476
+ this.queue.push({
477
+ mesh: withMorph,
478
+ ordinal,
479
+ material,
480
+ instances: this.loadedInstances.get(ordinal) ?? null,
481
+ });
482
+ this.arrived++;
483
+ this.set({ phase: 'geometry', partsDone: this.arrived });
484
+ }
485
+
486
+ /**
487
+ * Bring a region's paged level up, or free it. A level that arrived as assemblies (`paged`) is
488
+ * held as the copies the file carried; asked for, each mesh is expanded and uploaded on the
489
+ * loader's clocked queue in later `update`s, and `resident` turns true when the last is up. Freed,
490
+ * its meshes go and the copies stay. A level that is not paged ignores this.
491
+ *
492
+ * The caller decides from what it will draw: page in the levels `HlodSet` is choosing or about
493
+ * to, draw a coarser one until the fine one is resident, and page out what the eye has left.
494
+ */
495
+ pageRegion(id: number, level: number, resident: boolean): void {
496
+ const changes = this.regionStore.page(id, level, resident);
497
+ if (resident) {
498
+ for (const { ordinal, expand } of changes) {
499
+ this.pageQueue.push({ ordinal, material: this.materials[ordinal], expand });
500
+ }
501
+ return;
502
+ }
503
+ if (changes.length === 0) return;
504
+ const released = new Set(changes.map((c) => c.ordinal));
505
+ this.pageQueue = this.pageQueue.filter((item) => !released.has(item.ordinal));
506
+ }
507
+
508
+ /**
509
+ * A world's summed lights, unfitted, or null until `LVOL` lands and for a file carrying none.
510
+ * Handed straight to `createWorldLightField`, whose volume it is shaped as.
511
+ */
512
+ get lightVolume(): DrftLightVolume | null {
513
+ return this.loadedLightVolume;
514
+ }
515
+
387
516
  /**
388
517
  * The distance fields the file carries, each where it stands: at the loader's fit, and once a
389
518
  * copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
@@ -563,6 +692,15 @@ export class DrftLoader {
563
692
  onLights: (lights) => {
564
693
  this.loadedLights = lights;
565
694
  },
695
+ onColliders: (hulls) => {
696
+ this.loadedColliders = hulls;
697
+ },
698
+ onNavigation: (navigation) => {
699
+ this.loadedNavigation = navigation;
700
+ },
701
+ onEntities: (entities) => {
702
+ this.loadedEntities = entities;
703
+ },
566
704
  onFields: (fields) => {
567
705
  this.loadedFields = fields;
568
706
  this.placedFields = null;
@@ -570,6 +708,13 @@ export class DrftLoader {
570
708
  onInstances: (groups) => {
571
709
  for (const group of groups) this.loadedInstances.set(group.mesh, group.transforms);
572
710
  },
711
+ onLightVolume: (volume) => {
712
+ this.loadedLightVolume = volume;
713
+ },
714
+ onRegion: (region) => {
715
+ if (this.disposed) return;
716
+ this.regionStore.admit(region);
717
+ },
573
718
  onSkin: (skin) => {
574
719
  this.loadedSkins.push(skin);
575
720
  },
@@ -638,27 +783,22 @@ export class DrftLoader {
638
783
  this.set({ phase: 'materials' });
639
784
  },
640
785
 
641
- onMesh: (mesh, ordinal) => {
786
+ onKit: (pieces) => {
787
+ /* A piece is drawn only as copies: it is not a part, and the count said it was. */
788
+ this.set({ partsTotal: Math.max(0, this.state.partsTotal - pieces.length) });
789
+ },
790
+ onAssembly: (assembly, ordinal, piece) => {
642
791
  if (this.disposed) return;
643
- const material = this.materials[ordinal];
644
- /*
645
- * Deltas onto the mesh they name, before it is queued for upload. The container writes a
646
- * `MORP` immediately ahead of its `MESH` so this is in hand; a file that puts them the
647
- * other way round still opens, and that mesh simply does not morph — a defined
648
- * degradation rather than a wrong picture.
649
- */
650
- const morph = this.loadedMorphs.find((entry) => entry.mesh === ordinal);
651
- const withMorph =
652
- morph === undefined
653
- ? mesh
654
- : { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
655
- this.queue.push({
656
- mesh: withMorph,
657
- material,
658
- instances: this.loadedInstances.get(ordinal) ?? null,
659
- });
660
- this.arrived++;
661
- this.set({ phase: 'geometry', partsDone: this.arrived });
792
+ if (this.regionStore.isLevelMesh(ordinal)) {
793
+ this.regionStore.hold(ordinal, assembly, piece);
794
+ this.arrived++;
795
+ this.set({ phase: 'geometry', partsDone: this.arrived });
796
+ return;
797
+ }
798
+ this.take(expandAssembly(assembly, piece), ordinal);
799
+ },
800
+ onMesh: (mesh, ordinal) => {
801
+ this.take(mesh, ordinal);
662
802
  },
663
803
 
664
804
  onTexture: (texture, ordinal) => {
@@ -794,7 +934,27 @@ export class DrftLoader {
794
934
  ) {
795
935
  const next = this.queue.shift();
796
936
  if (next === undefined) break;
797
- if (this.uploadOne(next.mesh, next.material, next.instances)) changed = true;
937
+ if (this.uploadOne(next.mesh, next.material, next.instances, next.ordinal)) changed = true;
938
+ begun++;
939
+ }
940
+ /* Paged region meshes, after the stream's own: an expansion from the copies, then an upload. */
941
+ while (
942
+ this.pageQueue.length > 0 &&
943
+ mayBeginMore(begun, performance.now() - startedMs, msBudget)
944
+ ) {
945
+ const next = this.pageQueue.shift();
946
+ if (next === undefined) break;
947
+ if (this.uploadOne(next.expand(), next.material, null, next.ordinal)) changed = true;
948
+ begun++;
949
+ }
950
+ /* A region's prop batches, on the same clock: each is an allocation and an upload. Drained
951
+ before the merge below can begin, which is what lets `ready` not wait on them separately. */
952
+ while (
953
+ this.regionStore.busy &&
954
+ mayBeginMore(begun, performance.now() - startedMs, msBudget) &&
955
+ this.regionStore.buildNext()
956
+ ) {
957
+ changed = true;
798
958
  begun++;
799
959
  }
800
960
 
@@ -807,7 +967,9 @@ export class DrftLoader {
807
967
  ) {
808
968
  const ordinal = this.imageQueue.shift();
809
969
  if (ordinal !== undefined) {
810
- this.rebuildTextures(ordinal);
970
+ const take = this.options.onImage;
971
+ if (take === undefined) this.rebuildTextures(ordinal);
972
+ else take(this.chunks[ordinal]?.name ?? '', this.images[ordinal] ?? null);
811
973
  /*
812
974
  * Counted here, once, and only for the image itself.
813
975
  *
@@ -884,6 +1046,7 @@ export class DrftLoader {
884
1046
  dispose(): void {
885
1047
  if (this.disposed) return;
886
1048
  this.disposed = true;
1049
+ this.regionStore.dispose();
887
1050
  /*
888
1051
  * Everything this class ever made, each disposed once.
889
1052
  *
@@ -986,6 +1149,7 @@ export class DrftLoader {
986
1149
  mesh: MeshData,
987
1150
  material: DrftMaterial | undefined,
988
1151
  instances: Float32Array | null,
1152
+ ordinal: number,
989
1153
  ): boolean {
990
1154
  const fit = this.fit;
991
1155
  if (fit === null) return false;
@@ -1018,6 +1182,37 @@ export class DrftLoader {
1018
1182
  doubleSided,
1019
1183
  } = g;
1020
1184
 
1185
+ /*
1186
+ * A region's mesh — a level or a prop's prototype — is one part of its own, never grouped: the
1187
+ * merge would weld a region's levels together and one region to the next. See `regionStore.ts`.
1188
+ */
1189
+ if (this.regionStore.owns(ordinal)) {
1190
+ try {
1191
+ this.regionStore.arrived(ordinal, {
1192
+ mesh: this.renderer.createMesh(placed),
1193
+ albedo,
1194
+ orm,
1195
+ normal,
1196
+ emissive,
1197
+ opacity,
1198
+ reflectivity,
1199
+ roughnessScale,
1200
+ metallicScale,
1201
+ occlusionStrength,
1202
+ cutout,
1203
+ blend,
1204
+ doubleSided,
1205
+ glass: glassOf(g),
1206
+ reveal: 1,
1207
+ instances: null,
1208
+ });
1209
+ return true;
1210
+ } catch (error) {
1211
+ console.warn(`DrftLoader: region mesh ${ordinal} would not upload and was skipped`, error);
1212
+ return false;
1213
+ }
1214
+ }
1215
+
1021
1216
  /*
1022
1217
  * Added to its merged group here, while there is a budget governing how much of this happens
1023
1218
  * in one frame.
@@ -1123,10 +1318,9 @@ export class DrftLoader {
1123
1318
  /**
1124
1319
  * A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
1125
1320
  *
1126
- * **The fit is conjugated into each placement.** The prototype is uploaded with the fit already
1127
- * applied, as every part is, so a copy's matrix M becomes F·M·F⁻¹: the same turn, and a translation
1128
- * of t − R·t + s·u for the fit's scale s and offset t and the copy's own translation u. A fitted
1129
- * model's copies then land where its geometry does, and are not scaled twice.
1321
+ * **The fit is conjugated into each placement** by `fitCopies`, which a region's props share: the
1322
+ * prototype is uploaded with the fit already applied, so the copies land where its geometry does
1323
+ * and are not scaled twice.
1130
1324
  */
1131
1325
  private uploadInstanced(
1132
1326
  prototype: MeshData,
@@ -1136,36 +1330,8 @@ export class DrftLoader {
1136
1330
  ): boolean {
1137
1331
  try {
1138
1332
  const uploaded = this.renderer.createMesh(prototype);
1139
- const count = instances.length / 16;
1140
- const batch = this.renderer.createInstanced(uploaded, count);
1141
- const data = createMeshInstances(count);
1142
- const s = fit.scale;
1143
- for (let k = 0; k < count; k++) {
1144
- const at = k * 16;
1145
- for (let i = 0; i < 12; i++) data.models[at + i] = instances[at + i] as number;
1146
- const ux = instances[at + 12] as number;
1147
- const uy = instances[at + 13] as number;
1148
- const uz = instances[at + 14] as number;
1149
- /* R·t, with R the copy's own 3×3 read column-major. */
1150
- const rtx =
1151
- (instances[at] as number) * fit.x +
1152
- (instances[at + 4] as number) * fit.y +
1153
- (instances[at + 8] as number) * fit.z;
1154
- const rty =
1155
- (instances[at + 1] as number) * fit.x +
1156
- (instances[at + 5] as number) * fit.y +
1157
- (instances[at + 9] as number) * fit.z;
1158
- const rtz =
1159
- (instances[at + 2] as number) * fit.x +
1160
- (instances[at + 6] as number) * fit.y +
1161
- (instances[at + 10] as number) * fit.z;
1162
- data.models[at + 12] = fit.x - rtx + s * ux;
1163
- data.models[at + 13] = fit.y - rty + s * uy;
1164
- data.models[at + 14] = fit.z - rtz + s * uz;
1165
- data.models[at + 15] = 1;
1166
- }
1167
- data.tints.fill(1);
1168
- data.count = count;
1333
+ const data = fitCopies(instances, fit);
1334
+ const batch = this.renderer.createInstanced(uploaded, data.count);
1169
1335
  this.renderer.uploadInstanced(batch, data);
1170
1336
  const part: DrftPart = { mesh: uploaded, ...surface, reveal: 0, instances: { batch, data } };
1171
1337
  this.revealed.push(part);
package/src/index.ts CHANGED
@@ -33,6 +33,7 @@ export type { DrftFit, DrftLoaderOptions } from './drftLoader.ts';
33
33
  export { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
34
34
  export type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
35
35
  export type { DrftLoadPhase, DrftLoadProgress, DrftPart } from './loadProgress.ts';
36
+ export type { LoadedRegion, LoadedRegionLevel } from './regionStore.ts';
36
37
  export type { DrftFieldPlacement } from './fieldPlacement.ts';
37
38
  /* Addressing a texture by name, so a consumer never holds an ordinal. */
38
39
  export { TextureSet, textureColorSpaces } from './drftTextures.ts';