@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.
Files changed (54) hide show
  1. package/README.md +1 -1
  2. package/dist/bake/collapseHeap.d.ts +24 -0
  3. package/dist/bake/collapseHeap.js +80 -0
  4. package/dist/bake/collapseMeasures.d.ts +31 -0
  5. package/dist/bake/collapseMeasures.js +183 -0
  6. package/dist/bake/sceneField.d.ts +6 -0
  7. package/dist/bake/sceneField.js +48 -0
  8. package/dist/bake/sdf.d.ts +5 -5
  9. package/dist/bake/sdf.js +15 -95
  10. package/dist/bake/simplify.d.ts +42 -0
  11. package/dist/bake/simplify.js +350 -0
  12. package/dist/bake/surfaceDepth.d.ts +29 -0
  13. package/dist/bake/surfaceDepth.js +101 -0
  14. package/dist/downscale.d.ts +25 -0
  15. package/dist/downscale.js +50 -0
  16. package/dist/drawKey.d.ts +13 -1
  17. package/dist/drawKey.js +5 -3
  18. package/dist/drftLoader.d.ts +38 -7
  19. package/dist/drftLoader.js +186 -30
  20. package/dist/fieldPlacement.d.ts +22 -0
  21. package/dist/fieldPlacement.js +49 -0
  22. package/dist/gltf.d.ts +46 -1
  23. package/dist/gltf.js +101 -2
  24. package/dist/index.d.ts +9 -1
  25. package/dist/index.js +15 -1
  26. package/dist/loadProgress.d.ts +22 -1
  27. package/dist/orient.d.ts +9 -1
  28. package/dist/orient.js +60 -2
  29. package/dist/partMaterial.d.ts +19 -0
  30. package/dist/partMaterial.js +16 -0
  31. package/dist/readModel.d.ts +3 -1
  32. package/dist/repeats.d.ts +39 -0
  33. package/dist/repeats.js +391 -0
  34. package/dist/weld.d.ts +8 -0
  35. package/dist/weld.js +155 -34
  36. package/package.json +4 -4
  37. package/src/bake/collapseHeap.ts +81 -0
  38. package/src/bake/collapseMeasures.ts +215 -0
  39. package/src/bake/sceneField.ts +50 -0
  40. package/src/bake/sdf.ts +14 -99
  41. package/src/bake/simplify.ts +395 -0
  42. package/src/bake/surfaceDepth.ts +102 -0
  43. package/src/downscale.ts +61 -0
  44. package/src/drawKey.ts +17 -3
  45. package/src/drftLoader.ts +224 -38
  46. package/src/fieldPlacement.ts +74 -0
  47. package/src/gltf.ts +139 -3
  48. package/src/index.ts +24 -1
  49. package/src/loadProgress.ts +23 -1
  50. package/src/orient.ts +62 -3
  51. package/src/partMaterial.ts +42 -0
  52. package/src/readModel.ts +3 -1
  53. package/src/repeats.ts +442 -0
  54. 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 { streamDrft, CODEC_PNG, CODEC_RAW, CODEC_WEBP, DrftError } from '@driftengine/drft';
13
- import type { AnimationClip, DrftMorph, DrftNode, DrftSkin } from '@driftengine/drft';
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 reflectivity overrides, for the same reason as `transform`. */
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: { mesh: MeshData; material: DrftMaterial | undefined }[] = [];
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
- builder: MeshBuilder;
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({ mesh: withMorph, material });
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(mesh: MeshData, material: DrftMaterial | undefined): boolean {
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
- const single = new MeshBuilder();
899
- single.addMesh(dressed, fit.x, fit.y, fit.z, fit.scale);
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
- builder: new MeshBuilder(),
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 bypasses the builder, and is never merged.**
953
- *
954
- * `MeshBuilder` carries the attributes it knows and morph deltas are not among them, so a
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
- ...built,
1075
+ ...placed,
969
1076
  morphTargets: dressed.morphTargets.map((delta) => delta * fit.scale),
970
1077
  morphTargetCount: dressed.morphTargetCount,
971
1078
  }
972
- : built;
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.builder.build()),
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 group failed to upload, and then the outline is the only thing
1043
- * standing between a viewer and an empty room. Keeping it is the honest state: something
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 blob = new Blob([texture.bytes.slice()], { type: imageTypeFor(texture.codec) });
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
+ }