@driftengine/assets 4.3.0 → 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/src/drftLoader.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
/** Loading a `.drft` into drawable parts progressively, without stalling the frame. */
|
|
2
2
|
|
|
3
3
|
import type { MeshData } from '@driftengine/drft';
|
|
4
|
-
import type { Mesh } from '@driftengine/core';
|
|
5
4
|
import type { MeshHandle, RendererApi } from '@driftengine/core';
|
|
6
5
|
import type { SurfaceTextureHandle } from '@driftengine/core';
|
|
7
|
-
import { MeshBuilder } from '@driftengine/core';
|
|
6
|
+
import { MeshBuilder, concatMeshes, createMeshInstances, placeMesh } from '@driftengine/core';
|
|
8
7
|
import type { DrftMaterial } from '@driftengine/drft';
|
|
8
|
+
import type { DrftSdfvEntry } from '@driftengine/drft';
|
|
9
|
+
import { placeFields } from './fieldPlacement.ts';
|
|
10
|
+
import type { DrftFieldPlacement } from './fieldPlacement.ts';
|
|
9
11
|
import type { DrftTexture } from '@driftengine/drft';
|
|
10
12
|
import { TextureSet, textureColorSpaces } from './drftTextures.ts';
|
|
11
13
|
import { drawKeyOf, resolveDrawGrouping } from './drawKey.ts';
|
|
12
|
-
import {
|
|
13
|
-
import
|
|
14
|
+
import type { DrawSurfaceOverride } from './drawKey.ts';
|
|
15
|
+
import {
|
|
16
|
+
streamDrft,
|
|
17
|
+
CODEC_JPEG,
|
|
18
|
+
CODEC_PNG,
|
|
19
|
+
CODEC_RAW,
|
|
20
|
+
CODEC_WEBP,
|
|
21
|
+
DrftError,
|
|
22
|
+
} from '@driftengine/drft';
|
|
23
|
+
import type { AnimationClip, DrftLight, DrftMorph, DrftNode, DrftSkin } from '@driftengine/drft';
|
|
14
24
|
import type { DrftLoadProgress, DrftPart } from './loadProgress.ts';
|
|
15
25
|
import {
|
|
16
26
|
DEFAULT_REVEAL_SEC,
|
|
@@ -112,10 +122,8 @@ export interface DrftLoaderOptions {
|
|
|
112
122
|
* can have its paint decided here, per material, without the loader knowing what a car is.
|
|
113
123
|
*/
|
|
114
124
|
readonly transform?: (mesh: MeshData, material: DrftMaterial | undefined) => MeshData;
|
|
115
|
-
/** Per-material opacity and
|
|
116
|
-
readonly surface?: (
|
|
117
|
-
material: DrftMaterial | undefined,
|
|
118
|
-
) => { readonly opacity?: number; readonly reflectivity?: number } | undefined;
|
|
125
|
+
/** Per-material opacity, reflectivity and metalness overrides, for the same reason as `transform`. */
|
|
126
|
+
readonly surface?: (material: DrftMaterial | undefined) => DrawSurfaceOverride | undefined;
|
|
119
127
|
/**
|
|
120
128
|
* Draw a coarse whole model while the parts arrive, where the file carries one.
|
|
121
129
|
*
|
|
@@ -203,7 +211,19 @@ export class DrftLoader {
|
|
|
203
211
|
private readonly options: DrftLoaderOptions;
|
|
204
212
|
private readonly revealed: DrftPart[] = [];
|
|
205
213
|
/** Arrived, not yet uploaded. Drained by `update` at the budget. */
|
|
206
|
-
private readonly queue: {
|
|
214
|
+
private readonly queue: {
|
|
215
|
+
mesh: MeshData;
|
|
216
|
+
material: DrftMaterial | undefined;
|
|
217
|
+
/** Sixteen floats a copy when the file draws this mesh many times, else null. */
|
|
218
|
+
instances: Float32Array | null;
|
|
219
|
+
}[] = [];
|
|
220
|
+
/** `INST`'s placements by mesh ordinal, in hand before the meshes they place arrive. */
|
|
221
|
+
private readonly loadedInstances = new Map<number, Float32Array>();
|
|
222
|
+
/**
|
|
223
|
+
* The instanced parts, which the merge leaves alone: merging would bake one copy into a group
|
|
224
|
+
* and lose the rest. Kept here so the swap to merged groups carries them across.
|
|
225
|
+
*/
|
|
226
|
+
private readonly instancedParts: DrftPart[] = [];
|
|
207
227
|
/**
|
|
208
228
|
* Coarse levels that have arrived and not yet been uploaded, in arrival order.
|
|
209
229
|
*
|
|
@@ -238,7 +258,10 @@ export class DrftLoader {
|
|
|
238
258
|
metallicScale: number;
|
|
239
259
|
occlusionStrength: number;
|
|
240
260
|
cutout: number;
|
|
241
|
-
|
|
261
|
+
blend: boolean;
|
|
262
|
+
doubleSided: boolean;
|
|
263
|
+
/** The group's parts, placed, joined once when the group is built. See `concatMeshes`. */
|
|
264
|
+
members: MeshData[];
|
|
242
265
|
}
|
|
243
266
|
>();
|
|
244
267
|
/** Group keys still to be built and uploaded, drained one per frame after the stream ends. */
|
|
@@ -279,6 +302,10 @@ export class DrftLoader {
|
|
|
279
302
|
* hand on is invisible to every test of either side.
|
|
280
303
|
*/
|
|
281
304
|
private loadedNodes: readonly DrftNode[] = [];
|
|
305
|
+
private loadedLights: readonly DrftLight[] = [];
|
|
306
|
+
private loadedFields: readonly DrftSdfvEntry[] = [];
|
|
307
|
+
/** The fields placed, built once the fit and the fields are both known. See `fields`. */
|
|
308
|
+
private placedFields: readonly DrftFieldPlacement[] | null = null;
|
|
282
309
|
private readonly loadedSkins: DrftSkin[] = [];
|
|
283
310
|
private readonly loadedClips: AnimationClip[] = [];
|
|
284
311
|
/**
|
|
@@ -347,6 +374,28 @@ export class DrftLoader {
|
|
|
347
374
|
return this.loadedNodes;
|
|
348
375
|
}
|
|
349
376
|
|
|
377
|
+
/**
|
|
378
|
+
* The lights the file was authored with, in glTF's units, or empty. They arrive ahead of the
|
|
379
|
+
* geometry. What a candela is in this scene's light units is the caller's decision.
|
|
380
|
+
*/
|
|
381
|
+
get lights(): readonly DrftLight[] {
|
|
382
|
+
return this.loadedLights;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* The distance fields the file carries, each where it stands: at the loader's fit, and once a
|
|
387
|
+
* copy for a mesh drawn many times. Empty until the fields and the fit have both arrived, and for
|
|
388
|
+
* a file carrying none. A scene declares these to `addDistanceField` every frame, with the colour
|
|
389
|
+
* of the surface each covers. See `fieldPlacement.ts`.
|
|
390
|
+
*/
|
|
391
|
+
get fields(): readonly DrftFieldPlacement[] {
|
|
392
|
+
if (this.placedFields === null) {
|
|
393
|
+
if (this.loadedFields.length === 0 || this.fit === null) return [];
|
|
394
|
+
this.placedFields = placeFields(this.loadedFields, this.fit, this.loadedInstances);
|
|
395
|
+
}
|
|
396
|
+
return this.placedFields;
|
|
397
|
+
}
|
|
398
|
+
|
|
350
399
|
/** Skins the file carried, in the order their chunks appeared. Empty for a file with none. */
|
|
351
400
|
get skins(): readonly DrftSkin[] {
|
|
352
401
|
return this.loadedSkins;
|
|
@@ -509,6 +558,16 @@ export class DrftLoader {
|
|
|
509
558
|
onNodes: (nodes) => {
|
|
510
559
|
this.loadedNodes = nodes;
|
|
511
560
|
},
|
|
561
|
+
onLights: (lights) => {
|
|
562
|
+
this.loadedLights = lights;
|
|
563
|
+
},
|
|
564
|
+
onFields: (fields) => {
|
|
565
|
+
this.loadedFields = fields;
|
|
566
|
+
this.placedFields = null;
|
|
567
|
+
},
|
|
568
|
+
onInstances: (groups) => {
|
|
569
|
+
for (const group of groups) this.loadedInstances.set(group.mesh, group.transforms);
|
|
570
|
+
},
|
|
512
571
|
onSkin: (skin) => {
|
|
513
572
|
this.loadedSkins.push(skin);
|
|
514
573
|
},
|
|
@@ -591,7 +650,11 @@ export class DrftLoader {
|
|
|
591
650
|
morph === undefined
|
|
592
651
|
? mesh
|
|
593
652
|
: { ...mesh, morphTargets: morph.deltas, morphTargetCount: morph.targetCount };
|
|
594
|
-
this.queue.push({
|
|
653
|
+
this.queue.push({
|
|
654
|
+
mesh: withMorph,
|
|
655
|
+
material,
|
|
656
|
+
instances: this.loadedInstances.get(ordinal) ?? null,
|
|
657
|
+
});
|
|
595
658
|
this.arrived++;
|
|
596
659
|
this.set({ phase: 'geometry', partsDone: this.arrived });
|
|
597
660
|
},
|
|
@@ -729,7 +792,7 @@ export class DrftLoader {
|
|
|
729
792
|
) {
|
|
730
793
|
const next = this.queue.shift();
|
|
731
794
|
if (next === undefined) break;
|
|
732
|
-
if (this.uploadOne(next.mesh, next.material)) changed = true;
|
|
795
|
+
if (this.uploadOne(next.mesh, next.material, next.instances)) changed = true;
|
|
733
796
|
begun++;
|
|
734
797
|
}
|
|
735
798
|
|
|
@@ -792,10 +855,27 @@ export class DrftLoader {
|
|
|
792
855
|
if (this.mergeQueue.length === 0) {
|
|
793
856
|
this.merged = true;
|
|
794
857
|
this.swapInMerged();
|
|
795
|
-
this.set({ phase: 'ready', fraction: 1 });
|
|
796
858
|
changed = true;
|
|
797
859
|
}
|
|
798
860
|
}
|
|
861
|
+
|
|
862
|
+
/*
|
|
863
|
+
* **Ready once the picture is right, not once the parts are**, which is what `DrftLoadProgress`
|
|
864
|
+
* has promised consumers since `imagesDone` was corrected, and what this did not do: it said
|
|
865
|
+
* ready at the merge, with images still decoding behind it. A consumer that baked its light
|
|
866
|
+
* probes on `ready` baked them against untextured stone, and came out overexposed on exactly the
|
|
867
|
+
* runs where the decodes lost the race. Every image is counted whether it decoded or failed, so
|
|
868
|
+
* this cannot wait for ever on a bad one.
|
|
869
|
+
*/
|
|
870
|
+
if (
|
|
871
|
+
this.merged &&
|
|
872
|
+
this.state.phase !== 'ready' &&
|
|
873
|
+
this.imageQueue.length === 0 &&
|
|
874
|
+
this.state.imagesDone >= this.state.imagesTotal
|
|
875
|
+
) {
|
|
876
|
+
this.set({ phase: 'ready', fraction: 1 });
|
|
877
|
+
changed = true;
|
|
878
|
+
}
|
|
799
879
|
return changed;
|
|
800
880
|
}
|
|
801
881
|
|
|
@@ -816,6 +896,11 @@ export class DrftLoader {
|
|
|
816
896
|
for (const part of this.finished) meshes.add(part.mesh);
|
|
817
897
|
for (const part of this.mergedParts) meshes.add(part.mesh);
|
|
818
898
|
for (const mesh of this.singles) meshes.add(mesh);
|
|
899
|
+
for (const part of this.instancedParts) {
|
|
900
|
+
meshes.add(part.mesh);
|
|
901
|
+
if (part.instances !== null) this.renderer.disposeInstanced(part.instances.batch);
|
|
902
|
+
}
|
|
903
|
+
this.instancedParts.length = 0;
|
|
819
904
|
if (this.outlineMesh !== null) meshes.add(this.outlineMesh);
|
|
820
905
|
for (const mesh of meshes) this.renderer.disposeMesh(mesh);
|
|
821
906
|
this.revealed.length = 0;
|
|
@@ -877,7 +962,10 @@ export class DrftLoader {
|
|
|
877
962
|
metallicScale: 1,
|
|
878
963
|
occlusionStrength: 0,
|
|
879
964
|
cutout: 0,
|
|
965
|
+
blend: false,
|
|
966
|
+
doubleSided: false,
|
|
880
967
|
reveal: this.outlinePart?.reveal ?? 0,
|
|
968
|
+
instances: null,
|
|
881
969
|
};
|
|
882
970
|
const at = this.outlinePart === null ? -1 : this.revealed.indexOf(this.outlinePart);
|
|
883
971
|
/* First in the list, so it is drawn before the parts that stand in front of it. */
|
|
@@ -891,12 +979,17 @@ export class DrftLoader {
|
|
|
891
979
|
}
|
|
892
980
|
|
|
893
981
|
/** One part, transformed, dressed and uploaded. Returns whether it landed. */
|
|
894
|
-
private uploadOne(
|
|
982
|
+
private uploadOne(
|
|
983
|
+
mesh: MeshData,
|
|
984
|
+
material: DrftMaterial | undefined,
|
|
985
|
+
instances: Float32Array | null,
|
|
986
|
+
): boolean {
|
|
895
987
|
const fit = this.fit;
|
|
896
988
|
if (fit === null) return false;
|
|
897
989
|
const dressed = this.options.transform?.(mesh, material) ?? mesh;
|
|
898
|
-
|
|
899
|
-
|
|
990
|
+
/* The file's mesh itself where the fit is the identity, with its tangents and every other
|
|
991
|
+
attribute it carried. It went through a builder here, which copied it and dropped them. */
|
|
992
|
+
const placed = placeMesh(dressed, fit.x, fit.y, fit.z, fit.scale);
|
|
900
993
|
const override = this.options.surface?.(material);
|
|
901
994
|
/*
|
|
902
995
|
* **The resolution and the key both live in `drawKey.ts`, and that is the point of them.**
|
|
@@ -918,12 +1011,31 @@ export class DrftLoader {
|
|
|
918
1011
|
metallicScale,
|
|
919
1012
|
occlusionStrength,
|
|
920
1013
|
cutout,
|
|
1014
|
+
blend,
|
|
1015
|
+
doubleSided,
|
|
921
1016
|
} = g;
|
|
922
1017
|
|
|
923
1018
|
/*
|
|
924
1019
|
* Added to its merged group here, while there is a budget governing how much of this happens
|
|
925
1020
|
* in one frame.
|
|
926
1021
|
*/
|
|
1022
|
+
if (instances !== null) {
|
|
1023
|
+
return this.uploadInstanced(placed, instances, fit, {
|
|
1024
|
+
albedo,
|
|
1025
|
+
orm,
|
|
1026
|
+
normal,
|
|
1027
|
+
emissive,
|
|
1028
|
+
opacity,
|
|
1029
|
+
reflectivity,
|
|
1030
|
+
roughnessScale,
|
|
1031
|
+
metallicScale,
|
|
1032
|
+
occlusionStrength,
|
|
1033
|
+
cutout,
|
|
1034
|
+
blend,
|
|
1035
|
+
doubleSided,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
927
1039
|
const key = drawKeyOf(material, override);
|
|
928
1040
|
let group = this.groups.get(key);
|
|
929
1041
|
if (group === undefined) {
|
|
@@ -938,38 +1050,33 @@ export class DrftLoader {
|
|
|
938
1050
|
metallicScale,
|
|
939
1051
|
occlusionStrength,
|
|
940
1052
|
cutout,
|
|
941
|
-
|
|
1053
|
+
blend,
|
|
1054
|
+
doubleSided,
|
|
1055
|
+
members: [],
|
|
942
1056
|
};
|
|
943
1057
|
this.groups.set(key, group);
|
|
944
1058
|
}
|
|
945
1059
|
/* Not merged, for the reason above: a group's deltas would deform its neighbours. */
|
|
946
|
-
if (dressed.morphTargets === undefined)
|
|
947
|
-
group.builder.addMesh(dressed, fit.x, fit.y, fit.z, fit.scale);
|
|
948
|
-
}
|
|
1060
|
+
if (dressed.morphTargets === undefined) group.members.push(placed);
|
|
949
1061
|
|
|
950
1062
|
try {
|
|
951
1063
|
/*
|
|
952
|
-
* **A morphed part
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
* part built through it arrives on the GPU with its targets dropped. Merging is worse than
|
|
956
|
-
* that rather than merely lossy: a merged group concatenates vertices from several parts,
|
|
957
|
-
* and one part's deltas applied across that buffer would deform whatever geometry happened
|
|
958
|
-
* to follow it.
|
|
1064
|
+
* **A morphed part is never merged.** A merged group concatenates vertices from several
|
|
1065
|
+
* parts, and one part's deltas applied across that buffer would deform whatever geometry
|
|
1066
|
+
* happened to follow it.
|
|
959
1067
|
*
|
|
960
1068
|
* So the deltas are scaled by the fit and handed straight to `createMesh`. **Scaled and not
|
|
961
1069
|
* translated** — a delta is a displacement, and moving it by the fit's offset would drag
|
|
962
1070
|
* every vertex toward the origin of the model rather than deform it in place.
|
|
963
1071
|
*/
|
|
964
|
-
const built = single.build();
|
|
965
1072
|
const morphable =
|
|
966
1073
|
dressed.morphTargets !== undefined && dressed.morphTargetCount !== undefined
|
|
967
1074
|
? {
|
|
968
|
-
...
|
|
1075
|
+
...placed,
|
|
969
1076
|
morphTargets: dressed.morphTargets.map((delta) => delta * fit.scale),
|
|
970
1077
|
morphTargetCount: dressed.morphTargetCount,
|
|
971
1078
|
}
|
|
972
|
-
:
|
|
1079
|
+
: placed;
|
|
973
1080
|
const uploaded = this.renderer.createMesh(morphable);
|
|
974
1081
|
this.singles.push(uploaded);
|
|
975
1082
|
const part: DrftPart = {
|
|
@@ -984,7 +1091,10 @@ export class DrftLoader {
|
|
|
984
1091
|
metallicScale,
|
|
985
1092
|
occlusionStrength,
|
|
986
1093
|
cutout,
|
|
1094
|
+
blend,
|
|
1095
|
+
doubleSided,
|
|
987
1096
|
reveal: 0,
|
|
1097
|
+
instances: null,
|
|
988
1098
|
};
|
|
989
1099
|
this.revealed.push(part);
|
|
990
1100
|
if (this.options.keepReveal === true) this.arrivals.push(part);
|
|
@@ -1004,12 +1114,70 @@ export class DrftLoader {
|
|
|
1004
1114
|
* and differ in blend: merging on the image alone means one cannot be made translucent without
|
|
1005
1115
|
* taking the other with it.
|
|
1006
1116
|
*/
|
|
1117
|
+
/**
|
|
1118
|
+
* A mesh the file draws many times: uploaded once, placed by a batch, and kept out of the merge.
|
|
1119
|
+
*
|
|
1120
|
+
* **The fit is conjugated into each placement.** The prototype is uploaded with the fit already
|
|
1121
|
+
* applied, as every part is, so a copy's matrix M becomes F·M·F⁻¹: the same turn, and a translation
|
|
1122
|
+
* of t − R·t + s·u for the fit's scale s and offset t and the copy's own translation u. A fitted
|
|
1123
|
+
* model's copies then land where its geometry does, and are not scaled twice.
|
|
1124
|
+
*/
|
|
1125
|
+
private uploadInstanced(
|
|
1126
|
+
prototype: MeshData,
|
|
1127
|
+
instances: Float32Array,
|
|
1128
|
+
fit: { scale: number; x: number; y: number; z: number },
|
|
1129
|
+
surface: Omit<DrftPart, 'mesh' | 'reveal' | 'instances'>,
|
|
1130
|
+
): boolean {
|
|
1131
|
+
try {
|
|
1132
|
+
const uploaded = this.renderer.createMesh(prototype);
|
|
1133
|
+
const count = instances.length / 16;
|
|
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;
|
|
1163
|
+
this.renderer.uploadInstanced(batch, data);
|
|
1164
|
+
const part: DrftPart = { mesh: uploaded, ...surface, reveal: 0, instances: { batch, data } };
|
|
1165
|
+
this.revealed.push(part);
|
|
1166
|
+
this.instancedParts.push(part);
|
|
1167
|
+
if (this.options.keepReveal === true) this.arrivals.push(part);
|
|
1168
|
+
return true;
|
|
1169
|
+
} catch (error) {
|
|
1170
|
+
console.warn('DrftLoader: an instanced part would not upload and was skipped', error);
|
|
1171
|
+
return false;
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
|
|
1007
1175
|
private buildGroup(key: string): void {
|
|
1008
1176
|
const group = this.groups.get(key);
|
|
1009
1177
|
if (group === undefined) return;
|
|
1010
1178
|
try {
|
|
1011
1179
|
this.mergedParts.push({
|
|
1012
|
-
mesh: this.renderer.createMesh(group.
|
|
1180
|
+
mesh: this.renderer.createMesh(concatMeshes(group.members)),
|
|
1013
1181
|
albedo: group.albedo,
|
|
1014
1182
|
orm: group.orm,
|
|
1015
1183
|
normal: group.normal,
|
|
@@ -1020,7 +1188,10 @@ export class DrftLoader {
|
|
|
1020
1188
|
metallicScale: group.metallicScale,
|
|
1021
1189
|
occlusionStrength: group.occlusionStrength,
|
|
1022
1190
|
cutout: group.cutout,
|
|
1191
|
+
blend: group.blend,
|
|
1192
|
+
doubleSided: group.doubleSided,
|
|
1023
1193
|
reveal: 1,
|
|
1194
|
+
instances: null,
|
|
1024
1195
|
});
|
|
1025
1196
|
} catch (error) {
|
|
1026
1197
|
/* One bad group costs one surface rather than the model. */
|
|
@@ -1039,11 +1210,13 @@ export class DrftLoader {
|
|
|
1039
1210
|
*/
|
|
1040
1211
|
private swapInMerged(): void {
|
|
1041
1212
|
/*
|
|
1042
|
-
* Nothing merged means every
|
|
1043
|
-
* standing between a viewer and an empty room. Keeping it is the honest
|
|
1044
|
-
* incomplete rather than nothing at all.
|
|
1213
|
+
* Nothing merged and nothing instanced means every part failed to upload, and then the outline
|
|
1214
|
+
* is the only thing standing between a viewer and an empty room. Keeping it is the honest
|
|
1215
|
+
* state: something incomplete rather than nothing at all. **Instanced parts count**: a file
|
|
1216
|
+
* whose every mesh is instanced merges nothing, and a guard on merged groups alone kept its
|
|
1217
|
+
* outline on screen over the finished model for ever.
|
|
1045
1218
|
*/
|
|
1046
|
-
if (this.mergedParts.length === 0) return;
|
|
1219
|
+
if (this.mergedParts.length === 0 && this.instancedParts.length === 0) return;
|
|
1047
1220
|
/*
|
|
1048
1221
|
* The outline leaves in the same frame the model arrives. Two representations of one object
|
|
1049
1222
|
* is worse than either of them, and it is what a fade would otherwise cross-dissolve.
|
|
@@ -1059,6 +1232,11 @@ export class DrftLoader {
|
|
|
1059
1232
|
}
|
|
1060
1233
|
this.revealed.length = 0;
|
|
1061
1234
|
for (const part of this.mergedParts) this.revealed.push(part);
|
|
1235
|
+
/* Never merged, so carried across as they are: see `uploadInstanced`. */
|
|
1236
|
+
for (const part of this.instancedParts) {
|
|
1237
|
+
part.reveal = 1;
|
|
1238
|
+
this.revealed.push(part);
|
|
1239
|
+
}
|
|
1062
1240
|
this.mergedParts.length = 0;
|
|
1063
1241
|
/* Solid first, blended last, which is the whole of the sort: a translucent surface needs the
|
|
1064
1242
|
world behind it already drawn to blend against. */
|
|
@@ -1140,10 +1318,11 @@ export class DrftLoader {
|
|
|
1140
1318
|
* decoder that had no chance with them. Naming each codec and returning `null` for the one that is
|
|
1141
1319
|
* not an encoded image makes the omission a value rather than a default, and lets a test say so.
|
|
1142
1320
|
*/
|
|
1143
|
-
export function imageTypeFor(codec: number): string {
|
|
1321
|
+
export function imageTypeFor(codec: number): string | null {
|
|
1144
1322
|
if (codec === CODEC_PNG) return 'image/png';
|
|
1145
1323
|
if (codec === CODEC_WEBP) return 'image/webp';
|
|
1146
|
-
return 'image/jpeg';
|
|
1324
|
+
if (codec === CODEC_JPEG) return 'image/jpeg';
|
|
1325
|
+
return null;
|
|
1147
1326
|
}
|
|
1148
1327
|
|
|
1149
1328
|
/**
|
|
@@ -1228,7 +1407,14 @@ async function decodeImage(texture: DrftTexture, longestSide?: number): Promise<
|
|
|
1228
1407
|
AS_AUTHORED,
|
|
1229
1408
|
);
|
|
1230
1409
|
}
|
|
1231
|
-
const
|
|
1410
|
+
const type = imageTypeFor(texture.codec);
|
|
1411
|
+
if (type === null) {
|
|
1412
|
+
throw new DrftError(
|
|
1413
|
+
`texture codec ${texture.codec} is not one this loader can decode; bake it again with a ` +
|
|
1414
|
+
'current baker',
|
|
1415
|
+
);
|
|
1416
|
+
}
|
|
1417
|
+
const blob = new Blob([texture.bytes.slice()], { type });
|
|
1232
1418
|
if (longestSide === undefined) return await createImageBitmap(blob, AS_AUTHORED);
|
|
1233
1419
|
/*
|
|
1234
1420
|
* Resized while it decodes, which is the point: asking for a 256 pixel version of a 2048
|
|
@@ -0,0 +1,74 @@
|
|
|
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
|
+
import type { DrftSdfvEntry } from '@driftengine/drft';
|
|
13
|
+
|
|
14
|
+
/** One field, where it stands. */
|
|
15
|
+
export interface DrftFieldPlacement {
|
|
16
|
+
/** The field, in the object space of what it covers. It is a `FieldSource` as it stands. */
|
|
17
|
+
readonly source: DrftSdfvEntry;
|
|
18
|
+
/** World from object: the fit, and for a copied mesh that copy's placement inside it. */
|
|
19
|
+
readonly model: Float32Array;
|
|
20
|
+
/** The mesh the field covers, or `SDFV_WHOLE_FILE` for all of the file's static geometry. */
|
|
21
|
+
readonly mesh: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** The loader's fit: a uniform scale, then a translation. */
|
|
25
|
+
export interface FieldFit {
|
|
26
|
+
readonly scale: number;
|
|
27
|
+
readonly x: number;
|
|
28
|
+
readonly y: number;
|
|
29
|
+
readonly z: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Every field, placed. A whole-file field and the field of a mesh drawn once stand at the fit; the
|
|
34
|
+
* field of a copied mesh stands once a copy, at the fit composed with that copy's matrix.
|
|
35
|
+
*/
|
|
36
|
+
export function placeFields(
|
|
37
|
+
entries: readonly DrftSdfvEntry[],
|
|
38
|
+
fit: FieldFit,
|
|
39
|
+
instances: ReadonlyMap<number, Float32Array>,
|
|
40
|
+
): DrftFieldPlacement[] {
|
|
41
|
+
const placed: DrftFieldPlacement[] = [];
|
|
42
|
+
for (const source of entries) {
|
|
43
|
+
const copies = source.mesh === SDFV_WHOLE_FILE ? undefined : instances.get(source.mesh);
|
|
44
|
+
if (copies === undefined) {
|
|
45
|
+
placed.push({ source, model: fitted(fit, null, 0), mesh: source.mesh });
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
for (let at = 0; at + 16 <= copies.length; at += 16) {
|
|
49
|
+
placed.push({ source, model: fitted(fit, copies, at), mesh: source.mesh });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return placed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The fit, or the fit times the column-major matrix at `copies[at]`. The fit is `T · S` with a
|
|
57
|
+
* uniform `S`, so each column of the product is the copy's column scaled, plus the translation
|
|
58
|
+
* times that column's last row.
|
|
59
|
+
*/
|
|
60
|
+
function fitted(fit: FieldFit, copies: Float32Array | null, at: number): Float32Array {
|
|
61
|
+
const out = new Float32Array(16);
|
|
62
|
+
for (let column = 0; column < 4; column++) {
|
|
63
|
+
const base = column * 4;
|
|
64
|
+
const m0 = copies === null ? (column === 0 ? 1 : 0) : (copies[at + base] as number);
|
|
65
|
+
const m1 = copies === null ? (column === 1 ? 1 : 0) : (copies[at + base + 1] as number);
|
|
66
|
+
const m2 = copies === null ? (column === 2 ? 1 : 0) : (copies[at + base + 2] as number);
|
|
67
|
+
const m3 = copies === null ? (column === 3 ? 1 : 0) : (copies[at + base + 3] as number);
|
|
68
|
+
out[base] = fit.scale * m0 + fit.x * m3;
|
|
69
|
+
out[base + 1] = fit.scale * m1 + fit.y * m3;
|
|
70
|
+
out[base + 2] = fit.scale * m2 + fit.z * m3;
|
|
71
|
+
out[base + 3] = m3;
|
|
72
|
+
}
|
|
73
|
+
return out;
|
|
74
|
+
}
|