@needle-tools/gltf-progressive 3.4.0-rc → 4.0.0-alpha

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/lib/extension.js DELETED
@@ -1,1041 +0,0 @@
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 { getParam, 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 $progressiveTextureExtension = Symbol("needle-progressive-texture");
15
- export const EXTENSION_NAME = "NEEDLE_progressive";
16
- // #region EXT
17
- /**
18
- * The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
19
- * This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
20
- * @example
21
- * ```javascript
22
- * const loader = new GLTFLoader();
23
- * loader.register(new NEEDLE_progressive());
24
- * loader.load("model.glb", (gltf) => {
25
- * const mesh = gltf.scene.children[0] as Mesh;
26
- * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
27
- * console.log("Mesh with LOD level 1 loaded", mesh);
28
- * });
29
- * });
30
- * ```
31
- */
32
- export class NEEDLE_progressive {
33
- /** The name of the extension */
34
- get name() {
35
- return EXTENSION_NAME;
36
- }
37
- // #region PUBLIC API
38
- static getMeshLODExtension(geo) {
39
- const info = this.getAssignedLODInformation(geo);
40
- if (info?.key) {
41
- return this.lodInfos.get(info.key);
42
- }
43
- return null;
44
- }
45
- static getPrimitiveIndex(geo) {
46
- const index = this.getAssignedLODInformation(geo)?.index;
47
- if (index === undefined || index === null)
48
- return -1;
49
- return index;
50
- }
51
- static getMaterialMinMaxLODsCount(material, minmax) {
52
- const self = this;
53
- // we can cache this material min max data because it wont change at runtime
54
- const cacheKey = "LODS:minmax";
55
- const cached = material[cacheKey];
56
- if (cached != undefined)
57
- return cached;
58
- if (!minmax) {
59
- minmax = {
60
- min_count: Infinity,
61
- max_count: 0,
62
- lods: [],
63
- };
64
- }
65
- if (Array.isArray(material)) {
66
- for (const mat of material) {
67
- this.getMaterialMinMaxLODsCount(mat, minmax);
68
- }
69
- material[cacheKey] = minmax;
70
- return minmax;
71
- }
72
- if (debug === "verbose")
73
- console.log("getMaterialMinMaxLODsCount", material);
74
- if (material.type === "ShaderMaterial" || material.type === "RawShaderMaterial") {
75
- const mat = material;
76
- for (const slot of Object.keys(mat.uniforms)) {
77
- const val = mat.uniforms[slot].value;
78
- if (val?.isTexture === true) {
79
- processTexture(val, minmax);
80
- }
81
- }
82
- }
83
- else if (material.isMaterial) {
84
- for (const slot of Object.keys(material)) {
85
- const val = material[slot];
86
- if (val?.isTexture === true) {
87
- processTexture(val, minmax);
88
- }
89
- }
90
- }
91
- else {
92
- if (debug)
93
- console.warn(`[getMaterialMinMaxLODsCount] Unsupported material type: ${material.type}`);
94
- }
95
- material[cacheKey] = minmax;
96
- return minmax;
97
- function processTexture(tex, minmax) {
98
- const info = self.getAssignedLODInformation(tex);
99
- if (info) {
100
- const model = self.lodInfos.get(info.key);
101
- if (model && model.lods) {
102
- minmax.min_count = Math.min(minmax.min_count, model.lods.length);
103
- minmax.max_count = Math.max(minmax.max_count, model.lods.length);
104
- for (let i = 0; i < model.lods.length; i++) {
105
- const lod = model.lods[i];
106
- if (lod.width) {
107
- minmax.lods[i] = minmax.lods[i] || { min_height: Infinity, max_height: 0 };
108
- minmax.lods[i].min_height = Math.min(minmax.lods[i].min_height, lod.height);
109
- minmax.lods[i].max_height = Math.max(minmax.lods[i].max_height, lod.height);
110
- }
111
- }
112
- }
113
- }
114
- }
115
- }
116
- /** Check if a LOD level is available for a mesh or a texture
117
- * @param obj the mesh or texture to check
118
- * @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
119
- * @returns true if the LOD level is available (or if any LOD level is available if level is undefined)
120
- */
121
- static hasLODLevelAvailable(obj, level) {
122
- if (Array.isArray(obj)) {
123
- for (const mat of obj) {
124
- if (this.hasLODLevelAvailable(mat, level))
125
- return true;
126
- }
127
- return false;
128
- }
129
- if (obj.isMaterial === true) {
130
- for (const slot of Object.keys(obj)) {
131
- const val = obj[slot];
132
- if (val && val.isTexture) {
133
- if (this.hasLODLevelAvailable(val, level))
134
- return true;
135
- }
136
- }
137
- return false;
138
- }
139
- else if (obj.isGroup === true) {
140
- for (const child of obj.children) {
141
- if (child.isMesh === true) {
142
- if (this.hasLODLevelAvailable(child, level))
143
- return true;
144
- }
145
- }
146
- }
147
- let lodObject;
148
- let lodInformation;
149
- if (obj.isMesh) {
150
- lodObject = obj.geometry;
151
- }
152
- else if (obj.isBufferGeometry) {
153
- lodObject = obj;
154
- }
155
- else if (obj.isTexture) {
156
- lodObject = obj;
157
- }
158
- if (lodObject) {
159
- if (lodObject?.userData?.LODS) {
160
- const lods = lodObject.userData.LODS;
161
- lodInformation = this.lodInfos.get(lods.key);
162
- if (level === undefined)
163
- return lodInformation != undefined;
164
- if (lodInformation) {
165
- if (Array.isArray(lodInformation.lods)) {
166
- return level < lodInformation.lods.length;
167
- }
168
- return level === 0;
169
- }
170
- }
171
- }
172
- return false;
173
- }
174
- /** Load a different resolution of a mesh (if available)
175
- * @param context the context
176
- * @param source the sourceid of the file from which the mesh is loaded (this is usually the component's sourceId)
177
- * @param mesh the mesh to load the LOD for
178
- * @param level the level of detail to load (0 is the highest resolution)
179
- * @returns a promise that resolves to the mesh with the requested LOD level
180
- * @example
181
- * ```javascript
182
- * const mesh = this.gameObject as Mesh;
183
- * NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
184
- * console.log("Mesh with LOD level 1 loaded", mesh);
185
- * });
186
- * ```
187
- */
188
- static assignMeshLOD(mesh, level) {
189
- if (!mesh)
190
- return Promise.resolve(null);
191
- if (mesh instanceof Mesh || mesh.isMesh === true) {
192
- const currentGeometry = mesh.geometry;
193
- const lodinfo = this.getAssignedLODInformation(currentGeometry);
194
- if (!lodinfo) {
195
- return Promise.resolve(null);
196
- }
197
- for (const plugin of plugins) {
198
- plugin.onBeforeGetLODMesh?.(mesh, level);
199
- }
200
- // const info = this.onProgressiveLoadStart(context, source, mesh, null);
201
- mesh["LOD:requested level"] = level;
202
- return NEEDLE_progressive.getOrLoadLOD(currentGeometry, level).then(geo => {
203
- if (Array.isArray(geo)) {
204
- const index = lodinfo.index || 0;
205
- geo = geo[index];
206
- }
207
- if (mesh["LOD:requested level"] === level) {
208
- delete mesh["LOD:requested level"];
209
- if (geo && currentGeometry != geo) {
210
- const isGeometry = geo?.isBufferGeometry;
211
- // if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
212
- if (isGeometry) {
213
- mesh.geometry = geo;
214
- }
215
- else if (debug) {
216
- console.error("Invalid LOD geometry", geo);
217
- }
218
- }
219
- }
220
- // this.onProgressiveLoadEnd(info);
221
- return geo;
222
- }).catch(err => {
223
- // this.onProgressiveLoadEnd(info);
224
- console.error("Error loading mesh LOD", mesh, err);
225
- return null;
226
- });
227
- }
228
- else if (debug) {
229
- console.error("Invalid call to assignMeshLOD: Request mesh LOD but the object is not a mesh", mesh);
230
- }
231
- return Promise.resolve(null);
232
- }
233
- static assignTextureLOD(materialOrTexture, level = 0) {
234
- if (!materialOrTexture)
235
- return Promise.resolve(null);
236
- if (materialOrTexture.isMesh === true) {
237
- const mesh = materialOrTexture;
238
- if (Array.isArray(mesh.material)) {
239
- const arr = new Array();
240
- for (const mat of mesh.material) {
241
- const promise = this.assignTextureLOD(mat, level);
242
- arr.push(promise);
243
- }
244
- return Promise.all(arr).then(res => {
245
- const textures = new Array();
246
- for (const tex of res) {
247
- if (Array.isArray(tex)) {
248
- textures.push(...tex);
249
- }
250
- }
251
- return textures;
252
- });
253
- }
254
- else {
255
- return this.assignTextureLOD(mesh.material, level);
256
- }
257
- }
258
- if (materialOrTexture.isMaterial === true) {
259
- const material = materialOrTexture;
260
- const promises = [];
261
- const slots = new Array();
262
- // Handle custom shaders / uniforms progressive textures. This includes support for VRM shaders
263
- if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
264
- // iterate uniforms of custom shaders
265
- const shaderMaterial = material;
266
- for (const slot of Object.keys(shaderMaterial.uniforms)) {
267
- const val = shaderMaterial.uniforms[slot].value;
268
- if (val?.isTexture === true) {
269
- const task = this.assignTextureLODForSlot(val, level, material, slot).then(res => {
270
- if (res && shaderMaterial.uniforms[slot].value != res) {
271
- shaderMaterial.uniforms[slot].value = res;
272
- shaderMaterial.uniformsNeedUpdate = true;
273
- }
274
- return res;
275
- });
276
- promises.push(task);
277
- slots.push(slot);
278
- }
279
- }
280
- }
281
- else {
282
- for (const slot of Object.keys(material)) {
283
- const val = material[slot];
284
- if (val?.isTexture === true) {
285
- const task = this.assignTextureLODForSlot(val, level, material, slot);
286
- promises.push(task);
287
- slots.push(slot);
288
- }
289
- }
290
- }
291
- return Promise.all(promises).then(res => {
292
- const textures = new Array();
293
- for (let i = 0; i < res.length; i++) {
294
- const tex = res[i];
295
- const slot = slots[i];
296
- if (tex && tex.isTexture === true) {
297
- textures.push({ material, slot, texture: tex, level });
298
- }
299
- else {
300
- textures.push({ material, slot, texture: null, level });
301
- }
302
- }
303
- return textures;
304
- });
305
- }
306
- if (materialOrTexture instanceof Texture || materialOrTexture.isTexture === true) {
307
- const texture = materialOrTexture;
308
- return this.assignTextureLODForSlot(texture, level, null, null);
309
- }
310
- return Promise.resolve(null);
311
- }
312
- // #region INTERNAL
313
- static assignTextureLODForSlot(current, level, material, slot) {
314
- if (current?.isTexture !== true) {
315
- return Promise.resolve(null);
316
- }
317
- if (slot === "glyphMap") {
318
- return Promise.resolve(current);
319
- }
320
- return NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
321
- // this can currently not happen
322
- if (Array.isArray(tex)) {
323
- console.warn("Progressive: Got an array of textures for a texture slot, this should not happen...");
324
- return null;
325
- }
326
- if (tex?.isTexture === true) {
327
- if (tex != current) {
328
- if (material && slot) {
329
- const assigned = material[slot];
330
- // Check if the assigned texture LOD is higher quality than the current LOD
331
- // This is necessary for cases where e.g. a texture is updated via an explicit call to assignTextureLOD
332
- if (assigned && !debug) {
333
- const assignedLOD = this.getAssignedLODInformation(assigned);
334
- if (assignedLOD && assignedLOD?.level < level) {
335
- if (debug === "verbose")
336
- console.warn("Assigned texture level is already higher: ", assignedLOD.level, level, material, assigned, tex);
337
- return null;
338
- }
339
- // assigned.dispose();
340
- }
341
- // Since we're switching LOD level for the texture based on distance we can avoid uploading all the mipmaps
342
- if (reduceMipmaps && tex.mipmaps) {
343
- const prevCount = tex.mipmaps.length;
344
- tex.mipmaps.length = Math.min(tex.mipmaps.length, 3);
345
- if (prevCount !== tex.mipmaps.length) {
346
- if (debug)
347
- console.debug(`Reduced mipmap count from ${prevCount} to ${tex.mipmaps.length} for ${tex.uuid}: ${tex.image?.width}x${tex.image?.height}.`);
348
- }
349
- }
350
- material[slot] = tex;
351
- }
352
- // check if the old texture is still used by other objects
353
- // if not we dispose it...
354
- // this could also be handled elsewhere and not be done immediately
355
- // const users = getResourceUserCount(current);
356
- // if (!users) {
357
- // if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
358
- // current?.dispose();
359
- // }
360
- }
361
- // this.onProgressiveLoadEnd(info);
362
- return tex;
363
- }
364
- else if (debug == "verbose") {
365
- console.warn("No LOD found for", current, level);
366
- }
367
- // this.onProgressiveLoadEnd(info);
368
- return null;
369
- }).catch(err => {
370
- // this.onProgressiveLoadEnd(info);
371
- console.error("Error loading LOD", current, err);
372
- return null;
373
- });
374
- }
375
- parser;
376
- url;
377
- constructor(parser) {
378
- const url = parser.options.path;
379
- if (debug)
380
- console.log("Progressive extension registered for", url);
381
- this.parser = parser;
382
- this.url = url;
383
- }
384
- _isLoadingMesh;
385
- loadMesh = (meshIndex) => {
386
- if (this._isLoadingMesh)
387
- return null;
388
- const ext = this.parser.json.meshes[meshIndex]?.extensions?.[EXTENSION_NAME];
389
- if (!ext)
390
- return null;
391
- this._isLoadingMesh = true;
392
- return this.parser.getDependency("mesh", meshIndex).then(mesh => {
393
- this._isLoadingMesh = false;
394
- if (mesh) {
395
- NEEDLE_progressive.registerMesh(this.url, ext.guid, mesh, ext.lods?.length, 0, ext);
396
- }
397
- return mesh;
398
- });
399
- };
400
- // private _isLoadingTexture;
401
- // loadTexture = (textureIndex: number) => {
402
- // if (this._isLoadingTexture) return null;
403
- // const ext = this.parser.json.textures[textureIndex]?.extensions?.[EXTENSION_NAME] as NEEDLE_ext_progressive_texture;
404
- // if (!ext) return null;
405
- // this._isLoadingTexture = true;
406
- // return this.parser.getDependency("texture", textureIndex).then(tex => {
407
- // this._isLoadingTexture = false;
408
- // if (tex) {
409
- // NEEDLE_progressive.registerTexture(this.url, tex as Texture, ext.lods?.length, textureIndex, ext);
410
- // }
411
- // return tex;
412
- // });
413
- // }
414
- afterRoot(gltf) {
415
- if (debug)
416
- console.log("AFTER", this.url, gltf);
417
- this.parser.json.textures?.forEach((textureInfo, index) => {
418
- if (textureInfo?.extensions) {
419
- const ext = textureInfo?.extensions[EXTENSION_NAME];
420
- if (ext) {
421
- if (!ext.lods) {
422
- if (debug)
423
- console.warn("Texture has no LODs", ext);
424
- return;
425
- }
426
- let found = false;
427
- for (const key of this.parser.associations.keys()) {
428
- if (key.isTexture === true) {
429
- const val = this.parser.associations.get(key);
430
- if (val?.textures === index) {
431
- found = true;
432
- NEEDLE_progressive.registerTexture(this.url, key, ext.lods?.length, index, ext);
433
- }
434
- }
435
- }
436
- // If textures aren't used there are no associations - we still want to register the LOD info so we create one instance
437
- if (!found) {
438
- this.parser.getDependency("texture", index).then(tex => {
439
- if (tex) {
440
- NEEDLE_progressive.registerTexture(this.url, tex, ext.lods?.length, index, ext);
441
- }
442
- });
443
- }
444
- }
445
- }
446
- });
447
- this.parser.json.meshes?.forEach((meshInfo, index) => {
448
- if (meshInfo?.extensions) {
449
- const ext = meshInfo?.extensions[EXTENSION_NAME];
450
- if (ext && ext.lods) {
451
- let found = false;
452
- for (const entry of this.parser.associations.keys()) {
453
- if (entry.isMesh) {
454
- const val = this.parser.associations.get(entry);
455
- if (val?.meshes === index) {
456
- found = true;
457
- NEEDLE_progressive.registerMesh(this.url, ext.guid, entry, ext.lods.length, val.primitives, ext);
458
- }
459
- }
460
- }
461
- // Note: we use loadMesh rather than this method so the mesh is surely registered at the right time when the mesh is created
462
- // // If meshes aren't used there are no associations - we still want to register the LOD info so we create one instance
463
- // if (!found) {
464
- // this.parser.getDependency("mesh", index).then(mesh => {
465
- // if (mesh) {
466
- // NEEDLE_progressive.registerMesh(this.url, ext.guid, mesh as Mesh, ext.lods.length, undefined, ext);
467
- // }
468
- // });
469
- // }
470
- }
471
- }
472
- });
473
- return null;
474
- }
475
- /**
476
- * Register a texture with LOD information
477
- */
478
- static registerTexture = (url, tex, level, index, ext) => {
479
- if (!tex) {
480
- if (debug)
481
- console.error("!! gltf-progressive: Called register texture without texture");
482
- return;
483
- }
484
- if (debug) {
485
- const width = tex.image?.width || tex.source?.data?.width || 0;
486
- const height = tex.image?.height || tex.source?.data?.height || 0;
487
- console.log(`> gltf-progressive: register texture[${index}] "${tex.name || tex.uuid}", Current: ${width}x${height}, Max: ${ext.lods[0]?.width}x${ext.lods[0]?.height}, uuid: ${tex.uuid}`, ext, tex);
488
- }
489
- // 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)
490
- // see https://github.com/needle-tools/needle-engine-support/issues/133
491
- if (tex.source)
492
- tex.source[$progressiveTextureExtension] = ext;
493
- const key = ext.guid;
494
- NEEDLE_progressive.assignLODInformation(url, tex, key, level, index);
495
- NEEDLE_progressive.lodInfos.set(key, ext);
496
- NEEDLE_progressive.lowresCache.set(key, new WeakRef(tex));
497
- };
498
- /**
499
- * Register a mesh with LOD information
500
- */
501
- static registerMesh = (url, key, mesh, level, index, ext) => {
502
- const geometry = mesh.geometry;
503
- if (!geometry) {
504
- if (debug)
505
- console.warn("gltf-progressive: Register mesh without geometry");
506
- return;
507
- }
508
- if (!geometry.userData)
509
- geometry.userData = {};
510
- if (debug)
511
- console.log("> Progressive: register mesh " + mesh.name, { index, uuid: mesh.uuid }, ext, mesh);
512
- NEEDLE_progressive.assignLODInformation(url, geometry, key, level, index);
513
- NEEDLE_progressive.lodInfos.set(key, ext);
514
- const existingRef = NEEDLE_progressive.lowresCache.get(key);
515
- let existing = existingRef?.deref();
516
- if (existing) {
517
- existing.push(mesh.geometry);
518
- }
519
- else {
520
- existing = [mesh.geometry];
521
- }
522
- NEEDLE_progressive.lowresCache.set(key, new WeakRef(existing));
523
- if (level > 0 && !getRaycastMesh(mesh)) {
524
- registerRaycastMesh(mesh, geometry);
525
- }
526
- for (const plugin of plugins) {
527
- plugin.onRegisteredNewMesh?.(mesh, ext);
528
- }
529
- };
530
- /**
531
- * Dispose cached resources to free memory.
532
- * Call this when a model is removed from the scene to allow garbage collection of its LOD resources.
533
- * Calls three.js `.dispose()` on cached Textures and BufferGeometries to free GPU memory.
534
- * @param guid Optional GUID to dispose resources for a specific model. If omitted, all cached resources are cleared.
535
- */
536
- static dispose(guid) {
537
- if (guid) {
538
- this.lodInfos.delete(guid);
539
- // Dispose lowres cache entries (original proxy resources)
540
- const lowresRef = this.lowresCache.get(guid);
541
- if (lowresRef) {
542
- const lowres = lowresRef.deref();
543
- if (lowres) {
544
- if (lowres.isTexture) {
545
- lowres.dispose();
546
- }
547
- else if (Array.isArray(lowres)) {
548
- for (const geo of lowres)
549
- geo.dispose();
550
- }
551
- }
552
- this.lowresCache.delete(guid);
553
- }
554
- // Dispose previously loaded LOD entries
555
- for (const [key, entry] of this.previouslyLoaded) {
556
- if (key.includes(guid)) {
557
- this._disposeCacheEntry(entry);
558
- this.previouslyLoaded.delete(key);
559
- }
560
- }
561
- }
562
- else {
563
- this.lodInfos.clear();
564
- for (const [, entryRef] of this.lowresCache) {
565
- const entry = entryRef.deref();
566
- if (entry) {
567
- if (entry.isTexture) {
568
- entry.dispose();
569
- }
570
- else if (Array.isArray(entry)) {
571
- for (const geo of entry)
572
- geo.dispose();
573
- }
574
- }
575
- }
576
- this.lowresCache.clear();
577
- for (const [, entry] of this.previouslyLoaded) {
578
- this._disposeCacheEntry(entry);
579
- }
580
- this.previouslyLoaded.clear();
581
- }
582
- }
583
- /** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
584
- static _disposeCacheEntry(entry) {
585
- if (entry instanceof WeakRef) {
586
- // Single resource — deref and dispose if still alive
587
- const resource = entry.deref();
588
- resource?.dispose();
589
- }
590
- else {
591
- // Promise — may be in-flight or already resolved.
592
- // Attach disposal to run after resolution.
593
- entry.then(resource => {
594
- if (resource) {
595
- if (Array.isArray(resource)) {
596
- for (const geo of resource)
597
- geo.dispose();
598
- }
599
- else {
600
- resource.dispose();
601
- }
602
- }
603
- }).catch(() => { });
604
- }
605
- }
606
- /** A map of key = asset uuid and value = LOD information */
607
- static lodInfos = new Map();
608
- /** cache of already loaded mesh lods. Uses WeakRef for single resources to allow garbage collection when unused. */
609
- static previouslyLoaded = new Map();
610
- /** this contains the geometry/textures that were originally loaded. Uses WeakRef to allow garbage collection when unused. */
611
- static lowresCache = new Map();
612
- /**
613
- * FinalizationRegistry to automatically clean up `previouslyLoaded` cache entries
614
- * when their associated three.js resources are garbage collected by the browser.
615
- * The held value is the cache key string used in `previouslyLoaded`.
616
- */
617
- static _resourceRegistry = new FinalizationRegistry((cacheKey) => {
618
- const entry = NEEDLE_progressive.previouslyLoaded.get(cacheKey);
619
- console.debug(`[gltf-progressive] FinalizationRegistry cleanup: Resource GC'd for ${cacheKey}.`);
620
- // Only delete if the entry is still a WeakRef and the resource is gone
621
- if (entry instanceof WeakRef) {
622
- const derefed = entry.deref();
623
- if (!derefed) {
624
- NEEDLE_progressive.previouslyLoaded.delete(cacheKey);
625
- if (debug)
626
- console.log(`[gltf-progressive] Cache entry auto-cleaned (GC'd): ${cacheKey}`);
627
- }
628
- }
629
- });
630
- static workers = [];
631
- static _workersIndex = 0;
632
- static async getOrLoadLOD(current, level) {
633
- const debugverbose = debug == "verbose";
634
- /** this key is used to lookup the LOD information */
635
- const LOD = this.getAssignedLODInformation(current);
636
- if (!LOD) {
637
- if (debug)
638
- console.warn(`[gltf-progressive] No LOD information found: ${current.name}, uuid: ${current.uuid}, type: ${current.type}`, current);
639
- return null;
640
- }
641
- const LODKEY = LOD?.key;
642
- let lodInfo;
643
- const isTextureRequest = current.isTexture === true;
644
- // See https://github.com/needle-tools/needle-engine-support/issues/133
645
- if (isTextureRequest) {
646
- const tex = current;
647
- if (tex.source && tex.source[$progressiveTextureExtension])
648
- lodInfo = tex.source[$progressiveTextureExtension];
649
- }
650
- if (!lodInfo)
651
- lodInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
652
- if (!lodInfo) {
653
- if (debug)
654
- console.warn(`Can not load LOD ${level}: no LOD info found for \"${LODKEY}\" ${current.name}`, current.type, NEEDLE_progressive.lodInfos);
655
- }
656
- else {
657
- if (level > 0) {
658
- let useLowRes = false;
659
- const hasMultipleLevels = Array.isArray(lodInfo.lods);
660
- if (hasMultipleLevels && level >= lodInfo.lods.length) {
661
- useLowRes = true;
662
- }
663
- else if (!hasMultipleLevels) {
664
- useLowRes = true;
665
- }
666
- if (useLowRes) {
667
- const lowresRef = this.lowresCache.get(LODKEY);
668
- if (lowresRef) {
669
- const lowres = lowresRef.deref();
670
- if (lowres)
671
- return lowres;
672
- // Resource was GC'd, remove stale entry
673
- this.lowresCache.delete(LODKEY);
674
- if (debug)
675
- console.log(`[gltf-progressive] Lowres cache entry was GC'd: ${LODKEY}`);
676
- }
677
- // Fallback to current if lowres was GC'd
678
- return null;
679
- }
680
- }
681
- /** the unresolved LOD url */
682
- const unresolved_lod_url = Array.isArray(lodInfo.lods) ? lodInfo.lods[level]?.path : lodInfo.lods;
683
- // check if we have a uri
684
- if (!unresolved_lod_url) {
685
- if (debug && !lodInfo["missing:uri"]) {
686
- lodInfo["missing:uri"] = true;
687
- console.warn("Missing uri for progressive asset for LOD " + level, lodInfo);
688
- }
689
- return null;
690
- }
691
- /** the resolved LOD url */
692
- const lod_url = resolveUrl(LOD.url, unresolved_lod_url);
693
- // check if the requested file needs to be loaded via a GLTFLoader
694
- if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
695
- if (!lodInfo.guid) {
696
- console.warn("missing pointer for glb/gltf texture", lodInfo);
697
- return null;
698
- }
699
- // check if the requested file has already been loaded
700
- const KEY = lod_url + "_" + lodInfo.guid;
701
- const slot = await this.queue.slot(lod_url);
702
- // check if the requested file is currently being loaded or was previously loaded
703
- const existing = this.previouslyLoaded.get(KEY);
704
- if (existing !== undefined) {
705
- if (debugverbose)
706
- console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
707
- if (existing instanceof WeakRef) {
708
- // Previously resolved resource — check if still alive in memory
709
- const derefed = existing.deref();
710
- if (derefed) {
711
- let res = derefed;
712
- let resourceIsDisposed = false;
713
- if (res instanceof Texture && current instanceof Texture) {
714
- if (res.image?.data || res.source?.data) {
715
- res = this.copySettings(current, res);
716
- }
717
- else {
718
- resourceIsDisposed = true;
719
- }
720
- }
721
- else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
722
- if (!res.attributes.position?.array) {
723
- resourceIsDisposed = true;
724
- }
725
- }
726
- if (!resourceIsDisposed) {
727
- return res;
728
- }
729
- }
730
- // Resource was garbage collected or disposed — remove stale entry and re-load
731
- this.previouslyLoaded.delete(KEY);
732
- if (debug)
733
- console.log(`[gltf-progressive] Re-loading GC'd/disposed resource: ${KEY}`);
734
- }
735
- else {
736
- // Promise — loading in progress or previously completed
737
- let res = await existing.catch(err => {
738
- console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
739
- return null;
740
- });
741
- let resouceIsDisposed = false;
742
- if (res == null) {
743
- // if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
744
- // in which case we don't attempt to load it again
745
- }
746
- else if (res instanceof Texture && current instanceof Texture) {
747
- // check if the texture has been disposed or not
748
- if (res.image?.data || res.source?.data) {
749
- res = this.copySettings(current, res);
750
- }
751
- // if it has been disposed we need to load it again
752
- else {
753
- resouceIsDisposed = true;
754
- this.previouslyLoaded.delete(KEY);
755
- }
756
- }
757
- else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
758
- if (res.attributes.position?.array) {
759
- // the geometry is OK
760
- }
761
- else {
762
- resouceIsDisposed = true;
763
- this.previouslyLoaded.delete(KEY);
764
- }
765
- }
766
- if (!resouceIsDisposed) {
767
- return res;
768
- }
769
- }
770
- }
771
- // #region loading
772
- if (!slot.use) {
773
- if (debug)
774
- console.log(`LOD ${level} was aborted: ${lod_url}`);
775
- return null; // the request was aborted, we don't load it again
776
- }
777
- const ext = lodInfo;
778
- const request = new Promise(async (resolve, _) => {
779
- // const useWorker = true;
780
- if (useWorker) {
781
- const worker = await getWorker({});
782
- const res = await worker.load(lod_url);
783
- if (res.textures.length > 0) {
784
- // const textures = new Array<Texture>();
785
- for (const entry of res.textures) {
786
- let texture = entry.texture;
787
- NEEDLE_progressive.assignLODInformation(LOD.url, texture, LODKEY, level, undefined);
788
- if (current instanceof Texture) {
789
- texture = this.copySettings(current, texture);
790
- }
791
- if (texture)
792
- texture.guid = ext.guid;
793
- // textures.push(texture);
794
- return resolve(texture);
795
- }
796
- // if (textures.length > 0) {
797
- // return resolve(textures);
798
- // }
799
- }
800
- if (res.geometries.length > 0) {
801
- const geometries = new Array();
802
- for (const entry of res.geometries) {
803
- const newGeo = entry.geometry;
804
- NEEDLE_progressive.assignLODInformation(LOD.url, newGeo, LODKEY, level, entry.primitiveIndex);
805
- geometries.push(newGeo);
806
- }
807
- return resolve(geometries);
808
- }
809
- return resolve(null);
810
- }
811
- // Old loading
812
- const loader = new GLTFLoader();
813
- addDracoAndKTX2Loaders(loader);
814
- if (debug) {
815
- await new Promise(resolve => setTimeout(resolve, 1000));
816
- if (debugverbose)
817
- console.warn("Start loading (delayed) " + lod_url, ext.guid);
818
- }
819
- let url = lod_url;
820
- if (ext && Array.isArray(ext.lods)) {
821
- const lodinfo = ext.lods[level];
822
- if (lodinfo.hash) {
823
- url += "?v=" + lodinfo.hash;
824
- }
825
- }
826
- const gltf = await loader.loadAsync(url).catch(err => {
827
- console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
828
- return resolve(null);
829
- });
830
- if (!gltf) {
831
- return resolve(null);
832
- }
833
- const parser = gltf.parser;
834
- if (debugverbose)
835
- console.log("Loading finished " + lod_url, ext.guid);
836
- let index = 0;
837
- if (gltf.parser.json.textures) {
838
- let found = false;
839
- for (const tex of gltf.parser.json.textures) {
840
- // find the texture index
841
- if (tex?.extensions) {
842
- const other = tex?.extensions[EXTENSION_NAME];
843
- if (other?.guid) {
844
- if (other.guid === ext.guid) {
845
- found = true;
846
- break;
847
- }
848
- }
849
- }
850
- index++;
851
- }
852
- if (found) {
853
- let tex = await parser.getDependency("texture", index);
854
- if (tex) {
855
- NEEDLE_progressive.assignLODInformation(LOD.url, tex, LODKEY, level, undefined);
856
- }
857
- if (debugverbose)
858
- console.log("change \"" + current.name + "\" → \"" + tex.name + "\"", lod_url, index, tex, KEY);
859
- if (current instanceof Texture)
860
- tex = this.copySettings(current, tex);
861
- if (tex) {
862
- tex.guid = ext.guid;
863
- }
864
- return resolve(tex);
865
- }
866
- else if (debug) {
867
- console.warn("Could not find texture with guid", ext.guid, gltf.parser.json);
868
- }
869
- }
870
- index = 0;
871
- if (gltf.parser.json.meshes) {
872
- let found = false;
873
- for (const mesh of gltf.parser.json.meshes) {
874
- // find the mesh index
875
- if (mesh?.extensions) {
876
- const other = mesh?.extensions[EXTENSION_NAME];
877
- if (other?.guid) {
878
- if (other.guid === ext.guid) {
879
- found = true;
880
- break;
881
- }
882
- }
883
- }
884
- index++;
885
- }
886
- if (found) {
887
- const mesh = await parser.getDependency("mesh", index);
888
- if (debugverbose)
889
- console.log(`Loaded Mesh \"${mesh.name}\"`, lod_url, index, mesh, KEY);
890
- if (mesh.isMesh === true) {
891
- const geo = mesh.geometry;
892
- NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, 0);
893
- return resolve(geo);
894
- }
895
- else {
896
- const geometries = new Array();
897
- for (let i = 0; i < mesh.children.length; i++) {
898
- const child = mesh.children[i];
899
- if (child.isMesh === true) {
900
- const geo = child.geometry;
901
- NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, i);
902
- geometries.push(geo);
903
- }
904
- }
905
- return resolve(geometries);
906
- }
907
- }
908
- else if (debug) {
909
- console.warn("Could not find mesh with guid", ext.guid, gltf.parser.json);
910
- }
911
- }
912
- // we could not find a texture or mesh with the given guid
913
- return resolve(null);
914
- });
915
- this.previouslyLoaded.set(KEY, request);
916
- slot.use(request);
917
- const res = await request;
918
- // Optimize cache entry: replace loading promise with lightweight reference.
919
- // This releases closure variables captured during the loading function.
920
- if (res != null) {
921
- if (Array.isArray(res)) {
922
- // For BufferGeometry[] (multi-primitive meshes), use a resolved promise.
923
- // WeakRef can't be used here because callers only extract individual elements
924
- // from the array, so the array object itself would be GC'd immediately.
925
- this.previouslyLoaded.set(KEY, Promise.resolve(res));
926
- }
927
- else {
928
- // For single resources (Texture or BufferGeometry), use WeakRef to allow
929
- // garbage collection when the resource is no longer referenced by the scene.
930
- // The FinalizationRegistry will auto-clean this entry when the resource is GC'd.
931
- this.previouslyLoaded.set(KEY, new WeakRef(res));
932
- NEEDLE_progressive._resourceRegistry.register(res, KEY);
933
- }
934
- }
935
- else {
936
- // Failed load — replace with clean resolved promise to release loading closure.
937
- // Keeping the entry prevents retrying (existing behavior).
938
- this.previouslyLoaded.set(KEY, Promise.resolve(null));
939
- }
940
- return res;
941
- }
942
- else {
943
- if (current instanceof Texture) {
944
- if (debugverbose)
945
- console.log("Load texture from uri: " + lod_url);
946
- const loader = new TextureLoader();
947
- const tex = await loader.loadAsync(lod_url);
948
- if (tex) {
949
- tex.guid = lodInfo.guid;
950
- tex.flipY = false;
951
- tex.needsUpdate = true;
952
- tex.colorSpace = current.colorSpace;
953
- if (debugverbose)
954
- console.log(lodInfo, tex);
955
- }
956
- else if (debug)
957
- console.warn("failed loading", lod_url);
958
- return tex;
959
- }
960
- }
961
- }
962
- return null;
963
- }
964
- static maxConcurrent = 50;
965
- static queue = new PromiseQueue(NEEDLE_progressive.maxConcurrent, { debug: debug != false });
966
- static assignLODInformation(url, res, key, level, index) {
967
- if (!res)
968
- return;
969
- if (!res.userData)
970
- res.userData = {};
971
- const info = new LODInformation(url, key, level, index);
972
- res.userData.LODS = info;
973
- if ("source" in res && typeof res.source === "object")
974
- res.source.LODS = info; // for tiled textures
975
- }
976
- static getAssignedLODInformation(res) {
977
- if (!res)
978
- return null;
979
- if (res.userData?.LODS)
980
- return res.userData.LODS;
981
- if ("source" in res && res.source?.LODS)
982
- return res.source.LODS;
983
- return null;
984
- }
985
- // private static readonly _copiedTextures: WeakMap<Texture, Texture> = new Map();
986
- static copySettings(source, target) {
987
- if (!target) {
988
- return source;
989
- }
990
- // const existingCopy = source["LODS:COPY"];
991
- // don't copy again if the texture was processed before
992
- // we clone the source if it's animated
993
- // const existingClone = this._copiedTextures.get(source);
994
- // if (existingClone) {
995
- // return existingClone;
996
- // }
997
- // We need to clone e.g. when the same texture is used multiple times (but with e.g. different wrap settings)
998
- // This is relatively cheap since it only stores settings
999
- {
1000
- if (debug === "verbose")
1001
- console.debug("Copy texture settings\n", source.uuid, "\n", target.uuid);
1002
- target = target.clone();
1003
- }
1004
- // else {
1005
- // source = existingCopy;
1006
- // }
1007
- // this._copiedTextures.set(original, target);
1008
- // we re-use the offset and repeat settings because it might be animated
1009
- target.offset = source.offset;
1010
- target.repeat = source.repeat;
1011
- target.colorSpace = source.colorSpace;
1012
- target.magFilter = source.magFilter;
1013
- target.minFilter = source.minFilter;
1014
- target.wrapS = source.wrapS;
1015
- target.wrapT = source.wrapT;
1016
- target.flipY = source.flipY;
1017
- target.anisotropy = source.anisotropy;
1018
- if (!target.mipmaps)
1019
- target.generateMipmaps = source.generateMipmaps;
1020
- // if (!target.userData) target.userData = {};
1021
- // target["LODS:COPY"] = source;
1022
- // related: NE-4937
1023
- return target;
1024
- }
1025
- }
1026
- class LODInformation {
1027
- url;
1028
- /** the key to lookup the LOD information */
1029
- key;
1030
- level;
1031
- /** For multi objects (e.g. a group of meshes) this is the index of the object */
1032
- index;
1033
- constructor(url, key, level, index) {
1034
- this.url = url;
1035
- this.key = key;
1036
- this.level = level;
1037
- if (index != undefined)
1038
- this.index = index;
1039
- }
1040
- }
1041
- ;