@needle-tools/gltf-progressive 3.4.0-next.a9f46ea → 3.4.0-next.ed30751
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/CHANGELOG.md +3 -0
- package/README.md +9 -0
- package/gltf-progressive.js +732 -665
- package/gltf-progressive.min.js +9 -7
- package/gltf-progressive.umd.cjs +9 -7
- package/lib/extension.d.ts +22 -2
- package/lib/extension.js +158 -46
- package/lib/utils.internal.d.ts +8 -2
- package/lib/utils.internal.js +93 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
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 { getParam, PromiseQueue, resolveUrl } from "./utils.internal.js";
|
|
4
|
+
import { determineTextureMemoryInBytes, getParam, isMobileDevice, 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";
|
|
@@ -11,6 +11,7 @@ import { debug } from "./lods.debug.js";
|
|
|
11
11
|
import { getWorker } from "./worker/loader.mainthread.js";
|
|
12
12
|
const useWorker = getParam("gltf-progressive-worker");
|
|
13
13
|
const reduceMipmaps = getParam("gltf-progressive-reduce-mipmaps");
|
|
14
|
+
const debugGC = getParam("gltf-progressive-gc");
|
|
14
15
|
const $progressiveTextureExtension = Symbol("needle-progressive-texture");
|
|
15
16
|
export const EXTENSION_NAME = "NEEDLE_progressive";
|
|
16
17
|
// #region EXT
|
|
@@ -309,6 +310,16 @@ export class NEEDLE_progressive {
|
|
|
309
310
|
}
|
|
310
311
|
return Promise.resolve(null);
|
|
311
312
|
}
|
|
313
|
+
/**
|
|
314
|
+
* Set the maximum number of concurrent loading tasks for LOD resources. This limits how many LOD resources (meshes or textures) can be loaded at the same time to prevent overloading the network or GPU. If the limit is reached, additional loading requests will be queued and processed as previous ones finish.
|
|
315
|
+
* @default 50 on desktop, 20 on mobile devices
|
|
316
|
+
*/
|
|
317
|
+
static set maxConcurrentLoadingTasks(value) {
|
|
318
|
+
NEEDLE_progressive.queue.maxConcurrent = value;
|
|
319
|
+
}
|
|
320
|
+
static get maxConcurrentLoadingTasks() {
|
|
321
|
+
return NEEDLE_progressive.queue.maxConcurrent;
|
|
322
|
+
}
|
|
312
323
|
// #region INTERNAL
|
|
313
324
|
static assignTextureLODForSlot(current, level, material, slot) {
|
|
314
325
|
if (current?.isTexture !== true) {
|
|
@@ -334,29 +345,37 @@ export class NEEDLE_progressive {
|
|
|
334
345
|
if (assignedLOD && assignedLOD?.level < level) {
|
|
335
346
|
if (debug === "verbose")
|
|
336
347
|
console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
|
|
348
|
+
// Dispose the newly loaded texture since we're not using it
|
|
349
|
+
// (the assigned texture is higher quality, so we reject the new one)
|
|
350
|
+
// Note: We dispose directly here (not via untrackTextureUsage) because this texture
|
|
351
|
+
// was never tracked/used - it was rejected immediately upon loading
|
|
352
|
+
if (tex && tex !== assigned) {
|
|
353
|
+
if (debug || debugGC) {
|
|
354
|
+
console.log(`[gltf-progressive] Disposing rejected lower-quality texture LOD ${level} (assigned is ${assignedLOD.level})`, tex.uuid);
|
|
355
|
+
}
|
|
356
|
+
tex.dispose();
|
|
357
|
+
}
|
|
337
358
|
return null;
|
|
338
359
|
}
|
|
339
360
|
// assigned.dispose();
|
|
340
361
|
}
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
362
|
+
// Track reference count for new texture
|
|
363
|
+
this.trackTextureUsage(tex);
|
|
364
|
+
// Untrack the old texture (may dispose if ref count hits 0)
|
|
365
|
+
// This prevents accumulation of GPU VRAM while waiting for garbage collection
|
|
366
|
+
if (assigned && assigned !== tex) {
|
|
367
|
+
const wasDisposed = this.untrackTextureUsage(assigned);
|
|
368
|
+
if (wasDisposed && (debug || debugGC)) {
|
|
369
|
+
const assignedLOD = this.getAssignedLODInformation(assigned);
|
|
370
|
+
console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} → ${level} for ${material.name || material.type}.${slot}`, assigned.uuid);
|
|
348
371
|
}
|
|
349
372
|
}
|
|
350
373
|
material[slot] = tex;
|
|
351
374
|
}
|
|
352
|
-
//
|
|
353
|
-
//
|
|
354
|
-
//
|
|
355
|
-
//
|
|
356
|
-
// if (!users) {
|
|
357
|
-
// if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
|
|
358
|
-
// current?.dispose();
|
|
359
|
-
// }
|
|
375
|
+
// Note: We use reference counting above to track texture usage across multiple materials.
|
|
376
|
+
// When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
|
|
377
|
+
// not waiting for JavaScript garbage collection which may take seconds/minutes.
|
|
378
|
+
// This handles cases where the same texture is shared across multiple materials/objects.
|
|
360
379
|
}
|
|
361
380
|
// this.onProgressiveLoadEnd(info);
|
|
362
381
|
return tex;
|
|
@@ -531,6 +550,7 @@ export class NEEDLE_progressive {
|
|
|
531
550
|
* Dispose cached resources to free memory.
|
|
532
551
|
* Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
|
|
533
552
|
* Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
|
|
553
|
+
* Also clears reference counts for disposed textures.
|
|
534
554
|
* @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
|
|
535
555
|
*/
|
|
536
556
|
static dispose(guid) {
|
|
@@ -542,7 +562,9 @@ export class NEEDLE_progressive {
|
|
|
542
562
|
const lowres = lowresRef.deref();
|
|
543
563
|
if (lowres) {
|
|
544
564
|
if (lowres.isTexture) {
|
|
545
|
-
lowres
|
|
565
|
+
const tex = lowres;
|
|
566
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
567
|
+
tex.dispose();
|
|
546
568
|
}
|
|
547
569
|
else if (Array.isArray(lowres)) {
|
|
548
570
|
for (const geo of lowres)
|
|
@@ -552,20 +574,24 @@ export class NEEDLE_progressive {
|
|
|
552
574
|
this.lowresCache.delete(guid);
|
|
553
575
|
}
|
|
554
576
|
// Dispose previously loaded LOD entries
|
|
555
|
-
for (const [key, entry] of this.
|
|
577
|
+
for (const [key, entry] of this.cache) {
|
|
556
578
|
if (key.includes(guid)) {
|
|
557
579
|
this._disposeCacheEntry(entry);
|
|
558
|
-
this.
|
|
580
|
+
this.cache.delete(key);
|
|
559
581
|
}
|
|
560
582
|
}
|
|
561
583
|
}
|
|
562
584
|
else {
|
|
585
|
+
// Invalidate requests waiting for a queue slot as well as active loads.
|
|
586
|
+
this.cacheGeneration++;
|
|
563
587
|
this.lodInfos.clear();
|
|
564
588
|
for (const [, entryRef] of this.lowresCache) {
|
|
565
589
|
const entry = entryRef.deref();
|
|
566
590
|
if (entry) {
|
|
567
591
|
if (entry.isTexture) {
|
|
568
|
-
entry
|
|
592
|
+
const tex = entry;
|
|
593
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
594
|
+
tex.dispose();
|
|
569
595
|
}
|
|
570
596
|
else if (Array.isArray(entry)) {
|
|
571
597
|
for (const geo of entry)
|
|
@@ -574,10 +600,12 @@ export class NEEDLE_progressive {
|
|
|
574
600
|
}
|
|
575
601
|
}
|
|
576
602
|
this.lowresCache.clear();
|
|
577
|
-
for (const [, entry] of this.
|
|
603
|
+
for (const [, entry] of this.cache) {
|
|
578
604
|
this._disposeCacheEntry(entry);
|
|
579
605
|
}
|
|
580
|
-
this.
|
|
606
|
+
this.cache.clear();
|
|
607
|
+
// Clear all texture reference counts when disposing everything
|
|
608
|
+
this.textureRefCounts.clear();
|
|
581
609
|
}
|
|
582
610
|
}
|
|
583
611
|
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
@@ -585,11 +613,18 @@ export class NEEDLE_progressive {
|
|
|
585
613
|
if (entry instanceof WeakRef) {
|
|
586
614
|
// Single resource — deref and dispose if still alive
|
|
587
615
|
const resource = entry.deref();
|
|
588
|
-
resource
|
|
616
|
+
if (resource) {
|
|
617
|
+
// Clear ref count for textures
|
|
618
|
+
if (resource.isTexture) {
|
|
619
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
620
|
+
}
|
|
621
|
+
resource.dispose();
|
|
622
|
+
}
|
|
589
623
|
}
|
|
590
624
|
else {
|
|
591
625
|
// Promise — may be in-flight or already resolved.
|
|
592
626
|
// Attach disposal to run after resolution.
|
|
627
|
+
this.disposedRequests.add(entry);
|
|
593
628
|
entry.then(resource => {
|
|
594
629
|
if (resource) {
|
|
595
630
|
if (Array.isArray(resource)) {
|
|
@@ -597,6 +632,10 @@ export class NEEDLE_progressive {
|
|
|
597
632
|
geo.dispose();
|
|
598
633
|
}
|
|
599
634
|
else {
|
|
635
|
+
// Clear ref count for textures
|
|
636
|
+
if (resource.isTexture) {
|
|
637
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
638
|
+
}
|
|
600
639
|
resource.dispose();
|
|
601
640
|
}
|
|
602
641
|
}
|
|
@@ -606,30 +645,90 @@ export class NEEDLE_progressive {
|
|
|
606
645
|
/** A map of key = asset uuid and value = LOD information */
|
|
607
646
|
static lodInfos = new Map();
|
|
608
647
|
/** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
|
|
609
|
-
static
|
|
648
|
+
static cache = new Map();
|
|
610
649
|
/** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
|
|
611
650
|
static lowresCache = new Map();
|
|
651
|
+
/** Reference counting for textures to track usage across multiple materials/objects */
|
|
652
|
+
static textureRefCounts = new Map();
|
|
612
653
|
/**
|
|
613
654
|
* FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
|
|
614
655
|
* when their associated three.js resources are garbage collected by the browser.
|
|
615
656
|
* The held value is the cache key string used in `previouslyLoaded`.
|
|
616
657
|
*/
|
|
617
658
|
static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
|
|
618
|
-
const entry = NEEDLE_progressive.
|
|
619
|
-
|
|
659
|
+
const entry = NEEDLE_progressive.cache.get(cacheKey);
|
|
660
|
+
if (debug || debugGC)
|
|
661
|
+
console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
|
|
620
662
|
// Only delete if the entry is still a WeakRef and the resource is gone
|
|
621
663
|
if (entry instanceof WeakRef) {
|
|
622
664
|
const derefed = entry.deref();
|
|
623
665
|
if (!derefed) {
|
|
624
|
-
NEEDLE_progressive.
|
|
625
|
-
if (debug)
|
|
626
|
-
console.log(`[gltf-progressive] Cache entry
|
|
666
|
+
NEEDLE_progressive.cache.delete(cacheKey);
|
|
667
|
+
if (debug || debugGC)
|
|
668
|
+
console.log(`[gltf-progressive] ↪ Cache entry deleted (GC)`);
|
|
627
669
|
}
|
|
628
670
|
}
|
|
629
671
|
});
|
|
672
|
+
/**
|
|
673
|
+
* Track texture usage by incrementing reference count
|
|
674
|
+
*/
|
|
675
|
+
static trackTextureUsage(texture) {
|
|
676
|
+
const uuid = texture.uuid;
|
|
677
|
+
const count = this.textureRefCounts.get(uuid) || 0;
|
|
678
|
+
this.textureRefCounts.set(uuid, count + 1);
|
|
679
|
+
if (debug === "verbose") {
|
|
680
|
+
console.log(`[gltf-progressive] Track texture ${uuid}, refCount: ${count} → ${count + 1}`);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Untrack texture usage by decrementing reference count.
|
|
685
|
+
* Automatically disposes the texture when reference count reaches zero.
|
|
686
|
+
* @returns true if the texture was disposed, false otherwise
|
|
687
|
+
*/
|
|
688
|
+
static untrackTextureUsage(texture) {
|
|
689
|
+
const uuid = texture.uuid;
|
|
690
|
+
const count = this.textureRefCounts.get(uuid);
|
|
691
|
+
if (!count) {
|
|
692
|
+
// Texture wasn't tracked, dispose immediately (safe fallback)
|
|
693
|
+
if (debug === "verbose" || debugGC) {
|
|
694
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)`, 0);
|
|
695
|
+
}
|
|
696
|
+
texture.dispose();
|
|
697
|
+
return true;
|
|
698
|
+
}
|
|
699
|
+
const newCount = count - 1;
|
|
700
|
+
if (newCount <= 0) {
|
|
701
|
+
this.textureRefCounts.delete(uuid);
|
|
702
|
+
if (debug || debugGC) {
|
|
703
|
+
logDebugInfo(`[gltf-progressive] Memory: Dispose texture`, newCount);
|
|
704
|
+
}
|
|
705
|
+
texture.dispose();
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
else {
|
|
709
|
+
this.textureRefCounts.set(uuid, newCount);
|
|
710
|
+
if (debug === "verbose") {
|
|
711
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack texture`, newCount);
|
|
712
|
+
}
|
|
713
|
+
return false;
|
|
714
|
+
}
|
|
715
|
+
function logDebugInfo(prefix, newCount) {
|
|
716
|
+
let width = texture.image?.width || texture.source?.data?.width || 0;
|
|
717
|
+
let height = texture.image?.height || texture.source?.data?.height || 0;
|
|
718
|
+
const textureSize = width && height ? `${width}x${height}` : "N/A";
|
|
719
|
+
let memorySize = "N/A";
|
|
720
|
+
if (width && height) {
|
|
721
|
+
memorySize = `~${(determineTextureMemoryInBytes(texture) / (1024 * 1024)).toFixed(2)} MB`;
|
|
722
|
+
}
|
|
723
|
+
console.log(`${prefix} — ${texture.name} ${textureSize} (${memorySize}), refCount: ${count} → ${newCount}\n${uuid}`);
|
|
724
|
+
}
|
|
725
|
+
}
|
|
630
726
|
static workers = [];
|
|
727
|
+
static cacheGeneration = 0;
|
|
728
|
+
static disposedRequests = new WeakSet();
|
|
631
729
|
static _workersIndex = 0;
|
|
632
730
|
static async getOrLoadLOD(current, level) {
|
|
731
|
+
const generation = this.cacheGeneration;
|
|
633
732
|
const debugverbose = debug == "verbose";
|
|
634
733
|
/** this key is used to lookup the LOD information */
|
|
635
734
|
const LOD = this.getAssignedLODInformation(current);
|
|
@@ -699,8 +798,10 @@ export class NEEDLE_progressive {
|
|
|
699
798
|
// check if the requested file has already been loaded
|
|
700
799
|
const KEY = lod_url + "_" + lodInfo.guid;
|
|
701
800
|
const slot = await this.queue.slot(lod_url);
|
|
801
|
+
if (generation !== this.cacheGeneration)
|
|
802
|
+
return null;
|
|
702
803
|
// check if the requested file is currently being loaded or was previously loaded
|
|
703
|
-
const existing = this.
|
|
804
|
+
const existing = this.cache.get(KEY);
|
|
704
805
|
if (existing !== undefined) {
|
|
705
806
|
if (debugverbose)
|
|
706
807
|
console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
|
|
@@ -728,7 +829,7 @@ export class NEEDLE_progressive {
|
|
|
728
829
|
}
|
|
729
830
|
}
|
|
730
831
|
// Resource was garbage collected or disposed — remove stale entry and re-load
|
|
731
|
-
this.
|
|
832
|
+
this.cache.delete(KEY);
|
|
732
833
|
if (debug)
|
|
733
834
|
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
|
|
734
835
|
}
|
|
@@ -738,6 +839,8 @@ export class NEEDLE_progressive {
|
|
|
738
839
|
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
|
|
739
840
|
return null;
|
|
740
841
|
});
|
|
842
|
+
if (generation !== this.cacheGeneration || this.disposedRequests.has(existing))
|
|
843
|
+
return null;
|
|
741
844
|
let resouceIsDisposed = false;
|
|
742
845
|
if (res == null) {
|
|
743
846
|
// if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
|
|
@@ -751,7 +854,7 @@ export class NEEDLE_progressive {
|
|
|
751
854
|
// if it has been disposed we need to load it again
|
|
752
855
|
else {
|
|
753
856
|
resouceIsDisposed = true;
|
|
754
|
-
this.
|
|
857
|
+
this.cache.delete(KEY);
|
|
755
858
|
}
|
|
756
859
|
}
|
|
757
860
|
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
@@ -760,7 +863,7 @@ export class NEEDLE_progressive {
|
|
|
760
863
|
}
|
|
761
864
|
else {
|
|
762
865
|
resouceIsDisposed = true;
|
|
763
|
-
this.
|
|
866
|
+
this.cache.delete(KEY);
|
|
764
867
|
}
|
|
765
868
|
}
|
|
766
869
|
if (!resouceIsDisposed) {
|
|
@@ -912,30 +1015,36 @@ export class NEEDLE_progressive {
|
|
|
912
1015
|
// we could not find a texture or mesh with the given guid
|
|
913
1016
|
return resolve(null);
|
|
914
1017
|
});
|
|
915
|
-
this.
|
|
1018
|
+
this.cache.set(KEY, request);
|
|
916
1019
|
slot.use(request);
|
|
917
1020
|
const res = await request;
|
|
1021
|
+
// dispose() cleans up removed pending entries when they resolve. Do not
|
|
1022
|
+
// return those resources or overwrite a newer request for this key.
|
|
1023
|
+
if (generation !== this.cacheGeneration || this.cache.get(KEY) !== request)
|
|
1024
|
+
return null;
|
|
918
1025
|
// Optimize cache entry: replace loading promise with lightweight reference.
|
|
919
1026
|
// This releases closure variables captured during the loading function.
|
|
920
1027
|
if (res != null) {
|
|
921
|
-
if (
|
|
1028
|
+
if (res instanceof Texture) {
|
|
1029
|
+
// For Texture resources, use WeakRef to allow garbage collection.
|
|
1030
|
+
// The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
|
|
1031
|
+
this.cache.set(KEY, new WeakRef(res));
|
|
1032
|
+
NEEDLE_progressive._resourceRegistry.register(res, KEY);
|
|
1033
|
+
}
|
|
1034
|
+
else if (Array.isArray(res)) {
|
|
922
1035
|
// For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
this.previouslyLoaded.set(KEY, Promise.resolve(res));
|
|
1036
|
+
// This keeps geometries in memory as they should not be GC'd (mesh LODs stay cached).
|
|
1037
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
926
1038
|
}
|
|
927
1039
|
else {
|
|
928
|
-
// For single
|
|
929
|
-
|
|
930
|
-
// The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
|
|
931
|
-
this.previouslyLoaded.set(KEY, new WeakRef(res));
|
|
932
|
-
NEEDLE_progressive._resourceRegistry.register(res, KEY);
|
|
1040
|
+
// For single BufferGeometry, keep in memory (don't use WeakRef)
|
|
1041
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
933
1042
|
}
|
|
934
1043
|
}
|
|
935
1044
|
else {
|
|
936
1045
|
// Failed load — replace with clean resolved promise to release loading closure.
|
|
937
1046
|
// Keeping the entry prevents retrying (existing behavior).
|
|
938
|
-
this.
|
|
1047
|
+
this.cache.set(KEY, Promise.resolve(null));
|
|
939
1048
|
}
|
|
940
1049
|
return res;
|
|
941
1050
|
}
|
|
@@ -945,6 +1054,10 @@ export class NEEDLE_progressive {
|
|
|
945
1054
|
console.log("Load texture from uri: " + lod_url);
|
|
946
1055
|
const loader = new TextureLoader();
|
|
947
1056
|
const tex = await loader.loadAsync(lod_url);
|
|
1057
|
+
if (generation !== this.cacheGeneration) {
|
|
1058
|
+
tex?.dispose();
|
|
1059
|
+
return null;
|
|
1060
|
+
}
|
|
948
1061
|
if (tex) {
|
|
949
1062
|
tex.guid = lodInfo.guid;
|
|
950
1063
|
tex.flipY = false;
|
|
@@ -961,8 +1074,7 @@ export class NEEDLE_progressive {
|
|
|
961
1074
|
}
|
|
962
1075
|
return null;
|
|
963
1076
|
}
|
|
964
|
-
static
|
|
965
|
-
static queue = new PromiseQueue(NEEDLE_progressive.maxConcurrent, { debug: debug != false });
|
|
1077
|
+
static queue = new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false });
|
|
966
1078
|
static assignLODInformation(url, res, key, level, index) {
|
|
967
1079
|
if (!res)
|
|
968
1080
|
return;
|
package/lib/utils.internal.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Texture } from "three";
|
|
1
2
|
export declare function isDebugMode(): string | boolean;
|
|
2
3
|
export declare function getParam(name: string): boolean | string;
|
|
3
4
|
export declare function resolveUrl(source: string | undefined, uri: string): string;
|
|
@@ -18,11 +19,11 @@ export type SlotReturnValue<T = any> = {
|
|
|
18
19
|
* Use the `slot` method to request a slot for a promise with a specific key. The returned promise resolves to an object with a `use` method that can be called to add the promise to the queue.
|
|
19
20
|
*/
|
|
20
21
|
export declare class PromiseQueue<T = any> {
|
|
21
|
-
|
|
22
|
+
maxConcurrent: number;
|
|
22
23
|
private readonly _running;
|
|
23
24
|
private readonly _queue;
|
|
24
25
|
debug: boolean;
|
|
25
|
-
constructor(maxConcurrent
|
|
26
|
+
constructor(maxConcurrent: number, opts?: {
|
|
26
27
|
debug?: boolean;
|
|
27
28
|
});
|
|
28
29
|
private tick;
|
|
@@ -33,3 +34,8 @@ export declare class PromiseQueue<T = any> {
|
|
|
33
34
|
private add;
|
|
34
35
|
private internalUpdate;
|
|
35
36
|
}
|
|
37
|
+
export declare function determineTextureMemoryInBytes(texture: Texture): number;
|
|
38
|
+
/**
|
|
39
|
+
* Detect the GPU memory of the current device. This is a very rough estimate based on the renderer information, and may not be accurate. It returns the estimated memory in MB, or `undefined` if it cannot be detected.
|
|
40
|
+
*/
|
|
41
|
+
export declare function detectGPUMemory(): number | undefined;
|
package/lib/utils.internal.js
CHANGED
|
@@ -71,7 +71,7 @@ export class PromiseQueue {
|
|
|
71
71
|
_running = new Map();
|
|
72
72
|
_queue = [];
|
|
73
73
|
debug = false;
|
|
74
|
-
constructor(maxConcurrent
|
|
74
|
+
constructor(maxConcurrent, opts = {}) {
|
|
75
75
|
this.maxConcurrent = maxConcurrent;
|
|
76
76
|
this.debug = opts.debug ?? false;
|
|
77
77
|
window.requestAnimationFrame(this.tick);
|
|
@@ -115,3 +115,95 @@ export class PromiseQueue {
|
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
|
+
// #region Texture Memory
|
|
119
|
+
export function determineTextureMemoryInBytes(texture) {
|
|
120
|
+
const width = texture.image?.width ?? 0;
|
|
121
|
+
const height = texture.image?.height ?? 0;
|
|
122
|
+
const depth = texture.image?.depth ?? 1;
|
|
123
|
+
const mipLevels = Math.floor(Math.log2(Math.max(width, height, depth))) + 1;
|
|
124
|
+
const bytesPerPixel = getBytesPerPixel(texture);
|
|
125
|
+
const totalBytes = (width * height * depth * bytesPerPixel * (1 - Math.pow(0.25, mipLevels))) / (1 - 0.25);
|
|
126
|
+
return totalBytes;
|
|
127
|
+
}
|
|
128
|
+
function getBytesPerPixel(texture) {
|
|
129
|
+
// Determine channel count from format
|
|
130
|
+
let channels = 4; // Default RGBA
|
|
131
|
+
const format = texture.format;
|
|
132
|
+
if (format === 1024)
|
|
133
|
+
channels = 1; // RedFormat
|
|
134
|
+
else if (format === 1025)
|
|
135
|
+
channels = 1; // RedIntegerFormat
|
|
136
|
+
else if (format === 1026)
|
|
137
|
+
channels = 2; // RGFormat
|
|
138
|
+
else if (format === 1027)
|
|
139
|
+
channels = 2; // RGIntegerFormat
|
|
140
|
+
else if (format === 1022)
|
|
141
|
+
channels = 3; // RGBFormat
|
|
142
|
+
else if (format === 1029)
|
|
143
|
+
channels = 3; // RGBIntegerFormat
|
|
144
|
+
else if (format === 1023)
|
|
145
|
+
channels = 4; // RGBAFormat
|
|
146
|
+
else if (format === 1033)
|
|
147
|
+
channels = 4; // RGBAIntegerFormat
|
|
148
|
+
// Determine bytes per channel from type
|
|
149
|
+
let bytesPerChannel = 1; // UnsignedByteType default
|
|
150
|
+
const type = texture.type;
|
|
151
|
+
if (type === 1009)
|
|
152
|
+
bytesPerChannel = 1; // UnsignedByteType
|
|
153
|
+
else if (type === 1010)
|
|
154
|
+
bytesPerChannel = 1; // ByteType
|
|
155
|
+
else if (type === 1011)
|
|
156
|
+
bytesPerChannel = 2; // ShortType
|
|
157
|
+
else if (type === 1012)
|
|
158
|
+
bytesPerChannel = 2; // UnsignedShortType
|
|
159
|
+
else if (type === 1013)
|
|
160
|
+
bytesPerChannel = 4; // IntType
|
|
161
|
+
else if (type === 1014)
|
|
162
|
+
bytesPerChannel = 4; // UnsignedIntType
|
|
163
|
+
else if (type === 1015)
|
|
164
|
+
bytesPerChannel = 4; // FloatType
|
|
165
|
+
else if (type === 1016)
|
|
166
|
+
bytesPerChannel = 2; // HalfFloatType
|
|
167
|
+
const bytesPerPixel = channels * bytesPerChannel;
|
|
168
|
+
return bytesPerPixel;
|
|
169
|
+
}
|
|
170
|
+
// #region GPU
|
|
171
|
+
let rendererInfo;
|
|
172
|
+
/**
|
|
173
|
+
* Detect the GPU memory of the current device. This is a very rough estimate based on the renderer information, and may not be accurate. It returns the estimated memory in MB, or `undefined` if it cannot be detected.
|
|
174
|
+
*/
|
|
175
|
+
export function detectGPUMemory() {
|
|
176
|
+
if (rendererInfo !== undefined) {
|
|
177
|
+
return rendererInfo?.estimatedMemory;
|
|
178
|
+
}
|
|
179
|
+
const canvas = document.createElement('canvas');
|
|
180
|
+
const powerPreference = "high-performance";
|
|
181
|
+
const gl = canvas.getContext('webgl', { powerPreference }) || canvas.getContext('experimental-webgl', { powerPreference });
|
|
182
|
+
if (!gl) {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
if ("getExtension" in gl) {
|
|
186
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
187
|
+
if (debugInfo) {
|
|
188
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
189
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
190
|
+
// Estimate memory based on renderer information (this is a very rough estimate)
|
|
191
|
+
let estimatedMemory = 512;
|
|
192
|
+
if (/NVIDIA/i.test(renderer)) {
|
|
193
|
+
estimatedMemory = 2048;
|
|
194
|
+
}
|
|
195
|
+
else if (/AMD/i.test(renderer)) {
|
|
196
|
+
estimatedMemory = 1024;
|
|
197
|
+
}
|
|
198
|
+
else if (/Intel/i.test(renderer)) {
|
|
199
|
+
estimatedMemory = 512;
|
|
200
|
+
}
|
|
201
|
+
rendererInfo = { vendor, renderer, estimatedMemory };
|
|
202
|
+
return estimatedMemory;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
rendererInfo = null;
|
|
207
|
+
}
|
|
208
|
+
return undefined;
|
|
209
|
+
}
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-progressive",
|
|
3
|
-
"version": "3.4.0-next.
|
|
3
|
+
"version": "3.4.0-next.ed30751",
|
|
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": {
|