@driftengine/assets 4.2.1 → 4.4.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/bake/collapseHeap.d.ts +24 -0
- package/dist/bake/collapseHeap.js +80 -0
- package/dist/bake/collapseMeasures.d.ts +31 -0
- package/dist/bake/collapseMeasures.js +183 -0
- package/dist/bake/sceneField.d.ts +6 -0
- package/dist/bake/sceneField.js +48 -0
- package/dist/bake/sdf.d.ts +5 -5
- package/dist/bake/sdf.js +15 -95
- package/dist/bake/simplify.d.ts +42 -0
- package/dist/bake/simplify.js +350 -0
- package/dist/bake/surfaceDepth.d.ts +29 -0
- package/dist/bake/surfaceDepth.js +101 -0
- package/dist/downscale.d.ts +25 -0
- package/dist/downscale.js +50 -0
- package/dist/drawKey.d.ts +13 -1
- package/dist/drawKey.js +5 -3
- package/dist/drftLoader.d.ts +38 -7
- package/dist/drftLoader.js +186 -30
- package/dist/fieldPlacement.d.ts +22 -0
- package/dist/fieldPlacement.js +49 -0
- package/dist/gltf.d.ts +46 -1
- package/dist/gltf.js +101 -2
- package/dist/index.d.ts +9 -1
- package/dist/index.js +15 -1
- package/dist/loadProgress.d.ts +22 -1
- package/dist/orient.d.ts +9 -1
- package/dist/orient.js +60 -2
- package/dist/partMaterial.d.ts +19 -0
- package/dist/partMaterial.js +16 -0
- package/dist/readModel.d.ts +3 -1
- package/dist/repeats.d.ts +39 -0
- package/dist/repeats.js +391 -0
- package/dist/weld.d.ts +8 -0
- package/dist/weld.js +155 -34
- package/package.json +4 -4
- package/src/bake/collapseHeap.ts +81 -0
- package/src/bake/collapseMeasures.ts +215 -0
- package/src/bake/sceneField.ts +50 -0
- package/src/bake/sdf.ts +14 -99
- package/src/bake/simplify.ts +395 -0
- package/src/bake/surfaceDepth.ts +102 -0
- package/src/downscale.ts +61 -0
- package/src/drawKey.ts +17 -3
- package/src/drftLoader.ts +224 -38
- package/src/fieldPlacement.ts +74 -0
- package/src/gltf.ts +139 -3
- package/src/index.ts +24 -1
- package/src/loadProgress.ts +23 -1
- package/src/orient.ts +62 -3
- package/src/partMaterial.ts +42 -0
- package/src/readModel.ts +3 -1
- package/src/repeats.ts +442 -0
- package/src/weld.ts +164 -34
package/dist/drftLoader.d.ts
CHANGED
|
@@ -3,8 +3,10 @@ import type { MeshData } from '@driftengine/drft';
|
|
|
3
3
|
import type { RendererApi } from '@driftengine/core';
|
|
4
4
|
import type { SurfaceTextureHandle } from '@driftengine/core';
|
|
5
5
|
import type { DrftMaterial } from '@driftengine/drft';
|
|
6
|
+
import type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
6
7
|
import { TextureSet } from './drftTextures.ts';
|
|
7
|
-
import type {
|
|
8
|
+
import type { DrawSurfaceOverride } from './drawKey.ts';
|
|
9
|
+
import type { AnimationClip, DrftLight, DrftMorph, DrftNode, DrftSkin } from '@driftengine/drft';
|
|
8
10
|
import type { DrftLoadProgress, DrftPart } from './loadProgress.ts';
|
|
9
11
|
/**
|
|
10
12
|
* How a loaded model is placed: fitted to a size, or left exactly where its file puts it.
|
|
@@ -94,11 +96,8 @@ export interface DrftLoaderOptions {
|
|
|
94
96
|
* can have its paint decided here, per material, without the loader knowing what a car is.
|
|
95
97
|
*/
|
|
96
98
|
readonly transform?: (mesh: MeshData, material: DrftMaterial | undefined) => MeshData;
|
|
97
|
-
/** Per-material opacity and
|
|
98
|
-
readonly surface?: (material: DrftMaterial | undefined) =>
|
|
99
|
-
readonly opacity?: number;
|
|
100
|
-
readonly reflectivity?: number;
|
|
101
|
-
} | undefined;
|
|
99
|
+
/** Per-material opacity, reflectivity and metalness overrides, for the same reason as `transform`. */
|
|
100
|
+
readonly surface?: (material: DrftMaterial | undefined) => DrawSurfaceOverride | undefined;
|
|
102
101
|
/**
|
|
103
102
|
* Draw a coarse whole model while the parts arrive, where the file carries one.
|
|
104
103
|
*
|
|
@@ -186,6 +185,13 @@ export declare class DrftLoader {
|
|
|
186
185
|
private readonly revealed;
|
|
187
186
|
/** Arrived, not yet uploaded. Drained by `update` at the budget. */
|
|
188
187
|
private readonly queue;
|
|
188
|
+
/** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
|
|
189
|
+
private readonly loadedInstances;
|
|
190
|
+
/**
|
|
191
|
+
* The instanced parts, which the merge leaves alone: merging would bake one copy into a group
|
|
192
|
+
* and lose the rest. Kept here so the swap to merged groups carries them across.
|
|
193
|
+
*/
|
|
194
|
+
private readonly instancedParts;
|
|
189
195
|
/**
|
|
190
196
|
* Coarse levels that have arrived and not yet been uploaded, in arrival order.
|
|
191
197
|
*
|
|
@@ -246,6 +252,10 @@ export declare class DrftLoader {
|
|
|
246
252
|
* hand on is invisible to every test of either side.
|
|
247
253
|
*/
|
|
248
254
|
private loadedNodes;
|
|
255
|
+
private loadedLights;
|
|
256
|
+
private loadedFields;
|
|
257
|
+
/** The fields placed, built once the fit and the fields are both known. See `fields`. */
|
|
258
|
+
private placedFields;
|
|
249
259
|
private readonly loadedSkins;
|
|
250
260
|
private readonly loadedClips;
|
|
251
261
|
/**
|
|
@@ -291,6 +301,18 @@ export declare class DrftLoader {
|
|
|
291
301
|
* this class draws parts, and placing them under a graph is a scene decision.
|
|
292
302
|
*/
|
|
293
303
|
get nodes(): readonly DrftNode[];
|
|
304
|
+
/**
|
|
305
|
+
* The lights the file was authored with, in glTF's units, or empty. They arrive ahead of the
|
|
306
|
+
* geometry. What a candela is in this scene's light units is the caller's decision.
|
|
307
|
+
*/
|
|
308
|
+
get lights(): readonly DrftLight[];
|
|
309
|
+
/**
|
|
310
|
+
* The distance fields the file carries, each where it stands: at the loader's fit, and once a
|
|
311
|
+
* copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
|
|
312
|
+
* a file carrying none. A scene declares these to `addDistanceField` every frame, with the colour
|
|
313
|
+
* of the surface each covers. See `fieldPlacement.ts`.
|
|
314
|
+
*/
|
|
315
|
+
get fields(): readonly DrftFieldPlacement[];
|
|
294
316
|
/** Skins the file carried, in the order their chunks appeared. Empty for a file with none. */
|
|
295
317
|
get skins(): readonly DrftSkin[];
|
|
296
318
|
/** Clips the file carried, in the order their chunks appeared. Empty for a file with none. */
|
|
@@ -397,6 +419,15 @@ export declare class DrftLoader {
|
|
|
397
419
|
* and differ in blend: merging on the image alone means one cannot be made translucent without
|
|
398
420
|
* taking the other with it.
|
|
399
421
|
*/
|
|
422
|
+
/**
|
|
423
|
+
* A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
|
|
424
|
+
*
|
|
425
|
+
* **The fit is conjugated into each placement.** The prototype is uploaded with the fit already
|
|
426
|
+
* applied, as every part is, so a copy's matrix M becomes F·M·F⁻¹: the same turn, and a translation
|
|
427
|
+
* of t − R·t + s·u for the fit's scale s and offset t and the copy's own translation u. A fitted
|
|
428
|
+
* model's copies then land where its geometry does, and are not scaled twice.
|
|
429
|
+
*/
|
|
430
|
+
private uploadInstanced;
|
|
400
431
|
private buildGroup;
|
|
401
432
|
/**
|
|
402
433
|
* Put the merged groups on screen and throw the per-part meshes away.
|
|
@@ -427,7 +458,7 @@ export declare class DrftLoader {
|
|
|
427
458
|
* decoder that had no chance with them. Naming each codec and returning `null` for the one that is
|
|
428
459
|
* not an encoded image makes the omission a value rather than a default, and lets a test say so.
|
|
429
460
|
*/
|
|
430
|
-
export declare function imageTypeFor(codec: number): string;
|
|
461
|
+
export declare function imageTypeFor(codec: number): string | null;
|
|
431
462
|
/**
|
|
432
463
|
* Whether a codec is decoded from its own bytes rather than through an image decoder.
|
|
433
464
|
*
|
package/dist/drftLoader.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/** Loading a `.drft` into drawable parts progressively, without stalling the frame. */
|
|
2
|
-
import { MeshBuilder } from '@driftengine/core';
|
|
2
|
+
import { MeshBuilder, concatMeshes, createMeshInstances, placeMesh } from '@driftengine/core';
|
|
3
|
+
import { placeFields } from './fieldPlacement.js';
|
|
3
4
|
import { TextureSet, textureColorSpaces } from './drftTextures.js';
|
|
4
5
|
import { drawKeyOf, resolveDrawGrouping } from './drawKey.js';
|
|
5
|
-
import { streamDrft, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError } from '@driftengine/drft';
|
|
6
|
+
import { streamDrft, CODEC_JPEG, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError, } from '@driftengine/drft';
|
|
6
7
|
import { DEFAULT_REVEAL_SEC, DEFAULT_UPLOAD_MS_PER_FRAME, DEFAULT_UPLOADS_PER_FRAME, isDocumentResponse, mayBeginMore, } from './uploadBudget.js';
|
|
7
8
|
/**
|
|
8
9
|
* A `.drft` loaded progressively: the model builds up on screen instead of appearing.
|
|
@@ -36,6 +37,13 @@ export class DrftLoader {
|
|
|
36
37
|
revealed = [];
|
|
37
38
|
/** Arrived, not yet uploaded. Drained by `update` at the budget. */
|
|
38
39
|
queue = [];
|
|
40
|
+
/** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
|
|
41
|
+
loadedInstances = new Map();
|
|
42
|
+
/**
|
|
43
|
+
* The instanced parts, which the merge leaves alone: merging would bake one copy into a group
|
|
44
|
+
* and lose the rest. Kept here so the swap to merged groups carries them across.
|
|
45
|
+
*/
|
|
46
|
+
instancedParts = [];
|
|
39
47
|
/**
|
|
40
48
|
* Coarse levels that have arrived and not yet been uploaded, in arrival order.
|
|
41
49
|
*
|
|
@@ -96,6 +104,10 @@ export class DrftLoader {
|
|
|
96
104
|
* hand on is invisible to every test of either side.
|
|
97
105
|
*/
|
|
98
106
|
loadedNodes = [];
|
|
107
|
+
loadedLights = [];
|
|
108
|
+
loadedFields = [];
|
|
109
|
+
/** The fields placed, built once the fit and the fields are both known. See `fields`. */
|
|
110
|
+
placedFields = null;
|
|
99
111
|
loadedSkins = [];
|
|
100
112
|
loadedClips = [];
|
|
101
113
|
/**
|
|
@@ -159,6 +171,27 @@ export class DrftLoader {
|
|
|
159
171
|
get nodes() {
|
|
160
172
|
return this.loadedNodes;
|
|
161
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* The lights the file was authored with, in glTF's units, or empty. They arrive ahead of the
|
|
176
|
+
* geometry. What a candela is in this scene's light units is the caller's decision.
|
|
177
|
+
*/
|
|
178
|
+
get lights() {
|
|
179
|
+
return this.loadedLights;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* The distance fields the file carries, each where it stands: at the loader's fit, and once a
|
|
183
|
+
* copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
|
|
184
|
+
* a file carrying none. A scene declares these to `addDistanceField` every frame, with the colour
|
|
185
|
+
* of the surface each covers. See `fieldPlacement.ts`.
|
|
186
|
+
*/
|
|
187
|
+
get fields() {
|
|
188
|
+
if (this.placedFields === null) {
|
|
189
|
+
if (this.loadedFields.length === 0 || this.fit === null)
|
|
190
|
+
return [];
|
|
191
|
+
this.placedFields = placeFields(this.loadedFields, this.fit, this.loadedInstances);
|
|
192
|
+
}
|
|
193
|
+
return this.placedFields;
|
|
194
|
+
}
|
|
162
195
|
/** Skins the file carried, in the order their chunks appeared. Empty for a file with none. */
|
|
163
196
|
get skins() {
|
|
164
197
|
return this.loadedSkins;
|
|
@@ -313,6 +346,17 @@ export class DrftLoader {
|
|
|
313
346
|
onNodes: (nodes) => {
|
|
314
347
|
this.loadedNodes = nodes;
|
|
315
348
|
},
|
|
349
|
+
onLights: (lights) => {
|
|
350
|
+
this.loadedLights = lights;
|
|
351
|
+
},
|
|
352
|
+
onFields: (fields) => {
|
|
353
|
+
this.loadedFields = fields;
|
|
354
|
+
this.placedFields = null;
|
|
355
|
+
},
|
|
356
|
+
onInstances: (groups) => {
|
|
357
|
+
for (const group of groups)
|
|
358
|
+
this.loadedInstances.set(group.mesh, group.transforms);
|
|
359
|
+
},
|
|
316
360
|
onSkin: (skin) => {
|
|
317
361
|
this.loadedSkins.push(skin);
|
|
318
362
|
},
|
|
@@ -392,7 +436,11 @@ export class DrftLoader {
|
|
|
392
436
|
const withMorph = morph === undefined
|
|
393
437
|
? mesh
|
|
394
438
|
: { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
|
|
395
|
-
this.queue.push({
|
|
439
|
+
this.queue.push({
|
|
440
|
+
mesh: withMorph,
|
|
441
|
+
material,
|
|
442
|
+
instances: this.loadedInstances.get(ordinal) ?? null,
|
|
443
|
+
});
|
|
396
444
|
this.arrived++;
|
|
397
445
|
this.set({ phase: 'geometry', partsDone: this.arrived });
|
|
398
446
|
},
|
|
@@ -524,7 +572,7 @@ export class DrftLoader {
|
|
|
524
572
|
const next = this.queue.shift();
|
|
525
573
|
if (next === undefined)
|
|
526
574
|
break;
|
|
527
|
-
if (this.uploadOne(next.mesh, next.material))
|
|
575
|
+
if (this.uploadOne(next.mesh, next.material, next.instances))
|
|
528
576
|
changed = true;
|
|
529
577
|
begun++;
|
|
530
578
|
}
|
|
@@ -583,10 +631,24 @@ export class DrftLoader {
|
|
|
583
631
|
if (this.mergeQueue.length === 0) {
|
|
584
632
|
this.merged = true;
|
|
585
633
|
this.swapInMerged();
|
|
586
|
-
this.set({ phase: 'ready', fraction: 1 });
|
|
587
634
|
changed = true;
|
|
588
635
|
}
|
|
589
636
|
}
|
|
637
|
+
/*
|
|
638
|
+
* **Ready once the picture is right, not once the parts are**, which is what `DrftLoadProgress`
|
|
639
|
+
* has promised consumers since `imagesDone` was corrected, and what this did not do: it said
|
|
640
|
+
* ready at the merge, with images still decoding behind it. A consumer that baked its light
|
|
641
|
+
* probes on `ready` baked them against untextured stone, and came out overexposed on exactly the
|
|
642
|
+
* runs where the decodes lost the race. Every image is counted whether it decoded or failed, so
|
|
643
|
+
* this cannot wait for ever on a bad one.
|
|
644
|
+
*/
|
|
645
|
+
if (this.merged &&
|
|
646
|
+
this.state.phase !== 'ready' &&
|
|
647
|
+
this.imageQueue.length === 0 &&
|
|
648
|
+
this.state.imagesDone >= this.state.imagesTotal) {
|
|
649
|
+
this.set({ phase: 'ready', fraction: 1 });
|
|
650
|
+
changed = true;
|
|
651
|
+
}
|
|
590
652
|
return changed;
|
|
591
653
|
}
|
|
592
654
|
dispose() {
|
|
@@ -612,6 +674,12 @@ export class DrftLoader {
|
|
|
612
674
|
meshes.add(part.mesh);
|
|
613
675
|
for (const mesh of this.singles)
|
|
614
676
|
meshes.add(mesh);
|
|
677
|
+
for (const part of this.instancedParts) {
|
|
678
|
+
meshes.add(part.mesh);
|
|
679
|
+
if (part.instances !== null)
|
|
680
|
+
this.renderer.disposeInstanced(part.instances.batch);
|
|
681
|
+
}
|
|
682
|
+
this.instancedParts.length = 0;
|
|
615
683
|
if (this.outlineMesh !== null)
|
|
616
684
|
meshes.add(this.outlineMesh);
|
|
617
685
|
for (const mesh of meshes)
|
|
@@ -672,7 +740,10 @@ export class DrftLoader {
|
|
|
672
740
|
metallicScale: 1,
|
|
673
741
|
occlusionStrength: 0,
|
|
674
742
|
cutout: 0,
|
|
743
|
+
blend: false,
|
|
744
|
+
doubleSided: false,
|
|
675
745
|
reveal: this.outlinePart?.reveal ?? 0,
|
|
746
|
+
instances: null,
|
|
676
747
|
};
|
|
677
748
|
const at = this.outlinePart === null ? -1 : this.revealed.indexOf(this.outlinePart);
|
|
678
749
|
/* First in the list, so it is drawn before the parts that stand in front of it. */
|
|
@@ -688,13 +759,14 @@ export class DrftLoader {
|
|
|
688
759
|
return true;
|
|
689
760
|
}
|
|
690
761
|
/** One part, transformed, dressed and uploaded. Returns whether it landed. */
|
|
691
|
-
uploadOne(mesh, material) {
|
|
762
|
+
uploadOne(mesh, material, instances) {
|
|
692
763
|
const fit = this.fit;
|
|
693
764
|
if (fit === null)
|
|
694
765
|
return false;
|
|
695
766
|
const dressed = this.options.transform?.(mesh, material) ?? mesh;
|
|
696
|
-
|
|
697
|
-
|
|
767
|
+
/* The file's mesh itself where the fit is the identity, with its tangents and every other
|
|
768
|
+
attribute it carried. It went through a builder here, which copied it and dropped them. */
|
|
769
|
+
const placed = placeMesh(dressed, fit.x, fit.y, fit.z, fit.scale);
|
|
698
770
|
const override = this.options.surface?.(material);
|
|
699
771
|
/*
|
|
700
772
|
* **The resolution and the key both live in `drawKey.ts`, and that is the point of them.**
|
|
@@ -705,11 +777,27 @@ export class DrftLoader {
|
|
|
705
777
|
* was answering it with a copy of this code that went stale when `cutout` joined the key.
|
|
706
778
|
*/
|
|
707
779
|
const g = resolveDrawGrouping(material, override);
|
|
708
|
-
const { albedo, orm, normal, emissive, opacity, reflectivity, roughnessScale, metallicScale, occlusionStrength, cutout, } = g;
|
|
780
|
+
const { albedo, orm, normal, emissive, opacity, reflectivity, roughnessScale, metallicScale, occlusionStrength, cutout, blend, doubleSided, } = g;
|
|
709
781
|
/*
|
|
710
782
|
* Added to its merged group here, while there is a budget governing how much of this happens
|
|
711
783
|
* in one frame.
|
|
712
784
|
*/
|
|
785
|
+
if (instances !== null) {
|
|
786
|
+
return this.uploadInstanced(placed, instances, fit, {
|
|
787
|
+
albedo,
|
|
788
|
+
orm,
|
|
789
|
+
normal,
|
|
790
|
+
emissive,
|
|
791
|
+
opacity,
|
|
792
|
+
reflectivity,
|
|
793
|
+
roughnessScale,
|
|
794
|
+
metallicScale,
|
|
795
|
+
occlusionStrength,
|
|
796
|
+
cutout,
|
|
797
|
+
blend,
|
|
798
|
+
doubleSided,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
713
801
|
const key = drawKeyOf(material, override);
|
|
714
802
|
let group = this.groups.get(key);
|
|
715
803
|
if (group === undefined) {
|
|
@@ -724,36 +812,32 @@ export class DrftLoader {
|
|
|
724
812
|
metallicScale,
|
|
725
813
|
occlusionStrength,
|
|
726
814
|
cutout,
|
|
727
|
-
|
|
815
|
+
blend,
|
|
816
|
+
doubleSided,
|
|
817
|
+
members: [],
|
|
728
818
|
};
|
|
729
819
|
this.groups.set(key, group);
|
|
730
820
|
}
|
|
731
821
|
/* Not merged, for the reason above: a group's deltas would deform its neighbours. */
|
|
732
|
-
if (dressed.morphTargets === undefined)
|
|
733
|
-
group.
|
|
734
|
-
}
|
|
822
|
+
if (dressed.morphTargets === undefined)
|
|
823
|
+
group.members.push(placed);
|
|
735
824
|
try {
|
|
736
825
|
/*
|
|
737
|
-
* **A morphed part
|
|
738
|
-
*
|
|
739
|
-
*
|
|
740
|
-
* part built through it arrives on the GPU with its targets dropped. Merging is worse than
|
|
741
|
-
* that rather than merely lossy: a merged group concatenates vertices from several parts,
|
|
742
|
-
* and one part's deltas applied across that buffer would deform whatever geometry happened
|
|
743
|
-
* to follow it.
|
|
826
|
+
* **A morphed part is never merged.** A merged group concatenates vertices from several
|
|
827
|
+
* parts, and one part's deltas applied across that buffer would deform whatever geometry
|
|
828
|
+
* happened to follow it.
|
|
744
829
|
*
|
|
745
830
|
* So the deltas are scaled by the fit and handed straight to `createMesh`. **Scaled and not
|
|
746
831
|
* translated** — a delta is a displacement, and moving it by the fit's offset would drag
|
|
747
832
|
* every vertex toward the origin of the model rather than deform it in place.
|
|
748
833
|
*/
|
|
749
|
-
const built = single.build();
|
|
750
834
|
const morphable = dressed.morphTargets !== undefined && dressed.morphTargetCount !== undefined
|
|
751
835
|
? {
|
|
752
|
-
...
|
|
836
|
+
...placed,
|
|
753
837
|
morphTargets: dressed.morphTargets.map((delta) => delta * fit.scale),
|
|
754
838
|
morphTargetCount: dressed.morphTargetCount,
|
|
755
839
|
}
|
|
756
|
-
:
|
|
840
|
+
: placed;
|
|
757
841
|
const uploaded = this.renderer.createMesh(morphable);
|
|
758
842
|
this.singles.push(uploaded);
|
|
759
843
|
const part = {
|
|
@@ -768,7 +852,10 @@ export class DrftLoader {
|
|
|
768
852
|
metallicScale,
|
|
769
853
|
occlusionStrength,
|
|
770
854
|
cutout,
|
|
855
|
+
blend,
|
|
856
|
+
doubleSided,
|
|
771
857
|
reveal: 0,
|
|
858
|
+
instances: null,
|
|
772
859
|
};
|
|
773
860
|
this.revealed.push(part);
|
|
774
861
|
if (this.options.keepReveal === true)
|
|
@@ -789,13 +876,65 @@ export class DrftLoader {
|
|
|
789
876
|
* and differ in blend: merging on the image alone means one cannot be made translucent without
|
|
790
877
|
* taking the other with it.
|
|
791
878
|
*/
|
|
879
|
+
/**
|
|
880
|
+
* A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
|
|
881
|
+
*
|
|
882
|
+
* **The fit is conjugated into each placement.** The prototype is uploaded with the fit already
|
|
883
|
+
* applied, as every part is, so a copy's matrix M becomes F·M·F⁻¹: the same turn, and a translation
|
|
884
|
+
* of t − R·t + s·u for the fit's scale s and offset t and the copy's own translation u. A fitted
|
|
885
|
+
* model's copies then land where its geometry does, and are not scaled twice.
|
|
886
|
+
*/
|
|
887
|
+
uploadInstanced(prototype, instances, fit, surface) {
|
|
888
|
+
try {
|
|
889
|
+
const uploaded = this.renderer.createMesh(prototype);
|
|
890
|
+
const count = instances.length / 16;
|
|
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;
|
|
918
|
+
this.renderer.uploadInstanced(batch, data);
|
|
919
|
+
const part = { mesh: uploaded, ...surface, reveal: 0, instances: { batch, data } };
|
|
920
|
+
this.revealed.push(part);
|
|
921
|
+
this.instancedParts.push(part);
|
|
922
|
+
if (this.options.keepReveal === true)
|
|
923
|
+
this.arrivals.push(part);
|
|
924
|
+
return true;
|
|
925
|
+
}
|
|
926
|
+
catch (error) {
|
|
927
|
+
console.warn('DrftLoader: an instanced part would not upload and was skipped', error);
|
|
928
|
+
return false;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
792
931
|
buildGroup(key) {
|
|
793
932
|
const group = this.groups.get(key);
|
|
794
933
|
if (group === undefined)
|
|
795
934
|
return;
|
|
796
935
|
try {
|
|
797
936
|
this.mergedParts.push({
|
|
798
|
-
mesh: this.renderer.createMesh(group.
|
|
937
|
+
mesh: this.renderer.createMesh(concatMeshes(group.members)),
|
|
799
938
|
albedo: group.albedo,
|
|
800
939
|
orm: group.orm,
|
|
801
940
|
normal: group.normal,
|
|
@@ -806,7 +945,10 @@ export class DrftLoader {
|
|
|
806
945
|
metallicScale: group.metallicScale,
|
|
807
946
|
occlusionStrength: group.occlusionStrength,
|
|
808
947
|
cutout: group.cutout,
|
|
948
|
+
blend: group.blend,
|
|
949
|
+
doubleSided: group.doubleSided,
|
|
809
950
|
reveal: 1,
|
|
951
|
+
instances: null,
|
|
810
952
|
});
|
|
811
953
|
}
|
|
812
954
|
catch (error) {
|
|
@@ -825,11 +967,13 @@ export class DrftLoader {
|
|
|
825
967
|
*/
|
|
826
968
|
swapInMerged() {
|
|
827
969
|
/*
|
|
828
|
-
* Nothing merged means every
|
|
829
|
-
* standing between a viewer and an empty room. Keeping it is the honest
|
|
830
|
-
* incomplete rather than nothing at all.
|
|
970
|
+
* Nothing merged and nothing instanced means every part failed to upload, and then the outline
|
|
971
|
+
* is the only thing standing between a viewer and an empty room. Keeping it is the honest
|
|
972
|
+
* state: something incomplete rather than nothing at all. **Instanced parts count**: a file
|
|
973
|
+
* whose every mesh is instanced merges nothing, and a guard on merged groups alone kept its
|
|
974
|
+
* outline on screen over the finished model for ever.
|
|
831
975
|
*/
|
|
832
|
-
if (this.mergedParts.length === 0)
|
|
976
|
+
if (this.mergedParts.length === 0 && this.instancedParts.length === 0)
|
|
833
977
|
return;
|
|
834
978
|
/*
|
|
835
979
|
* The outline leaves in the same frame the model arrives. Two representations of one object
|
|
@@ -847,6 +991,11 @@ export class DrftLoader {
|
|
|
847
991
|
this.revealed.length = 0;
|
|
848
992
|
for (const part of this.mergedParts)
|
|
849
993
|
this.revealed.push(part);
|
|
994
|
+
/* Never merged, so carried across as they are: see `uploadInstanced`. */
|
|
995
|
+
for (const part of this.instancedParts) {
|
|
996
|
+
part.reveal = 1;
|
|
997
|
+
this.revealed.push(part);
|
|
998
|
+
}
|
|
850
999
|
this.mergedParts.length = 0;
|
|
851
1000
|
/* Solid first, blended last, which is the whole of the sort: a translucent surface needs the
|
|
852
1001
|
world behind it already drawn to blend against. */
|
|
@@ -933,7 +1082,9 @@ export function imageTypeFor(codec) {
|
|
|
933
1082
|
return 'image/png';
|
|
934
1083
|
if (codec === CODEC_WEBP)
|
|
935
1084
|
return 'image/webp';
|
|
936
|
-
|
|
1085
|
+
if (codec === CODEC_JPEG)
|
|
1086
|
+
return 'image/jpeg';
|
|
1087
|
+
return null;
|
|
937
1088
|
}
|
|
938
1089
|
/**
|
|
939
1090
|
* Whether a codec is decoded from its own bytes rather than through an image decoder.
|
|
@@ -1006,7 +1157,12 @@ async function decodeImage(texture, longestSide) {
|
|
|
1006
1157
|
}
|
|
1007
1158
|
return await createImageBitmap(new ImageData(new Uint8ClampedArray(texture.bytes.slice(0, pixels * 4)), texture.width, texture.height), AS_AUTHORED);
|
|
1008
1159
|
}
|
|
1009
|
-
const
|
|
1160
|
+
const type = imageTypeFor(texture.codec);
|
|
1161
|
+
if (type === null) {
|
|
1162
|
+
throw new DrftError(`texture codec ${texture.codec} is not one this loader can decode; bake it again with a ` +
|
|
1163
|
+
'current baker');
|
|
1164
|
+
}
|
|
1165
|
+
const blob = new Blob([texture.bytes.slice()], { type });
|
|
1010
1166
|
if (longestSide === undefined)
|
|
1011
1167
|
return await createImageBitmap(blob, AS_AUTHORED);
|
|
1012
1168
|
/*
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { DrftSdfvEntry } from '@driftengine/drft';
|
|
2
|
+
/** One field, where it stands. */
|
|
3
|
+
export interface DrftFieldPlacement {
|
|
4
|
+
/** The field, in the object space of what it covers. It is a `FieldSource` as it stands. */
|
|
5
|
+
readonly source: DrftSdfvEntry;
|
|
6
|
+
/** World from object: the fit, and for a copied mesh that copy's placement inside it. */
|
|
7
|
+
readonly model: Float32Array;
|
|
8
|
+
/** The mesh the field covers, or `SDFV_WHOLE_FILE` for all of the file's static geometry. */
|
|
9
|
+
readonly mesh: number;
|
|
10
|
+
}
|
|
11
|
+
/** The loader's fit: a uniform scale, then a translation. */
|
|
12
|
+
export interface FieldFit {
|
|
13
|
+
readonly scale: number;
|
|
14
|
+
readonly x: number;
|
|
15
|
+
readonly y: number;
|
|
16
|
+
readonly z: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Every field, placed. A whole-file field and the field of a mesh drawn once stand at the fit; the
|
|
20
|
+
* field of a copied mesh stands once a copy, at the fit composed with that copy's matrix.
|
|
21
|
+
*/
|
|
22
|
+
export declare function placeFields(entries: readonly DrftSdfvEntry[], fit: FieldFit, instances: ReadonlyMap<number, Float32Array>): DrftFieldPlacement[];
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a file's distance fields stand once its model has been placed: the loader's fit, and each
|
|
3
|
+
* copy's own placement for a mesh the file draws many times.
|
|
4
|
+
*
|
|
5
|
+
* **The loader's to say, because the loader moved the model.** A field is in the file's own space,
|
|
6
|
+
* and a loader that fits a model to a footprint has scaled and moved every vertex; a field handed
|
|
7
|
+
* on as it was read would trace light through the model where it was authored rather than where it
|
|
8
|
+
* is drawn. The fit is a uniform scale and a translation, which is also what keeps a distance a
|
|
9
|
+
* distance: `composeGlobalField` refuses a non-uniform scale for that reason.
|
|
10
|
+
*/
|
|
11
|
+
import { SDFV_WHOLE_FILE } from '@driftengine/drft';
|
|
12
|
+
/**
|
|
13
|
+
* Every field, placed. A whole-file field and the field of a mesh drawn once stand at the fit; the
|
|
14
|
+
* field of a copied mesh stands once a copy, at the fit composed with that copy's matrix.
|
|
15
|
+
*/
|
|
16
|
+
export function placeFields(entries, fit, instances) {
|
|
17
|
+
const placed = [];
|
|
18
|
+
for (const source of entries) {
|
|
19
|
+
const copies = source.mesh === SDFV_WHOLE_FILE ? undefined : instances.get(source.mesh);
|
|
20
|
+
if (copies === undefined) {
|
|
21
|
+
placed.push({ source, model: fitted(fit, null, 0), mesh: source.mesh });
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
for (let at = 0; at + 16 <= copies.length; at += 16) {
|
|
25
|
+
placed.push({ source, model: fitted(fit, copies, at), mesh: source.mesh });
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return placed;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The fit, or the fit times the column-major matrix at `copies[at]`. The fit is `T · S` with a
|
|
32
|
+
* uniform `S`, so each column of the product is the copy's column scaled, plus the translation
|
|
33
|
+
* times that column's last row.
|
|
34
|
+
*/
|
|
35
|
+
function fitted(fit, copies, at) {
|
|
36
|
+
const out = new Float32Array(16);
|
|
37
|
+
for (let column = 0; column < 4; column++) {
|
|
38
|
+
const base = column * 4;
|
|
39
|
+
const m0 = copies === null ? (column === 0 ? 1 : 0) : copies[at + base];
|
|
40
|
+
const m1 = copies === null ? (column === 1 ? 1 : 0) : copies[at + base + 1];
|
|
41
|
+
const m2 = copies === null ? (column === 2 ? 1 : 0) : copies[at + base + 2];
|
|
42
|
+
const m3 = copies === null ? (column === 3 ? 1 : 0) : copies[at + base + 3];
|
|
43
|
+
out[base] = fit.scale * m0 + fit.x * m3;
|
|
44
|
+
out[base + 1] = fit.scale * m1 + fit.y * m3;
|
|
45
|
+
out[base + 2] = fit.scale * m2 + fit.z * m3;
|
|
46
|
+
out[base + 3] = m3;
|
|
47
|
+
}
|
|
48
|
+
return out;
|
|
49
|
+
}
|
package/dist/gltf.d.ts
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* scene graph is flattened here: each primitive's vertices are transformed by its node's
|
|
17
17
|
* world matrix on the way through, and the graph does not survive into the engine.
|
|
18
18
|
*/
|
|
19
|
-
import type { DrftNode, MeshData } from '@driftengine/drft';
|
|
19
|
+
import type { DrftLight, DrftNode, MeshData } from '@driftengine/drft';
|
|
20
20
|
import type { DrftMaterial } from '@driftengine/drft';
|
|
21
21
|
import type { AssetReference } from './assetPath.ts';
|
|
22
22
|
interface GltfAccessor {
|
|
@@ -109,6 +109,7 @@ interface GltfMaterial {
|
|
|
109
109
|
emissiveFactor?: number[];
|
|
110
110
|
/** `OPAQUE`, `MASK` or `BLEND`. Absent means opaque, which the specification states. */
|
|
111
111
|
alphaMode?: string;
|
|
112
|
+
doubleSided?: boolean;
|
|
112
113
|
alphaCutoff?: number;
|
|
113
114
|
name?: string;
|
|
114
115
|
/**
|
|
@@ -134,6 +135,22 @@ interface GltfImage {
|
|
|
134
135
|
interface GltfTexture {
|
|
135
136
|
source?: number;
|
|
136
137
|
sampler?: number;
|
|
138
|
+
/**
|
|
139
|
+
* Where an image in a newer codec is named. glTF's own `source` is the fallback, and a file that
|
|
140
|
+
* *requires* the extension usually carries none, so the extension's index is the only route to
|
|
141
|
+
* the picture.
|
|
142
|
+
*/
|
|
143
|
+
extensions?: {
|
|
144
|
+
EXT_texture_webp?: {
|
|
145
|
+
source?: number;
|
|
146
|
+
};
|
|
147
|
+
EXT_texture_avif?: {
|
|
148
|
+
source?: number;
|
|
149
|
+
};
|
|
150
|
+
KHR_texture_basisu?: {
|
|
151
|
+
source?: number;
|
|
152
|
+
};
|
|
153
|
+
};
|
|
137
154
|
}
|
|
138
155
|
interface GltfNode {
|
|
139
156
|
mesh?: number;
|
|
@@ -145,6 +162,24 @@ interface GltfNode {
|
|
|
145
162
|
rotation?: number[];
|
|
146
163
|
scale?: number[];
|
|
147
164
|
name?: string;
|
|
165
|
+
/** Which of the document's punctual lights hangs on this node. */
|
|
166
|
+
extensions?: {
|
|
167
|
+
KHR_lights_punctual?: {
|
|
168
|
+
light?: number;
|
|
169
|
+
};
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
/** One `KHR_lights_punctual` light, as the extension states it. */
|
|
173
|
+
interface GltfPunctualLight {
|
|
174
|
+
type?: string;
|
|
175
|
+
name?: string;
|
|
176
|
+
color?: number[];
|
|
177
|
+
intensity?: number;
|
|
178
|
+
range?: number;
|
|
179
|
+
spot?: {
|
|
180
|
+
innerConeAngle?: number;
|
|
181
|
+
outerConeAngle?: number;
|
|
182
|
+
};
|
|
148
183
|
}
|
|
149
184
|
export interface GltfDocument {
|
|
150
185
|
asset?: {
|
|
@@ -169,8 +204,17 @@ export interface GltfDocument {
|
|
|
169
204
|
materials?: GltfMaterial[];
|
|
170
205
|
images?: GltfImage[];
|
|
171
206
|
textures?: GltfTexture[];
|
|
207
|
+
/** Extensions the file states it cannot be read correctly without. */
|
|
208
|
+
extensionsRequired?: string[];
|
|
209
|
+
extensionsUsed?: string[];
|
|
172
210
|
skins?: GltfSkinDef[];
|
|
173
211
|
animations?: GltfAnimationDef[];
|
|
212
|
+
/** The document's own lights, which nodes name by index. */
|
|
213
|
+
extensions?: {
|
|
214
|
+
KHR_lights_punctual?: {
|
|
215
|
+
lights?: GltfPunctualLight[];
|
|
216
|
+
};
|
|
217
|
+
};
|
|
174
218
|
}
|
|
175
219
|
/** A skin, as glTF declares one. `skeleton` is a hint this reader does not need and ignores. */
|
|
176
220
|
export interface GltfSkinDef {
|
|
@@ -238,5 +282,6 @@ export declare function gltfToMeshes(doc: GltfDocument, buffers: readonly Uint8A
|
|
|
238
282
|
materials: DrftMaterial[];
|
|
239
283
|
textures: AssetReference[];
|
|
240
284
|
nodes: DrftNode[];
|
|
285
|
+
lights: DrftLight[];
|
|
241
286
|
};
|
|
242
287
|
export {};
|