@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/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/drawKey.d.ts
CHANGED
|
@@ -23,6 +23,15 @@ export interface DrawSurfaceOverride {
|
|
|
23
23
|
* gallery and read as a black blotch. 0 says dielectric.
|
|
24
24
|
*/
|
|
25
25
|
readonly metallicScale?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Glass, in place of what the file says: the share of light that passes, how milky it is, and
|
|
28
|
+
* the colour it takes (`GlassOptions` in the renderer). For a model whose panes arrive as an
|
|
29
|
+
* opaque dark material, because its format or its exporter had no word for glass. Transmission
|
|
30
|
+
* above zero draws the part blended.
|
|
31
|
+
*/
|
|
32
|
+
readonly transmission?: number;
|
|
33
|
+
readonly frost?: number;
|
|
34
|
+
readonly tint?: readonly [number, number, number];
|
|
26
35
|
}
|
|
27
36
|
/**
|
|
28
37
|
* The fields a merged group carries, resolved from a material and its override.
|
|
@@ -47,6 +56,13 @@ export interface DrawGrouping {
|
|
|
47
56
|
/** Whether both faces are drawn and lit. A merge of a two-sided part and a one-sided one would cull
|
|
48
57
|
* one of them or unhide the back of the other, so it is part of the key. */
|
|
49
58
|
readonly doubleSided: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* Glass. Zero transmission is not glass, and then frost is 0 and the tint white whatever the
|
|
61
|
+
* file or the override said, so a stray value on an opaque surface never splits a draw.
|
|
62
|
+
*/
|
|
63
|
+
readonly transmission: number;
|
|
64
|
+
readonly frost: number;
|
|
65
|
+
readonly tint: readonly [number, number, number];
|
|
50
66
|
}
|
|
51
67
|
/**
|
|
52
68
|
* Resolve what a part will actually be drawn with.
|
|
@@ -56,7 +72,7 @@ export interface DrawGrouping {
|
|
|
56
72
|
* 0, which is the identity that lets a file saying nothing about a property draw the same as one
|
|
57
73
|
* that did not have the property to say.
|
|
58
74
|
*
|
|
59
|
-
* The override wins over the material for the
|
|
75
|
+
* The override wins over the material for the fields it may carry, because a consumer dressing
|
|
60
76
|
* a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
|
|
61
77
|
* a fact about the *image* a material wears, so nothing outside the file has anything to say
|
|
62
78
|
* about it.
|
package/dist/drawKey.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const CLEAR = [1, 1, 1];
|
|
1
2
|
/**
|
|
2
3
|
* Resolve what a part will actually be drawn with.
|
|
3
4
|
*
|
|
@@ -6,12 +7,14 @@
|
|
|
6
7
|
* 0, which is the identity that lets a file saying nothing about a property draw the same as one
|
|
7
8
|
* that did not have the property to say.
|
|
8
9
|
*
|
|
9
|
-
* The override wins over the material for the
|
|
10
|
+
* The override wins over the material for the fields it may carry, because a consumer dressing
|
|
10
11
|
* a surface by name knows more than a default an exporter wrote. A cutout is not among them: it is
|
|
11
12
|
* a fact about the *image* a material wears, so nothing outside the file has anything to say
|
|
12
13
|
* about it.
|
|
13
14
|
*/
|
|
14
15
|
export function resolveDrawGrouping(material, override) {
|
|
16
|
+
const transmission = Math.max(0, override?.transmission ?? material?.transmission ?? 0);
|
|
17
|
+
const glass = transmission > 0;
|
|
15
18
|
return {
|
|
16
19
|
albedo: material?.albedo ?? -1,
|
|
17
20
|
orm: material?.ormMap ?? -1,
|
|
@@ -23,8 +26,11 @@ export function resolveDrawGrouping(material, override) {
|
|
|
23
26
|
metallicScale: override?.metallicScale ?? material?.metallicScale ?? 1,
|
|
24
27
|
occlusionStrength: material?.occlusionStrength ?? 0,
|
|
25
28
|
cutout: material?.cutout ?? 0,
|
|
26
|
-
blend: material?.blend === true,
|
|
29
|
+
blend: material?.blend === true || glass,
|
|
27
30
|
doubleSided: material?.doubleSided === true,
|
|
31
|
+
transmission,
|
|
32
|
+
frost: glass ? (override?.frost ?? material?.frost ?? 0) : 0,
|
|
33
|
+
tint: glass ? (override?.tint ?? material?.tint ?? CLEAR) : CLEAR,
|
|
28
34
|
};
|
|
29
35
|
}
|
|
30
36
|
/**
|
|
@@ -40,5 +46,5 @@ export function resolveDrawGrouping(material, override) {
|
|
|
40
46
|
*/
|
|
41
47
|
export function drawKeyOf(material, override) {
|
|
42
48
|
const g = resolveDrawGrouping(material, override);
|
|
43
|
-
return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}`;
|
|
49
|
+
return `${g.albedo}:${g.orm}:${g.normal}:${g.emissive}:${g.opacity}:${g.reflectivity}:${g.roughnessScale}:${g.metallicScale}:${g.occlusionStrength}:${g.cutout}:${g.blend ? 1 : 0}:${g.doubleSided ? 1 : 0}:${g.transmission}:${g.frost}:${g.tint[0]},${g.tint[1]},${g.tint[2]}`;
|
|
44
50
|
}
|
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
|
*
|
|
@@ -742,6 +876,7 @@ export class DrftLoader {
|
|
|
742
876
|
cutout: 0,
|
|
743
877
|
blend: false,
|
|
744
878
|
doubleSided: false,
|
|
879
|
+
glass: null,
|
|
745
880
|
reveal: this.outlinePart?.reveal ?? 0,
|
|
746
881
|
instances: null,
|
|
747
882
|
};
|
|
@@ -759,7 +894,7 @@ export class DrftLoader {
|
|
|
759
894
|
return true;
|
|
760
895
|
}
|
|
761
896
|
/** One part, transformed, dressed and uploaded. Returns whether it landed. */
|
|
762
|
-
uploadOne(mesh, material, instances) {
|
|
897
|
+
uploadOne(mesh, material, instances, ordinal) {
|
|
763
898
|
const fit = this.fit;
|
|
764
899
|
if (fit === null)
|
|
765
900
|
return false;
|
|
@@ -778,6 +913,37 @@ export class DrftLoader {
|
|
|
778
913
|
*/
|
|
779
914
|
const g = resolveDrawGrouping(material, override);
|
|
780
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
|
+
}
|
|
781
947
|
/*
|
|
782
948
|
* Added to its merged group here, while there is a budget governing how much of this happens
|
|
783
949
|
* in one frame.
|
|
@@ -796,6 +962,7 @@ export class DrftLoader {
|
|
|
796
962
|
cutout,
|
|
797
963
|
blend,
|
|
798
964
|
doubleSided,
|
|
965
|
+
glass: glassOf(g),
|
|
799
966
|
});
|
|
800
967
|
}
|
|
801
968
|
const key = drawKeyOf(material, override);
|
|
@@ -814,6 +981,7 @@ export class DrftLoader {
|
|
|
814
981
|
cutout,
|
|
815
982
|
blend,
|
|
816
983
|
doubleSided,
|
|
984
|
+
glass: glassOf(g),
|
|
817
985
|
members: [],
|
|
818
986
|
};
|
|
819
987
|
this.groups.set(key, group);
|
|
@@ -854,6 +1022,7 @@ export class DrftLoader {
|
|
|
854
1022
|
cutout,
|
|
855
1023
|
blend,
|
|
856
1024
|
doubleSided,
|
|
1025
|
+
glass: group.glass,
|
|
857
1026
|
reveal: 0,
|
|
858
1027
|
instances: null,
|
|
859
1028
|
};
|
|
@@ -879,42 +1048,15 @@ export class DrftLoader {
|
|
|
879
1048
|
/**
|
|
880
1049
|
* A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
|
|
881
1050
|
*
|
|
882
|
-
* **The fit is conjugated into each placement
|
|
883
|
-
*
|
|
884
|
-
*
|
|
885
|
-
* 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.
|
|
886
1054
|
*/
|
|
887
1055
|
uploadInstanced(prototype, instances, fit, surface) {
|
|
888
1056
|
try {
|
|
889
1057
|
const uploaded = this.renderer.createMesh(prototype);
|
|
890
|
-
const
|
|
891
|
-
const batch = this.renderer.createInstanced(uploaded, count);
|
|
892
|
-
const data = createMeshInstances(count);
|
|
893
|
-
const s = fit.scale;
|
|
894
|
-
for (let k = 0; k < count; k++) {
|
|
895
|
-
const at = k * 16;
|
|
896
|
-
for (let i = 0; i < 12; i++)
|
|
897
|
-
data.models[at + i] = instances[at + i];
|
|
898
|
-
const ux = instances[at + 12];
|
|
899
|
-
const uy = instances[at + 13];
|
|
900
|
-
const uz = instances[at + 14];
|
|
901
|
-
/* R·t, with R the copy's own 3×3 read column-major. */
|
|
902
|
-
const rtx = instances[at] * fit.x +
|
|
903
|
-
instances[at + 4] * fit.y +
|
|
904
|
-
instances[at + 8] * fit.z;
|
|
905
|
-
const rty = instances[at + 1] * fit.x +
|
|
906
|
-
instances[at + 5] * fit.y +
|
|
907
|
-
instances[at + 9] * fit.z;
|
|
908
|
-
const rtz = instances[at + 2] * fit.x +
|
|
909
|
-
instances[at + 6] * fit.y +
|
|
910
|
-
instances[at + 10] * fit.z;
|
|
911
|
-
data.models[at + 12] = fit.x - rtx + s * ux;
|
|
912
|
-
data.models[at + 13] = fit.y - rty + s * uy;
|
|
913
|
-
data.models[at + 14] = fit.z - rtz + s * uz;
|
|
914
|
-
data.models[at + 15] = 1;
|
|
915
|
-
}
|
|
916
|
-
data.tints.fill(1);
|
|
917
|
-
data.count = count;
|
|
1058
|
+
const data = fitCopies(instances, fit);
|
|
1059
|
+
const batch = this.renderer.createInstanced(uploaded, data.count);
|
|
918
1060
|
this.renderer.uploadInstanced(batch, data);
|
|
919
1061
|
const part = { mesh: uploaded, ...surface, reveal: 0, instances: { batch, data } };
|
|
920
1062
|
this.revealed.push(part);
|
|
@@ -947,6 +1089,7 @@ export class DrftLoader {
|
|
|
947
1089
|
cutout: group.cutout,
|
|
948
1090
|
blend: group.blend,
|
|
949
1091
|
doubleSided: group.doubleSided,
|
|
1092
|
+
glass: group.glass,
|
|
950
1093
|
reveal: 1,
|
|
951
1094
|
instances: null,
|
|
952
1095
|
});
|
|
@@ -1068,6 +1211,12 @@ export class DrftLoader {
|
|
|
1068
1211
|
};
|
|
1069
1212
|
}
|
|
1070
1213
|
}
|
|
1214
|
+
/** A group's glass as the renderer takes it, or null for a group that lets no light through. */
|
|
1215
|
+
function glassOf(g) {
|
|
1216
|
+
if (g.transmission <= 0)
|
|
1217
|
+
return null;
|
|
1218
|
+
return { transmission: g.transmission, frost: g.frost, tint: [g.tint[0], g.tint[1], g.tint[2]] };
|
|
1219
|
+
}
|
|
1071
1220
|
/**
|
|
1072
1221
|
* The MIME type a codec's bytes carry, or `null` for one the browser has no decoder for.
|
|
1073
1222
|
*
|
package/dist/gltf.d.ts
CHANGED
|
@@ -80,16 +80,35 @@ interface GltfSpecularGlossiness {
|
|
|
80
80
|
texCoord?: number;
|
|
81
81
|
};
|
|
82
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The three extensions that together say a material is glass: how much light passes
|
|
85
|
+
* (`KHR_materials_transmission`), how much of it is scattered on the way and the colour it takes
|
|
86
|
+
* (`KHR_materials_diffuse_transmission`), and the colour a thickness of it absorbs towards
|
|
87
|
+
* (`KHR_materials_volume`). Only the fields this reader maps are named.
|
|
88
|
+
*/
|
|
89
|
+
interface GltfTransmission {
|
|
90
|
+
transmissionFactor?: number;
|
|
91
|
+
}
|
|
92
|
+
interface GltfDiffuseTransmission {
|
|
93
|
+
diffuseTransmissionFactor?: number;
|
|
94
|
+
diffuseTransmissionColorFactor?: number[];
|
|
95
|
+
}
|
|
96
|
+
interface GltfVolume {
|
|
97
|
+
attenuationColor?: number[];
|
|
98
|
+
}
|
|
83
99
|
interface GltfMaterial {
|
|
84
100
|
pbrMetallicRoughness?: GltfPbrMetallicRoughness;
|
|
85
101
|
/**
|
|
86
|
-
* The material extensions this reader knows
|
|
102
|
+
* The material extensions this reader knows, and everything else is ignored.
|
|
87
103
|
*
|
|
88
|
-
* Read as
|
|
89
|
-
*
|
|
104
|
+
* Read as optional fields rather than as `Record<string, unknown>` so that adding one costs a
|
|
105
|
+
* name here and a branch where it is read, and so that a typo in either is a compile error.
|
|
90
106
|
*/
|
|
91
107
|
extensions?: {
|
|
92
108
|
KHR_materials_pbrSpecularGlossiness?: GltfSpecularGlossiness;
|
|
109
|
+
KHR_materials_transmission?: GltfTransmission;
|
|
110
|
+
KHR_materials_diffuse_transmission?: GltfDiffuseTransmission;
|
|
111
|
+
KHR_materials_volume?: GltfVolume;
|
|
93
112
|
};
|
|
94
113
|
normalTexture?: {
|
|
95
114
|
index: number;
|