@needle-tools/gltf-progressive 3.4.0-rc → 4.0.0-alpha.1
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 +6 -0
- package/README.md +9 -0
- package/gltf-progressive.js +721 -642
- package/gltf-progressive.min.js +9 -7
- package/gltf-progressive.umd.cjs +9 -7
- package/lib/extension.d.ts +20 -2
- package/lib/extension.js +142 -50
- package/lib/loaders.d.ts +1 -8
- package/lib/loaders.js +1 -0
- package/lib/lods.manager.js +4 -2
- package/lib/utils.d.ts +1 -1
- package/lib/utils.internal.d.ts +35 -2
- package/lib/utils.internal.js +123 -1
- package/lib/version.js +1 -1
- package/lib/worker/loader.mainthread.js +3 -4
- package/package.json +5 -5
- /package/lib/worker/{loader.worker.js → gltf-progressive.worker.js} +0 -0
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, getSourceData, getTextureDimensions, hasPixelData, 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;
|
|
@@ -482,8 +501,7 @@ export class NEEDLE_progressive {
|
|
|
482
501
|
return;
|
|
483
502
|
}
|
|
484
503
|
if (debug) {
|
|
485
|
-
const width
|
|
486
|
-
const height = tex.image?.height || tex.source?.data?.height || 0;
|
|
504
|
+
const { width, height } = getTextureDimensions(tex);
|
|
487
505
|
console.log(`> gltf-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);
|
|
488
506
|
}
|
|
489
507
|
// 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)
|
|
@@ -531,6 +549,7 @@ export class NEEDLE_progressive {
|
|
|
531
549
|
* Dispose cached resources to free memory.
|
|
532
550
|
* Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
|
|
533
551
|
* Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
|
|
552
|
+
* Also clears reference counts for disposed textures.
|
|
534
553
|
* @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
|
|
535
554
|
*/
|
|
536
555
|
static dispose(guid) {
|
|
@@ -542,7 +561,9 @@ export class NEEDLE_progressive {
|
|
|
542
561
|
const lowres = lowresRef.deref();
|
|
543
562
|
if (lowres) {
|
|
544
563
|
if (lowres.isTexture) {
|
|
545
|
-
lowres
|
|
564
|
+
const tex = lowres;
|
|
565
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
566
|
+
tex.dispose();
|
|
546
567
|
}
|
|
547
568
|
else if (Array.isArray(lowres)) {
|
|
548
569
|
for (const geo of lowres)
|
|
@@ -552,10 +573,10 @@ export class NEEDLE_progressive {
|
|
|
552
573
|
this.lowresCache.delete(guid);
|
|
553
574
|
}
|
|
554
575
|
// Dispose previously loaded LOD entries
|
|
555
|
-
for (const [key, entry] of this.
|
|
576
|
+
for (const [key, entry] of this.cache) {
|
|
556
577
|
if (key.includes(guid)) {
|
|
557
578
|
this._disposeCacheEntry(entry);
|
|
558
|
-
this.
|
|
579
|
+
this.cache.delete(key);
|
|
559
580
|
}
|
|
560
581
|
}
|
|
561
582
|
}
|
|
@@ -565,7 +586,9 @@ export class NEEDLE_progressive {
|
|
|
565
586
|
const entry = entryRef.deref();
|
|
566
587
|
if (entry) {
|
|
567
588
|
if (entry.isTexture) {
|
|
568
|
-
entry
|
|
589
|
+
const tex = entry;
|
|
590
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
591
|
+
tex.dispose();
|
|
569
592
|
}
|
|
570
593
|
else if (Array.isArray(entry)) {
|
|
571
594
|
for (const geo of entry)
|
|
@@ -574,10 +597,12 @@ export class NEEDLE_progressive {
|
|
|
574
597
|
}
|
|
575
598
|
}
|
|
576
599
|
this.lowresCache.clear();
|
|
577
|
-
for (const [, entry] of this.
|
|
600
|
+
for (const [, entry] of this.cache) {
|
|
578
601
|
this._disposeCacheEntry(entry);
|
|
579
602
|
}
|
|
580
|
-
this.
|
|
603
|
+
this.cache.clear();
|
|
604
|
+
// Clear all texture reference counts when disposing everything
|
|
605
|
+
this.textureRefCounts.clear();
|
|
581
606
|
}
|
|
582
607
|
}
|
|
583
608
|
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
@@ -585,7 +610,13 @@ export class NEEDLE_progressive {
|
|
|
585
610
|
if (entry instanceof WeakRef) {
|
|
586
611
|
// Single resource — deref and dispose if still alive
|
|
587
612
|
const resource = entry.deref();
|
|
588
|
-
resource
|
|
613
|
+
if (resource) {
|
|
614
|
+
// Clear ref count for textures
|
|
615
|
+
if (resource.isTexture) {
|
|
616
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
617
|
+
}
|
|
618
|
+
resource.dispose();
|
|
619
|
+
}
|
|
589
620
|
}
|
|
590
621
|
else {
|
|
591
622
|
// Promise — may be in-flight or already resolved.
|
|
@@ -597,6 +628,10 @@ export class NEEDLE_progressive {
|
|
|
597
628
|
geo.dispose();
|
|
598
629
|
}
|
|
599
630
|
else {
|
|
631
|
+
// Clear ref count for textures
|
|
632
|
+
if (resource.isTexture) {
|
|
633
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
634
|
+
}
|
|
600
635
|
resource.dispose();
|
|
601
636
|
}
|
|
602
637
|
}
|
|
@@ -606,27 +641,83 @@ export class NEEDLE_progressive {
|
|
|
606
641
|
/** A map of key = asset uuid and value = LOD information */
|
|
607
642
|
static lodInfos = new Map();
|
|
608
643
|
/** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
|
|
609
|
-
static
|
|
644
|
+
static cache = new Map();
|
|
610
645
|
/** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
|
|
611
646
|
static lowresCache = new Map();
|
|
647
|
+
/** Reference counting for textures to track usage across multiple materials/objects */
|
|
648
|
+
static textureRefCounts = new Map();
|
|
612
649
|
/**
|
|
613
650
|
* FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
|
|
614
651
|
* when their associated three.js resources are garbage collected by the browser.
|
|
615
652
|
* The held value is the cache key string used in `previouslyLoaded`.
|
|
616
653
|
*/
|
|
617
654
|
static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
|
|
618
|
-
const entry = NEEDLE_progressive.
|
|
619
|
-
|
|
655
|
+
const entry = NEEDLE_progressive.cache.get(cacheKey);
|
|
656
|
+
if (debug || debugGC)
|
|
657
|
+
console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
|
|
620
658
|
// Only delete if the entry is still a WeakRef and the resource is gone
|
|
621
659
|
if (entry instanceof WeakRef) {
|
|
622
660
|
const derefed = entry.deref();
|
|
623
661
|
if (!derefed) {
|
|
624
|
-
NEEDLE_progressive.
|
|
625
|
-
if (debug)
|
|
626
|
-
console.log(`[gltf-progressive] Cache entry
|
|
662
|
+
NEEDLE_progressive.cache.delete(cacheKey);
|
|
663
|
+
if (debug || debugGC)
|
|
664
|
+
console.log(`[gltf-progressive] ↪ Cache entry deleted (GC)`);
|
|
627
665
|
}
|
|
628
666
|
}
|
|
629
667
|
});
|
|
668
|
+
/**
|
|
669
|
+
* Track texture usage by incrementing reference count
|
|
670
|
+
*/
|
|
671
|
+
static trackTextureUsage(texture) {
|
|
672
|
+
const uuid = texture.uuid;
|
|
673
|
+
const count = this.textureRefCounts.get(uuid) || 0;
|
|
674
|
+
this.textureRefCounts.set(uuid, count + 1);
|
|
675
|
+
if (debug === "verbose") {
|
|
676
|
+
console.log(`[gltf-progressive] Track texture ${uuid}, refCount: ${count} → ${count + 1}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Untrack texture usage by decrementing reference count.
|
|
681
|
+
* Automatically disposes the texture when reference count reaches zero.
|
|
682
|
+
* @returns true if the texture was disposed, false otherwise
|
|
683
|
+
*/
|
|
684
|
+
static untrackTextureUsage(texture) {
|
|
685
|
+
const uuid = texture.uuid;
|
|
686
|
+
const count = this.textureRefCounts.get(uuid);
|
|
687
|
+
if (!count) {
|
|
688
|
+
// Texture wasn't tracked, dispose immediately (safe fallback)
|
|
689
|
+
if (debug === "verbose" || debugGC) {
|
|
690
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)`, 0);
|
|
691
|
+
}
|
|
692
|
+
texture.dispose();
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
const newCount = count - 1;
|
|
696
|
+
if (newCount <= 0) {
|
|
697
|
+
this.textureRefCounts.delete(uuid);
|
|
698
|
+
if (debug || debugGC) {
|
|
699
|
+
logDebugInfo(`[gltf-progressive] Memory: Dispose texture`, newCount);
|
|
700
|
+
}
|
|
701
|
+
texture.dispose();
|
|
702
|
+
return true;
|
|
703
|
+
}
|
|
704
|
+
else {
|
|
705
|
+
this.textureRefCounts.set(uuid, newCount);
|
|
706
|
+
if (debug === "verbose") {
|
|
707
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack texture`, newCount);
|
|
708
|
+
}
|
|
709
|
+
return false;
|
|
710
|
+
}
|
|
711
|
+
function logDebugInfo(prefix, newCount) {
|
|
712
|
+
let { width, height } = getTextureDimensions(texture);
|
|
713
|
+
const textureSize = width && height ? `${width}x${height}` : "N/A";
|
|
714
|
+
let memorySize = "N/A";
|
|
715
|
+
if (width && height) {
|
|
716
|
+
memorySize = `~${(determineTextureMemoryInBytes(texture) / (1024 * 1024)).toFixed(2)} MB`;
|
|
717
|
+
}
|
|
718
|
+
console.log(`${prefix} — ${texture.name} ${textureSize} (${memorySize}), refCount: ${count} → ${newCount}\n${uuid}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
630
721
|
static workers = [];
|
|
631
722
|
static _workersIndex = 0;
|
|
632
723
|
static async getOrLoadLOD(current, level) {
|
|
@@ -700,7 +791,7 @@ export class NEEDLE_progressive {
|
|
|
700
791
|
const KEY = lod_url + "_" + lodInfo.guid;
|
|
701
792
|
const slot = await this.queue.slot(lod_url);
|
|
702
793
|
// check if the requested file is currently being loaded or was previously loaded
|
|
703
|
-
const existing = this.
|
|
794
|
+
const existing = this.cache.get(KEY);
|
|
704
795
|
if (existing !== undefined) {
|
|
705
796
|
if (debugverbose)
|
|
706
797
|
console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
|
|
@@ -711,7 +802,7 @@ export class NEEDLE_progressive {
|
|
|
711
802
|
let res = derefed;
|
|
712
803
|
let resourceIsDisposed = false;
|
|
713
804
|
if (res instanceof Texture && current instanceof Texture) {
|
|
714
|
-
if (res.image
|
|
805
|
+
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
715
806
|
res = this.copySettings(current, res);
|
|
716
807
|
}
|
|
717
808
|
else {
|
|
@@ -728,7 +819,7 @@ export class NEEDLE_progressive {
|
|
|
728
819
|
}
|
|
729
820
|
}
|
|
730
821
|
// Resource was garbage collected or disposed — remove stale entry and re-load
|
|
731
|
-
this.
|
|
822
|
+
this.cache.delete(KEY);
|
|
732
823
|
if (debug)
|
|
733
824
|
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
|
|
734
825
|
}
|
|
@@ -745,13 +836,13 @@ export class NEEDLE_progressive {
|
|
|
745
836
|
}
|
|
746
837
|
else if (res instanceof Texture && current instanceof Texture) {
|
|
747
838
|
// check if the texture has been disposed or not
|
|
748
|
-
if (res.image
|
|
839
|
+
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
749
840
|
res = this.copySettings(current, res);
|
|
750
841
|
}
|
|
751
842
|
// if it has been disposed we need to load it again
|
|
752
843
|
else {
|
|
753
844
|
resouceIsDisposed = true;
|
|
754
|
-
this.
|
|
845
|
+
this.cache.delete(KEY);
|
|
755
846
|
}
|
|
756
847
|
}
|
|
757
848
|
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
@@ -760,7 +851,7 @@ export class NEEDLE_progressive {
|
|
|
760
851
|
}
|
|
761
852
|
else {
|
|
762
853
|
resouceIsDisposed = true;
|
|
763
|
-
this.
|
|
854
|
+
this.cache.delete(KEY);
|
|
764
855
|
}
|
|
765
856
|
}
|
|
766
857
|
if (!resouceIsDisposed) {
|
|
@@ -912,30 +1003,32 @@ export class NEEDLE_progressive {
|
|
|
912
1003
|
// we could not find a texture or mesh with the given guid
|
|
913
1004
|
return resolve(null);
|
|
914
1005
|
});
|
|
915
|
-
this.
|
|
1006
|
+
this.cache.set(KEY, request);
|
|
916
1007
|
slot.use(request);
|
|
917
1008
|
const res = await request;
|
|
918
1009
|
// Optimize cache entry: replace loading promise with lightweight reference.
|
|
919
1010
|
// This releases closure variables captured during the loading function.
|
|
920
1011
|
if (res != null) {
|
|
921
|
-
if (
|
|
1012
|
+
if (res instanceof Texture) {
|
|
1013
|
+
// For Texture resources, use WeakRef to allow garbage collection.
|
|
1014
|
+
// The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
|
|
1015
|
+
this.cache.set(KEY, new WeakRef(res));
|
|
1016
|
+
NEEDLE_progressive._resourceRegistry.register(res, KEY);
|
|
1017
|
+
}
|
|
1018
|
+
else if (Array.isArray(res)) {
|
|
922
1019
|
// For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
this.previouslyLoaded.set(KEY, Promise.resolve(res));
|
|
1020
|
+
// This keeps geometries in memory as they should not be GC'd (mesh LODs stay cached).
|
|
1021
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
926
1022
|
}
|
|
927
1023
|
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);
|
|
1024
|
+
// For single BufferGeometry, keep in memory (don't use WeakRef)
|
|
1025
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
933
1026
|
}
|
|
934
1027
|
}
|
|
935
1028
|
else {
|
|
936
1029
|
// Failed load — replace with clean resolved promise to release loading closure.
|
|
937
1030
|
// Keeping the entry prevents retrying (existing behavior).
|
|
938
|
-
this.
|
|
1031
|
+
this.cache.set(KEY, Promise.resolve(null));
|
|
939
1032
|
}
|
|
940
1033
|
return res;
|
|
941
1034
|
}
|
|
@@ -961,8 +1054,7 @@ export class NEEDLE_progressive {
|
|
|
961
1054
|
}
|
|
962
1055
|
return null;
|
|
963
1056
|
}
|
|
964
|
-
static
|
|
965
|
-
static queue = new PromiseQueue(NEEDLE_progressive.maxConcurrent, { debug: debug != false });
|
|
1057
|
+
static queue = new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false });
|
|
966
1058
|
static assignLODInformation(url, res, key, level, index) {
|
|
967
1059
|
if (!res)
|
|
968
1060
|
return;
|
package/lib/loaders.d.ts
CHANGED
|
@@ -28,14 +28,7 @@ export declare function setKTX2TranscoderLocation(location: string): void;
|
|
|
28
28
|
export declare function createLoaders(renderer: WebGLRenderer | null): {
|
|
29
29
|
dracoLoader: DRACOLoader;
|
|
30
30
|
ktx2Loader: KTX2Loader;
|
|
31
|
-
meshoptDecoder:
|
|
32
|
-
supported: boolean;
|
|
33
|
-
ready: Promise<void>;
|
|
34
|
-
decodeVertexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, filter?: string) => void;
|
|
35
|
-
decodeIndexBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
|
|
36
|
-
decodeIndexSequence: (target: Uint8Array, count: number, size: number, source: Uint8Array) => void;
|
|
37
|
-
decodeGltfBuffer: (target: Uint8Array, count: number, size: number, source: Uint8Array, mode: string, filter?: string) => void;
|
|
38
|
-
};
|
|
31
|
+
meshoptDecoder: any;
|
|
39
32
|
};
|
|
40
33
|
export declare function addDracoAndKTX2Loaders(loader: GLTFLoader): void;
|
|
41
34
|
/**
|
package/lib/loaders.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
// @ts-ignore @types/three re-exports from meshoptimizer which may not resolve in all configs
|
|
1
2
|
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js';
|
|
2
3
|
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader.js';
|
|
3
4
|
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
|
package/lib/lods.manager.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
// @ts-ignore Timer exists at runtime in r183 but @types/three doesn't export it from main entry yet
|
|
2
|
+
import { Box3, Matrix4, Mesh, MeshStandardMaterial, Sphere, Timer, Vector3 } from "three";
|
|
2
3
|
import { NEEDLE_progressive } from "./extension.js";
|
|
3
4
|
import { createLoaders } from "./loaders.js";
|
|
4
5
|
import { getParam, isDevelopmentServer, isMobileDevice } from "./utils.internal.js";
|
|
@@ -171,7 +172,7 @@ export class LODsManager {
|
|
|
171
172
|
// })
|
|
172
173
|
}
|
|
173
174
|
#originalRender;
|
|
174
|
-
#clock = new
|
|
175
|
+
#clock = new Timer();
|
|
175
176
|
#frame = 0;
|
|
176
177
|
#delta = 0;
|
|
177
178
|
#time = 0;
|
|
@@ -197,6 +198,7 @@ export class LODsManager {
|
|
|
197
198
|
if (renderTarget == null || ("isXRRenderTarget" in renderTarget && renderTarget.isXRRenderTarget)) {
|
|
198
199
|
stack = 0;
|
|
199
200
|
self.#frame += 1;
|
|
201
|
+
self.#clock.update();
|
|
200
202
|
self.#delta = self.#clock.getDelta();
|
|
201
203
|
self.#time += self.#delta;
|
|
202
204
|
self._fpsBuffer.shift();
|
package/lib/utils.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export declare const isSSR: boolean;
|
|
|
5
5
|
* @param obj the object to get the raycast mesh from
|
|
6
6
|
* @returns the raycast mesh or null if not set
|
|
7
7
|
*/
|
|
8
|
-
export declare function getRaycastMesh(obj: Object3D): BufferGeometry<any> | null;
|
|
8
|
+
export declare function getRaycastMesh(obj: Object3D): BufferGeometry<any, any> | null;
|
|
9
9
|
/**
|
|
10
10
|
* Set the raycast mesh for an object.
|
|
11
11
|
* The raycast mesh is a low poly version of the mesh used for raycasting. It is set when a mesh that has LOD level with more vertices is discovered for the first time
|
package/lib/utils.internal.d.ts
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
import { Texture } from "three";
|
|
2
|
+
/** Represents the possible shapes of texture image/source data in three.js.
|
|
3
|
+
* Source.data is typed as `{}` in r183 but at runtime can be ImageBitmap, HTMLImageElement, etc. */
|
|
4
|
+
export type TextureImageData = {
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
depth?: number;
|
|
8
|
+
data?: ArrayBufferView | null;
|
|
9
|
+
};
|
|
10
|
+
/** Check if a value has image-like dimensions (width/height) */
|
|
11
|
+
export declare function hasImageDimensions(value: unknown): value is {
|
|
12
|
+
width: number;
|
|
13
|
+
height: number;
|
|
14
|
+
};
|
|
15
|
+
/** Check if a value has pixel data (e.g. typed array from a DataTexture) */
|
|
16
|
+
export declare function hasPixelData(value: unknown): value is {
|
|
17
|
+
data: ArrayBufferView;
|
|
18
|
+
};
|
|
19
|
+
/** Get the source data of a texture, typed for dimension/data access */
|
|
20
|
+
export declare function getSourceData(tex: Texture): TextureImageData | null;
|
|
21
|
+
/** Get the image of a texture, typed for dimension/data access.
|
|
22
|
+
* In r183, Texture.image is typed as `{}` but at runtime is an ImageBitmap, HTMLImageElement, etc. */
|
|
23
|
+
export declare function getTextureImage(tex: Texture): TextureImageData | null;
|
|
24
|
+
/** Get width/height of a texture from image or source data */
|
|
25
|
+
export declare function getTextureDimensions(tex: Texture): {
|
|
26
|
+
width: number;
|
|
27
|
+
height: number;
|
|
28
|
+
};
|
|
1
29
|
export declare function isDebugMode(): string | boolean;
|
|
2
30
|
export declare function getParam(name: string): boolean | string;
|
|
3
31
|
export declare function resolveUrl(source: string | undefined, uri: string): string;
|
|
@@ -18,11 +46,11 @@ export type SlotReturnValue<T = any> = {
|
|
|
18
46
|
* 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
47
|
*/
|
|
20
48
|
export declare class PromiseQueue<T = any> {
|
|
21
|
-
|
|
49
|
+
maxConcurrent: number;
|
|
22
50
|
private readonly _running;
|
|
23
51
|
private readonly _queue;
|
|
24
52
|
debug: boolean;
|
|
25
|
-
constructor(maxConcurrent
|
|
53
|
+
constructor(maxConcurrent: number, opts?: {
|
|
26
54
|
debug?: boolean;
|
|
27
55
|
});
|
|
28
56
|
private tick;
|
|
@@ -33,3 +61,8 @@ export declare class PromiseQueue<T = any> {
|
|
|
33
61
|
private add;
|
|
34
62
|
private internalUpdate;
|
|
35
63
|
}
|
|
64
|
+
export declare function determineTextureMemoryInBytes(texture: Texture): number;
|
|
65
|
+
/**
|
|
66
|
+
* 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.
|
|
67
|
+
*/
|
|
68
|
+
export declare function detectGPUMemory(): number | undefined;
|
package/lib/utils.internal.js
CHANGED
|
@@ -1,3 +1,32 @@
|
|
|
1
|
+
import { RedFormat, RedIntegerFormat, RGFormat, RGIntegerFormat, RGBFormat, RGBAFormat, RGBAIntegerFormat } from "three";
|
|
2
|
+
/** Check if a value has image-like dimensions (width/height) */
|
|
3
|
+
export function hasImageDimensions(value) {
|
|
4
|
+
return value != null && typeof value.width === 'number' && typeof value.height === 'number';
|
|
5
|
+
}
|
|
6
|
+
/** Check if a value has pixel data (e.g. typed array from a DataTexture) */
|
|
7
|
+
export function hasPixelData(value) {
|
|
8
|
+
return value != null && value.data != null;
|
|
9
|
+
}
|
|
10
|
+
/** Get the source data of a texture, typed for dimension/data access */
|
|
11
|
+
export function getSourceData(tex) {
|
|
12
|
+
const data = tex.source?.data;
|
|
13
|
+
return data != null && typeof data === 'object' ? data : null;
|
|
14
|
+
}
|
|
15
|
+
/** Get the image of a texture, typed for dimension/data access.
|
|
16
|
+
* In r183, Texture.image is typed as `{}` but at runtime is an ImageBitmap, HTMLImageElement, etc. */
|
|
17
|
+
export function getTextureImage(tex) {
|
|
18
|
+
const img = tex.image;
|
|
19
|
+
return img != null && typeof img === 'object' ? img : null;
|
|
20
|
+
}
|
|
21
|
+
/** Get width/height of a texture from image or source data */
|
|
22
|
+
export function getTextureDimensions(tex) {
|
|
23
|
+
const img = getTextureImage(tex);
|
|
24
|
+
const src = getSourceData(tex);
|
|
25
|
+
return {
|
|
26
|
+
width: img?.width || src?.width || 0,
|
|
27
|
+
height: img?.height || src?.height || 0,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
1
30
|
const debug = getParam("debugprogressive");
|
|
2
31
|
export function isDebugMode() {
|
|
3
32
|
return debug;
|
|
@@ -71,7 +100,7 @@ export class PromiseQueue {
|
|
|
71
100
|
_running = new Map();
|
|
72
101
|
_queue = [];
|
|
73
102
|
debug = false;
|
|
74
|
-
constructor(maxConcurrent
|
|
103
|
+
constructor(maxConcurrent, opts = {}) {
|
|
75
104
|
this.maxConcurrent = maxConcurrent;
|
|
76
105
|
this.debug = opts.debug ?? false;
|
|
77
106
|
window.requestAnimationFrame(this.tick);
|
|
@@ -115,3 +144,96 @@ export class PromiseQueue {
|
|
|
115
144
|
}
|
|
116
145
|
}
|
|
117
146
|
}
|
|
147
|
+
// #region Texture Memory
|
|
148
|
+
export function determineTextureMemoryInBytes(texture) {
|
|
149
|
+
const img = texture.image;
|
|
150
|
+
const width = img?.width ?? 0;
|
|
151
|
+
const height = img?.height ?? 0;
|
|
152
|
+
const depth = img?.depth ?? 1;
|
|
153
|
+
const mipLevels = Math.floor(Math.log2(Math.max(width, height, depth))) + 1;
|
|
154
|
+
const bytesPerPixel = getBytesPerPixel(texture);
|
|
155
|
+
const totalBytes = (width * height * depth * bytesPerPixel * (1 - Math.pow(0.25, mipLevels))) / (1 - 0.25);
|
|
156
|
+
return totalBytes;
|
|
157
|
+
}
|
|
158
|
+
function getBytesPerPixel(texture) {
|
|
159
|
+
// Determine channel count from format
|
|
160
|
+
let channels = 4; // Default RGBA
|
|
161
|
+
const format = texture.format;
|
|
162
|
+
if (format === RedFormat)
|
|
163
|
+
channels = 1;
|
|
164
|
+
else if (format === RedIntegerFormat)
|
|
165
|
+
channels = 1;
|
|
166
|
+
else if (format === RGFormat)
|
|
167
|
+
channels = 2;
|
|
168
|
+
else if (format === RGIntegerFormat)
|
|
169
|
+
channels = 2;
|
|
170
|
+
else if (format === RGBFormat)
|
|
171
|
+
channels = 3;
|
|
172
|
+
else if (format === 1029)
|
|
173
|
+
channels = 3; // RGBIntegerFormat (not exported in r183)
|
|
174
|
+
else if (format === RGBAFormat)
|
|
175
|
+
channels = 4;
|
|
176
|
+
else if (format === RGBAIntegerFormat)
|
|
177
|
+
channels = 4;
|
|
178
|
+
// Determine bytes per channel from type
|
|
179
|
+
let bytesPerChannel = 1; // UnsignedByteType default
|
|
180
|
+
const type = texture.type;
|
|
181
|
+
if (type === 1009)
|
|
182
|
+
bytesPerChannel = 1; // UnsignedByteType
|
|
183
|
+
else if (type === 1010)
|
|
184
|
+
bytesPerChannel = 1; // ByteType
|
|
185
|
+
else if (type === 1011)
|
|
186
|
+
bytesPerChannel = 2; // ShortType
|
|
187
|
+
else if (type === 1012)
|
|
188
|
+
bytesPerChannel = 2; // UnsignedShortType
|
|
189
|
+
else if (type === 1013)
|
|
190
|
+
bytesPerChannel = 4; // IntType
|
|
191
|
+
else if (type === 1014)
|
|
192
|
+
bytesPerChannel = 4; // UnsignedIntType
|
|
193
|
+
else if (type === 1015)
|
|
194
|
+
bytesPerChannel = 4; // FloatType
|
|
195
|
+
else if (type === 1016)
|
|
196
|
+
bytesPerChannel = 2; // HalfFloatType
|
|
197
|
+
const bytesPerPixel = channels * bytesPerChannel;
|
|
198
|
+
return bytesPerPixel;
|
|
199
|
+
}
|
|
200
|
+
// #region GPU
|
|
201
|
+
let rendererInfo;
|
|
202
|
+
/**
|
|
203
|
+
* 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.
|
|
204
|
+
*/
|
|
205
|
+
export function detectGPUMemory() {
|
|
206
|
+
if (rendererInfo !== undefined) {
|
|
207
|
+
return rendererInfo?.estimatedMemory;
|
|
208
|
+
}
|
|
209
|
+
const canvas = document.createElement('canvas');
|
|
210
|
+
const powerPreference = "high-performance";
|
|
211
|
+
const gl = canvas.getContext('webgl', { powerPreference }) || canvas.getContext('experimental-webgl', { powerPreference });
|
|
212
|
+
if (!gl) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
if ("getExtension" in gl) {
|
|
216
|
+
const debugInfo = gl.getExtension('WEBGL_debug_renderer_info');
|
|
217
|
+
if (debugInfo) {
|
|
218
|
+
const vendor = gl.getParameter(debugInfo.UNMASKED_VENDOR_WEBGL);
|
|
219
|
+
const renderer = gl.getParameter(debugInfo.UNMASKED_RENDERER_WEBGL);
|
|
220
|
+
// Estimate memory based on renderer information (this is a very rough estimate)
|
|
221
|
+
let estimatedMemory = 512;
|
|
222
|
+
if (/NVIDIA/i.test(renderer)) {
|
|
223
|
+
estimatedMemory = 2048;
|
|
224
|
+
}
|
|
225
|
+
else if (/AMD/i.test(renderer)) {
|
|
226
|
+
estimatedMemory = 1024;
|
|
227
|
+
}
|
|
228
|
+
else if (/Intel/i.test(renderer)) {
|
|
229
|
+
estimatedMemory = 512;
|
|
230
|
+
}
|
|
231
|
+
rendererInfo = { vendor, renderer, estimatedMemory };
|
|
232
|
+
return estimatedMemory;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
else {
|
|
236
|
+
rendererInfo = null;
|
|
237
|
+
}
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
package/lib/version.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Box3, BufferAttribute, BufferGeometry, CompressedTexture, InterleavedBuffer, InterleavedBufferAttribute, Matrix3, Sphere, Texture, Vector3 } from "three";
|
|
2
2
|
import { createLoaders, GET_LOADER_LOCATION_CONFIG } from "../loaders.js";
|
|
3
|
-
import { isMobileDevice } from "../utils.internal.js";
|
|
3
|
+
import { getTextureDimensions, isMobileDevice } from "../utils.internal.js";
|
|
4
4
|
import { debug } from "../lods.debug.js";
|
|
5
5
|
const workers = new Array();
|
|
6
6
|
let getWorkerId = 0;
|
|
@@ -21,7 +21,7 @@ export function getWorker(opts) {
|
|
|
21
21
|
class GLTFLoaderWorker {
|
|
22
22
|
worker;
|
|
23
23
|
static async createWorker(opts) {
|
|
24
|
-
const worker = new Worker(new URL(`./
|
|
24
|
+
const worker = new Worker(new URL(`./gltf-progressive.worker.js`, import.meta.url), {
|
|
25
25
|
type: 'module',
|
|
26
26
|
});
|
|
27
27
|
const instance = new GLTFLoaderWorker(worker, opts);
|
|
@@ -152,8 +152,7 @@ function processReceivedData(data) {
|
|
|
152
152
|
let newTexture = null;
|
|
153
153
|
if (texture.isCompressedTexture) {
|
|
154
154
|
const mipmaps = texture.mipmaps;
|
|
155
|
-
const width
|
|
156
|
-
const height = texture.image?.height || texture.source?.data?.height || -1;
|
|
155
|
+
const { width, height } = getTextureDimensions(texture);
|
|
157
156
|
newTexture = new CompressedTexture(mipmaps, width, height, texture.format, texture.type, texture.mapping, texture.wrapS, texture.wrapT, texture.magFilter, texture.minFilter, texture.anisotropy, texture.colorSpace);
|
|
158
157
|
}
|
|
159
158
|
else {
|