@needle-tools/gltf-progressive 3.0.1 → 3.1.0-next.f550970

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/lib/extension.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { BufferGeometry, Mesh, Texture, TextureLoader } from "three";
2
2
  import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
3
  import { addDracoAndKTX2Loaders } from "./loaders.js";
4
- import { resolveUrl } from "./utils.internal.js";
4
+ import { PromiseQueue, resolveUrl } from "./utils.internal.js";
5
5
  import { getRaycastMesh, registerRaycastMesh } from "./utils.js";
6
6
  // All of this has to be removed
7
7
  // import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
@@ -309,8 +309,10 @@ export class NEEDLE_progressive {
309
309
  }
310
310
  return NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
311
311
  // this can currently not happen
312
- if (Array.isArray(tex))
312
+ if (Array.isArray(tex)) {
313
+ console.warn("Progressive: Got an array of textures for a texture slot, this should not happen", tex);
313
314
  return null;
315
+ }
314
316
  if (tex?.isTexture === true) {
315
317
  if (tex != current) {
316
318
  if (material && slot) {
@@ -375,6 +377,20 @@ export class NEEDLE_progressive {
375
377
  return mesh;
376
378
  });
377
379
  };
380
+ // private _isLoadingTexture;
381
+ // loadTexture = (textureIndex: number) => {
382
+ // if (this._isLoadingTexture) return null;
383
+ // const ext = this.parser.json.textures[textureIndex]?.extensions?.[EXTENSION_NAME] as NEEDLE_ext_progressive_texture;
384
+ // if (!ext) return null;
385
+ // this._isLoadingTexture = true;
386
+ // return this.parser.getDependency("texture", textureIndex).then(tex => {
387
+ // this._isLoadingTexture = false;
388
+ // if (tex) {
389
+ // NEEDLE_progressive.registerTexture(this.url, tex as Texture, ext.lods?.length, textureIndex, ext);
390
+ // }
391
+ // return tex;
392
+ // });
393
+ // }
378
394
  afterRoot(gltf) {
379
395
  if (debug)
380
396
  console.log("AFTER", this.url, gltf);
@@ -440,13 +456,16 @@ export class NEEDLE_progressive {
440
456
  * Register a texture with LOD information
441
457
  */
442
458
  static registerTexture = (url, tex, level, index, ext) => {
443
- if (debug)
444
- console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
445
459
  if (!tex) {
446
460
  if (debug)
447
- console.error("gltf-progressive: Register texture without texture");
461
+ console.error("gltf-progressive: Called register texture without texture");
448
462
  return;
449
463
  }
464
+ if (debug) {
465
+ const width = tex.image?.width || tex.source?.data?.width || 0;
466
+ const height = tex.image?.height || tex.source?.data?.height || 0;
467
+ console.log(`> Progressive: register texture[${index}] "${tex.name || tex.uuid}", Current: ${width}x${height}, Max: ${ext.lods[0]?.width}x${ext.lods[0]?.height}, uuid: ${tex.uuid}`, ext, tex);
468
+ }
450
469
  // Put the extension info into the source (seems like tiled textures are cloned and the userdata etc is not properly copied BUT the source of course is not cloned)
451
470
  // see https://github.com/needle-tools/needle-engine-support/issues/133
452
471
  if (tex.source)
@@ -494,25 +513,27 @@ export class NEEDLE_progressive {
494
513
  static async getOrLoadLOD(current, level) {
495
514
  const debugverbose = debug == "verbose";
496
515
  /** this key is used to lookup the LOD information */
497
- const LOD = current.userData.LODS;
516
+ const LOD = this.getAssignedLODInformation(current);
498
517
  if (!LOD) {
518
+ if (debug)
519
+ console.warn(`[gltf-progressive] No LOD information found: ${current.name}, uuid: ${current.uuid}, type: ${current.type}`, current);
499
520
  return null;
500
521
  }
501
522
  const LODKEY = LOD?.key;
502
- let progressiveInfo;
523
+ let lodInfo;
503
524
  // See https://github.com/needle-tools/needle-engine-support/issues/133
504
525
  if (current.isTexture === true) {
505
526
  const tex = current;
506
527
  if (tex.source && tex.source[$progressiveTextureExtension])
507
- progressiveInfo = tex.source[$progressiveTextureExtension];
528
+ lodInfo = tex.source[$progressiveTextureExtension];
508
529
  }
509
- if (!progressiveInfo)
510
- progressiveInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
511
- if (progressiveInfo) {
530
+ if (!lodInfo)
531
+ lodInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
532
+ if (lodInfo) {
512
533
  if (level > 0) {
513
534
  let useLowRes = false;
514
- const hasMultipleLevels = Array.isArray(progressiveInfo.lods);
515
- if (hasMultipleLevels && level >= progressiveInfo.lods.length) {
535
+ const hasMultipleLevels = Array.isArray(lodInfo.lods);
536
+ if (hasMultipleLevels && level >= lodInfo.lods.length) {
516
537
  useLowRes = true;
517
538
  }
518
539
  else if (!hasMultipleLevels) {
@@ -524,12 +545,12 @@ export class NEEDLE_progressive {
524
545
  }
525
546
  }
526
547
  /** the unresolved LOD url */
527
- const unresolved_lod_url = Array.isArray(progressiveInfo.lods) ? progressiveInfo.lods[level]?.path : progressiveInfo.lods;
548
+ const unresolved_lod_url = Array.isArray(lodInfo.lods) ? lodInfo.lods[level]?.path : lodInfo.lods;
528
549
  // check if we have a uri
529
550
  if (!unresolved_lod_url) {
530
- if (debug && !progressiveInfo["missing:uri"]) {
531
- progressiveInfo["missing:uri"] = true;
532
- console.warn("Missing uri for progressive asset for LOD " + level, progressiveInfo);
551
+ if (debug && !lodInfo["missing:uri"]) {
552
+ lodInfo["missing:uri"] = true;
553
+ console.warn("Missing uri for progressive asset for LOD " + level, lodInfo);
533
554
  }
534
555
  return null;
535
556
  }
@@ -537,12 +558,12 @@ export class NEEDLE_progressive {
537
558
  const lod_url = resolveUrl(LOD.url, unresolved_lod_url);
538
559
  // check if the requested file needs to be loaded via a GLTFLoader
539
560
  if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
540
- if (!progressiveInfo.guid) {
541
- console.warn("missing pointer for glb/gltf texture", progressiveInfo);
561
+ if (!lodInfo.guid) {
562
+ console.warn("missing pointer for glb/gltf texture", lodInfo);
542
563
  return null;
543
564
  }
544
565
  // check if the requested file has already been loaded
545
- const KEY = lod_url + "_" + progressiveInfo.guid;
566
+ const KEY = lod_url + "_" + lodInfo.guid;
546
567
  // check if the requested file is currently being loaded
547
568
  const existing = this.previouslyLoaded.get(KEY);
548
569
  if (existing !== undefined) {
@@ -581,7 +602,13 @@ export class NEEDLE_progressive {
581
602
  return res;
582
603
  }
583
604
  }
584
- const ext = progressiveInfo;
605
+ const slot = await this.queue.slot(lod_url);
606
+ if (!slot.use) {
607
+ if (debug)
608
+ console.log(`LOD ${level} was aborted: ${lod_url}`);
609
+ return null; // the request was aborted, we don't load it again
610
+ }
611
+ const ext = lodInfo;
585
612
  const request = new Promise(async (resolve, _) => {
586
613
  const loader = new GLTFLoader();
587
614
  addDracoAndKTX2Loaders(loader);
@@ -599,10 +626,11 @@ export class NEEDLE_progressive {
599
626
  }
600
627
  const gltf = await loader.loadAsync(url).catch(err => {
601
628
  console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
602
- return null;
629
+ return resolve(null);
603
630
  });
604
- if (!gltf)
605
- return null;
631
+ if (!gltf) {
632
+ return resolve(null);
633
+ }
606
634
  const parser = gltf.parser;
607
635
  if (debugverbose)
608
636
  console.log("Loading finished " + lod_url, ext.guid);
@@ -687,6 +715,7 @@ export class NEEDLE_progressive {
687
715
  return resolve(null);
688
716
  });
689
717
  this.previouslyLoaded.set(KEY, request);
718
+ slot.use(request);
690
719
  const res = await request;
691
720
  return res;
692
721
  }
@@ -697,12 +726,12 @@ export class NEEDLE_progressive {
697
726
  const loader = new TextureLoader();
698
727
  const tex = await loader.loadAsync(lod_url);
699
728
  if (tex) {
700
- tex.guid = progressiveInfo.guid;
729
+ tex.guid = lodInfo.guid;
701
730
  tex.flipY = false;
702
731
  tex.needsUpdate = true;
703
732
  tex.colorSpace = current.colorSpace;
704
733
  if (debugverbose)
705
- console.log(progressiveInfo, tex);
734
+ console.log(lodInfo, tex);
706
735
  }
707
736
  else if (debug)
708
737
  console.warn("failed loading", lod_url);
@@ -716,6 +745,7 @@ export class NEEDLE_progressive {
716
745
  }
717
746
  return null;
718
747
  }
748
+ static queue = new PromiseQueue(100, { debug: debug != false });
719
749
  static assignLODInformation(url, res, key, level, index) {
720
750
  if (!res)
721
751
  return;
@@ -723,9 +753,17 @@ export class NEEDLE_progressive {
723
753
  res.userData = {};
724
754
  const info = new LODInformation(url, key, level, index);
725
755
  res.userData.LODS = info;
756
+ if ("source" in res && typeof res.source === "object")
757
+ res.source.LODS = info; // for tiled textures
726
758
  }
727
759
  static getAssignedLODInformation(res) {
728
- return res?.userData?.LODS || null;
760
+ if (!res)
761
+ return null;
762
+ if (res.userData?.LODS)
763
+ return res.userData.LODS;
764
+ if ("source" in res && res.source?.LODS)
765
+ return res.source.LODS;
766
+ return null;
729
767
  }
730
768
  // private static readonly _copiedTextures: WeakMap<Texture, Texture> = new Map();
731
769
  static copySettings(source, target) {
@@ -149,6 +149,8 @@ export declare class LODsManager {
149
149
  private static corner3;
150
150
  private static readonly _tempPtInside;
151
151
  private static isInside;
152
+ private static skinnedMeshBoundsFrameOffsetCounter;
153
+ private static $skinnedMeshBoundsOffset;
152
154
  private calculateLodLevel;
153
155
  }
154
156
  declare class LOD_state {
@@ -457,6 +457,8 @@ export class LODsManager {
457
457
  const pt1 = this._tempPtInside.set(centerx, centery, min.z).applyMatrix4(matrix);
458
458
  return pt1.z < 0;
459
459
  }
460
+ static skinnedMeshBoundsFrameOffsetCounter = 0;
461
+ static $skinnedMeshBoundsOffset = Symbol("gltf-progressive-skinnedMeshBoundsOffset");
460
462
  calculateLodLevel(camera, mesh, state, desiredDensity, result) {
461
463
  if (!mesh) {
462
464
  result.mesh_lod = -1;
@@ -502,16 +504,25 @@ export class LODsManager {
502
504
  skinnedMesh.computeBoundingBox();
503
505
  }
504
506
  // Fix: https://linear.app/needle/issue/NE-5264
505
- else if (this.skinnedMeshAutoUpdateBoundsInterval > 0
506
- && state.frames % this.skinnedMeshAutoUpdateBoundsInterval === 0) {
507
- // use lowres geometry for bounding box calculation
508
- const raycastmesh = getRaycastMesh(skinnedMesh);
509
- const originalGeometry = skinnedMesh.geometry;
510
- if (raycastmesh) {
511
- skinnedMesh.geometry = raycastmesh;
507
+ else if (this.skinnedMeshAutoUpdateBoundsInterval > 0) {
508
+ // Save a frame offset per object to stagger updates of skinned meshes across multiple frames
509
+ // This isn't a perfect solution to improve perf impact of skinned mesh updates (e.g. large skinned meshes would still be costly)
510
+ // But for many smaller meshes it helps to avoid spikes in performance
511
+ if (!skinnedMesh[LODsManager.$skinnedMeshBoundsOffset]) {
512
+ const offset = LODsManager.skinnedMeshBoundsFrameOffsetCounter++;
513
+ skinnedMesh[LODsManager.$skinnedMeshBoundsOffset] = offset;
514
+ }
515
+ const frameOffset = skinnedMesh[LODsManager.$skinnedMeshBoundsOffset];
516
+ if ((state.frames + frameOffset) % this.skinnedMeshAutoUpdateBoundsInterval === 0) {
517
+ // use lowres geometry for bounding box calculation
518
+ const raycastmesh = getRaycastMesh(skinnedMesh);
519
+ const originalGeometry = skinnedMesh.geometry;
520
+ if (raycastmesh) {
521
+ skinnedMesh.geometry = raycastmesh;
522
+ }
523
+ skinnedMesh.computeBoundingBox();
524
+ skinnedMesh.geometry = originalGeometry;
512
525
  }
513
- skinnedMesh.computeBoundingBox();
514
- skinnedMesh.geometry = originalGeometry;
515
526
  }
516
527
  boundingBox = skinnedMesh.boundingBox;
517
528
  }
@@ -637,6 +648,10 @@ export class LODsManager {
637
648
  const lod = mesh_lods[l];
638
649
  const densityForThisLevel = lod.densities?.[primitive_index] || lod.density || .00001;
639
650
  const resultingDensity = densityForThisLevel / state.lastScreenCoverage;
651
+ if (primitive_index > 0 && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
652
+ window["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
653
+ console.warn(`[Needle Progressive] Detected usage of mesh without primitive densities. This might cause incorrect LOD level selection: Consider re-optimizing your model by updating your Needle Integration, Needle glTF Pipeline or running optimization again on Needle Cloud.`);
654
+ }
640
655
  if (resultingDensity < desiredDensity) {
641
656
  expectedLevel = l;
642
657
  break;
@@ -4,3 +4,23 @@ export declare function resolveUrl(source: string | undefined, uri: string): str
4
4
  /** @returns `true` if it's a phone or tablet */
5
5
  export declare function isMobileDevice(): boolean;
6
6
  export declare function isDevelopmentServer(): boolean;
7
+ type SlotReturnValue = {
8
+ use?: ((promise: Promise<any>) => void);
9
+ };
10
+ export declare class PromiseQueue {
11
+ readonly maxConcurrent: number;
12
+ private readonly _running;
13
+ private readonly _queue;
14
+ debug: boolean;
15
+ constructor(maxConcurrent?: number, opts?: {
16
+ debug?: boolean;
17
+ });
18
+ private tick;
19
+ /**
20
+ * Request a slot for a promise with a specific key. This function returns a promise with a `use` method that can be called to add the promise to the queue.
21
+ */
22
+ slot(key: string): Promise<SlotReturnValue>;
23
+ private add;
24
+ private internalUpdate;
25
+ }
26
+ export {};
@@ -58,3 +58,52 @@ export function isDevelopmentServer() {
58
58
  const isDevelopment = url.hostname === "127.0.0.1" || isLocalhostOrIpAddress;
59
59
  return isDevelopment;
60
60
  }
61
+ export class PromiseQueue {
62
+ maxConcurrent;
63
+ _running = new Map();
64
+ _queue = [];
65
+ debug = false;
66
+ constructor(maxConcurrent = 100, opts = {}) {
67
+ this.maxConcurrent = maxConcurrent;
68
+ this.debug = opts.debug ?? false;
69
+ window.requestAnimationFrame(this.tick);
70
+ }
71
+ tick = () => {
72
+ this.internalUpdate();
73
+ setTimeout(this.tick, 10);
74
+ };
75
+ /**
76
+ * Request a slot for a promise with a specific key. This function returns a promise with a `use` method that can be called to add the promise to the queue.
77
+ */
78
+ slot(key) {
79
+ if (this.debug)
80
+ console.debug(`[PromiseQueue]: Requesting slot for key ${key}, running: ${this._running.size}, waiting: ${this._queue.length}`);
81
+ return new Promise((resolve) => {
82
+ this._queue.push({ key, resolve });
83
+ });
84
+ }
85
+ add(key, promise) {
86
+ if (this._running.has(key))
87
+ return;
88
+ this._running.set(key, promise);
89
+ promise.finally(() => {
90
+ this._running.delete(key);
91
+ if (this.debug)
92
+ console.debug(`[PromiseQueue]: Promise finished now running: ${this._running.size}, waiting: ${this._queue.length}. (finished ${key})`);
93
+ });
94
+ if (this.debug)
95
+ console.debug(`[PromiseQueue]: Added new promise, now running: ${this._running.size}, waiting: ${this._queue.length}. (added ${key})`);
96
+ }
97
+ internalUpdate() {
98
+ // Run for as many free slots as we can
99
+ const diff = this.maxConcurrent - this._running.size;
100
+ for (let i = 0; i < diff && this._queue.length > 0; i++) {
101
+ if (this.debug)
102
+ console.debug(`[PromiseQueue]: Running ${this._running.size} promises, waiting for ${this._queue.length} more.`);
103
+ const { key, resolve } = this._queue.shift();
104
+ resolve({
105
+ use: (promise) => this.add(key, promise)
106
+ });
107
+ }
108
+ }
109
+ }
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // replaced at build time
2
- export const version = "3.0.1";
2
+ export const version = "3.1.0";
3
3
  globalThis["GLTF_PROGRESSIVE_VERSION"] = version;
4
4
  console.debug(`[gltf-progressive] version ${version || "-"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-progressive",
3
- "version": "3.0.1",
3
+ "version": "3.1.0-next.f550970",
4
4
  "description": "three.js support for loading glTF or GLB files that contain progressive loading data",
5
5
  "homepage": "https://needle.tools",
6
6
  "author": {
@@ -72,4 +72,4 @@
72
72
  "vite": "<= 4.3.9"
73
73
  },
74
74
  "types": "./lib/index.d.ts"
75
- }
75
+ }