@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.
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
@@ -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 { AnimationClip, DrftMorph, DrftNode, DrftSkin } from '@driftengine/drft';
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 reflectivity overrides, for the same reason as `transform`. */
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
  *
@@ -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({ mesh: withMorph, material });
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
- const single = new MeshBuilder();
697
- single.addMesh(dressed, fit.x, fit.y, fit.z, fit.scale);
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
- builder: new MeshBuilder(),
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.builder.addMesh(dressed, fit.x, fit.y, fit.z, fit.scale);
734
- }
822
+ if (dressed.morphTargets === undefined)
823
+ group.members.push(placed);
735
824
  try {
736
825
  /*
737
- * **A morphed part bypasses the builder, and is never merged.**
738
- *
739
- * `MeshBuilder` carries the attributes it knows and morph deltas are not among them, so a
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
- ...built,
836
+ ...placed,
753
837
  morphTargets: dressed.morphTargets.map((delta) => delta * fit.scale),
754
838
  morphTargetCount: dressed.morphTargetCount,
755
839
  }
756
- : built;
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.builder.build()),
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 group failed to upload, and then the outline is the only thing
829
- * standing between a viewer and an empty room. Keeping it is the honest state: something
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
- return 'image/jpeg';
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 blob = new Blob([texture.bytes.slice()], { type: imageTypeFor(texture.codec) });
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 {};