@needle-tools/gltf-progressive 3.4.0-rc → 3.5.0-canary.4d42385
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/LICENSE +21 -0
- package/README.md +9 -0
- package/gltf-progressive.js +780 -610
- package/gltf-progressive.min.js +9 -7
- package/gltf-progressive.umd.cjs +9 -7
- package/lib/extension.d.ts +65 -5
- package/lib/extension.js +184 -48
- package/lib/lods.manager.d.ts +63 -3
- package/lib/lods.manager.js +70 -2
- package/lib/utils.internal.d.ts +8 -2
- package/lib/utils.internal.js +95 -2
- package/lib/version.js +1 -1
- package/lib/worker/loader.mainthread.js +1 -1
- package/package.json +4 -2
- /package/lib/worker/{loader.worker.js → gltf-progressive.worker.js} +0 -0
package/lib/extension.d.ts
CHANGED
|
@@ -42,8 +42,31 @@ type TextureLODsMinMaxInfo = {
|
|
|
42
42
|
export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
43
43
|
/** The name of the extension */
|
|
44
44
|
get name(): string;
|
|
45
|
+
/**
|
|
46
|
+
* Get the progressive mesh LOD extension data associated with a geometry.
|
|
47
|
+
* Returns the extension metadata (available LOD levels, vertex/index counts, densities) if the geometry was registered with progressive LODs, or `null` otherwise.
|
|
48
|
+
*
|
|
49
|
+
* @param geo - The buffer geometry to look up.
|
|
50
|
+
* @returns The mesh LOD extension data, or `null` if no progressive LODs are registered for this geometry.
|
|
51
|
+
*/
|
|
45
52
|
static getMeshLODExtension(geo: BufferGeometry): NEEDLE_ext_progressive_mesh | null;
|
|
53
|
+
/**
|
|
54
|
+
* Get the glTF primitive index for a geometry within its parent mesh.
|
|
55
|
+
* A single glTF mesh node can contain multiple primitives (sub-geometries). This returns which primitive the geometry corresponds to.
|
|
56
|
+
*
|
|
57
|
+
* @param geo - The buffer geometry to look up.
|
|
58
|
+
* @returns The zero-based primitive index, or `-1` if no LOD information is assigned to this geometry.
|
|
59
|
+
*/
|
|
46
60
|
static getPrimitiveIndex(geo: BufferGeometry): number;
|
|
61
|
+
/**
|
|
62
|
+
* Compute the minimum and maximum number of texture LOD levels across all textures of a material (or array of materials).
|
|
63
|
+
* Iterates over all texture slots on the material and collects LOD count ranges and per-level resolution bounds.
|
|
64
|
+
* Results are cached on the material so subsequent calls are free.
|
|
65
|
+
*
|
|
66
|
+
* @param material - A single material or an array of materials to inspect.
|
|
67
|
+
* @param minmax - Optional accumulator to merge results into (used internally for recursive calls with material arrays).
|
|
68
|
+
* @returns An object with `min_count` / `max_count` (the range of LOD levels across all textures) and a per-level `lods` array with `min_height` / `max_height`.
|
|
69
|
+
*/
|
|
47
70
|
static getMaterialMinMaxLODsCount(material: Material | Material[], minmax?: TextureLODsMinMaxInfo): TextureLODsMinMaxInfo;
|
|
48
71
|
/** Check if a LOD level is available for a mesh or a texture
|
|
49
72
|
* @param obj the mesh or texture to check
|
|
@@ -76,6 +99,12 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
76
99
|
static assignTextureLOD(materialOrTexture: Material, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
|
|
77
100
|
static assignTextureLOD(materialOrTexture: Mesh, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
|
|
78
101
|
static assignTextureLOD(materialOrTexture: Texture, level: number): Promise<Texture | null>;
|
|
102
|
+
/**
|
|
103
|
+
* 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.
|
|
104
|
+
* @default 50 on desktop, 20 on mobile devices
|
|
105
|
+
*/
|
|
106
|
+
static set maxConcurrentLoadingTasks(value: number);
|
|
107
|
+
static get maxConcurrentLoadingTasks(): number;
|
|
79
108
|
private static assignTextureLODForSlot;
|
|
80
109
|
private readonly parser;
|
|
81
110
|
private readonly url;
|
|
@@ -84,17 +113,36 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
84
113
|
loadMesh: (meshIndex: number) => Promise<any> | null;
|
|
85
114
|
afterRoot(gltf: GLTF): null;
|
|
86
115
|
/**
|
|
87
|
-
* Register a texture with LOD information
|
|
116
|
+
* Register a texture with progressive LOD information. This associates the texture with its LOD extension data
|
|
117
|
+
* so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
|
|
118
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
119
|
+
*
|
|
120
|
+
* @param url - The source URL of the glTF file this texture was loaded from.
|
|
121
|
+
* @param tex - The three.js Texture instance to register.
|
|
122
|
+
* @param level - The LOD level this texture represents (0 = highest resolution).
|
|
123
|
+
* @param index - The texture index within the glTF file.
|
|
124
|
+
* @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
|
|
88
125
|
*/
|
|
89
126
|
static registerTexture: (url: string, tex: Texture, level: number, index: number, ext: NEEDLE_ext_progressive_texture) => void;
|
|
90
127
|
/**
|
|
91
|
-
* Register a mesh with LOD information
|
|
128
|
+
* Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
|
|
129
|
+
* so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
|
|
130
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
131
|
+
* If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
|
|
132
|
+
*
|
|
133
|
+
* @param url - The source URL of the glTF file this mesh was loaded from.
|
|
134
|
+
* @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
|
|
135
|
+
* @param mesh - The three.js Mesh instance to register.
|
|
136
|
+
* @param level - The LOD level this mesh represents (0 = highest resolution / full density).
|
|
137
|
+
* @param index - The primitive index within the glTF mesh node.
|
|
138
|
+
* @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
|
|
92
139
|
*/
|
|
93
140
|
static registerMesh: (url: string, key: string, mesh: Mesh, level: number, index: number, ext: NEEDLE_ext_progressive_mesh) => void;
|
|
94
141
|
/**
|
|
95
142
|
* Dispose cached resources to free memory.
|
|
96
143
|
* Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
|
|
97
144
|
* Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
|
|
145
|
+
* Also clears reference counts for disposed textures.
|
|
98
146
|
* @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
|
|
99
147
|
*/
|
|
100
148
|
static dispose(guid?: string): void;
|
|
@@ -103,20 +151,32 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
103
151
|
/** A map of key = asset uuid and value = LOD information */
|
|
104
152
|
private static readonly lodInfos;
|
|
105
153
|
/** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
|
|
106
|
-
private static readonly
|
|
154
|
+
private static readonly cache;
|
|
107
155
|
/** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
|
|
108
156
|
private static readonly lowresCache;
|
|
157
|
+
/** Reference counting for textures to track usage across multiple materials/objects */
|
|
158
|
+
private static readonly textureRefCounts;
|
|
109
159
|
/**
|
|
110
160
|
* FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
|
|
111
161
|
* when their associated three.js resources are garbage collected by the browser.
|
|
112
162
|
* The held value is the cache key string used in `previouslyLoaded`.
|
|
113
163
|
*/
|
|
114
164
|
private static readonly _resourceRegistry;
|
|
165
|
+
/**
|
|
166
|
+
* Track texture usage by incrementing reference count
|
|
167
|
+
*/
|
|
168
|
+
private static trackTextureUsage;
|
|
169
|
+
/**
|
|
170
|
+
* Untrack texture usage by decrementing reference count.
|
|
171
|
+
* Automatically disposes the texture when reference count reaches zero.
|
|
172
|
+
* @returns true if the texture was disposed, false otherwise
|
|
173
|
+
*/
|
|
174
|
+
private static untrackTextureUsage;
|
|
115
175
|
private static readonly workers;
|
|
116
176
|
private static _workersIndex;
|
|
117
177
|
private static getOrLoadLOD;
|
|
118
|
-
private static
|
|
119
|
-
private static queue;
|
|
178
|
+
private static _queue;
|
|
179
|
+
private static get queue();
|
|
120
180
|
private static assignLODInformation;
|
|
121
181
|
private static getAssignedLODInformation;
|
|
122
182
|
private static copySettings;
|
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
|
|
@@ -35,6 +36,13 @@ export class NEEDLE_progressive {
|
|
|
35
36
|
return EXTENSION_NAME;
|
|
36
37
|
}
|
|
37
38
|
// #region PUBLIC API
|
|
39
|
+
/**
|
|
40
|
+
* Get the progressive mesh LOD extension data associated with a geometry.
|
|
41
|
+
* Returns the extension metadata (available LOD levels, vertex/index counts, densities) if the geometry was registered with progressive LODs, or `null` otherwise.
|
|
42
|
+
*
|
|
43
|
+
* @param geo - The buffer geometry to look up.
|
|
44
|
+
* @returns The mesh LOD extension data, or `null` if no progressive LODs are registered for this geometry.
|
|
45
|
+
*/
|
|
38
46
|
static getMeshLODExtension(geo) {
|
|
39
47
|
const info = this.getAssignedLODInformation(geo);
|
|
40
48
|
if (info?.key) {
|
|
@@ -42,12 +50,28 @@ export class NEEDLE_progressive {
|
|
|
42
50
|
}
|
|
43
51
|
return null;
|
|
44
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Get the glTF primitive index for a geometry within its parent mesh.
|
|
55
|
+
* A single glTF mesh node can contain multiple primitives (sub-geometries). This returns which primitive the geometry corresponds to.
|
|
56
|
+
*
|
|
57
|
+
* @param geo - The buffer geometry to look up.
|
|
58
|
+
* @returns The zero-based primitive index, or `-1` if no LOD information is assigned to this geometry.
|
|
59
|
+
*/
|
|
45
60
|
static getPrimitiveIndex(geo) {
|
|
46
61
|
const index = this.getAssignedLODInformation(geo)?.index;
|
|
47
62
|
if (index === undefined || index === null)
|
|
48
63
|
return -1;
|
|
49
64
|
return index;
|
|
50
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Compute the minimum and maximum number of texture LOD levels across all textures of a material (or array of materials).
|
|
68
|
+
* Iterates over all texture slots on the material and collects LOD count ranges and per-level resolution bounds.
|
|
69
|
+
* Results are cached on the material so subsequent calls are free.
|
|
70
|
+
*
|
|
71
|
+
* @param material - A single material or an array of materials to inspect.
|
|
72
|
+
* @param minmax - Optional accumulator to merge results into (used internally for recursive calls with material arrays).
|
|
73
|
+
* @returns An object with `min_count` / `max_count` (the range of LOD levels across all textures) and a per-level `lods` array with `min_height` / `max_height`.
|
|
74
|
+
*/
|
|
51
75
|
static getMaterialMinMaxLODsCount(material, minmax) {
|
|
52
76
|
const self = this;
|
|
53
77
|
// we can cache this material min max data because it wont change at runtime
|
|
@@ -309,6 +333,16 @@ export class NEEDLE_progressive {
|
|
|
309
333
|
}
|
|
310
334
|
return Promise.resolve(null);
|
|
311
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* 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.
|
|
338
|
+
* @default 50 on desktop, 20 on mobile devices
|
|
339
|
+
*/
|
|
340
|
+
static set maxConcurrentLoadingTasks(value) {
|
|
341
|
+
NEEDLE_progressive.queue.maxConcurrent = value;
|
|
342
|
+
}
|
|
343
|
+
static get maxConcurrentLoadingTasks() {
|
|
344
|
+
return NEEDLE_progressive.queue.maxConcurrent;
|
|
345
|
+
}
|
|
312
346
|
// #region INTERNAL
|
|
313
347
|
static assignTextureLODForSlot(current, level, material, slot) {
|
|
314
348
|
if (current?.isTexture !== true) {
|
|
@@ -334,29 +368,37 @@ export class NEEDLE_progressive {
|
|
|
334
368
|
if (assignedLOD && assignedLOD?.level < level) {
|
|
335
369
|
if (debug === "verbose")
|
|
336
370
|
console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
|
|
371
|
+
// Dispose the newly loaded texture since we're not using it
|
|
372
|
+
// (the assigned texture is higher quality, so we reject the new one)
|
|
373
|
+
// Note: We dispose directly here (not via untrackTextureUsage) because this texture
|
|
374
|
+
// was never tracked/used - it was rejected immediately upon loading
|
|
375
|
+
if (tex && tex !== assigned) {
|
|
376
|
+
if (debug || debugGC) {
|
|
377
|
+
console.log(`[gltf-progressive] Disposing rejected lower-quality texture LOD ${level} (assigned is ${assignedLOD.level})`, tex.uuid);
|
|
378
|
+
}
|
|
379
|
+
tex.dispose();
|
|
380
|
+
}
|
|
337
381
|
return null;
|
|
338
382
|
}
|
|
339
383
|
// assigned.dispose();
|
|
340
384
|
}
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
385
|
+
// Track reference count for new texture
|
|
386
|
+
this.trackTextureUsage(tex);
|
|
387
|
+
// Untrack the old texture (may dispose if ref count hits 0)
|
|
388
|
+
// This prevents accumulation of GPU VRAM while waiting for garbage collection
|
|
389
|
+
if (assigned && assigned !== tex) {
|
|
390
|
+
const wasDisposed = this.untrackTextureUsage(assigned);
|
|
391
|
+
if (wasDisposed && (debug || debugGC)) {
|
|
392
|
+
const assignedLOD = this.getAssignedLODInformation(assigned);
|
|
393
|
+
console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} → ${level} for ${material.name || material.type}.${slot}`, assigned.uuid);
|
|
348
394
|
}
|
|
349
395
|
}
|
|
350
396
|
material[slot] = tex;
|
|
351
397
|
}
|
|
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
|
-
// }
|
|
398
|
+
// Note: We use reference counting above to track texture usage across multiple materials.
|
|
399
|
+
// When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
|
|
400
|
+
// not waiting for JavaScript garbage collection which may take seconds/minutes.
|
|
401
|
+
// This handles cases where the same texture is shared across multiple materials/objects.
|
|
360
402
|
}
|
|
361
403
|
// this.onProgressiveLoadEnd(info);
|
|
362
404
|
return tex;
|
|
@@ -473,7 +515,15 @@ export class NEEDLE_progressive {
|
|
|
473
515
|
return null;
|
|
474
516
|
}
|
|
475
517
|
/**
|
|
476
|
-
* Register a texture with LOD information
|
|
518
|
+
* Register a texture with progressive LOD information. This associates the texture with its LOD extension data
|
|
519
|
+
* so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
|
|
520
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
521
|
+
*
|
|
522
|
+
* @param url - The source URL of the glTF file this texture was loaded from.
|
|
523
|
+
* @param tex - The three.js Texture instance to register.
|
|
524
|
+
* @param level - The LOD level this texture represents (0 = highest resolution).
|
|
525
|
+
* @param index - The texture index within the glTF file.
|
|
526
|
+
* @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
|
|
477
527
|
*/
|
|
478
528
|
static registerTexture = (url, tex, level, index, ext) => {
|
|
479
529
|
if (!tex) {
|
|
@@ -496,7 +546,17 @@ export class NEEDLE_progressive {
|
|
|
496
546
|
NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
|
|
497
547
|
};
|
|
498
548
|
/**
|
|
499
|
-
* Register a mesh with LOD information
|
|
549
|
+
* Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
|
|
550
|
+
* so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
|
|
551
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
552
|
+
* If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
|
|
553
|
+
*
|
|
554
|
+
* @param url - The source URL of the glTF file this mesh was loaded from.
|
|
555
|
+
* @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
|
|
556
|
+
* @param mesh - The three.js Mesh instance to register.
|
|
557
|
+
* @param level - The LOD level this mesh represents (0 = highest resolution / full density).
|
|
558
|
+
* @param index - The primitive index within the glTF mesh node.
|
|
559
|
+
* @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
|
|
500
560
|
*/
|
|
501
561
|
static registerMesh = (url, key, mesh, level, index, ext) => {
|
|
502
562
|
const geometry = mesh.geometry;
|
|
@@ -531,6 +591,7 @@ export class NEEDLE_progressive {
|
|
|
531
591
|
* Dispose cached resources to free memory.
|
|
532
592
|
* Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
|
|
533
593
|
* Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
|
|
594
|
+
* Also clears reference counts for disposed textures.
|
|
534
595
|
* @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
|
|
535
596
|
*/
|
|
536
597
|
static dispose(guid) {
|
|
@@ -542,7 +603,9 @@ export class NEEDLE_progressive {
|
|
|
542
603
|
const lowres = lowresRef.deref();
|
|
543
604
|
if (lowres) {
|
|
544
605
|
if (lowres.isTexture) {
|
|
545
|
-
lowres
|
|
606
|
+
const tex = lowres;
|
|
607
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
608
|
+
tex.dispose();
|
|
546
609
|
}
|
|
547
610
|
else if (Array.isArray(lowres)) {
|
|
548
611
|
for (const geo of lowres)
|
|
@@ -552,10 +615,10 @@ export class NEEDLE_progressive {
|
|
|
552
615
|
this.lowresCache.delete(guid);
|
|
553
616
|
}
|
|
554
617
|
// Dispose previously loaded LOD entries
|
|
555
|
-
for (const [key, entry] of this.
|
|
618
|
+
for (const [key, entry] of this.cache) {
|
|
556
619
|
if (key.includes(guid)) {
|
|
557
620
|
this._disposeCacheEntry(entry);
|
|
558
|
-
this.
|
|
621
|
+
this.cache.delete(key);
|
|
559
622
|
}
|
|
560
623
|
}
|
|
561
624
|
}
|
|
@@ -565,7 +628,9 @@ export class NEEDLE_progressive {
|
|
|
565
628
|
const entry = entryRef.deref();
|
|
566
629
|
if (entry) {
|
|
567
630
|
if (entry.isTexture) {
|
|
568
|
-
entry
|
|
631
|
+
const tex = entry;
|
|
632
|
+
this.textureRefCounts.delete(tex.uuid); // Clear ref count
|
|
633
|
+
tex.dispose();
|
|
569
634
|
}
|
|
570
635
|
else if (Array.isArray(entry)) {
|
|
571
636
|
for (const geo of entry)
|
|
@@ -574,10 +639,12 @@ export class NEEDLE_progressive {
|
|
|
574
639
|
}
|
|
575
640
|
}
|
|
576
641
|
this.lowresCache.clear();
|
|
577
|
-
for (const [, entry] of this.
|
|
642
|
+
for (const [, entry] of this.cache) {
|
|
578
643
|
this._disposeCacheEntry(entry);
|
|
579
644
|
}
|
|
580
|
-
this.
|
|
645
|
+
this.cache.clear();
|
|
646
|
+
// Clear all texture reference counts when disposing everything
|
|
647
|
+
this.textureRefCounts.clear();
|
|
581
648
|
}
|
|
582
649
|
}
|
|
583
650
|
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
@@ -585,7 +652,13 @@ export class NEEDLE_progressive {
|
|
|
585
652
|
if (entry instanceof WeakRef) {
|
|
586
653
|
// Single resource — deref and dispose if still alive
|
|
587
654
|
const resource = entry.deref();
|
|
588
|
-
resource
|
|
655
|
+
if (resource) {
|
|
656
|
+
// Clear ref count for textures
|
|
657
|
+
if (resource.isTexture) {
|
|
658
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
659
|
+
}
|
|
660
|
+
resource.dispose();
|
|
661
|
+
}
|
|
589
662
|
}
|
|
590
663
|
else {
|
|
591
664
|
// Promise — may be in-flight or already resolved.
|
|
@@ -597,6 +670,10 @@ export class NEEDLE_progressive {
|
|
|
597
670
|
geo.dispose();
|
|
598
671
|
}
|
|
599
672
|
else {
|
|
673
|
+
// Clear ref count for textures
|
|
674
|
+
if (resource.isTexture) {
|
|
675
|
+
this.textureRefCounts.delete(resource.uuid);
|
|
676
|
+
}
|
|
600
677
|
resource.dispose();
|
|
601
678
|
}
|
|
602
679
|
}
|
|
@@ -606,27 +683,84 @@ export class NEEDLE_progressive {
|
|
|
606
683
|
/** A map of key = asset uuid and value = LOD information */
|
|
607
684
|
static lodInfos = new Map();
|
|
608
685
|
/** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
|
|
609
|
-
static
|
|
686
|
+
static cache = new Map();
|
|
610
687
|
/** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
|
|
611
688
|
static lowresCache = new Map();
|
|
689
|
+
/** Reference counting for textures to track usage across multiple materials/objects */
|
|
690
|
+
static textureRefCounts = new Map();
|
|
612
691
|
/**
|
|
613
692
|
* FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
|
|
614
693
|
* when their associated three.js resources are garbage collected by the browser.
|
|
615
694
|
* The held value is the cache key string used in `previouslyLoaded`.
|
|
616
695
|
*/
|
|
617
696
|
static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
|
|
618
|
-
const entry = NEEDLE_progressive.
|
|
619
|
-
|
|
697
|
+
const entry = NEEDLE_progressive.cache.get(cacheKey);
|
|
698
|
+
if (debug || debugGC)
|
|
699
|
+
console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
|
|
620
700
|
// Only delete if the entry is still a WeakRef and the resource is gone
|
|
621
701
|
if (entry instanceof WeakRef) {
|
|
622
702
|
const derefed = entry.deref();
|
|
623
703
|
if (!derefed) {
|
|
624
|
-
NEEDLE_progressive.
|
|
625
|
-
if (debug)
|
|
626
|
-
console.log(`[gltf-progressive] Cache entry
|
|
704
|
+
NEEDLE_progressive.cache.delete(cacheKey);
|
|
705
|
+
if (debug || debugGC)
|
|
706
|
+
console.log(`[gltf-progressive] ↪ Cache entry deleted (GC)`);
|
|
627
707
|
}
|
|
628
708
|
}
|
|
629
709
|
});
|
|
710
|
+
/**
|
|
711
|
+
* Track texture usage by incrementing reference count
|
|
712
|
+
*/
|
|
713
|
+
static trackTextureUsage(texture) {
|
|
714
|
+
const uuid = texture.uuid;
|
|
715
|
+
const count = this.textureRefCounts.get(uuid) || 0;
|
|
716
|
+
this.textureRefCounts.set(uuid, count + 1);
|
|
717
|
+
if (debug === "verbose") {
|
|
718
|
+
console.log(`[gltf-progressive] Track texture ${uuid}, refCount: ${count} → ${count + 1}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/**
|
|
722
|
+
* Untrack texture usage by decrementing reference count.
|
|
723
|
+
* Automatically disposes the texture when reference count reaches zero.
|
|
724
|
+
* @returns true if the texture was disposed, false otherwise
|
|
725
|
+
*/
|
|
726
|
+
static untrackTextureUsage(texture) {
|
|
727
|
+
const uuid = texture.uuid;
|
|
728
|
+
const count = this.textureRefCounts.get(uuid);
|
|
729
|
+
if (!count) {
|
|
730
|
+
// Texture wasn't tracked, dispose immediately (safe fallback)
|
|
731
|
+
if (debug === "verbose" || debugGC) {
|
|
732
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)`, 0);
|
|
733
|
+
}
|
|
734
|
+
texture.dispose();
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
const newCount = count - 1;
|
|
738
|
+
if (newCount <= 0) {
|
|
739
|
+
this.textureRefCounts.delete(uuid);
|
|
740
|
+
if (debug || debugGC) {
|
|
741
|
+
logDebugInfo(`[gltf-progressive] Memory: Dispose texture`, newCount);
|
|
742
|
+
}
|
|
743
|
+
texture.dispose();
|
|
744
|
+
return true;
|
|
745
|
+
}
|
|
746
|
+
else {
|
|
747
|
+
this.textureRefCounts.set(uuid, newCount);
|
|
748
|
+
if (debug === "verbose") {
|
|
749
|
+
logDebugInfo(`[gltf-progressive] Memory: Untrack texture`, newCount);
|
|
750
|
+
}
|
|
751
|
+
return false;
|
|
752
|
+
}
|
|
753
|
+
function logDebugInfo(prefix, newCount) {
|
|
754
|
+
let width = texture.image?.width || texture.source?.data?.width || 0;
|
|
755
|
+
let height = texture.image?.height || texture.source?.data?.height || 0;
|
|
756
|
+
const textureSize = width && height ? `${width}x${height}` : "N/A";
|
|
757
|
+
let memorySize = "N/A";
|
|
758
|
+
if (width && height) {
|
|
759
|
+
memorySize = `~${(determineTextureMemoryInBytes(texture) / (1024 * 1024)).toFixed(2)} MB`;
|
|
760
|
+
}
|
|
761
|
+
console.log(`${prefix} — ${texture.name} ${textureSize} (${memorySize}), refCount: ${count} → ${newCount}\n${uuid}`);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
630
764
|
static workers = [];
|
|
631
765
|
static _workersIndex = 0;
|
|
632
766
|
static async getOrLoadLOD(current, level) {
|
|
@@ -700,7 +834,7 @@ export class NEEDLE_progressive {
|
|
|
700
834
|
const KEY = lod_url + "_" + lodInfo.guid;
|
|
701
835
|
const slot = await this.queue.slot(lod_url);
|
|
702
836
|
// check if the requested file is currently being loaded or was previously loaded
|
|
703
|
-
const existing = this.
|
|
837
|
+
const existing = this.cache.get(KEY);
|
|
704
838
|
if (existing !== undefined) {
|
|
705
839
|
if (debugverbose)
|
|
706
840
|
console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
|
|
@@ -728,7 +862,7 @@ export class NEEDLE_progressive {
|
|
|
728
862
|
}
|
|
729
863
|
}
|
|
730
864
|
// Resource was garbage collected or disposed — remove stale entry and re-load
|
|
731
|
-
this.
|
|
865
|
+
this.cache.delete(KEY);
|
|
732
866
|
if (debug)
|
|
733
867
|
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
|
|
734
868
|
}
|
|
@@ -751,7 +885,7 @@ export class NEEDLE_progressive {
|
|
|
751
885
|
// if it has been disposed we need to load it again
|
|
752
886
|
else {
|
|
753
887
|
resouceIsDisposed = true;
|
|
754
|
-
this.
|
|
888
|
+
this.cache.delete(KEY);
|
|
755
889
|
}
|
|
756
890
|
}
|
|
757
891
|
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
@@ -760,7 +894,7 @@ export class NEEDLE_progressive {
|
|
|
760
894
|
}
|
|
761
895
|
else {
|
|
762
896
|
resouceIsDisposed = true;
|
|
763
|
-
this.
|
|
897
|
+
this.cache.delete(KEY);
|
|
764
898
|
}
|
|
765
899
|
}
|
|
766
900
|
if (!resouceIsDisposed) {
|
|
@@ -912,30 +1046,32 @@ export class NEEDLE_progressive {
|
|
|
912
1046
|
// we could not find a texture or mesh with the given guid
|
|
913
1047
|
return resolve(null);
|
|
914
1048
|
});
|
|
915
|
-
this.
|
|
1049
|
+
this.cache.set(KEY, request);
|
|
916
1050
|
slot.use(request);
|
|
917
1051
|
const res = await request;
|
|
918
1052
|
// Optimize cache entry: replace loading promise with lightweight reference.
|
|
919
1053
|
// This releases closure variables captured during the loading function.
|
|
920
1054
|
if (res != null) {
|
|
921
|
-
if (
|
|
1055
|
+
if (res instanceof Texture) {
|
|
1056
|
+
// For Texture resources, use WeakRef to allow garbage collection.
|
|
1057
|
+
// The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
|
|
1058
|
+
this.cache.set(KEY, new WeakRef(res));
|
|
1059
|
+
NEEDLE_progressive._resourceRegistry.register(res, KEY);
|
|
1060
|
+
}
|
|
1061
|
+
else if (Array.isArray(res)) {
|
|
922
1062
|
// For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
|
|
923
|
-
//
|
|
924
|
-
|
|
925
|
-
this.previouslyLoaded.set(KEY, Promise.resolve(res));
|
|
1063
|
+
// This keeps geometries in memory as they should not be GC'd (mesh LODs stay cached).
|
|
1064
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
926
1065
|
}
|
|
927
1066
|
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);
|
|
1067
|
+
// For single BufferGeometry, keep in memory (don't use WeakRef)
|
|
1068
|
+
this.cache.set(KEY, Promise.resolve(res));
|
|
933
1069
|
}
|
|
934
1070
|
}
|
|
935
1071
|
else {
|
|
936
1072
|
// Failed load — replace with clean resolved promise to release loading closure.
|
|
937
1073
|
// Keeping the entry prevents retrying (existing behavior).
|
|
938
|
-
this.
|
|
1074
|
+
this.cache.set(KEY, Promise.resolve(null));
|
|
939
1075
|
}
|
|
940
1076
|
return res;
|
|
941
1077
|
}
|
|
@@ -961,8 +1097,8 @@ export class NEEDLE_progressive {
|
|
|
961
1097
|
}
|
|
962
1098
|
return null;
|
|
963
1099
|
}
|
|
964
|
-
static
|
|
965
|
-
static queue
|
|
1100
|
+
static _queue;
|
|
1101
|
+
static get queue() { return this._queue ??= new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false }); }
|
|
966
1102
|
static assignLODInformation(url, res, key, level, index) {
|
|
967
1103
|
if (!res)
|
|
968
1104
|
return;
|
package/lib/lods.manager.d.ts
CHANGED
|
@@ -98,7 +98,31 @@ export declare class LODsManager {
|
|
|
98
98
|
private readonly _newPromiseGroups;
|
|
99
99
|
private _promiseGroupIds;
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
101
|
+
* Returns a promise that resolves once all LOD requests initiated during the next render cycles have finished loading.
|
|
102
|
+
* This is useful for hiding low-resolution placeholders (e.g. with a loading overlay or CSS blur) until high-quality assets are ready.
|
|
103
|
+
*
|
|
104
|
+
* By default, the returned promise captures LOD loading requests for 2 frames and resolves when all of them complete.
|
|
105
|
+
* Use `waitForFirstCapture` if no LOD requests may happen immediately (e.g. after a scene switch).
|
|
106
|
+
*
|
|
107
|
+
* @param opts - Optional configuration for how long to capture and what to wait for. See {@link PromiseGroupOptions}.
|
|
108
|
+
* @returns A promise that resolves with `{ cancelled, awaited_count, resolved_count }` once all captured LOD loads complete (or the signal aborts).
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* // Wait for initial LODs to finish loading, then remove a blur overlay
|
|
113
|
+
* const result = await lodsManager.awaitLoading({
|
|
114
|
+
* frames: 5,
|
|
115
|
+
* signal: AbortSignal.timeout(10_000),
|
|
116
|
+
* });
|
|
117
|
+
* console.log(`Loaded ${result.resolved_count} of ${result.awaited_count} LODs`);
|
|
118
|
+
* document.querySelector('.blur-overlay')?.remove();
|
|
119
|
+
* ```
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```ts
|
|
123
|
+
* // Wait until at least one LOD starts loading before resolving
|
|
124
|
+
* await lodsManager.awaitLoading({ waitForFirstCapture: true });
|
|
125
|
+
* ```
|
|
102
126
|
*/
|
|
103
127
|
awaitLoading(opts?: PromiseGroupOptions): Promise<{
|
|
104
128
|
cancelled: boolean;
|
|
@@ -107,8 +131,29 @@ export declare class LODsManager {
|
|
|
107
131
|
}>;
|
|
108
132
|
private _postprocessPromiseGroups;
|
|
109
133
|
private readonly _lodchangedlisteners;
|
|
110
|
-
|
|
111
|
-
|
|
134
|
+
/**
|
|
135
|
+
* Register a listener that is called whenever a mesh or texture LOD level has finished loading and has been applied.
|
|
136
|
+
* The listener receives the type of asset (`"mesh"` or `"texture"`), the new LOD level, and the affected object.
|
|
137
|
+
*
|
|
138
|
+
* @param evt - The event type. Currently only `"changed"` is supported.
|
|
139
|
+
* @param listener - Callback invoked after a LOD swap completes.
|
|
140
|
+
* @return A function to unregister the listener.
|
|
141
|
+
*
|
|
142
|
+
* @example
|
|
143
|
+
* ```ts
|
|
144
|
+
* lodsManager.addEventListener("changed", ({ type, level, object }) => {
|
|
145
|
+
* console.log(`${type} LOD changed to level ${level}`, object);
|
|
146
|
+
* });
|
|
147
|
+
* ```
|
|
148
|
+
*/
|
|
149
|
+
addEventListener(evt: "changed", listener: LODChangedEventListener): () => void;
|
|
150
|
+
/**
|
|
151
|
+
* Remove a previously registered `"changed"` event listener.
|
|
152
|
+
* @param evt - The event type (`"changed"`).
|
|
153
|
+
* @param listener - The listener to remove.
|
|
154
|
+
* @return `true` if the listener was found and removed, `false` otherwise.
|
|
155
|
+
*/
|
|
156
|
+
removeEventListener(evt: "changed", listener: LODChangedEventListener): boolean;
|
|
112
157
|
private constructor();
|
|
113
158
|
private _fpsBuffer;
|
|
114
159
|
/**
|
|
@@ -116,6 +161,21 @@ export declare class LODsManager {
|
|
|
116
161
|
*/
|
|
117
162
|
enable(): void;
|
|
118
163
|
disable(): void;
|
|
164
|
+
/**
|
|
165
|
+
* Manually trigger a LOD update for a scene and camera.
|
|
166
|
+
* Only needed when {@link manual} is set to `true` — otherwise LOD updates happen automatically on each render call.
|
|
167
|
+
*
|
|
168
|
+
* @param scene - The scene containing objects with progressive LODs.
|
|
169
|
+
* @param camera - The camera used to determine screen coverage and LOD levels.
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* ```ts
|
|
173
|
+
* const lodsManager = LODsManager.get(renderer);
|
|
174
|
+
* lodsManager.manual = true;
|
|
175
|
+
* // ... later, trigger an update at a specific point:
|
|
176
|
+
* lodsManager.update(scene, camera);
|
|
177
|
+
* ```
|
|
178
|
+
*/
|
|
119
179
|
update(scene: Scene, camera: Camera): void;
|
|
120
180
|
private onAfterRender;
|
|
121
181
|
/**
|