@needle-tools/gltf-progressive 3.5.0-rc → 3.6.0-alpha.2
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 +14 -1
- package/LICENSE +21 -0
- package/gltf-progressive.js +896 -636
- package/gltf-progressive.min.js +9 -9
- package/gltf-progressive.umd.cjs +9 -9
- package/lib/extension.d.ts +65 -6
- package/lib/extension.js +278 -110
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/lods.debug.js +1 -1
- package/lib/lods.manager.d.ts +90 -15
- package/lib/lods.manager.js +282 -150
- package/lib/lods.promise.js +1 -1
- package/lib/plugins/modelviewer.js +1 -1
- package/lib/version.js +1 -1
- package/package.json +4 -2
package/lib/extension.d.ts
CHANGED
|
@@ -24,6 +24,10 @@ type TextureLODsMinMaxInfo = {
|
|
|
24
24
|
max_height: number;
|
|
25
25
|
}>;
|
|
26
26
|
};
|
|
27
|
+
export type AssignTextureLODOptions = {
|
|
28
|
+
/** Force the exact requested level. Intended for explicit/debug LOD overrides. */
|
|
29
|
+
force?: boolean;
|
|
30
|
+
};
|
|
27
31
|
/**
|
|
28
32
|
* The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
|
|
29
33
|
* This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
|
|
@@ -42,8 +46,31 @@ type TextureLODsMinMaxInfo = {
|
|
|
42
46
|
export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
43
47
|
/** The name of the extension */
|
|
44
48
|
get name(): string;
|
|
49
|
+
/**
|
|
50
|
+
* Get the progressive mesh LOD extension data associated with a geometry.
|
|
51
|
+
* Returns the extension metadata (available LOD levels, vertex/index counts, densities) if the geometry was registered with progressive LODs, or `null` otherwise.
|
|
52
|
+
*
|
|
53
|
+
* @param geo - The buffer geometry to look up.
|
|
54
|
+
* @returns The mesh LOD extension data, or `null` if no progressive LODs are registered for this geometry.
|
|
55
|
+
*/
|
|
45
56
|
static getMeshLODExtension(geo: BufferGeometry): NEEDLE_ext_progressive_mesh | null;
|
|
57
|
+
/**
|
|
58
|
+
* Get the glTF primitive index for a geometry within its parent mesh.
|
|
59
|
+
* A single glTF mesh node can contain multiple primitives (sub-geometries). This returns which primitive the geometry corresponds to.
|
|
60
|
+
*
|
|
61
|
+
* @param geo - The buffer geometry to look up.
|
|
62
|
+
* @returns The zero-based primitive index, or `-1` if no LOD information is assigned to this geometry.
|
|
63
|
+
*/
|
|
46
64
|
static getPrimitiveIndex(geo: BufferGeometry): number;
|
|
65
|
+
/**
|
|
66
|
+
* Compute the minimum and maximum number of texture LOD levels across all textures of a material (or array of materials).
|
|
67
|
+
* Iterates over all texture slots on the material and collects LOD count ranges and per-level resolution bounds.
|
|
68
|
+
* Results are cached on the material so subsequent calls are free.
|
|
69
|
+
*
|
|
70
|
+
* @param material - A single material or an array of materials to inspect.
|
|
71
|
+
* @param minmax - Optional accumulator to merge results into (used internally for recursive calls with material arrays).
|
|
72
|
+
* @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`.
|
|
73
|
+
*/
|
|
47
74
|
static getMaterialMinMaxLODsCount(material: Material | Material[], minmax?: TextureLODsMinMaxInfo): TextureLODsMinMaxInfo;
|
|
48
75
|
/** Check if a LOD level is available for a mesh or a texture
|
|
49
76
|
* @param obj the mesh or texture to check
|
|
@@ -65,7 +92,10 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
65
92
|
* });
|
|
66
93
|
* ```
|
|
67
94
|
*/
|
|
68
|
-
static assignMeshLOD(mesh: Mesh, level: number
|
|
95
|
+
static assignMeshLOD(mesh: Mesh, level: number, options?: {
|
|
96
|
+
/** If false, load and return the requested geometry without assigning it to the mesh. Pass a function to apply it manually. */
|
|
97
|
+
apply?: boolean | ((geometry: BufferGeometry, level: number, mesh: Mesh) => boolean | void);
|
|
98
|
+
}): Promise<BufferGeometry | null>;
|
|
69
99
|
/** Load a different resolution of a texture (if available)
|
|
70
100
|
* @param context the context
|
|
71
101
|
* @param source the sourceid of the file from which the texture is loaded (this is usually the component's sourceId)
|
|
@@ -73,9 +103,9 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
73
103
|
* @param level the level of detail to load (0 is the highest resolution) - currently only 0 is supported
|
|
74
104
|
* @returns a promise that resolves to the material or texture with the requested LOD level
|
|
75
105
|
*/
|
|
76
|
-
static assignTextureLOD(materialOrTexture: Material, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
|
|
77
|
-
static assignTextureLOD(materialOrTexture: Mesh, level: number): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
|
|
78
|
-
static assignTextureLOD(materialOrTexture: Texture, level: number): Promise<Texture | null>;
|
|
106
|
+
static assignTextureLOD(materialOrTexture: Material, level: number, options?: AssignTextureLODOptions): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
|
|
107
|
+
static assignTextureLOD(materialOrTexture: Mesh, level: number, options?: AssignTextureLODOptions): Promise<Array<ProgressiveMaterialTextureLoadingResult> | null>;
|
|
108
|
+
static assignTextureLOD(materialOrTexture: Texture, level: number, options?: AssignTextureLODOptions): Promise<Texture | null>;
|
|
79
109
|
/**
|
|
80
110
|
* 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.
|
|
81
111
|
* @default 50 on desktop, 20 on mobile devices
|
|
@@ -83,6 +113,16 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
83
113
|
static set maxConcurrentLoadingTasks(value: number);
|
|
84
114
|
static get maxConcurrentLoadingTasks(): number;
|
|
85
115
|
private static assignTextureLODForSlot;
|
|
116
|
+
private static trackedTextureSlots;
|
|
117
|
+
private static pendingTextureSlotRequests;
|
|
118
|
+
private static trackCurrentMaterialTextureSlots;
|
|
119
|
+
private static getPendingTextureSlotRequest;
|
|
120
|
+
private static setPendingTextureSlotRequest;
|
|
121
|
+
private static getMaterialTextureSlot;
|
|
122
|
+
private static setMaterialTextureSlot;
|
|
123
|
+
private static assignTrackedTextureSlot;
|
|
124
|
+
private static ensureTrackedTextureSlot;
|
|
125
|
+
private static releaseTrackedTextureSlot;
|
|
86
126
|
private readonly parser;
|
|
87
127
|
private readonly url;
|
|
88
128
|
constructor(parser: GLTFParser);
|
|
@@ -90,11 +130,29 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
90
130
|
loadMesh: (meshIndex: number) => Promise<any> | null;
|
|
91
131
|
afterRoot(gltf: GLTF): null;
|
|
92
132
|
/**
|
|
93
|
-
* Register a texture with LOD information
|
|
133
|
+
* Register a texture with progressive LOD information. This associates the texture with its LOD extension data
|
|
134
|
+
* so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
|
|
135
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
136
|
+
*
|
|
137
|
+
* @param url - The source URL of the glTF file this texture was loaded from.
|
|
138
|
+
* @param tex - The three.js Texture instance to register.
|
|
139
|
+
* @param level - The LOD level this texture represents (0 = highest resolution).
|
|
140
|
+
* @param index - The texture index within the glTF file.
|
|
141
|
+
* @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
|
|
94
142
|
*/
|
|
95
143
|
static registerTexture: (url: string, tex: Texture, level: number, index: number, ext: NEEDLE_ext_progressive_texture) => void;
|
|
96
144
|
/**
|
|
97
|
-
* Register a mesh with LOD information
|
|
145
|
+
* Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
|
|
146
|
+
* so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
|
|
147
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
148
|
+
* If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
|
|
149
|
+
*
|
|
150
|
+
* @param url - The source URL of the glTF file this mesh was loaded from.
|
|
151
|
+
* @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
|
|
152
|
+
* @param mesh - The three.js Mesh instance to register.
|
|
153
|
+
* @param level - The LOD level this mesh represents (0 = highest resolution / full density).
|
|
154
|
+
* @param index - The primitive index within the glTF mesh node.
|
|
155
|
+
* @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
|
|
98
156
|
*/
|
|
99
157
|
static registerMesh: (url: string, key: string, mesh: Mesh, level: number, index: number, ext: NEEDLE_ext_progressive_mesh) => void;
|
|
100
158
|
/**
|
|
@@ -134,6 +192,7 @@ export declare class NEEDLE_progressive implements GLTFLoaderPlugin {
|
|
|
134
192
|
private static readonly workers;
|
|
135
193
|
private static _workersIndex;
|
|
136
194
|
private static getOrLoadLOD;
|
|
195
|
+
private static tryResolveLODCacheEntry;
|
|
137
196
|
private static _queue;
|
|
138
197
|
private static get queue();
|
|
139
198
|
private static assignLODInformation;
|
package/lib/extension.js
CHANGED
|
@@ -36,6 +36,13 @@ export class NEEDLE_progressive {
|
|
|
36
36
|
return EXTENSION_NAME;
|
|
37
37
|
}
|
|
38
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
|
+
*/
|
|
39
46
|
static getMeshLODExtension(geo) {
|
|
40
47
|
const info = this.getAssignedLODInformation(geo);
|
|
41
48
|
if (info?.key) {
|
|
@@ -43,12 +50,28 @@ export class NEEDLE_progressive {
|
|
|
43
50
|
}
|
|
44
51
|
return null;
|
|
45
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
|
+
*/
|
|
46
60
|
static getPrimitiveIndex(geo) {
|
|
47
61
|
const index = this.getAssignedLODInformation(geo)?.index;
|
|
48
62
|
if (index === undefined || index === null)
|
|
49
63
|
return -1;
|
|
50
64
|
return index;
|
|
51
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
|
+
*/
|
|
52
75
|
static getMaterialMinMaxLODsCount(material, minmax) {
|
|
53
76
|
const self = this;
|
|
54
77
|
// we can cache this material min max data because it wont change at runtime
|
|
@@ -186,7 +209,7 @@ export class NEEDLE_progressive {
|
|
|
186
209
|
* });
|
|
187
210
|
* ```
|
|
188
211
|
*/
|
|
189
|
-
static assignMeshLOD(mesh, level) {
|
|
212
|
+
static assignMeshLOD(mesh, level, options) {
|
|
190
213
|
if (!mesh)
|
|
191
214
|
return Promise.resolve(null);
|
|
192
215
|
if (mesh instanceof Mesh || mesh.isMesh === true) {
|
|
@@ -210,11 +233,16 @@ export class NEEDLE_progressive {
|
|
|
210
233
|
if (geo && currentGeometry != geo) {
|
|
211
234
|
const isGeometry = geo?.isBufferGeometry;
|
|
212
235
|
// if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
|
|
213
|
-
if (isGeometry) {
|
|
214
|
-
|
|
236
|
+
if (!isGeometry) {
|
|
237
|
+
if (debug) {
|
|
238
|
+
console.error("Invalid LOD geometry", geo);
|
|
239
|
+
}
|
|
215
240
|
}
|
|
216
|
-
else if (
|
|
217
|
-
|
|
241
|
+
else if (typeof options?.apply === "function") {
|
|
242
|
+
options.apply(geo, level, mesh);
|
|
243
|
+
}
|
|
244
|
+
else if (options?.apply !== false) {
|
|
245
|
+
mesh.geometry = geo;
|
|
218
246
|
}
|
|
219
247
|
}
|
|
220
248
|
}
|
|
@@ -231,15 +259,16 @@ export class NEEDLE_progressive {
|
|
|
231
259
|
}
|
|
232
260
|
return Promise.resolve(null);
|
|
233
261
|
}
|
|
234
|
-
static assignTextureLOD(materialOrTexture, level = 0) {
|
|
262
|
+
static assignTextureLOD(materialOrTexture, level = 0, options) {
|
|
235
263
|
if (!materialOrTexture)
|
|
236
264
|
return Promise.resolve(null);
|
|
265
|
+
const force = options?.force === true;
|
|
237
266
|
if (materialOrTexture.isMesh === true) {
|
|
238
267
|
const mesh = materialOrTexture;
|
|
239
268
|
if (Array.isArray(mesh.material)) {
|
|
240
269
|
const arr = new Array();
|
|
241
270
|
for (const mat of mesh.material) {
|
|
242
|
-
const promise = this.assignTextureLOD(mat, level);
|
|
271
|
+
const promise = this.assignTextureLOD(mat, level, options);
|
|
243
272
|
arr.push(promise);
|
|
244
273
|
}
|
|
245
274
|
return Promise.all(arr).then(res => {
|
|
@@ -253,13 +282,14 @@ export class NEEDLE_progressive {
|
|
|
253
282
|
});
|
|
254
283
|
}
|
|
255
284
|
else {
|
|
256
|
-
return this.assignTextureLOD(mesh.material, level);
|
|
285
|
+
return this.assignTextureLOD(mesh.material, level, options);
|
|
257
286
|
}
|
|
258
287
|
}
|
|
259
288
|
if (materialOrTexture.isMaterial === true) {
|
|
260
289
|
const material = materialOrTexture;
|
|
261
290
|
const promises = [];
|
|
262
291
|
const slots = new Array();
|
|
292
|
+
this.trackCurrentMaterialTextureSlots(material);
|
|
263
293
|
// Handle custom shaders / uniforms progressive textures. This includes support for VRM shaders
|
|
264
294
|
if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
|
|
265
295
|
// iterate uniforms of custom shaders
|
|
@@ -267,7 +297,7 @@ export class NEEDLE_progressive {
|
|
|
267
297
|
for (const slot of Object.keys(shaderMaterial.uniforms)) {
|
|
268
298
|
const val = shaderMaterial.uniforms[slot].value;
|
|
269
299
|
if (val?.isTexture === true) {
|
|
270
|
-
const task = this.assignTextureLODForSlot(val, level, material, slot).then(res => {
|
|
300
|
+
const task = this.assignTextureLODForSlot(val, level, material, slot, force).then(res => {
|
|
271
301
|
if (res && shaderMaterial.uniforms[slot].value != res) {
|
|
272
302
|
shaderMaterial.uniforms[slot].value = res;
|
|
273
303
|
shaderMaterial.uniformsNeedUpdate = true;
|
|
@@ -283,7 +313,7 @@ export class NEEDLE_progressive {
|
|
|
283
313
|
for (const slot of Object.keys(material)) {
|
|
284
314
|
const val = material[slot];
|
|
285
315
|
if (val?.isTexture === true) {
|
|
286
|
-
const task = this.assignTextureLODForSlot(val, level, material, slot);
|
|
316
|
+
const task = this.assignTextureLODForSlot(val, level, material, slot, force);
|
|
287
317
|
promises.push(task);
|
|
288
318
|
slots.push(slot);
|
|
289
319
|
}
|
|
@@ -306,7 +336,7 @@ export class NEEDLE_progressive {
|
|
|
306
336
|
}
|
|
307
337
|
if (materialOrTexture instanceof Texture || materialOrTexture.isTexture === true) {
|
|
308
338
|
const texture = materialOrTexture;
|
|
309
|
-
return this.assignTextureLODForSlot(texture, level, null, null);
|
|
339
|
+
return this.assignTextureLODForSlot(texture, level, null, null, force);
|
|
310
340
|
}
|
|
311
341
|
return Promise.resolve(null);
|
|
312
342
|
}
|
|
@@ -321,14 +351,29 @@ export class NEEDLE_progressive {
|
|
|
321
351
|
return NEEDLE_progressive.queue.maxConcurrent;
|
|
322
352
|
}
|
|
323
353
|
// #region INTERNAL
|
|
324
|
-
static assignTextureLODForSlot(current, level, material, slot) {
|
|
354
|
+
static assignTextureLODForSlot(current, level, material, slot, force) {
|
|
325
355
|
if (current?.isTexture !== true) {
|
|
326
356
|
return Promise.resolve(null);
|
|
327
357
|
}
|
|
328
358
|
if (slot === "glyphMap") {
|
|
329
359
|
return Promise.resolve(current);
|
|
330
360
|
}
|
|
331
|
-
|
|
361
|
+
const currentLOD = this.getAssignedLODInformation(current);
|
|
362
|
+
if (currentLOD) {
|
|
363
|
+
if (currentLOD.level === level) {
|
|
364
|
+
return Promise.resolve(current);
|
|
365
|
+
}
|
|
366
|
+
if (!force && currentLOD.level < level) {
|
|
367
|
+
return Promise.resolve(current);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
if (material && slot) {
|
|
371
|
+
const pending = this.getPendingTextureSlotRequest(material, slot);
|
|
372
|
+
if (pending && pending.level === level && pending.force === force) {
|
|
373
|
+
return pending.promise;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
const promise = NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
|
|
332
377
|
// this can currently not happen
|
|
333
378
|
if (Array.isArray(tex)) {
|
|
334
379
|
console.warn("Progressive: Got an array of textures for a texture slot, this should not happen...");
|
|
@@ -337,40 +382,19 @@ export class NEEDLE_progressive {
|
|
|
337
382
|
if (tex?.isTexture === true) {
|
|
338
383
|
if (tex != current) {
|
|
339
384
|
if (material && slot) {
|
|
340
|
-
const assigned = material
|
|
385
|
+
const assigned = this.getMaterialTextureSlot(material, slot) ?? current;
|
|
341
386
|
// Check if the assigned texture LOD is higher quality than the current LOD
|
|
342
387
|
// This is necessary for cases where e.g. a texture is updated via an explicit call to assignTextureLOD
|
|
343
|
-
if (assigned && !
|
|
388
|
+
if (assigned && !force) {
|
|
344
389
|
const assignedLOD = this.getAssignedLODInformation(assigned);
|
|
345
390
|
if (assignedLOD && assignedLOD?.level < level) {
|
|
346
391
|
if (debug === "verbose")
|
|
347
392
|
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
|
-
}
|
|
358
393
|
return null;
|
|
359
394
|
}
|
|
360
395
|
// assigned.dispose();
|
|
361
396
|
}
|
|
362
|
-
|
|
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);
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
material[slot] = tex;
|
|
397
|
+
this.assignTrackedTextureSlot(material, slot, tex);
|
|
374
398
|
}
|
|
375
399
|
// Note: We use reference counting above to track texture usage across multiple materials.
|
|
376
400
|
// When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
|
|
@@ -390,6 +414,129 @@ export class NEEDLE_progressive {
|
|
|
390
414
|
console.error("Error loading LOD", current, err);
|
|
391
415
|
return null;
|
|
392
416
|
});
|
|
417
|
+
if (material && slot) {
|
|
418
|
+
this.setPendingTextureSlotRequest(material, slot, level, force, promise);
|
|
419
|
+
}
|
|
420
|
+
return promise;
|
|
421
|
+
}
|
|
422
|
+
// Track material slots, not just texture objects. A shared fallback texture can be
|
|
423
|
+
// referenced by many slots and should only be disposed after every slot moved away.
|
|
424
|
+
static trackedTextureSlots = new WeakMap();
|
|
425
|
+
static pendingTextureSlotRequests = new WeakMap();
|
|
426
|
+
static trackCurrentMaterialTextureSlots(material) {
|
|
427
|
+
if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
|
|
428
|
+
const shaderMaterial = material;
|
|
429
|
+
for (const slot of Object.keys(shaderMaterial.uniforms)) {
|
|
430
|
+
const value = shaderMaterial.uniforms[slot].value;
|
|
431
|
+
if (value?.isTexture === true) {
|
|
432
|
+
this.ensureTrackedTextureSlot(material, slot, value);
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return;
|
|
436
|
+
}
|
|
437
|
+
for (const slot of Object.keys(material)) {
|
|
438
|
+
const value = material[slot];
|
|
439
|
+
if (value?.isTexture === true) {
|
|
440
|
+
this.ensureTrackedTextureSlot(material, slot, value);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
static getPendingTextureSlotRequest(material, slot) {
|
|
445
|
+
return this.pendingTextureSlotRequests.get(material)?.get(slot);
|
|
446
|
+
}
|
|
447
|
+
static setPendingTextureSlotRequest(material, slot, level, force, promise) {
|
|
448
|
+
let slots = this.pendingTextureSlotRequests.get(material);
|
|
449
|
+
if (!slots) {
|
|
450
|
+
slots = new Map();
|
|
451
|
+
this.pendingTextureSlotRequests.set(material, slots);
|
|
452
|
+
}
|
|
453
|
+
const request = { level, force, promise };
|
|
454
|
+
slots.set(slot, request);
|
|
455
|
+
promise.finally(() => {
|
|
456
|
+
const current = slots.get(slot);
|
|
457
|
+
if (current === request) {
|
|
458
|
+
slots.delete(slot);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
static getMaterialTextureSlot(material, slot) {
|
|
463
|
+
const uniforms = material.uniforms;
|
|
464
|
+
const uniform = uniforms?.[slot];
|
|
465
|
+
if (uniform?.value?.isTexture === true) {
|
|
466
|
+
return uniform.value;
|
|
467
|
+
}
|
|
468
|
+
const value = material[slot];
|
|
469
|
+
if (value?.isTexture === true) {
|
|
470
|
+
return value;
|
|
471
|
+
}
|
|
472
|
+
return null;
|
|
473
|
+
}
|
|
474
|
+
static setMaterialTextureSlot(material, slot, texture) {
|
|
475
|
+
const uniforms = material.uniforms;
|
|
476
|
+
const uniform = uniforms?.[slot];
|
|
477
|
+
if (uniform?.value?.isTexture === true) {
|
|
478
|
+
uniform.value = texture;
|
|
479
|
+
material.uniformsNeedUpdate = true;
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
material[slot] = texture;
|
|
483
|
+
}
|
|
484
|
+
static assignTrackedTextureSlot(material, slot, texture) {
|
|
485
|
+
let slots = this.trackedTextureSlots.get(material);
|
|
486
|
+
if (!slots) {
|
|
487
|
+
slots = new Map();
|
|
488
|
+
this.trackedTextureSlots.set(material, slots);
|
|
489
|
+
}
|
|
490
|
+
const assigned = this.getMaterialTextureSlot(material, slot);
|
|
491
|
+
let previousTracked = slots.get(slot);
|
|
492
|
+
if (!previousTracked && assigned) {
|
|
493
|
+
previousTracked = this.ensureTrackedTextureSlot(material, slot, assigned);
|
|
494
|
+
}
|
|
495
|
+
else if (previousTracked && assigned && previousTracked !== assigned) {
|
|
496
|
+
this.releaseTrackedTextureSlot(material, slot, previousTracked);
|
|
497
|
+
previousTracked = this.ensureTrackedTextureSlot(material, slot, assigned);
|
|
498
|
+
}
|
|
499
|
+
if (previousTracked === texture && assigned === texture) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (previousTracked && previousTracked !== texture) {
|
|
503
|
+
this.releaseTrackedTextureSlot(material, slot, previousTracked);
|
|
504
|
+
}
|
|
505
|
+
if (previousTracked !== texture) {
|
|
506
|
+
this.trackTextureUsage(texture);
|
|
507
|
+
slots.set(slot, texture);
|
|
508
|
+
}
|
|
509
|
+
if (assigned !== texture) {
|
|
510
|
+
this.setMaterialTextureSlot(material, slot, texture);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
static ensureTrackedTextureSlot(material, slot, texture) {
|
|
514
|
+
let slots = this.trackedTextureSlots.get(material);
|
|
515
|
+
if (!slots) {
|
|
516
|
+
slots = new Map();
|
|
517
|
+
this.trackedTextureSlots.set(material, slots);
|
|
518
|
+
}
|
|
519
|
+
const previous = slots.get(slot);
|
|
520
|
+
if (previous === texture) {
|
|
521
|
+
return previous;
|
|
522
|
+
}
|
|
523
|
+
if (previous) {
|
|
524
|
+
this.releaseTrackedTextureSlot(material, slot, previous);
|
|
525
|
+
}
|
|
526
|
+
this.trackTextureUsage(texture);
|
|
527
|
+
slots.set(slot, texture);
|
|
528
|
+
return texture;
|
|
529
|
+
}
|
|
530
|
+
static releaseTrackedTextureSlot(material, slot, texture) {
|
|
531
|
+
const slots = this.trackedTextureSlots.get(material);
|
|
532
|
+
if (slots?.get(slot) === texture) {
|
|
533
|
+
slots.delete(slot);
|
|
534
|
+
}
|
|
535
|
+
const wasDisposed = this.untrackTextureUsage(texture);
|
|
536
|
+
if (wasDisposed && (debug || debugGC)) {
|
|
537
|
+
const assignedLOD = this.getAssignedLODInformation(texture);
|
|
538
|
+
console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} for ${material.name || material.type}.${slot}`, texture.uuid);
|
|
539
|
+
}
|
|
393
540
|
}
|
|
394
541
|
parser;
|
|
395
542
|
url;
|
|
@@ -492,7 +639,15 @@ export class NEEDLE_progressive {
|
|
|
492
639
|
return null;
|
|
493
640
|
}
|
|
494
641
|
/**
|
|
495
|
-
* Register a texture with LOD information
|
|
642
|
+
* Register a texture with progressive LOD information. This associates the texture with its LOD extension data
|
|
643
|
+
* so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
|
|
644
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
645
|
+
*
|
|
646
|
+
* @param url - The source URL of the glTF file this texture was loaded from.
|
|
647
|
+
* @param tex - The three.js Texture instance to register.
|
|
648
|
+
* @param level - The LOD level this texture represents (0 = highest resolution).
|
|
649
|
+
* @param index - The texture index within the glTF file.
|
|
650
|
+
* @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
|
|
496
651
|
*/
|
|
497
652
|
static registerTexture = (url, tex, level, index, ext) => {
|
|
498
653
|
if (!tex) {
|
|
@@ -515,7 +670,17 @@ export class NEEDLE_progressive {
|
|
|
515
670
|
NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
|
|
516
671
|
};
|
|
517
672
|
/**
|
|
518
|
-
* Register a mesh with LOD information
|
|
673
|
+
* Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
|
|
674
|
+
* so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
|
|
675
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
676
|
+
* If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
|
|
677
|
+
*
|
|
678
|
+
* @param url - The source URL of the glTF file this mesh was loaded from.
|
|
679
|
+
* @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
|
|
680
|
+
* @param mesh - The three.js Mesh instance to register.
|
|
681
|
+
* @param level - The LOD level this mesh represents (0 = highest resolution / full density).
|
|
682
|
+
* @param index - The primitive index within the glTF mesh node.
|
|
683
|
+
* @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
|
|
519
684
|
*/
|
|
520
685
|
static registerMesh = (url, key, mesh, level, index, ext) => {
|
|
521
686
|
const geometry = mesh.geometry;
|
|
@@ -604,6 +769,8 @@ export class NEEDLE_progressive {
|
|
|
604
769
|
this.cache.clear();
|
|
605
770
|
// Clear all texture reference counts when disposing everything
|
|
606
771
|
this.textureRefCounts.clear();
|
|
772
|
+
this.trackedTextureSlots = new WeakMap();
|
|
773
|
+
this.pendingTextureSlotRequests = new WeakMap();
|
|
607
774
|
}
|
|
608
775
|
}
|
|
609
776
|
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
@@ -710,8 +877,8 @@ export class NEEDLE_progressive {
|
|
|
710
877
|
return false;
|
|
711
878
|
}
|
|
712
879
|
function logDebugInfo(prefix, newCount) {
|
|
713
|
-
|
|
714
|
-
|
|
880
|
+
const width = texture.image?.width || texture.source?.data?.width || 0;
|
|
881
|
+
const height = texture.image?.height || texture.source?.data?.height || 0;
|
|
715
882
|
const textureSize = width && height ? `${width}x${height}` : "N/A";
|
|
716
883
|
let memorySize = "N/A";
|
|
717
884
|
if (width && height) {
|
|
@@ -791,76 +958,16 @@ export class NEEDLE_progressive {
|
|
|
791
958
|
}
|
|
792
959
|
// check if the requested file has already been loaded
|
|
793
960
|
const KEY = lod_url + "_" + lodInfo.guid;
|
|
794
|
-
const slot = await this.queue.slot(lod_url);
|
|
795
961
|
// check if the requested file is currently being loaded or was previously loaded
|
|
796
|
-
const
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
let resourceIsDisposed = false;
|
|
806
|
-
if (res instanceof Texture && current instanceof Texture) {
|
|
807
|
-
if (res.image?.data || res.source?.data) {
|
|
808
|
-
res = this.copySettings(current, res);
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
resourceIsDisposed = true;
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
815
|
-
if (!res.attributes.position?.array) {
|
|
816
|
-
resourceIsDisposed = true;
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
if (!resourceIsDisposed) {
|
|
820
|
-
return res;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
// Resource was garbage collected or disposed — remove stale entry and re-load
|
|
824
|
-
this.cache.delete(KEY);
|
|
825
|
-
if (debug)
|
|
826
|
-
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
|
|
827
|
-
}
|
|
828
|
-
else {
|
|
829
|
-
// Promise — loading in progress or previously completed
|
|
830
|
-
let res = await existing.catch(err => {
|
|
831
|
-
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
|
|
832
|
-
return null;
|
|
833
|
-
});
|
|
834
|
-
let resouceIsDisposed = false;
|
|
835
|
-
if (res == null) {
|
|
836
|
-
// if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
|
|
837
|
-
// in which case we don't attempt to load it again
|
|
838
|
-
}
|
|
839
|
-
else if (res instanceof Texture && current instanceof Texture) {
|
|
840
|
-
// check if the texture has been disposed or not
|
|
841
|
-
if (res.image?.data || res.source?.data) {
|
|
842
|
-
res = this.copySettings(current, res);
|
|
843
|
-
}
|
|
844
|
-
// if it has been disposed we need to load it again
|
|
845
|
-
else {
|
|
846
|
-
resouceIsDisposed = true;
|
|
847
|
-
this.cache.delete(KEY);
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
851
|
-
if (res.attributes.position?.array) {
|
|
852
|
-
// the geometry is OK
|
|
853
|
-
}
|
|
854
|
-
else {
|
|
855
|
-
resouceIsDisposed = true;
|
|
856
|
-
this.cache.delete(KEY);
|
|
857
|
-
}
|
|
858
|
-
}
|
|
859
|
-
if (!resouceIsDisposed) {
|
|
860
|
-
return res;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
}
|
|
962
|
+
const cached = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
|
|
963
|
+
if (cached.found)
|
|
964
|
+
return cached.value;
|
|
965
|
+
const slot = await this.queue.slot(lod_url);
|
|
966
|
+
// Another request can fill the cache while this one waits for a queue slot.
|
|
967
|
+
// Re-checking here avoids duplicate loads for heavily instanced assets.
|
|
968
|
+
const cachedAfterQueue = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
|
|
969
|
+
if (cachedAfterQueue.found)
|
|
970
|
+
return cachedAfterQueue.value;
|
|
864
971
|
// #region loading
|
|
865
972
|
if (!slot.use) {
|
|
866
973
|
if (debug)
|
|
@@ -1056,6 +1163,67 @@ export class NEEDLE_progressive {
|
|
|
1056
1163
|
}
|
|
1057
1164
|
return null;
|
|
1058
1165
|
}
|
|
1166
|
+
static async tryResolveLODCacheEntry(existing, key, lodUrl, current, level, debugverbose) {
|
|
1167
|
+
if (existing === undefined) {
|
|
1168
|
+
return { found: false };
|
|
1169
|
+
}
|
|
1170
|
+
if (debugverbose)
|
|
1171
|
+
console.log(`LOD ${level} was already loading/loaded: ${key}`);
|
|
1172
|
+
if (existing instanceof WeakRef) {
|
|
1173
|
+
const derefed = existing.deref();
|
|
1174
|
+
if (derefed) {
|
|
1175
|
+
let res = derefed;
|
|
1176
|
+
let resourceIsDisposed = false;
|
|
1177
|
+
if (res instanceof Texture && current instanceof Texture) {
|
|
1178
|
+
if (res.image?.data || res.source?.data) {
|
|
1179
|
+
res = this.copySettings(current, res);
|
|
1180
|
+
}
|
|
1181
|
+
else {
|
|
1182
|
+
resourceIsDisposed = true;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
1186
|
+
if (!res.attributes.position?.array) {
|
|
1187
|
+
resourceIsDisposed = true;
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
if (!resourceIsDisposed) {
|
|
1191
|
+
return { found: true, value: res };
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
this.cache.delete(key);
|
|
1195
|
+
if (debug)
|
|
1196
|
+
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${key}`);
|
|
1197
|
+
return { found: false };
|
|
1198
|
+
}
|
|
1199
|
+
let res = await existing.catch(err => {
|
|
1200
|
+
console.error(`Error loading LOD ${level} from ${lodUrl}\n`, err);
|
|
1201
|
+
return null;
|
|
1202
|
+
});
|
|
1203
|
+
let resourceIsDisposed = false;
|
|
1204
|
+
if (res == null) {
|
|
1205
|
+
// Failed loads stay cached as null so we don't retry the same missing resource forever.
|
|
1206
|
+
}
|
|
1207
|
+
else if (res instanceof Texture && current instanceof Texture) {
|
|
1208
|
+
if (res.image?.data || res.source?.data) {
|
|
1209
|
+
res = this.copySettings(current, res);
|
|
1210
|
+
}
|
|
1211
|
+
else {
|
|
1212
|
+
resourceIsDisposed = true;
|
|
1213
|
+
this.cache.delete(key);
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
1217
|
+
if (!res.attributes.position?.array) {
|
|
1218
|
+
resourceIsDisposed = true;
|
|
1219
|
+
this.cache.delete(key);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
if (resourceIsDisposed) {
|
|
1223
|
+
return { found: false };
|
|
1224
|
+
}
|
|
1225
|
+
return { found: true, value: res };
|
|
1226
|
+
}
|
|
1059
1227
|
static _queue;
|
|
1060
1228
|
static get queue() { return this._queue ??= new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false }); }
|
|
1061
1229
|
static assignLODInformation(url, res, key, level, index) {
|