@needle-tools/gltf-progressive 4.0.0-alpha → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/LICENSE +21 -0
  3. package/README.md +47 -11
  4. package/examples/modelviewer-multiple.html +4 -4
  5. package/examples/modelviewer.html +4 -4
  6. package/examples/offscreen/index.html +15 -0
  7. package/examples/offscreen/main.js +98 -0
  8. package/examples/react-three-fiber/index.html +2 -2
  9. package/examples/react-three-fiber/package-lock.json +482 -484
  10. package/examples/react-three-fiber/package.json +4 -4
  11. package/examples/react-three-fiber/src/App.tsx +76 -21
  12. package/examples/react-three-fiber/src/styles.css +2 -4
  13. package/examples/react-three-fiber/vite.config.js +2 -2
  14. package/examples/shared/example-utils.js +297 -0
  15. package/examples/shared/example.css +44 -0
  16. package/examples/shared/runtime.js +34 -0
  17. package/examples/threejs/index.html +6 -10
  18. package/examples/threejs/main.js +5 -23
  19. package/examples/webgpu/index.html +15 -0
  20. package/examples/webgpu/main.js +105 -0
  21. package/examples/worker-rendering/index.html +15 -0
  22. package/examples/worker-rendering/main.js +109 -0
  23. package/examples/worker-rendering/worker.js +166 -0
  24. package/gltf-progressive.js +1924 -0
  25. package/gltf-progressive.min.js +10 -0
  26. package/gltf-progressive.umd.cjs +10 -0
  27. package/lib/extension.d.ts +208 -0
  28. package/lib/extension.js +1373 -0
  29. package/lib/extension.model.d.ts +33 -0
  30. package/lib/extension.model.js +1 -0
  31. package/lib/index.d.ts +22 -0
  32. package/lib/index.js +87 -0
  33. package/lib/loaders.d.ts +41 -0
  34. package/lib/loaders.js +176 -0
  35. package/lib/lods.debug.d.ts +4 -0
  36. package/lib/lods.debug.js +43 -0
  37. package/lib/lods.manager.d.ts +243 -0
  38. package/lib/lods.manager.js +923 -0
  39. package/lib/lods.promise.d.ts +68 -0
  40. package/lib/lods.promise.js +108 -0
  41. package/lib/plugins/index.d.ts +2 -0
  42. package/lib/plugins/index.js +1 -0
  43. package/lib/plugins/modelviewer.d.ts +1 -0
  44. package/lib/plugins/modelviewer.js +223 -0
  45. package/lib/plugins/plugin.d.ts +23 -0
  46. package/lib/plugins/plugin.js +5 -0
  47. package/lib/utils.d.ts +30 -0
  48. package/lib/utils.internal.d.ts +68 -0
  49. package/lib/utils.internal.js +253 -0
  50. package/lib/utils.js +82 -0
  51. package/lib/version.d.ts +1 -0
  52. package/lib/version.js +4 -0
  53. package/lib/worker/gltf-progressive.worker.js +165 -0
  54. package/lib/worker/loader.mainthread.d.ts +45 -0
  55. package/lib/worker/loader.mainthread.js +195 -0
  56. package/package.json +13 -19
