@needle-tools/gltf-progressive 4.0.0-alpha.1 → 4.0.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 +21 -1
- package/LICENSE +21 -0
- package/README.md +47 -11
- package/examples/modelviewer-multiple.html +4 -4
- package/examples/modelviewer.html +4 -4
- package/examples/offscreen/index.html +15 -0
- package/examples/offscreen/main.js +98 -0
- package/examples/react-three-fiber/index.html +2 -2
- package/examples/react-three-fiber/package-lock.json +482 -484
- package/examples/react-three-fiber/package.json +4 -4
- package/examples/react-three-fiber/src/App.tsx +76 -21
- package/examples/react-three-fiber/src/styles.css +2 -4
- package/examples/react-three-fiber/vite.config.js +2 -2
- package/examples/shared/example-utils.js +297 -0
- package/examples/shared/example.css +44 -0
- package/examples/shared/runtime.js +34 -0
- package/examples/threejs/index.html +6 -10
- package/examples/threejs/main.js +5 -23
- package/examples/webgpu/index.html +15 -0
- package/examples/webgpu/main.js +105 -0
- package/examples/worker-rendering/index.html +15 -0
- package/examples/worker-rendering/main.js +109 -0
- package/examples/worker-rendering/worker.js +166 -0
- package/gltf-progressive.js +1025 -669
- package/gltf-progressive.min.js +9 -9
- package/gltf-progressive.umd.cjs +9 -9
- package/lib/extension.d.ts +73 -7
- package/lib/extension.js +351 -111
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/loaders.js +17 -3
- package/lib/lods.debug.js +2 -2
- package/lib/lods.manager.d.ts +93 -15
- package/lib/lods.manager.js +331 -157
- package/lib/lods.promise.js +1 -1
- package/lib/plugins/modelviewer.js +1 -1
- package/lib/utils.internal.js +20 -6
- package/lib/version.js +1 -1
- package/lib/worker/loader.mainthread.js +4 -1
- package/package.json +10 -3
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) {
|
|
@@ -200,21 +223,32 @@ export class NEEDLE_progressive {
|
|
|
200
223
|
}
|
|
201
224
|
// const info = this.onProgressiveLoadStart(context, source, mesh, null);
|
|
202
225
|
mesh["LOD:requested level"] = level;
|
|
203
|
-
|
|
226
|
+
const shouldLoad = () => mesh["LOD:requested level"] === level || this.shouldApplyStaleMeshLOD(mesh, level);
|
|
227
|
+
return NEEDLE_progressive.getOrLoadLOD(currentGeometry, level, {
|
|
228
|
+
isCurrent: shouldLoad,
|
|
229
|
+
}).then(geo => {
|
|
204
230
|
if (Array.isArray(geo)) {
|
|
205
231
|
const index = lodinfo.index || 0;
|
|
206
232
|
geo = geo[index];
|
|
207
233
|
}
|
|
208
|
-
|
|
209
|
-
|
|
234
|
+
const isLatestRequest = mesh["LOD:requested level"] === level;
|
|
235
|
+
if (isLatestRequest || this.shouldApplyStaleMeshLOD(mesh, level)) {
|
|
236
|
+
if (isLatestRequest) {
|
|
237
|
+
delete mesh["LOD:requested level"];
|
|
238
|
+
}
|
|
210
239
|
if (geo && currentGeometry != geo) {
|
|
211
240
|
const isGeometry = geo?.isBufferGeometry;
|
|
212
241
|
// if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
|
|
213
|
-
if (isGeometry) {
|
|
214
|
-
|
|
242
|
+
if (!isGeometry) {
|
|
243
|
+
if (debug) {
|
|
244
|
+
console.error("Invalid LOD geometry", geo);
|
|
245
|
+
}
|
|
215
246
|
}
|
|
216
|
-
else if (
|
|
217
|
-
|
|
247
|
+
else if (typeof options?.apply === "function") {
|
|
248
|
+
options.apply(geo, level, mesh);
|
|
249
|
+
}
|
|
250
|
+
else if (options?.apply !== false) {
|
|
251
|
+
mesh.geometry = geo;
|
|
218
252
|
}
|
|
219
253
|
}
|
|
220
254
|
}
|
|
@@ -231,15 +265,16 @@ export class NEEDLE_progressive {
|
|
|
231
265
|
}
|
|
232
266
|
return Promise.resolve(null);
|
|
233
267
|
}
|
|
234
|
-
static assignTextureLOD(materialOrTexture, level = 0) {
|
|
268
|
+
static assignTextureLOD(materialOrTexture, level = 0, options) {
|
|
235
269
|
if (!materialOrTexture)
|
|
236
270
|
return Promise.resolve(null);
|
|
271
|
+
const force = options?.force === true;
|
|
237
272
|
if (materialOrTexture.isMesh === true) {
|
|
238
273
|
const mesh = materialOrTexture;
|
|
239
274
|
if (Array.isArray(mesh.material)) {
|
|
240
275
|
const arr = new Array();
|
|
241
276
|
for (const mat of mesh.material) {
|
|
242
|
-
const promise = this.assignTextureLOD(mat, level);
|
|
277
|
+
const promise = this.assignTextureLOD(mat, level, options);
|
|
243
278
|
arr.push(promise);
|
|
244
279
|
}
|
|
245
280
|
return Promise.all(arr).then(res => {
|
|
@@ -253,13 +288,14 @@ export class NEEDLE_progressive {
|
|
|
253
288
|
});
|
|
254
289
|
}
|
|
255
290
|
else {
|
|
256
|
-
return this.assignTextureLOD(mesh.material, level);
|
|
291
|
+
return this.assignTextureLOD(mesh.material, level, options);
|
|
257
292
|
}
|
|
258
293
|
}
|
|
259
294
|
if (materialOrTexture.isMaterial === true) {
|
|
260
295
|
const material = materialOrTexture;
|
|
261
296
|
const promises = [];
|
|
262
297
|
const slots = new Array();
|
|
298
|
+
this.trackCurrentMaterialTextureSlots(material);
|
|
263
299
|
// Handle custom shaders / uniforms progressive textures. This includes support for VRM shaders
|
|
264
300
|
if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
|
|
265
301
|
// iterate uniforms of custom shaders
|
|
@@ -267,7 +303,7 @@ export class NEEDLE_progressive {
|
|
|
267
303
|
for (const slot of Object.keys(shaderMaterial.uniforms)) {
|
|
268
304
|
const val = shaderMaterial.uniforms[slot].value;
|
|
269
305
|
if (val?.isTexture === true) {
|
|
270
|
-
const task = this.assignTextureLODForSlot(val, level, material, slot).then(res => {
|
|
306
|
+
const task = this.assignTextureLODForSlot(val, level, material, slot, force).then(res => {
|
|
271
307
|
if (res && shaderMaterial.uniforms[slot].value != res) {
|
|
272
308
|
shaderMaterial.uniforms[slot].value = res;
|
|
273
309
|
shaderMaterial.uniformsNeedUpdate = true;
|
|
@@ -283,7 +319,7 @@ export class NEEDLE_progressive {
|
|
|
283
319
|
for (const slot of Object.keys(material)) {
|
|
284
320
|
const val = material[slot];
|
|
285
321
|
if (val?.isTexture === true) {
|
|
286
|
-
const task = this.assignTextureLODForSlot(val, level, material, slot);
|
|
322
|
+
const task = this.assignTextureLODForSlot(val, level, material, slot, force);
|
|
287
323
|
promises.push(task);
|
|
288
324
|
slots.push(slot);
|
|
289
325
|
}
|
|
@@ -306,7 +342,7 @@ export class NEEDLE_progressive {
|
|
|
306
342
|
}
|
|
307
343
|
if (materialOrTexture instanceof Texture || materialOrTexture.isTexture === true) {
|
|
308
344
|
const texture = materialOrTexture;
|
|
309
|
-
return this.assignTextureLODForSlot(texture, level, null, null);
|
|
345
|
+
return this.assignTextureLODForSlot(texture, level, null, null, force);
|
|
310
346
|
}
|
|
311
347
|
return Promise.resolve(null);
|
|
312
348
|
}
|
|
@@ -321,14 +357,36 @@ export class NEEDLE_progressive {
|
|
|
321
357
|
return NEEDLE_progressive.queue.maxConcurrent;
|
|
322
358
|
}
|
|
323
359
|
// #region INTERNAL
|
|
324
|
-
static assignTextureLODForSlot(current, level, material, slot) {
|
|
360
|
+
static assignTextureLODForSlot(current, level, material, slot, force) {
|
|
325
361
|
if (current?.isTexture !== true) {
|
|
326
362
|
return Promise.resolve(null);
|
|
327
363
|
}
|
|
328
364
|
if (slot === "glyphMap") {
|
|
329
365
|
return Promise.resolve(current);
|
|
330
366
|
}
|
|
331
|
-
|
|
367
|
+
const currentLOD = this.getAssignedLODInformation(current);
|
|
368
|
+
if (currentLOD) {
|
|
369
|
+
if (currentLOD.level === level) {
|
|
370
|
+
return Promise.resolve(current);
|
|
371
|
+
}
|
|
372
|
+
if (!force && currentLOD.level < level) {
|
|
373
|
+
return Promise.resolve(current);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (material && slot) {
|
|
377
|
+
const pending = this.getPendingTextureSlotRequest(material, slot);
|
|
378
|
+
if (pending && pending.level === level && pending.force === force) {
|
|
379
|
+
return pending.promise;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
const requestId = material && slot ? this.nextTextureSlotRequestId(material, slot, level, force) : 0;
|
|
383
|
+
const isCurrentRequest = () => !material || !slot || this.getLatestTextureSlotRequest(material, slot)?.id === requestId;
|
|
384
|
+
const shouldLoad = () => isCurrentRequest() || this.shouldApplyStaleTextureSlotLOD(material, slot, level, force);
|
|
385
|
+
const promise = NEEDLE_progressive.getOrLoadLOD(current, level, {
|
|
386
|
+
isCurrent: shouldLoad,
|
|
387
|
+
}).then(tex => {
|
|
388
|
+
if (!isCurrentRequest() && !this.shouldApplyStaleTextureSlotLOD(material, slot, level, force))
|
|
389
|
+
return null;
|
|
332
390
|
// this can currently not happen
|
|
333
391
|
if (Array.isArray(tex)) {
|
|
334
392
|
console.warn("Progressive: Got an array of textures for a texture slot, this should not happen...");
|
|
@@ -337,40 +395,19 @@ export class NEEDLE_progressive {
|
|
|
337
395
|
if (tex?.isTexture === true) {
|
|
338
396
|
if (tex != current) {
|
|
339
397
|
if (material && slot) {
|
|
340
|
-
const assigned = material
|
|
398
|
+
const assigned = this.getMaterialTextureSlot(material, slot) ?? current;
|
|
341
399
|
// Check if the assigned texture LOD is higher quality than the current LOD
|
|
342
400
|
// This is necessary for cases where e.g. a texture is updated via an explicit call to assignTextureLOD
|
|
343
|
-
if (assigned && !
|
|
401
|
+
if (assigned && !force) {
|
|
344
402
|
const assignedLOD = this.getAssignedLODInformation(assigned);
|
|
345
403
|
if (assignedLOD && assignedLOD?.level < level) {
|
|
346
404
|
if (debug === "verbose")
|
|
347
405
|
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
406
|
return null;
|
|
359
407
|
}
|
|
360
408
|
// assigned.dispose();
|
|
361
409
|
}
|
|
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;
|
|
410
|
+
this.assignTrackedTextureSlot(material, slot, tex);
|
|
374
411
|
}
|
|
375
412
|
// Note: We use reference counting above to track texture usage across multiple materials.
|
|
376
413
|
// When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
|
|
@@ -390,6 +427,166 @@ export class NEEDLE_progressive {
|
|
|
390
427
|
console.error("Error loading LOD", current, err);
|
|
391
428
|
return null;
|
|
392
429
|
});
|
|
430
|
+
if (material && slot) {
|
|
431
|
+
this.setPendingTextureSlotRequest(material, slot, level, force, requestId, promise);
|
|
432
|
+
}
|
|
433
|
+
return promise;
|
|
434
|
+
}
|
|
435
|
+
// Track material slots, not just texture objects. A shared fallback texture can be
|
|
436
|
+
// referenced by many slots and should only be disposed after every slot moved away.
|
|
437
|
+
static trackedTextureSlots = new WeakMap();
|
|
438
|
+
static pendingTextureSlotRequests = new WeakMap();
|
|
439
|
+
static latestTextureSlotRequests = new WeakMap();
|
|
440
|
+
static textureSlotRequestId = 0;
|
|
441
|
+
static trackCurrentMaterialTextureSlots(material) {
|
|
442
|
+
if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
|
|
443
|
+
const shaderMaterial = material;
|
|
444
|
+
for (const slot of Object.keys(shaderMaterial.uniforms)) {
|
|
445
|
+
const value = shaderMaterial.uniforms[slot].value;
|
|
446
|
+
if (value?.isTexture === true) {
|
|
447
|
+
this.ensureTrackedTextureSlot(material, slot, value);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
for (const slot of Object.keys(material)) {
|
|
453
|
+
const value = material[slot];
|
|
454
|
+
if (value?.isTexture === true) {
|
|
455
|
+
this.ensureTrackedTextureSlot(material, slot, value);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
static getPendingTextureSlotRequest(material, slot) {
|
|
460
|
+
return this.pendingTextureSlotRequests.get(material)?.get(slot);
|
|
461
|
+
}
|
|
462
|
+
static nextTextureSlotRequestId(material, slot, level, force) {
|
|
463
|
+
let slots = this.latestTextureSlotRequests.get(material);
|
|
464
|
+
if (!slots) {
|
|
465
|
+
slots = new Map();
|
|
466
|
+
this.latestTextureSlotRequests.set(material, slots);
|
|
467
|
+
}
|
|
468
|
+
const id = ++this.textureSlotRequestId;
|
|
469
|
+
slots.set(slot, { id, level, force });
|
|
470
|
+
return id;
|
|
471
|
+
}
|
|
472
|
+
static getLatestTextureSlotRequest(material, slot) {
|
|
473
|
+
return this.latestTextureSlotRequests.get(material)?.get(slot);
|
|
474
|
+
}
|
|
475
|
+
static shouldApplyStaleTextureSlotLOD(material, slot, level, force) {
|
|
476
|
+
if (!material || !slot)
|
|
477
|
+
return false;
|
|
478
|
+
const latest = this.getLatestTextureSlotRequest(material, slot);
|
|
479
|
+
const assigned = this.getMaterialTextureSlot(material, slot);
|
|
480
|
+
const assignedLODLevel = this.getAssignedLODInformation(assigned)?.level ?? Infinity;
|
|
481
|
+
if (level >= assignedLODLevel)
|
|
482
|
+
return false;
|
|
483
|
+
if (force) {
|
|
484
|
+
if (!latest)
|
|
485
|
+
return false;
|
|
486
|
+
return level >= latest.level;
|
|
487
|
+
}
|
|
488
|
+
return true;
|
|
489
|
+
}
|
|
490
|
+
static shouldApplyStaleMeshLOD(mesh, level) {
|
|
491
|
+
const latestLevel = mesh["LOD:requested level"];
|
|
492
|
+
if (typeof latestLevel !== "number")
|
|
493
|
+
return false;
|
|
494
|
+
const assignedLODLevel = this.getAssignedLODInformation(mesh.geometry)?.level ?? Infinity;
|
|
495
|
+
return level < assignedLODLevel && level >= latestLevel;
|
|
496
|
+
}
|
|
497
|
+
static setPendingTextureSlotRequest(material, slot, level, force, id, promise) {
|
|
498
|
+
let slots = this.pendingTextureSlotRequests.get(material);
|
|
499
|
+
if (!slots) {
|
|
500
|
+
slots = new Map();
|
|
501
|
+
this.pendingTextureSlotRequests.set(material, slots);
|
|
502
|
+
}
|
|
503
|
+
const request = { level, force, id, promise };
|
|
504
|
+
slots.set(slot, request);
|
|
505
|
+
promise.finally(() => {
|
|
506
|
+
const current = slots.get(slot);
|
|
507
|
+
if (current?.id === id) {
|
|
508
|
+
slots.delete(slot);
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
static getMaterialTextureSlot(material, slot) {
|
|
513
|
+
const uniforms = material.uniforms;
|
|
514
|
+
const uniform = uniforms?.[slot];
|
|
515
|
+
if (uniform?.value?.isTexture === true) {
|
|
516
|
+
return uniform.value;
|
|
517
|
+
}
|
|
518
|
+
const value = material[slot];
|
|
519
|
+
if (value?.isTexture === true) {
|
|
520
|
+
return value;
|
|
521
|
+
}
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
static setMaterialTextureSlot(material, slot, texture) {
|
|
525
|
+
const uniforms = material.uniforms;
|
|
526
|
+
const uniform = uniforms?.[slot];
|
|
527
|
+
if (uniform?.value?.isTexture === true) {
|
|
528
|
+
uniform.value = texture;
|
|
529
|
+
material.uniformsNeedUpdate = true;
|
|
530
|
+
return;
|
|
531
|
+
}
|
|
532
|
+
material[slot] = texture;
|
|
533
|
+
}
|
|
534
|
+
static assignTrackedTextureSlot(material, slot, texture) {
|
|
535
|
+
let slots = this.trackedTextureSlots.get(material);
|
|
536
|
+
if (!slots) {
|
|
537
|
+
slots = new Map();
|
|
538
|
+
this.trackedTextureSlots.set(material, slots);
|
|
539
|
+
}
|
|
540
|
+
const assigned = this.getMaterialTextureSlot(material, slot);
|
|
541
|
+
let previousTracked = slots.get(slot);
|
|
542
|
+
if (!previousTracked && assigned) {
|
|
543
|
+
previousTracked = this.ensureTrackedTextureSlot(material, slot, assigned);
|
|
544
|
+
}
|
|
545
|
+
else if (previousTracked && assigned && previousTracked !== assigned) {
|
|
546
|
+
this.releaseTrackedTextureSlot(material, slot, previousTracked);
|
|
547
|
+
previousTracked = this.ensureTrackedTextureSlot(material, slot, assigned);
|
|
548
|
+
}
|
|
549
|
+
if (previousTracked === texture && assigned === texture) {
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (previousTracked && previousTracked !== texture) {
|
|
553
|
+
this.releaseTrackedTextureSlot(material, slot, previousTracked);
|
|
554
|
+
}
|
|
555
|
+
if (previousTracked !== texture) {
|
|
556
|
+
this.trackTextureUsage(texture);
|
|
557
|
+
slots.set(slot, texture);
|
|
558
|
+
}
|
|
559
|
+
if (assigned !== texture) {
|
|
560
|
+
this.setMaterialTextureSlot(material, slot, texture);
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
static ensureTrackedTextureSlot(material, slot, texture) {
|
|
564
|
+
let slots = this.trackedTextureSlots.get(material);
|
|
565
|
+
if (!slots) {
|
|
566
|
+
slots = new Map();
|
|
567
|
+
this.trackedTextureSlots.set(material, slots);
|
|
568
|
+
}
|
|
569
|
+
const previous = slots.get(slot);
|
|
570
|
+
if (previous === texture) {
|
|
571
|
+
return previous;
|
|
572
|
+
}
|
|
573
|
+
if (previous) {
|
|
574
|
+
this.releaseTrackedTextureSlot(material, slot, previous);
|
|
575
|
+
}
|
|
576
|
+
this.trackTextureUsage(texture);
|
|
577
|
+
slots.set(slot, texture);
|
|
578
|
+
return texture;
|
|
579
|
+
}
|
|
580
|
+
static releaseTrackedTextureSlot(material, slot, texture) {
|
|
581
|
+
const slots = this.trackedTextureSlots.get(material);
|
|
582
|
+
if (slots?.get(slot) === texture) {
|
|
583
|
+
slots.delete(slot);
|
|
584
|
+
}
|
|
585
|
+
const wasDisposed = this.untrackTextureUsage(texture);
|
|
586
|
+
if (wasDisposed && (debug || debugGC)) {
|
|
587
|
+
const assignedLOD = this.getAssignedLODInformation(texture);
|
|
588
|
+
console.log(`[gltf-progressive] Disposed old texture LOD ${assignedLOD?.level ?? '?'} for ${material.name || material.type}.${slot}`, texture.uuid);
|
|
589
|
+
}
|
|
393
590
|
}
|
|
394
591
|
parser;
|
|
395
592
|
url;
|
|
@@ -492,7 +689,15 @@ export class NEEDLE_progressive {
|
|
|
492
689
|
return null;
|
|
493
690
|
}
|
|
494
691
|
/**
|
|
495
|
-
* Register a texture with LOD information
|
|
692
|
+
* Register a texture with progressive LOD information. This associates the texture with its LOD extension data
|
|
693
|
+
* so the LODs manager can later swap it for higher or lower resolution versions based on screen coverage.
|
|
694
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
695
|
+
*
|
|
696
|
+
* @param url - The source URL of the glTF file this texture was loaded from.
|
|
697
|
+
* @param tex - The three.js Texture instance to register.
|
|
698
|
+
* @param level - The LOD level this texture represents (0 = highest resolution).
|
|
699
|
+
* @param index - The texture index within the glTF file.
|
|
700
|
+
* @param ext - The parsed progressive texture extension data containing all available LOD levels and their dimensions.
|
|
496
701
|
*/
|
|
497
702
|
static registerTexture = (url, tex, level, index, ext) => {
|
|
498
703
|
if (!tex) {
|
|
@@ -514,7 +719,17 @@ export class NEEDLE_progressive {
|
|
|
514
719
|
NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
|
|
515
720
|
};
|
|
516
721
|
/**
|
|
517
|
-
* Register a mesh with LOD information
|
|
722
|
+
* Register a mesh with progressive LOD information. This associates the mesh geometry with its LOD extension data
|
|
723
|
+
* so the LODs manager can later swap it for higher or lower density versions based on screen coverage.
|
|
724
|
+
* Typically called during glTF loading when the progressive extension is parsed.
|
|
725
|
+
* If the mesh is registered at a level > 0 (i.e. not full resolution), a raycast mesh is automatically preserved for accurate picking.
|
|
726
|
+
*
|
|
727
|
+
* @param url - The source URL of the glTF file this mesh was loaded from.
|
|
728
|
+
* @param key - A unique key identifying this mesh's LOD group (typically derived from the extension GUID).
|
|
729
|
+
* @param mesh - The three.js Mesh instance to register.
|
|
730
|
+
* @param level - The LOD level this mesh represents (0 = highest resolution / full density).
|
|
731
|
+
* @param index - The primitive index within the glTF mesh node.
|
|
732
|
+
* @param ext - The parsed progressive mesh extension data containing all available LOD levels with vertex/index counts and densities.
|
|
518
733
|
*/
|
|
519
734
|
static registerMesh = (url, key, mesh, level, index, ext) => {
|
|
520
735
|
const geometry = mesh.geometry;
|
|
@@ -603,6 +818,10 @@ export class NEEDLE_progressive {
|
|
|
603
818
|
this.cache.clear();
|
|
604
819
|
// Clear all texture reference counts when disposing everything
|
|
605
820
|
this.textureRefCounts.clear();
|
|
821
|
+
this.trackedTextureSlots = new WeakMap();
|
|
822
|
+
this.pendingTextureSlotRequests = new WeakMap();
|
|
823
|
+
this.latestTextureSlotRequests = new WeakMap();
|
|
824
|
+
this.textureSlotRequestId = 0;
|
|
606
825
|
}
|
|
607
826
|
}
|
|
608
827
|
/** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
|
|
@@ -720,7 +939,7 @@ export class NEEDLE_progressive {
|
|
|
720
939
|
}
|
|
721
940
|
static workers = [];
|
|
722
941
|
static _workersIndex = 0;
|
|
723
|
-
static async getOrLoadLOD(current, level) {
|
|
942
|
+
static async getOrLoadLOD(current, level, options) {
|
|
724
943
|
const debugverbose = debug == "verbose";
|
|
725
944
|
/** this key is used to lookup the LOD information */
|
|
726
945
|
const LOD = this.getAssignedLODInformation(current);
|
|
@@ -789,76 +1008,26 @@ export class NEEDLE_progressive {
|
|
|
789
1008
|
}
|
|
790
1009
|
// check if the requested file has already been loaded
|
|
791
1010
|
const KEY = lod_url + "_" + lodInfo.guid;
|
|
792
|
-
const slot = await this.queue.slot(lod_url);
|
|
793
1011
|
// check if the requested file is currently being loaded or was previously loaded
|
|
794
|
-
const
|
|
795
|
-
if (
|
|
1012
|
+
const cached = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
|
|
1013
|
+
if (cached.found)
|
|
1014
|
+
return cached.value;
|
|
1015
|
+
if (options?.isCurrent?.() === false) {
|
|
796
1016
|
if (debugverbose)
|
|
797
|
-
console.log(`LOD ${level}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
806
|
-
res = this.copySettings(current, res);
|
|
807
|
-
}
|
|
808
|
-
else {
|
|
809
|
-
resourceIsDisposed = true;
|
|
810
|
-
}
|
|
811
|
-
}
|
|
812
|
-
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
813
|
-
if (!res.attributes.position?.array) {
|
|
814
|
-
resourceIsDisposed = true;
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
if (!resourceIsDisposed) {
|
|
818
|
-
return res;
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
// Resource was garbage collected or disposed — remove stale entry and re-load
|
|
822
|
-
this.cache.delete(KEY);
|
|
823
|
-
if (debug)
|
|
824
|
-
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
|
|
825
|
-
}
|
|
826
|
-
else {
|
|
827
|
-
// Promise — loading in progress or previously completed
|
|
828
|
-
let res = await existing.catch(err => {
|
|
829
|
-
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
|
|
830
|
-
return null;
|
|
831
|
-
});
|
|
832
|
-
let resouceIsDisposed = false;
|
|
833
|
-
if (res == null) {
|
|
834
|
-
// if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
|
|
835
|
-
// in which case we don't attempt to load it again
|
|
836
|
-
}
|
|
837
|
-
else if (res instanceof Texture && current instanceof Texture) {
|
|
838
|
-
// check if the texture has been disposed or not
|
|
839
|
-
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
840
|
-
res = this.copySettings(current, res);
|
|
841
|
-
}
|
|
842
|
-
// if it has been disposed we need to load it again
|
|
843
|
-
else {
|
|
844
|
-
resouceIsDisposed = true;
|
|
845
|
-
this.cache.delete(KEY);
|
|
846
|
-
}
|
|
847
|
-
}
|
|
848
|
-
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
849
|
-
if (res.attributes.position?.array) {
|
|
850
|
-
// the geometry is OK
|
|
851
|
-
}
|
|
852
|
-
else {
|
|
853
|
-
resouceIsDisposed = true;
|
|
854
|
-
this.cache.delete(KEY);
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
if (!resouceIsDisposed) {
|
|
858
|
-
return res;
|
|
859
|
-
}
|
|
860
|
-
}
|
|
1017
|
+
console.log(`Skipping stale LOD ${level} request before queue: ${lod_url}`);
|
|
1018
|
+
return null;
|
|
1019
|
+
}
|
|
1020
|
+
const slot = await this.queue.slot(lod_url);
|
|
1021
|
+
if (options?.isCurrent?.() === false) {
|
|
1022
|
+
if (debugverbose)
|
|
1023
|
+
console.log(`Skipping stale LOD ${level} request after queue: ${lod_url}`);
|
|
1024
|
+
return null;
|
|
861
1025
|
}
|
|
1026
|
+
// Another request can fill the cache while this one waits for a queue slot.
|
|
1027
|
+
// Re-checking here avoids duplicate loads for heavily instanced assets.
|
|
1028
|
+
const cachedAfterQueue = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
|
|
1029
|
+
if (cachedAfterQueue.found)
|
|
1030
|
+
return cachedAfterQueue.value;
|
|
862
1031
|
// #region loading
|
|
863
1032
|
if (!slot.use) {
|
|
864
1033
|
if (debug)
|
|
@@ -1034,10 +1203,19 @@ export class NEEDLE_progressive {
|
|
|
1034
1203
|
}
|
|
1035
1204
|
else {
|
|
1036
1205
|
if (current instanceof Texture) {
|
|
1206
|
+
if (options?.isCurrent?.() === false) {
|
|
1207
|
+
if (debugverbose)
|
|
1208
|
+
console.log(`Skipping stale texture LOD ${level} request: ${lod_url}`);
|
|
1209
|
+
return null;
|
|
1210
|
+
}
|
|
1037
1211
|
if (debugverbose)
|
|
1038
1212
|
console.log("Load texture from uri: " + lod_url);
|
|
1039
1213
|
const loader = new TextureLoader();
|
|
1040
1214
|
const tex = await loader.loadAsync(lod_url);
|
|
1215
|
+
if (options?.isCurrent?.() === false) {
|
|
1216
|
+
tex?.dispose();
|
|
1217
|
+
return null;
|
|
1218
|
+
}
|
|
1041
1219
|
if (tex) {
|
|
1042
1220
|
tex.guid = lodInfo.guid;
|
|
1043
1221
|
tex.flipY = false;
|
|
@@ -1054,7 +1232,69 @@ export class NEEDLE_progressive {
|
|
|
1054
1232
|
}
|
|
1055
1233
|
return null;
|
|
1056
1234
|
}
|
|
1057
|
-
static
|
|
1235
|
+
static async tryResolveLODCacheEntry(existing, key, lodUrl, current, level, debugverbose) {
|
|
1236
|
+
if (existing === undefined) {
|
|
1237
|
+
return { found: false };
|
|
1238
|
+
}
|
|
1239
|
+
if (debugverbose)
|
|
1240
|
+
console.log(`LOD ${level} was already loading/loaded: ${key}`);
|
|
1241
|
+
if (existing instanceof WeakRef) {
|
|
1242
|
+
const derefed = existing.deref();
|
|
1243
|
+
if (derefed) {
|
|
1244
|
+
let res = derefed;
|
|
1245
|
+
let resourceIsDisposed = false;
|
|
1246
|
+
if (res instanceof Texture && current instanceof Texture) {
|
|
1247
|
+
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
1248
|
+
res = this.copySettings(current, res);
|
|
1249
|
+
}
|
|
1250
|
+
else {
|
|
1251
|
+
resourceIsDisposed = true;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
1255
|
+
if (!res.attributes.position?.array) {
|
|
1256
|
+
resourceIsDisposed = true;
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
if (!resourceIsDisposed) {
|
|
1260
|
+
return { found: true, value: res };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
this.cache.delete(key);
|
|
1264
|
+
if (debug)
|
|
1265
|
+
console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${key}`);
|
|
1266
|
+
return { found: false };
|
|
1267
|
+
}
|
|
1268
|
+
let res = await existing.catch(err => {
|
|
1269
|
+
console.error(`Error loading LOD ${level} from ${lodUrl}\n`, err);
|
|
1270
|
+
return null;
|
|
1271
|
+
});
|
|
1272
|
+
let resourceIsDisposed = false;
|
|
1273
|
+
if (res == null) {
|
|
1274
|
+
// Failed loads stay cached as null so we don't retry the same missing resource forever.
|
|
1275
|
+
}
|
|
1276
|
+
else if (res instanceof Texture && current instanceof Texture) {
|
|
1277
|
+
if (hasPixelData(res.image) || getSourceData(res)) {
|
|
1278
|
+
res = this.copySettings(current, res);
|
|
1279
|
+
}
|
|
1280
|
+
else {
|
|
1281
|
+
resourceIsDisposed = true;
|
|
1282
|
+
this.cache.delete(key);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
1286
|
+
if (!res.attributes.position?.array) {
|
|
1287
|
+
resourceIsDisposed = true;
|
|
1288
|
+
this.cache.delete(key);
|
|
1289
|
+
}
|
|
1290
|
+
}
|
|
1291
|
+
if (resourceIsDisposed) {
|
|
1292
|
+
return { found: false };
|
|
1293
|
+
}
|
|
1294
|
+
return { found: true, value: res };
|
|
1295
|
+
}
|
|
1296
|
+
static _queue;
|
|
1297
|
+
static get queue() { return this._queue ??= new PromiseQueue(isMobileDevice() ? 20 : 50, { debug: debug != false }); }
|
|
1058
1298
|
static assignLODInformation(url, res, key, level, index) {
|
|
1059
1299
|
if (!res)
|
|
1060
1300
|
return;
|
package/lib/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { version as VERSION } from "./version.js";
|
|
2
2
|
export * from "./extension.js";
|
|
3
3
|
export * from "./plugins/index.js";
|
|
4
|
-
export { LODsManager, type LOD_Results } from "./lods.manager.js";
|
|
4
|
+
export { LODsManager, calculateMeshLODLevel, getLODColor, lodDebugColors, type LOD_Results, type MeshLODSelectionOptions, type MeshLODSelectionResult } from "./lods.manager.js";
|
|
5
5
|
export { setDracoDecoderLocation, setKTX2TranscoderLocation, createLoaders, addDracoAndKTX2Loaders, configureLoader } from "./loaders.js";
|
|
6
6
|
export { getRaycastMesh, registerRaycastMesh, useRaycastMeshes } from "./utils.js";
|
|
7
7
|
import { WebGLRenderer } from "three";
|
package/lib/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export { version as VERSION } from "./version.js";
|
|
2
2
|
export * from "./extension.js";
|
|
3
3
|
export * from "./plugins/index.js";
|
|
4
|
-
export { LODsManager } from "./lods.manager.js";
|
|
4
|
+
export { LODsManager, calculateMeshLODLevel, getLODColor, lodDebugColors } from "./lods.manager.js";
|
|
5
5
|
export { setDracoDecoderLocation, setKTX2TranscoderLocation, createLoaders, addDracoAndKTX2Loaders, configureLoader } from "./loaders.js";
|
|
6
6
|
export { getRaycastMesh, registerRaycastMesh, useRaycastMeshes } from "./utils.js";
|
|
7
7
|
import { addDracoAndKTX2Loaders, configureLoader, createLoaders } from "./loaders.js";
|