@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/src/drftLoader.ts
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type { MeshData } from '@driftengine/drft';
|
|
4
4
|
import type { MeshHandle, RendererApi } from '@driftengine/core';
|
|
5
|
-
import type { SurfaceTextureHandle } from '@driftengine/core';
|
|
6
|
-
import { MeshBuilder, concatMeshes,
|
|
5
|
+
import type { GlassOptions, SurfaceTextureHandle } 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';
|
|
@@ -11,7 +11,7 @@ import type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
|
11
11
|
import type { DrftTexture } from '@driftengine/drft';
|
|
12
12
|
import { TextureSet, textureColorSpaces } from './drftTextures.ts';
|
|
13
13
|
import { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
14
|
-
import type { DrawSurfaceOverride } from './drawKey.ts';
|
|
14
|
+
import type { DrawGrouping, DrawSurfaceOverride } from './drawKey.ts';
|
|
15
15
|
import {
|
|
16
16
|
streamDrft,
|
|
17
17
|
CODEC_JPEG,
|
|
@@ -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
|
/**
|
|
@@ -260,6 +291,8 @@ export class DrftLoader {
|
|
|
260
291
|
cutout: number;
|
|
261
292
|
blend: boolean;
|
|
262
293
|
doubleSided: boolean;
|
|
294
|
+
/** Built once when the group is made, and shared by every part drawn from it. */
|
|
295
|
+
glass: GlassOptions | null;
|
|
263
296
|
/** The group's parts, placed, joined once when the group is built. See `concatMeshes`. */
|
|
264
297
|
members: MeshData[];
|
|
265
298
|
}
|
|
@@ -303,6 +336,10 @@ export class DrftLoader {
|
|
|
303
336
|
*/
|
|
304
337
|
private loadedNodes: readonly DrftNode[] = [];
|
|
305
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;
|
|
306
343
|
private loadedFields: readonly DrftSdfvEntry[] = [];
|
|
307
344
|
/** The fields placed, built once the fit and the fields are both known. See `fields`. */
|
|
308
345
|
private placedFields: readonly DrftFieldPlacement[] | null = null;
|
|
@@ -335,10 +372,13 @@ export class DrftLoader {
|
|
|
335
372
|
private merged = false;
|
|
336
373
|
private disposed = false;
|
|
337
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;
|
|
338
377
|
|
|
339
378
|
constructor(renderer: RendererApi, options: DrftLoaderOptions = {}) {
|
|
340
379
|
this.renderer = renderer;
|
|
341
380
|
this.options = options;
|
|
381
|
+
this.regionStore = new RegionStore(renderer, () => this.fit);
|
|
342
382
|
}
|
|
343
383
|
|
|
344
384
|
get progress(): DrftLoadProgress {
|
|
@@ -382,6 +422,97 @@ export class DrftLoader {
|
|
|
382
422
|
return this.loadedLights;
|
|
383
423
|
}
|
|
384
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
|
+
|
|
385
516
|
/**
|
|
386
517
|
* The distance fields the file carries, each where it stands: at the loader's fit, and once a
|
|
387
518
|
* copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
|
|
@@ -561,6 +692,15 @@ export class DrftLoader {
|
|
|
561
692
|
onLights: (lights) => {
|
|
562
693
|
this.loadedLights = lights;
|
|
563
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
|
+
},
|
|
564
704
|
onFields: (fields) => {
|
|
565
705
|
this.loadedFields = fields;
|
|
566
706
|
this.placedFields = null;
|
|
@@ -568,6 +708,13 @@ export class DrftLoader {
|
|
|
568
708
|
onInstances: (groups) => {
|
|
569
709
|
for (const group of groups) this.loadedInstances.set(group.mesh, group.transforms);
|
|
570
710
|
},
|
|
711
|
+
onLightVolume: (volume) => {
|
|
712
|
+
this.loadedLightVolume = volume;
|
|
713
|
+
},
|
|
714
|
+
onRegion: (region) => {
|
|
715
|
+
if (this.disposed) return;
|
|
716
|
+
this.regionStore.admit(region);
|
|
717
|
+
},
|
|
571
718
|
onSkin: (skin) => {
|
|
572
719
|
this.loadedSkins.push(skin);
|
|
573
720
|
},
|
|
@@ -636,27 +783,22 @@ export class DrftLoader {
|
|
|
636
783
|
this.set({ phase: 'materials' });
|
|
637
784
|
},
|
|
638
785
|
|
|
639
|
-
|
|
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) => {
|
|
640
791
|
if (this.disposed) return;
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
? mesh
|
|
652
|
-
: { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
|
|
653
|
-
this.queue.push({
|
|
654
|
-
mesh: withMorph,
|
|
655
|
-
material,
|
|
656
|
-
instances: this.loadedInstances.get(ordinal) ?? null,
|
|
657
|
-
});
|
|
658
|
-
this.arrived++;
|
|
659
|
-
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);
|
|
660
802
|
},
|
|
661
803
|
|
|
662
804
|
onTexture: (texture, ordinal) => {
|
|
@@ -792,7 +934,27 @@ export class DrftLoader {
|
|
|
792
934
|
) {
|
|
793
935
|
const next = this.queue.shift();
|
|
794
936
|
if (next === undefined) break;
|
|
795
|
-
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;
|
|
796
958
|
begun++;
|
|
797
959
|
}
|
|
798
960
|
|
|
@@ -805,7 +967,9 @@ export class DrftLoader {
|
|
|
805
967
|
) {
|
|
806
968
|
const ordinal = this.imageQueue.shift();
|
|
807
969
|
if (ordinal !== undefined) {
|
|
808
|
-
this.
|
|
970
|
+
const take = this.options.onImage;
|
|
971
|
+
if (take === undefined) this.rebuildTextures(ordinal);
|
|
972
|
+
else take(this.chunks[ordinal]?.name ?? '', this.images[ordinal] ?? null);
|
|
809
973
|
/*
|
|
810
974
|
* Counted here, once, and only for the image itself.
|
|
811
975
|
*
|
|
@@ -882,6 +1046,7 @@ export class DrftLoader {
|
|
|
882
1046
|
dispose(): void {
|
|
883
1047
|
if (this.disposed) return;
|
|
884
1048
|
this.disposed = true;
|
|
1049
|
+
this.regionStore.dispose();
|
|
885
1050
|
/*
|
|
886
1051
|
* Everything this class ever made, each disposed once.
|
|
887
1052
|
*
|
|
@@ -964,6 +1129,7 @@ export class DrftLoader {
|
|
|
964
1129
|
cutout: 0,
|
|
965
1130
|
blend: false,
|
|
966
1131
|
doubleSided: false,
|
|
1132
|
+
glass: null,
|
|
967
1133
|
reveal: this.outlinePart?.reveal ?? 0,
|
|
968
1134
|
instances: null,
|
|
969
1135
|
};
|
|
@@ -983,6 +1149,7 @@ export class DrftLoader {
|
|
|
983
1149
|
mesh: MeshData,
|
|
984
1150
|
material: DrftMaterial | undefined,
|
|
985
1151
|
instances: Float32Array | null,
|
|
1152
|
+
ordinal: number,
|
|
986
1153
|
): boolean {
|
|
987
1154
|
const fit = this.fit;
|
|
988
1155
|
if (fit === null) return false;
|
|
@@ -1015,6 +1182,37 @@ export class DrftLoader {
|
|
|
1015
1182
|
doubleSided,
|
|
1016
1183
|
} = g;
|
|
1017
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
|
+
|
|
1018
1216
|
/*
|
|
1019
1217
|
* Added to its merged group here, while there is a budget governing how much of this happens
|
|
1020
1218
|
* in one frame.
|
|
@@ -1033,6 +1231,7 @@ export class DrftLoader {
|
|
|
1033
1231
|
cutout,
|
|
1034
1232
|
blend,
|
|
1035
1233
|
doubleSided,
|
|
1234
|
+
glass: glassOf(g),
|
|
1036
1235
|
});
|
|
1037
1236
|
}
|
|
1038
1237
|
|
|
@@ -1052,6 +1251,7 @@ export class DrftLoader {
|
|
|
1052
1251
|
cutout,
|
|
1053
1252
|
blend,
|
|
1054
1253
|
doubleSided,
|
|
1254
|
+
glass: glassOf(g),
|
|
1055
1255
|
members: [],
|
|
1056
1256
|
};
|
|
1057
1257
|
this.groups.set(key, group);
|
|
@@ -1093,6 +1293,7 @@ export class DrftLoader {
|
|
|
1093
1293
|
cutout,
|
|
1094
1294
|
blend,
|
|
1095
1295
|
doubleSided,
|
|
1296
|
+
glass: group.glass,
|
|
1096
1297
|
reveal: 0,
|
|
1097
1298
|
instances: null,
|
|
1098
1299
|
};
|
|
@@ -1117,10 +1318,9 @@ export class DrftLoader {
|
|
|
1117
1318
|
/**
|
|
1118
1319
|
* A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
|
|
1119
1320
|
*
|
|
1120
|
-
* **The fit is conjugated into each placement
|
|
1121
|
-
*
|
|
1122
|
-
*
|
|
1123
|
-
* 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.
|
|
1124
1324
|
*/
|
|
1125
1325
|
private uploadInstanced(
|
|
1126
1326
|
prototype: MeshData,
|
|
@@ -1130,36 +1330,8 @@ export class DrftLoader {
|
|
|
1130
1330
|
): boolean {
|
|
1131
1331
|
try {
|
|
1132
1332
|
const uploaded = this.renderer.createMesh(prototype);
|
|
1133
|
-
const
|
|
1134
|
-
const batch = this.renderer.createInstanced(uploaded, count);
|
|
1135
|
-
const data = createMeshInstances(count);
|
|
1136
|
-
const s = fit.scale;
|
|
1137
|
-
for (let k = 0; k < count; k++) {
|
|
1138
|
-
const at = k * 16;
|
|
1139
|
-
for (let i = 0; i < 12; i++) data.models[at + i] = instances[at + i] as number;
|
|
1140
|
-
const ux = instances[at + 12] as number;
|
|
1141
|
-
const uy = instances[at + 13] as number;
|
|
1142
|
-
const uz = instances[at + 14] as number;
|
|
1143
|
-
/* R·t, with R the copy's own 3×3 read column-major. */
|
|
1144
|
-
const rtx =
|
|
1145
|
-
(instances[at] as number) * fit.x +
|
|
1146
|
-
(instances[at + 4] as number) * fit.y +
|
|
1147
|
-
(instances[at + 8] as number) * fit.z;
|
|
1148
|
-
const rty =
|
|
1149
|
-
(instances[at + 1] as number) * fit.x +
|
|
1150
|
-
(instances[at + 5] as number) * fit.y +
|
|
1151
|
-
(instances[at + 9] as number) * fit.z;
|
|
1152
|
-
const rtz =
|
|
1153
|
-
(instances[at + 2] as number) * fit.x +
|
|
1154
|
-
(instances[at + 6] as number) * fit.y +
|
|
1155
|
-
(instances[at + 10] as number) * fit.z;
|
|
1156
|
-
data.models[at + 12] = fit.x - rtx + s * ux;
|
|
1157
|
-
data.models[at + 13] = fit.y - rty + s * uy;
|
|
1158
|
-
data.models[at + 14] = fit.z - rtz + s * uz;
|
|
1159
|
-
data.models[at + 15] = 1;
|
|
1160
|
-
}
|
|
1161
|
-
data.tints.fill(1);
|
|
1162
|
-
data.count = count;
|
|
1333
|
+
const data = fitCopies(instances, fit);
|
|
1334
|
+
const batch = this.renderer.createInstanced(uploaded, data.count);
|
|
1163
1335
|
this.renderer.uploadInstanced(batch, data);
|
|
1164
1336
|
const part: DrftPart = { mesh: uploaded, ...surface, reveal: 0, instances: { batch, data } };
|
|
1165
1337
|
this.revealed.push(part);
|
|
@@ -1190,6 +1362,7 @@ export class DrftLoader {
|
|
|
1190
1362
|
cutout: group.cutout,
|
|
1191
1363
|
blend: group.blend,
|
|
1192
1364
|
doubleSided: group.doubleSided,
|
|
1365
|
+
glass: group.glass,
|
|
1193
1366
|
reveal: 1,
|
|
1194
1367
|
instances: null,
|
|
1195
1368
|
});
|
|
@@ -1309,6 +1482,12 @@ export class DrftLoader {
|
|
|
1309
1482
|
}
|
|
1310
1483
|
}
|
|
1311
1484
|
|
|
1485
|
+
/** A group's glass as the renderer takes it, or null for a group that lets no light through. */
|
|
1486
|
+
function glassOf(g: DrawGrouping): GlassOptions | null {
|
|
1487
|
+
if (g.transmission <= 0) return null;
|
|
1488
|
+
return { transmission: g.transmission, frost: g.frost, tint: [g.tint[0], g.tint[1], g.tint[2]] };
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1312
1491
|
/**
|
|
1313
1492
|
* The MIME type a codec's bytes carry, or `null` for one the browser has no decoder for.
|
|
1314
1493
|
*
|
package/src/gltf.ts
CHANGED
|
@@ -77,15 +77,36 @@ interface GltfSpecularGlossiness {
|
|
|
77
77
|
/** RGB the specular colour, A the glossiness. Both multiply the factors above. */
|
|
78
78
|
specularGlossinessTexture?: { index: number; texCoord?: number };
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The three extensions that together say a material is glass: how much light passes
|
|
82
|
+
* (`KHR_materials_transmission`), how much of it is scattered on the way and the colour it takes
|
|
83
|
+
* (`KHR_materials_diffuse_transmission`), and the colour a thickness of it absorbs towards
|
|
84
|
+
* (`KHR_materials_volume`). Only the fields this reader maps are named.
|
|
85
|
+
*/
|
|
86
|
+
interface GltfTransmission {
|
|
87
|
+
transmissionFactor?: number;
|
|
88
|
+
}
|
|
89
|
+
interface GltfDiffuseTransmission {
|
|
90
|
+
diffuseTransmissionFactor?: number;
|
|
91
|
+
diffuseTransmissionColorFactor?: number[];
|
|
92
|
+
}
|
|
93
|
+
interface GltfVolume {
|
|
94
|
+
attenuationColor?: number[];
|
|
95
|
+
}
|
|
80
96
|
interface GltfMaterial {
|
|
81
97
|
pbrMetallicRoughness?: GltfPbrMetallicRoughness;
|
|
82
98
|
/**
|
|
83
|
-
* The material extensions this reader knows
|
|
99
|
+
* The material extensions this reader knows, and everything else is ignored.
|
|
84
100
|
*
|
|
85
|
-
* Read as
|
|
86
|
-
*
|
|
101
|
+
* Read as optional fields rather than as `Record<string, unknown>` so that adding one costs a
|
|
102
|
+
* name here and a branch where it is read, and so that a typo in either is a compile error.
|
|
87
103
|
*/
|
|
88
|
-
extensions?: {
|
|
104
|
+
extensions?: {
|
|
105
|
+
KHR_materials_pbrSpecularGlossiness?: GltfSpecularGlossiness;
|
|
106
|
+
KHR_materials_transmission?: GltfTransmission;
|
|
107
|
+
KHR_materials_diffuse_transmission?: GltfDiffuseTransmission;
|
|
108
|
+
KHR_materials_volume?: GltfVolume;
|
|
109
|
+
};
|
|
89
110
|
normalTexture?: { index: number; texCoord?: number; scale?: number };
|
|
90
111
|
/** `strength` is a mix from 1, not a multiply: 0 means unoccluded. */
|
|
91
112
|
occlusionTexture?: { index: number; texCoord?: number; strength?: number };
|
|
@@ -640,6 +661,8 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
|
|
|
640
661
|
* A separate occlusion image is dropped rather than loaded into a second unit. One map, one
|
|
641
662
|
* unit, and `textureUnitBudget.test.ts` says the next unit belongs to emissive.
|
|
642
663
|
*/
|
|
664
|
+
const glass = glassOf(material, modulated ? undefined : roughness);
|
|
665
|
+
|
|
643
666
|
const occlusion = material?.occlusionTexture;
|
|
644
667
|
const occlusionStrength =
|
|
645
668
|
modulated && occlusion !== undefined && occlusion.index === ormMap
|
|
@@ -662,7 +685,7 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
|
|
|
662
685
|
* cannot say the material blends, and a leaf whose shape is all in its texture's alpha drew as a
|
|
663
686
|
* solid card. So it is said.
|
|
664
687
|
*/
|
|
665
|
-
blend: blends,
|
|
688
|
+
blend: blends || glass.transmission > 0,
|
|
666
689
|
/* Seen from both faces: a curtain, a leaf card. The specification's default is one face. */
|
|
667
690
|
doubleSided: material?.doubleSided === true,
|
|
668
691
|
/* Only in MASK mode: the specification says `alphaCutoff` has no meaning in the other two. */
|
|
@@ -678,9 +701,49 @@ function materialOf(material: GltfMaterial | undefined): DrftMaterial {
|
|
|
678
701
|
metallicScale: modulated ? (pbr?.metallicFactor ?? 1) : 1,
|
|
679
702
|
occlusionStrength,
|
|
680
703
|
albedo: pbr?.baseColorTexture?.index ?? -1,
|
|
704
|
+
transmission: glass.transmission,
|
|
705
|
+
frost: glass.frost,
|
|
706
|
+
tint: glass.tint,
|
|
681
707
|
};
|
|
682
708
|
}
|
|
683
709
|
|
|
710
|
+
const unit = (value: number | undefined, fallback: number): number =>
|
|
711
|
+
Math.min(1, Math.max(0, value ?? fallback));
|
|
712
|
+
|
|
713
|
+
/**
|
|
714
|
+
* What a material's extensions say about letting light through, as this engine's glass.
|
|
715
|
+
*
|
|
716
|
+
* **Rough glass is frosted glass.** `KHR_materials_transmission` says its roughness blurs what is
|
|
717
|
+
* seen through the surface, and that blur is exactly what `frost` is, so a transmissive material's
|
|
718
|
+
* roughness raises its frost and a diffusely transmitting share raises it too — whichever says
|
|
719
|
+
* more. Only transmission makes a material glass: a surface stating diffuse transmission alone is a
|
|
720
|
+
* leaf or a sheet of paper, which this engine's glass would draw see-through and let every shadow
|
|
721
|
+
* pass, so it stays the opaque surface it was. What that gives up is a paper shade stated by that
|
|
722
|
+
* extension alone, which arrives opaque; a per-material override is the door for it.
|
|
723
|
+
*
|
|
724
|
+
* `roughness` is `undefined` where a map carries the roughness per texel: the factor then only
|
|
725
|
+
* scales the map, so it says nothing about how rough the glass is, and frost comes from the
|
|
726
|
+
* diffuse share alone rather than from a number the file did not state.
|
|
727
|
+
*/
|
|
728
|
+
function glassOf(
|
|
729
|
+
material: GltfMaterial | undefined,
|
|
730
|
+
roughness: number | undefined,
|
|
731
|
+
): { transmission: number; frost: number; tint: [number, number, number] } {
|
|
732
|
+
const ext = material?.extensions;
|
|
733
|
+
const transmission = unit(ext?.KHR_materials_transmission?.transmissionFactor, 0);
|
|
734
|
+
if (transmission <= 0) return { transmission: 0, frost: 0, tint: [1, 1, 1] };
|
|
735
|
+
const diffuse = ext?.KHR_materials_diffuse_transmission;
|
|
736
|
+
const frost = Math.max(unit(diffuse?.diffuseTransmissionFactor, 0), unit(roughness, 0));
|
|
737
|
+
const colour =
|
|
738
|
+
diffuse?.diffuseTransmissionColorFactor ?? ext?.KHR_materials_volume?.attenuationColor;
|
|
739
|
+
const tint: [number, number, number] = [
|
|
740
|
+
unit(colour?.[0], 1),
|
|
741
|
+
unit(colour?.[1], 1),
|
|
742
|
+
unit(colour?.[2], 1),
|
|
743
|
+
];
|
|
744
|
+
return { transmission, frost, tint };
|
|
745
|
+
}
|
|
746
|
+
|
|
684
747
|
/** What a caller can tell the reader about what it is going to do with the result. */
|
|
685
748
|
export interface GltfReadOptions {
|
|
686
749
|
/**
|
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';
|
package/src/loadProgress.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
|
/**
|
|
4
4
|
* The vocabulary of a load in flight: which phase it is in, how far it has got, and what a
|
|
@@ -134,6 +134,13 @@ export interface DrftPart {
|
|
|
134
134
|
readonly blend: boolean;
|
|
135
135
|
/** Whether both faces are seen: glTF's `doubleSided`. Drawn without culling, each face lit as the front. */
|
|
136
136
|
readonly doubleSided: boolean;
|
|
137
|
+
/**
|
|
138
|
+
* The glass this part is, from its file or from `DrftLoaderOptions.surface`, or null for a part
|
|
139
|
+
* that lets no light through. A glass part is also `blend`, so a caller drawing blended parts
|
|
140
|
+
* translucent passes this as the draw's `glass` and every pane arrives as the file or the
|
|
141
|
+
* override said. One object per merged group, shared by its parts, so it is not rebuilt per draw.
|
|
142
|
+
*/
|
|
143
|
+
readonly glass: GlassOptions | null;
|
|
137
144
|
/**
|
|
138
145
|
* The copies this part is drawn as, or null for a part drawn once.
|
|
139
146
|
*
|