@@ -0,0 +1,1373 @@
1
+ import { BufferGeometry, Mesh, Texture, TextureLoader } from "three";
2
+ import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
3
+ import { addDracoAndKTX2Loaders } from "./loaders.js";
4
+ import { determineTextureMemoryInBytes, getParam, getSourceData, getTextureDimensions, hasPixelData, isMobileDevice, PromiseQueue, resolveUrl } from "./utils.internal.js";
5
+ import { getRaycastMesh, registerRaycastMesh } from "./utils.js";
6
+ // All of this has to be removed
7
+ // import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
8
+ // import { PromiseAllWithErrors, resolveUrl } from "../../engine_utils.js";
9
+ import { plugins } from "./plugins/plugin.js";
10
+ import { debug } from "./lods.debug.js";
11
+ import { getWorker } from "./worker/loader.mainthread.js";
12
+ const useWorker = getParam("gltf-progressive-worker");
13
+ const reduceMipmaps = getParam("gltf-progressive-reduce-mipmaps");
14
+ const debugGC = getParam("gltf-progressive-gc");
15
+ const $progressiveTextureExtension = Symbol("needle-progressive-texture");
16
+ export const EXTENSION_NAME = "NEEDLE_progressive";
17
+ // #region EXT
18
+ /**
19
+ * The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
20
+ * This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
21
+ * @example
22
+ * ```javascript
23
+ * const loader = new GLTFLoader();
24
+ * loader.register(new NEEDLE_progressive());
25
+ * loader.load("model.glb", (gltf) => {
26
+ * const mesh = gltf.scene.children[0] as Mesh;
27
+ * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
28
+ * console.log("Mesh with LOD level 1 loaded", mesh);
29
+ * });
30
+ * });
31
+ * ```
32
+ */
33
+ export class NEEDLE_progressive {
34
+ /** The name of the extension */
35
+ get name() {
36
+ return EXTENSION_NAME;
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
+ */
46
+ static getMeshLODExtension(geo) {
47
+ const info = this.getAssignedLODInformation(geo);
48
+ if (info?.key) {
49
+ return this.lodInfos.get(info.key);
50
+ }
51
+ return null;
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
+ */
60
+ static getPrimitiveIndex(geo) {
61
+ const index = this.getAssignedLODInformation(geo)?.index;
62
+ if (index === undefined || index === null)
63
+ return -1;
64
+ return index;
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
+ */
75
+ static getMaterialMinMaxLODsCount(material, minmax) {
76
+ const self = this;
77
+ // we can cache this material min max data because it wont change at runtime
78
+ const cacheKey = "LODS:minmax";
79
+ const cached = material[cacheKey];
80
+ if (cached != undefined)
81
+ return cached;
82
+ if (!minmax) {
83
+ minmax = {
84
+ min_count: Infinity,
85
+ max_count: 0,
86
+ lods: [],
87
+ };
88
+ }
89
+ if (Array.isArray(material)) {
90
+ for (const mat of material) {
91
+ this.getMaterialMinMaxLODsCount(mat, minmax);
92
+ }
93
+ material[cacheKey] = minmax;
94
+ return minmax;
95
+ }
96
+ if (debug === "verbose")
97
+ console.log("getMaterialMinMaxLODsCount", material);
98
+ if (material.type === "ShaderMaterial" || material.type === "RawShaderMaterial") {
99
+ const mat = material;
100
+ for (const slot of Object.keys(mat.uniforms)) {
101
+ const val = mat.uniforms[slot].value;
102
+ if (val?.isTexture === true) {
103
+ processTexture(val, minmax);
104
+ }
105
+ }
106
+ }
107
+ else if (material.isMaterial) {
108
+ for (const slot of Object.keys(material)) {
109
+ const val = material[slot];
110
+ if (val?.isTexture === true) {
111
+ processTexture(val, minmax);
112
+ }
113
+ }
114
+ }
115
+ else {
116
+ if (debug)
117
+ console.warn(`[getMaterialMinMaxLODsCount] Unsupported material type: ${material.type}`);
118
+ }
119
+ material[cacheKey] = minmax;
120
+ return minmax;
121
+ function processTexture(tex, minmax) {
122
+ const info = self.getAssignedLODInformation(tex);
123
+ if (info) {
124
+ const model = self.lodInfos.get(info.key);
125
+ if (model && model.lods) {
126
+ minmax.min_count = Math.min(minmax.min_count, model.lods.length);
127
+ minmax.max_count = Math.max(minmax.max_count, model.lods.length);
128
+ for (let i = 0; i < model.lods.length; i++) {
129
+ const lod = model.lods[i];
130
+ if (lod.width) {
131
+ minmax.lods[i] = minmax.lods[i] || { min_height: Infinity, max_height: 0 };
132
+ minmax.lods[i].min_height = Math.min(minmax.lods[i].min_height, lod.height);
133
+ minmax.lods[i].max_height = Math.max(minmax.lods[i].max_height, lod.height);
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ /** Check if a LOD level is available for a mesh or a texture
141
+ * @param obj the mesh or texture to check
142
+ * @param level the level of detail to check for (0 is the highest resolution). If undefined, the function checks if any LOD level is available
143
+ * @returns true if the LOD level is available (or if any LOD level is available if level is undefined)
144
+ */
145
+ static hasLODLevelAvailable(obj, level) {
146
+ if (Array.isArray(obj)) {
147
+ for (const mat of obj) {
148
+ if (this.hasLODLevelAvailable(mat, level))
149
+ return true;
150
+ }
151
+ return false;
152
+ }
153
+ if (obj.isMaterial === true) {
154
+ for (const slot of Object.keys(obj)) {
155
+ const val = obj[slot];
156
+ if (val && val.isTexture) {
157
+ if (this.hasLODLevelAvailable(val, level))
158
+ return true;
159
+ }
160
+ }
161
+ return false;
162
+ }
163
+ else if (obj.isGroup === true) {
164
+ for (const child of obj.children) {
165
+ if (child.isMesh === true) {
166
+ if (this.hasLODLevelAvailable(child, level))
167
+ return true;
168
+ }
169
+ }
170
+ }
171
+ let lodObject;
172
+ let lodInformation;
173
+ if (obj.isMesh) {
174
+ lodObject = obj.geometry;
175
+ }
176
+ else if (obj.isBufferGeometry) {
177
+ lodObject = obj;
178
+ }
179
+ else if (obj.isTexture) {
180
+ lodObject = obj;
181
+ }
182
+ if (lodObject) {
183
+ if (lodObject?.userData?.LODS) {
184
+ const lods = lodObject.userData.LODS;
185
+ lodInformation = this.lodInfos.get(lods.key);
186
+ if (level === undefined)
187
+ return lodInformation != undefined;
188
+ if (lodInformation) {
189
+ if (Array.isArray(lodInformation.lods)) {
190
+ return level < lodInformation.lods.length;
191
+ }
192
+ return level === 0;
193
+ }
194
+ }
195
+ }
196
+ return false;
197
+ }
198
+ /** Load a different resolution of a mesh (if available)
199
+ * @param context the context
200
+ * @param source the sourceid of the file from which the mesh is loaded (this is usually the component's sourceId)
201
+ * @param mesh the mesh to load the LOD for
202
+ * @param level the level of detail to load (0 is the highest resolution)
203
+ * @returns a promise that resolves to the mesh with the requested LOD level
204
+ * @example
205
+ * ```javascript
206
+ * const mesh = this.gameObject as Mesh;
207
+ * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
208
+ * console.log("Mesh with LOD level 1 loaded", mesh);
209
+ * });
210
+ * ```
211
+ */
212
+ static assignMeshLOD(mesh, level, options) {
213
+ if (!mesh)
214
+ return Promise.resolve(null);
215
+ if (mesh instanceof Mesh || mesh.isMesh === true) {
216
+ const currentGeometry = mesh.geometry;
217
+ const lodinfo = this.getAssignedLODInformation(currentGeometry);
218
+ if (!lodinfo) {
219
+ return Promise.resolve(null);
220
+ }
221
+ for (const plugin of plugins) {
222
+ plugin.onBeforeGetLODMesh?.(mesh, level);
223
+ }
224
+ // const info = this.onProgressiveLoadStart(context, source, mesh, null);
225
+ mesh["LOD:requested level"] = level;
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 => {
230
+ if (Array.isArray(geo)) {
231
+ const index = lodinfo.index || 0;
232
+ geo = geo[index];
233
+ }
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
+ }
239
+ if (geo && currentGeometry != geo) {
240
+ const isGeometry = geo?.isBufferGeometry;
241
+ // if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
242
+ if (!isGeometry) {
243
+ if (debug) {
244
+ console.error("Invalid LOD geometry", geo);
245
+ }
246
+ }
247
+ else if (typeof options?.apply === "function") {
248
+ options.apply(geo, level, mesh);
249
+ }
250
+ else if (options?.apply !== false) {
251
+ mesh.geometry = geo;
252
+ }
253
+ }
254
+ }
255
+ // this.onProgressiveLoadEnd(info);
256
+ return geo;
257
+ }).catch(err => {
258
+ // this.onProgressiveLoadEnd(info);
259
+ console.error("Error loading mesh LOD", mesh, err);
260
+ return null;
261
+ });
262
+ }
263
+ else if (debug) {
264
+ console.error("Invalid call to assignMeshLOD: Request mesh LOD but the object is not a mesh", mesh);
265
+ }
266
+ return Promise.resolve(null);
267
+ }
268
+ static assignTextureLOD(materialOrTexture, level = 0, options) {
269
+ if (!materialOrTexture)
270
+ return Promise.resolve(null);
271
+ const force = options?.force === true;
272
+ if (materialOrTexture.isMesh === true) {
273
+ const mesh = materialOrTexture;
274
+ if (Array.isArray(mesh.material)) {
275
+ const arr = new Array();
276
+ for (const mat of mesh.material) {
277
+ const promise = this.assignTextureLOD(mat, level, options);
278
+ arr.push(promise);
279
+ }
280
+ return Promise.all(arr).then(res => {
281
+ const textures = new Array();
282
+ for (const tex of res) {
283
+ if (Array.isArray(tex)) {
284
+ textures.push(...tex);
285
+ }
286
+ }
287
+ return textures;
288
+ });
289
+ }
290
+ else {
291
+ return this.assignTextureLOD(mesh.material, level, options);
292
+ }
293
+ }
294
+ if (materialOrTexture.isMaterial === true) {
295
+ const material = materialOrTexture;
296
+ const promises = [];
297
+ const slots = new Array();
298
+ this.trackCurrentMaterialTextureSlots(material);
299
+ // Handle custom shaders / uniforms progressive textures. This includes support for VRM shaders
300
+ if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
301
+ // iterate uniforms of custom shaders
302
+ const shaderMaterial = material;
303
+ for (const slot of Object.keys(shaderMaterial.uniforms)) {
304
+ const val = shaderMaterial.uniforms[slot].value;
305
+ if (val?.isTexture === true) {
306
+ const task = this.assignTextureLODForSlot(val, level, material, slot, force).then(res => {
307
+ if (res && shaderMaterial.uniforms[slot].value != res) {
308
+ shaderMaterial.uniforms[slot].value = res;
309
+ shaderMaterial.uniformsNeedUpdate = true;
310
+ }
311
+ return res;
312
+ });
313
+ promises.push(task);
314
+ slots.push(slot);
315
+ }
316
+ }
317
+ }
318
+ else {
319
+ for (const slot of Object.keys(material)) {
320
+ const val = material[slot];
321
+ if (val?.isTexture === true) {
322
+ const task = this.assignTextureLODForSlot(val, level, material, slot, force);
323
+ promises.push(task);
324
+ slots.push(slot);
325
+ }
326
+ }
327
+ }
328
+ return Promise.all(promises).then(res => {
329
+ const textures = new Array();
330
+ for (let i = 0; i < res.length; i++) {
331
+ const tex = res[i];
332
+ const slot = slots[i];
333
+ if (tex && tex.isTexture === true) {
334
+ textures.push({ material, slot, texture: tex, level });
335
+ }
336
+ else {
337
+ textures.push({ material, slot, texture: null, level });
338
+ }
339
+ }
340
+ return textures;
341
+ });
342
+ }
343
+ if (materialOrTexture instanceof Texture || materialOrTexture.isTexture === true) {
344
+ const texture = materialOrTexture;
345
+ return this.assignTextureLODForSlot(texture, level, null, null, force);
346
+ }
347
+ return Promise.resolve(null);
348
+ }
349
+ /**
350
+ * 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.
351
+ * @default 50 on desktop, 20 on mobile devices
352
+ */
353
+ static set maxConcurrentLoadingTasks(value) {
354
+ NEEDLE_progressive.queue.maxConcurrent = value;
355
+ }
356
+ static get maxConcurrentLoadingTasks() {
357
+ return NEEDLE_progressive.queue.maxConcurrent;
358
+ }
359
+ // #region INTERNAL
360
+ static assignTextureLODForSlot(current, level, material, slot, force) {
361
+ if (current?.isTexture !== true) {
362
+ return Promise.resolve(null);
363
+ }
364
+ if (slot === "glyphMap") {
365
+ return Promise.resolve(current);
366
+ }
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;
390
+ // this can currently not happen
391
+ if (Array.isArray(tex)) {
392
+ console.warn("Progressive: Got an array of textures for a texture slot, this should not happen...");
393
+ return null;
394
+ }
395
+ if (tex?.isTexture === true) {
396
+ if (tex != current) {
397
+ if (material && slot) {
398
+ const assigned = this.getMaterialTextureSlot(material, slot) ?? current;
399
+ // Check if the assigned texture LOD is higher quality than the current LOD
400
+ // This is necessary for cases where e.g. a texture is updated via an explicit call to assignTextureLOD
401
+ if (assigned && !force) {
402
+ const assignedLOD = this.getAssignedLODInformation(assigned);
403
+ if (assignedLOD && assignedLOD?.level < level) {
404
+ if (debug === "verbose")
405
+ console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
406
+ return null;
407
+ }
408
+ // assigned.dispose();
409
+ }
410
+ this.assignTrackedTextureSlot(material, slot, tex);
411
+ }
412
+ // Note: We use reference counting above to track texture usage across multiple materials.
413
+ // When the reference count hits zero, GPU memory (VRAM) is freed immediately via gl.deleteTexture(),
414
+ // not waiting for JavaScript garbage collection which may take seconds/minutes.
415
+ // This handles cases where the same texture is shared across multiple materials/objects.
416
+ }
417
+ // this.onProgressiveLoadEnd(info);
418
+ return tex;
419
+ }
420
+ else if (debug == "verbose") {
421
+ console.warn("No LOD found for", current, level);
422
+ }
423
+ // this.onProgressiveLoadEnd(info);
424
+ return null;
425
+ }).catch(err => {
426
+ // this.onProgressiveLoadEnd(info);
427
+ console.error("Error loading LOD", current, err);
428
+ return null;
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
+ }
590
+ }
591
+ parser;
592
+ url;
593
+ constructor(parser) {
594
+ const url = parser.options.path;
595
+ if (debug)
596
+ console.log("Progressive extension registered for", url);
597
+ this.parser = parser;
598
+ this.url = url;
599
+ }
600
+ _isLoadingMesh;
601
+ loadMesh = (meshIndex) => {
602
+ if (this._isLoadingMesh)
603
+ return null;
604
+ const ext = this.parser.json.meshes[meshIndex]?.extensions?.[EXTENSION_NAME];
605
+ if (!ext)
606
+ return null;
607
+ this._isLoadingMesh = true;
608
+ return this.parser.getDependency("mesh", meshIndex).then(mesh => {
609
+ this._isLoadingMesh = false;
610
+ if (mesh) {
611
+ NEEDLE_progressive.registerMesh(this.url, ext.guid, mesh, ext.lods?.length, 0, ext);
612
+ }
613
+ return mesh;
614
+ });
615
+ };
616
+ // private _isLoadingTexture;
617
+ // loadTexture = (textureIndex: number) => {
618
+ // if (this._isLoadingTexture) return null;
619
+ // const ext = this.parser.json.textures[textureIndex]?.extensions?.[EXTENSION_NAME] as NEEDLE_ext_progressive_texture;
620
+ // if (!ext) return null;
621
+ // this._isLoadingTexture = true;
622
+ // return this.parser.getDependency("texture", textureIndex).then(tex => {
623
+ // this._isLoadingTexture = false;
624
+ // if (tex) {
625
+ // NEEDLE_progressive.registerTexture(this.url, tex as Texture, ext.lods?.length, textureIndex, ext);
626
+ // }
627
+ // return tex;
628
+ // });
629
+ // }
630
+ afterRoot(gltf) {
631
+ if (debug)
632
+ console.log("AFTER", this.url, gltf);
633
+ this.parser.json.textures?.forEach((textureInfo, index) => {
634
+ if (textureInfo?.extensions) {
635
+ const ext = textureInfo?.extensions[EXTENSION_NAME];
636
+ if (ext) {
637
+ if (!ext.lods) {
638
+ if (debug)
639
+ console.warn("Texture has no LODs", ext);
640
+ return;
641
+ }
642
+ let found = false;
643
+ for (const key of this.parser.associations.keys()) {
644
+ if (key.isTexture === true) {
645
+ const val = this.parser.associations.get(key);
646
+ if (val?.textures === index) {
647
+ found = true;
648
+ NEEDLE_progressive.registerTexture(this.url, key, ext.lods?.length, index, ext);
649
+ }
650
+ }
651
+ }
652
+ // If textures aren't used there are no associations - we still want to register the LOD info so we create one instance
653
+ if (!found) {
654
+ this.parser.getDependency("texture", index).then(tex => {
655
+ if (tex) {
656
+ NEEDLE_progressive.registerTexture(this.url, tex, ext.lods?.length, index, ext);
657
+ }
658
+ });
659
+ }
660
+ }
661
+ }
662
+ });
663
+ this.parser.json.meshes?.forEach((meshInfo, index) => {
664
+ if (meshInfo?.extensions) {
665
+ const ext = meshInfo?.extensions[EXTENSION_NAME];
666
+ if (ext && ext.lods) {
667
+ let found = false;
668
+ for (const entry of this.parser.associations.keys()) {
669
+ if (entry.isMesh) {
670
+ const val = this.parser.associations.get(entry);
671
+ if (val?.meshes === index) {
672
+ found = true;
673
+ NEEDLE_progressive.registerMesh(this.url, ext.guid, entry, ext.lods.length, val.primitives, ext);
674
+ }
675
+ }
676
+ }
677
+ // Note: we use loadMesh rather than this method so the mesh is surely registered at the right time when the mesh is created
678
+ // // If meshes aren't used there are no associations - we still want to register the LOD info so we create one instance
679
+ // if (!found) {
680
+ // this.parser.getDependency("mesh", index).then(mesh => {
681
+ // if (mesh) {
682
+ // NEEDLE_progressive.registerMesh(this.url, ext.guid, mesh as Mesh, ext.lods.length, undefined, ext);
683
+ // }
684
+ // });
685
+ // }
686
+ }
687
+ }
688
+ });
689
+ return null;
690
+ }
691
+ /**
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.
701
+ */
702
+ static registerTexture = (url, tex, level, index, ext) => {
703
+ if (!tex) {
704
+ if (debug)
705
+ console.error("!! gltf-progressive: Called register texture without texture");
706
+ return;
707
+ }
708
+ if (debug) {
709
+ const { width, height } = getTextureDimensions(tex);
710
+ console.log(`> gltf-progressive: register texture[${index}] "${tex.name || tex.uuid}", Current: ${width}x${height}, Max: ${ext.lods[0]?.width}x${ext.lods[0]?.height}, uuid: ${tex.uuid}`, ext, tex);
711
+ }
712
+ // Put the extension info into the source (seems like tiled textures are cloned and the userdata etc is not properly copied BUT the source of course is not cloned)
713
+ // see https://github.com/needle-tools/needle-engine-support/issues/133
714
+ if (tex.source)
715
+ tex.source[$progressiveTextureExtension] = ext;
716
+ const key = ext.guid;
717
+ NEEDLE_progressive.assignLODInformation(url, tex, key, level, index);
718
+ NEEDLE_progressive.lodInfos.set(key, ext);
719
+ NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
720
+ };
721
+ /**
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.
733
+ */
734
+ static registerMesh = (url, key, mesh, level, index, ext) => {
735
+ const geometry = mesh.geometry;
736
+ if (!geometry) {
737
+ if (debug)
738
+ console.warn("gltf-progressive: Register mesh without geometry");
739
+ return;
740
+ }
741
+ if (!geometry.userData)
742
+ geometry.userData = {};
743
+ if (debug)
744
+ console.log("> Progressive: register mesh " + mesh.name, { index, uuid: mesh.uuid }, ext, mesh);
745
+ NEEDLE_progressive.assignLODInformation(url, geometry, key, level, index);
746
+ NEEDLE_progressive.lodInfos.set(key, ext);
747
+ const existingRef = NEEDLE_progressive.lowresCache.get(key);
748
+ let existing = existingRef?.deref();
749
+ if (existing) {
750
+ existing.push(mesh.geometry);
751
+ }
752
+ else {
753
+ existing = [mesh.geometry];
754
+ }
755
+ NEEDLE_progressive.lowresCache.set(key, new WeakRef(existing));
756
+ if (level > 0 && !getRaycastMesh(mesh)) {
757
+ registerRaycastMesh(mesh, geometry);
758
+ }
759
+ for (const plugin of plugins) {
760
+ plugin.onRegisteredNewMesh?.(mesh, ext);
761
+ }
762
+ };
763
+ /**
764
+ * Dispose cached resources to free memory.
765
+ * Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
766
+ * Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
767
+ * Also clears reference counts for disposed textures.
768
+ * @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
769
+ */
770
+ static dispose(guid) {
771
+ if (guid) {
772
+ this.lodInfos.delete(guid);
773
+ // Dispose lowres cache entries (original proxy resources)
774
+ const lowresRef = this.lowresCache.get(guid);
775
+ if (lowresRef) {
776
+ const lowres = lowresRef.deref();
777
+ if (lowres) {
778
+ if (lowres.isTexture) {
779
+ const tex = lowres;
780
+ this.textureRefCounts.delete(tex.uuid); // Clear ref count
781
+ tex.dispose();
782
+ }
783
+ else if (Array.isArray(lowres)) {
784
+ for (const geo of lowres)
785
+ geo.dispose();
786
+ }
787
+ }
788
+ this.lowresCache.delete(guid);
789
+ }
790
+ // Dispose previously loaded LOD entries
791
+ for (const [key, entry] of this.cache) {
792
+ if (key.includes(guid)) {
793
+ this._disposeCacheEntry(entry);
794
+ this.cache.delete(key);
795
+ }
796
+ }
797
+ }
798
+ else {
799
+ this.lodInfos.clear();
800
+ for (const [, entryRef] of this.lowresCache) {
801
+ const entry = entryRef.deref();
802
+ if (entry) {
803
+ if (entry.isTexture) {
804
+ const tex = entry;
805
+ this.textureRefCounts.delete(tex.uuid); // Clear ref count
806
+ tex.dispose();
807
+ }
808
+ else if (Array.isArray(entry)) {
809
+ for (const geo of entry)
810
+ geo.dispose();
811
+ }
812
+ }
813
+ }
814
+ this.lowresCache.clear();
815
+ for (const [, entry] of this.cache) {
816
+ this._disposeCacheEntry(entry);
817
+ }
818
+ this.cache.clear();
819
+ // Clear all texture reference counts when disposing everything
820
+ this.textureRefCounts.clear();
821
+ this.trackedTextureSlots = new WeakMap();
822
+ this.pendingTextureSlotRequests = new WeakMap();
823
+ this.latestTextureSlotRequests = new WeakMap();
824
+ this.textureSlotRequestId = 0;
825
+ }
826
+ }
827
+ /** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
828
+ static _disposeCacheEntry(entry) {
829
+ if (entry instanceof WeakRef) {
830
+ // Single resource — deref and dispose if still alive
831
+ const resource = entry.deref();
832
+ if (resource) {
833
+ // Clear ref count for textures
834
+ if (resource.isTexture) {
835
+ this.textureRefCounts.delete(resource.uuid);
836
+ }
837
+ resource.dispose();
838
+ }
839
+ }
840
+ else {
841
+ // Promise — may be in-flight or already resolved.
842
+ // Attach disposal to run after resolution.
843
+ entry.then(resource => {
844
+ if (resource) {
845
+ if (Array.isArray(resource)) {
846
+ for (const geo of resource)
847
+ geo.dispose();
848
+ }
849
+ else {
850
+ // Clear ref count for textures
851
+ if (resource.isTexture) {
852
+ this.textureRefCounts.delete(resource.uuid);
853
+ }
854
+ resource.dispose();
855
+ }
856
+ }
857
+ }).catch(() => { });
858
+ }
859
+ }
860
+ /** A map of key = asset uuid and value = LOD information */
861
+ static lodInfos = new Map();
862
+ /** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
863
+ static cache = new Map();
864
+ /** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
865
+ static lowresCache = new Map();
866
+ /** Reference counting for textures to track usage across multiple materials/objects */
867
+ static textureRefCounts = new Map();
868
+ /**
869
+ * FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
870
+ * when their associated three.js resources are garbage collected by the browser.
871
+ * The held value is the cache key string used in `previouslyLoaded`.
872
+ */
873
+ static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
874
+ const entry = NEEDLE_progressive.cache.get(cacheKey);
875
+ if (debug || debugGC)
876
+ console.debug(`[gltf-progressive] Memory: Resource GC'd\n${cacheKey}`);
877
+ // Only delete if the entry is still a WeakRef and the resource is gone
878
+ if (entry instanceof WeakRef) {
879
+ const derefed = entry.deref();
880
+ if (!derefed) {
881
+ NEEDLE_progressive.cache.delete(cacheKey);
882
+ if (debug || debugGC)
883
+ console.log(`[gltf-progressive] ↪ Cache entry deleted (GC)`);
884
+ }
885
+ }
886
+ });
887
+ /**
888
+ * Track texture usage by incrementing reference count
889
+ */
890
+ static trackTextureUsage(texture) {
891
+ const uuid = texture.uuid;
892
+ const count = this.textureRefCounts.get(uuid) || 0;
893
+ this.textureRefCounts.set(uuid, count + 1);
894
+ if (debug === "verbose") {
895
+ console.log(`[gltf-progressive] Track texture ${uuid}, refCount: ${count} → ${count + 1}`);
896
+ }
897
+ }
898
+ /**
899
+ * Untrack texture usage by decrementing reference count.
900
+ * Automatically disposes the texture when reference count reaches zero.
901
+ * @returns true if the texture was disposed, false otherwise
902
+ */
903
+ static untrackTextureUsage(texture) {
904
+ const uuid = texture.uuid;
905
+ const count = this.textureRefCounts.get(uuid);
906
+ if (!count) {
907
+ // Texture wasn't tracked, dispose immediately (safe fallback)
908
+ if (debug === "verbose" || debugGC) {
909
+ logDebugInfo(`[gltf-progressive] Memory: Untrack untracked texture (dispose immediately)`, 0);
910
+ }
911
+ texture.dispose();
912
+ return true;
913
+ }
914
+ const newCount = count - 1;
915
+ if (newCount <= 0) {
916
+ this.textureRefCounts.delete(uuid);
917
+ if (debug || debugGC) {
918
+ logDebugInfo(`[gltf-progressive] Memory: Dispose texture`, newCount);
919
+ }
920
+ texture.dispose();
921
+ return true;
922
+ }
923
+ else {
924
+ this.textureRefCounts.set(uuid, newCount);
925
+ if (debug === "verbose") {
926
+ logDebugInfo(`[gltf-progressive] Memory: Untrack texture`, newCount);
927
+ }
928
+ return false;
929
+ }
930
+ function logDebugInfo(prefix, newCount) {
931
+ let { width, height } = getTextureDimensions(texture);
932
+ const textureSize = width && height ? `${width}x${height}` : "N/A";
933
+ let memorySize = "N/A";
934
+ if (width && height) {
935
+ memorySize = `~${(determineTextureMemoryInBytes(texture) / (1024 * 1024)).toFixed(2)} MB`;
936
+ }
937
+ console.log(`${prefix} — ${texture.name} ${textureSize} (${memorySize}), refCount: ${count} → ${newCount}\n${uuid}`);
938
+ }
939
+ }
940
+ static workers = [];
941
+ static _workersIndex = 0;
942
+ static async getOrLoadLOD(current, level, options) {
943
+ const debugverbose = debug == "verbose";
944
+ /** this key is used to lookup the LOD information */
945
+ const LOD = this.getAssignedLODInformation(current);
946
+ if (!LOD) {
947
+ if (debug)
948
+ console.warn(`[gltf-progressive] No LOD information found: ${current.name}, uuid: ${current.uuid}, type: ${current.type}`, current);
949
+ return null;
950
+ }
951
+ const LODKEY = LOD?.key;
952
+ let lodInfo;
953
+ const isTextureRequest = current.isTexture === true;
954
+ // See https://github.com/needle-tools/needle-engine-support/issues/133
955
+ if (isTextureRequest) {
956
+ const tex = current;
957
+ if (tex.source && tex.source[$progressiveTextureExtension])
958
+ lodInfo = tex.source[$progressiveTextureExtension];
959
+ }
960
+ if (!lodInfo)
961
+ lodInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
962
+ if (!lodInfo) {
963
+ if (debug)
964
+ console.warn(`Can not load LOD ${level}: no LOD info found for \"${LODKEY}\" ${current.name}`, current.type, NEEDLE_progressive.lodInfos);
965
+ }
966
+ else {
967
+ if (level > 0) {
968
+ let useLowRes = false;
969
+ const hasMultipleLevels = Array.isArray(lodInfo.lods);
970
+ if (hasMultipleLevels && level >= lodInfo.lods.length) {
971
+ useLowRes = true;
972
+ }
973
+ else if (!hasMultipleLevels) {
974
+ useLowRes = true;
975
+ }
976
+ if (useLowRes) {
977
+ const lowresRef = this.lowresCache.get(LODKEY);
978
+ if (lowresRef) {
979
+ const lowres = lowresRef.deref();
980
+ if (lowres)
981
+ return lowres;
982
+ // Resource was GC'd, remove stale entry
983
+ this.lowresCache.delete(LODKEY);
984
+ if (debug)
985
+ console.log(`[gltf-progressive] Lowres cache entry was GC'd: ${LODKEY}`);
986
+ }
987
+ // Fallback to current if lowres was GC'd
988
+ return null;
989
+ }
990
+ }
991
+ /** the unresolved LOD url */
992
+ const unresolved_lod_url = Array.isArray(lodInfo.lods) ? lodInfo.lods[level]?.path : lodInfo.lods;
993
+ // check if we have a uri
994
+ if (!unresolved_lod_url) {
995
+ if (debug && !lodInfo["missing:uri"]) {
996
+ lodInfo["missing:uri"] = true;
997
+ console.warn("Missing uri for progressive asset for LOD " + level, lodInfo);
998
+ }
999
+ return null;
1000
+ }
1001
+ /** the resolved LOD url */
1002
+ const lod_url = resolveUrl(LOD.url, unresolved_lod_url);
1003
+ // check if the requested file needs to be loaded via a GLTFLoader
1004
+ if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
1005
+ if (!lodInfo.guid) {
1006
+ console.warn("missing pointer for glb/gltf texture", lodInfo);
1007
+ return null;
1008
+ }
1009
+ // check if the requested file has already been loaded
1010
+ const KEY = lod_url + "_" + lodInfo.guid;
1011
+ // check if the requested file is currently being loaded or was previously loaded
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) {
1016
+ if (debugverbose)
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;
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;
1031
+ // #region loading
1032
+ if (!slot.use) {
1033
+ if (debug)
1034
+ console.log(`LOD ${level} was aborted: ${lod_url}`);
1035
+ return null; // the request was aborted, we don't load it again
1036
+ }
1037
+ const ext = lodInfo;
1038
+ const request = new Promise(async (resolve, _) => {
1039
+ // const useWorker = true;
1040
+ if (useWorker) {
1041
+ const worker = await getWorker({});
1042
+ const res = await worker.load(lod_url);
1043
+ if (res.textures.length > 0) {
1044
+ // const textures = new Array<Texture>();
1045
+ for (const entry of res.textures) {
1046
+ let texture = entry.texture;
1047
+ NEEDLE_progressive.assignLODInformation(LOD.url, texture, LODKEY, level, undefined);
1048
+ if (current instanceof Texture) {
1049
+ texture = this.copySettings(current, texture);
1050
+ }
1051
+ if (texture)
1052
+ texture.guid = ext.guid;
1053
+ // textures.push(texture);
1054
+ return resolve(texture);
1055
+ }
1056
+ // if (textures.length > 0) {
1057
+ // return resolve(textures);
1058
+ // }
1059
+ }
1060
+ if (res.geometries.length > 0) {
1061
+ const geometries = new Array();
1062
+ for (const entry of res.geometries) {
1063
+ const newGeo = entry.geometry;
1064
+ NEEDLE_progressive.assignLODInformation(LOD.url, newGeo, LODKEY, level, entry.primitiveIndex);
1065
+ geometries.push(newGeo);
1066
+ }
1067
+ return resolve(geometries);
1068
+ }
1069
+ return resolve(null);
1070
+ }
1071
+ // Old loading
1072
+ const loader = new GLTFLoader();
1073
+ addDracoAndKTX2Loaders(loader);
1074
+ if (debug) {
1075
+ await new Promise(resolve => setTimeout(resolve, 1000));
1076
+ if (debugverbose)
1077
+ console.warn("Start loading (delayed) " + lod_url, ext.guid);
1078
+ }
1079
+ let url = lod_url;
1080
+ if (ext && Array.isArray(ext.lods)) {
1081
+ const lodinfo = ext.lods[level];
1082
+ if (lodinfo.hash) {
1083
+ url += "?v=" + lodinfo.hash;
1084
+ }
1085
+ }
1086
+ const gltf = await loader.loadAsync(url).catch(err => {
1087
+ console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
1088
+ return resolve(null);
1089
+ });
1090
+ if (!gltf) {
1091
+ return resolve(null);
1092
+ }
1093
+ const parser = gltf.parser;
1094
+ if (debugverbose)
1095
+ console.log("Loading finished " + lod_url, ext.guid);
1096
+ let index = 0;
1097
+ if (gltf.parser.json.textures) {
1098
+ let found = false;
1099
+ for (const tex of gltf.parser.json.textures) {
1100
+ // find the texture index
1101
+ if (tex?.extensions) {
1102
+ const other = tex?.extensions[EXTENSION_NAME];
1103
+ if (other?.guid) {
1104
+ if (other.guid === ext.guid) {
1105
+ found = true;
1106
+ break;
1107
+ }
1108
+ }
1109
+ }
1110
+ index++;
1111
+ }
1112
+ if (found) {
1113
+ let tex = await parser.getDependency("texture", index);
1114
+ if (tex) {
1115
+ NEEDLE_progressive.assignLODInformation(LOD.url, tex, LODKEY, level, undefined);
1116
+ }
1117
+ if (debugverbose)
1118
+ console.log("change \"" + current.name + "\" → \"" + tex.name + "\"", lod_url, index, tex, KEY);
1119
+ if (current instanceof Texture)
1120
+ tex = this.copySettings(current, tex);
1121
+ if (tex) {
1122
+ tex.guid = ext.guid;
1123
+ }
1124
+ return resolve(tex);
1125
+ }
1126
+ else if (debug) {
1127
+ console.warn("Could not find texture with guid", ext.guid, gltf.parser.json);
1128
+ }
1129
+ }
1130
+ index = 0;
1131
+ if (gltf.parser.json.meshes) {
1132
+ let found = false;
1133
+ for (const mesh of gltf.parser.json.meshes) {
1134
+ // find the mesh index
1135
+ if (mesh?.extensions) {
1136
+ const other = mesh?.extensions[EXTENSION_NAME];
1137
+ if (other?.guid) {
1138
+ if (other.guid === ext.guid) {
1139
+ found = true;
1140
+ break;
1141
+ }
1142
+ }
1143
+ }
1144
+ index++;
1145
+ }
1146
+ if (found) {
1147
+ const mesh = await parser.getDependency("mesh", index);
1148
+ if (debugverbose)
1149
+ console.log(`Loaded Mesh \"${mesh.name}\"`, lod_url, index, mesh, KEY);
1150
+ if (mesh.isMesh === true) {
1151
+ const geo = mesh.geometry;
1152
+ NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, 0);
1153
+ return resolve(geo);
1154
+ }
1155
+ else {
1156
+ const geometries = new Array();
1157
+ for (let i = 0; i < mesh.children.length; i++) {
1158
+ const child = mesh.children[i];
1159
+ if (child.isMesh === true) {
1160
+ const geo = child.geometry;
1161
+ NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, i);
1162
+ geometries.push(geo);
1163
+ }
1164
+ }
1165
+ return resolve(geometries);
1166
+ }
1167
+ }
1168
+ else if (debug) {
1169
+ console.warn("Could not find mesh with guid", ext.guid, gltf.parser.json);
1170
+ }
1171
+ }
1172
+ // we could not find a texture or mesh with the given guid
1173
+ return resolve(null);
1174
+ });
1175
+ this.cache.set(KEY, request);
1176
+ slot.use(request);
1177
+ const res = await request;
1178
+ // Optimize cache entry: replace loading promise with lightweight reference.
1179
+ // This releases closure variables captured during the loading function.
1180
+ if (res != null) {
1181
+ if (res instanceof Texture) {
1182
+ // For Texture resources, use WeakRef to allow garbage collection.
1183
+ // The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
1184
+ this.cache.set(KEY, new WeakRef(res));
1185
+ NEEDLE_progressive._resourceRegistry.register(res, KEY);
1186
+ }
1187
+ else if (Array.isArray(res)) {
1188
+ // For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
1189
+ // This keeps geometries in memory as they should not be GC'd (mesh LODs stay cached).
1190
+ this.cache.set(KEY, Promise.resolve(res));
1191
+ }
1192
+ else {
1193
+ // For single BufferGeometry, keep in memory (don't use WeakRef)
1194
+ this.cache.set(KEY, Promise.resolve(res));
1195
+ }
1196
+ }
1197
+ else {
1198
+ // Failed load — replace with clean resolved promise to release loading closure.
1199
+ // Keeping the entry prevents retrying (existing behavior).
1200
+ this.cache.set(KEY, Promise.resolve(null));
1201
+ }
1202
+ return res;
1203
+ }
1204
+ else {
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
+ }
1211
+ if (debugverbose)
1212
+ console.log("Load texture from uri: " + lod_url);
1213
+ const loader = new TextureLoader();
1214
+ const tex = await loader.loadAsync(lod_url);
1215
+ if (options?.isCurrent?.() === false) {
1216
+ tex?.dispose();
1217
+ return null;
1218
+ }
1219
+ if (tex) {
1220
+ tex.guid = lodInfo.guid;
1221
+ tex.flipY = false;
1222
+ tex.needsUpdate = true;
1223
+ tex.colorSpace = current.colorSpace;
1224
+ if (debugverbose)
1225
+ console.log(lodInfo, tex);
1226
+ }
1227
+ else if (debug)
1228
+ console.warn("failed loading", lod_url);
1229
+ return tex;
1230
+ }
1231
+ }
1232
+ }
1233
+ return null;
1234
+ }
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 }); }
1298
+ static assignLODInformation(url, res, key, level, index) {
1299
+ if (!res)
1300
+ return;
1301
+ if (!res.userData)
1302
+ res.userData = {};
1303
+ const info = new LODInformation(url, key, level, index);
1304
+ res.userData.LODS = info;
1305
+ if ("source" in res && typeof res.source === "object")
1306
+ res.source.LODS = info; // for tiled textures
1307
+ }
1308
+ static getAssignedLODInformation(res) {
1309
+ if (!res)
1310
+ return null;
1311
+ if (res.userData?.LODS)
1312
+ return res.userData.LODS;
1313
+ if ("source" in res && res.source?.LODS)
1314
+ return res.source.LODS;
1315
+ return null;
1316
+ }
1317
+ // private static readonly _copiedTextures: WeakMap<Texture, Texture> = new Map();
1318
+ static copySettings(source, target) {
1319
+ if (!target) {
1320
+ return source;
1321
+ }
1322
+ // const existingCopy = source["LODS:COPY"];
1323
+ // don't copy again if the texture was processed before
1324
+ // we clone the source if it's animated
1325
+ // const existingClone = this._copiedTextures.get(source);
1326
+ // if (existingClone) {
1327
+ // return existingClone;
1328
+ // }
1329
+ // We need to clone e.g. when the same texture is used multiple times (but with e.g. different wrap settings)
1330
+ // This is relatively cheap since it only stores settings
1331
+ {
1332
+ if (debug === "verbose")
1333
+ console.debug("Copy texture settings\n", source.uuid, "\n", target.uuid);
1334
+ target = target.clone();
1335
+ }
1336
+ // else {
1337
+ // source = existingCopy;
1338
+ // }
1339
+ // this._copiedTextures.set(original, target);
1340
+ // we re-use the offset and repeat settings because it might be animated
1341
+ target.offset = source.offset;
1342
+ target.repeat = source.repeat;
1343
+ target.colorSpace = source.colorSpace;
1344
+ target.magFilter = source.magFilter;
1345
+ target.minFilter = source.minFilter;
1346
+ target.wrapS = source.wrapS;
1347
+ target.wrapT = source.wrapT;
1348
+ target.flipY = source.flipY;
1349
+ target.anisotropy = source.anisotropy;
1350
+ if (!target.mipmaps)
1351
+ target.generateMipmaps = source.generateMipmaps;
1352
+ // if (!target.userData) target.userData = {};
1353
+ // target["LODS:COPY"] = source;
1354
+ // related: NE-4937
1355
+ return target;
1356
+ }
1357
+ }
1358
+ class LODInformation {
1359
+ url;
1360
+ /** the key to lookup the LOD information */
1361
+ key;
1362
+ level;
1363
+ /** For multi objects (e.g. a group of meshes) this is the index of the object */
1364
+ index;
1365
+ constructor(url, key, level, index) {
1366
+ this.url = url;
1367
+ this.key = key;
1368
+ this.level = level;
1369
+ if (index != undefined)
1370
+ this.index = index;
1371
+ }
1372
+ }
1373
+ ;