@needle-tools/gltf-progressive 1.0.0-alpha.3 → 1.0.0-alpha.4
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 +673 -0
- package/lib/extension.js.map +1 -0
- package/lib/index.js +22 -0
- package/lib/index.js.map +1 -0
- package/lib/loaders.js +36 -0
- package/lib/loaders.js.map +1 -0
- package/lib/lods_manager.js +337 -0
- package/lib/lods_manager.js.map +1 -0
- package/lib/plugins/index.js +3 -0
- package/lib/plugins/index.js.map +1 -0
- package/lib/plugins/modelviewer.js +103 -0
- package/lib/plugins/modelviewer.js.map +1 -0
- package/lib/plugins/plugin.js +5 -0
- package/lib/plugins/plugin.js.map +1 -0
- package/lib/utils.js +37 -0
- package/lib/utils.js.map +1 -0
- package/package.json +3 -2
- /package/{types → lib}/extension.d.ts +0 -0
- /package/{types → lib}/index.d.ts +0 -0
- /package/{types → lib}/loaders.d.ts +0 -0
- /package/{types → lib}/lods_manager.d.ts +0 -0
- /package/{types → lib}/plugins/index.d.ts +0 -0
- /package/{types → lib}/plugins/modelviewer.d.ts +0 -0
- /package/{types → lib}/plugins/plugin.d.ts +0 -0
- /package/{types → lib}/utils.d.ts +0 -0
package/lib/extension.js
ADDED
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { BufferGeometry, Material, Mesh, RawShaderMaterial, Texture, TextureLoader } from "three";
|
|
2
|
+
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
|
|
3
|
+
import { addDracoAndKTX2Loaders } from "./loaders.js";
|
|
4
|
+
import { getParam, resolveUrl } from "./utils.js";
|
|
5
|
+
// All of this has to be removed
|
|
6
|
+
// import { getRaycastMesh, setRaycastMesh } from "../../engine_physics.js";
|
|
7
|
+
// import { PromiseAllWithErrors, resolveUrl } from "../../engine_utils.js";
|
|
8
|
+
import { plugins } from "./plugins/plugin.js";
|
|
9
|
+
export const EXTENSION_NAME = "NEEDLE_progressive";
|
|
10
|
+
const debug = getParam("debugprogressive");
|
|
11
|
+
const $progressiveTextureExtension = Symbol("needle-progressive-texture");
|
|
12
|
+
const debug_toggle_maps = new Map();
|
|
13
|
+
const debug_materials = new Set();
|
|
14
|
+
if (debug) {
|
|
15
|
+
let currentDebugLodLevel = -1;
|
|
16
|
+
let maxLevel = 2;
|
|
17
|
+
let wireframe = false;
|
|
18
|
+
function debugToggleProgressive() {
|
|
19
|
+
currentDebugLodLevel += 1;
|
|
20
|
+
console.log("Toggle LOD level", currentDebugLodLevel, debug_toggle_maps);
|
|
21
|
+
debug_toggle_maps.forEach((arr, obj) => {
|
|
22
|
+
for (const key of arr.keys) {
|
|
23
|
+
const cur = obj[key];
|
|
24
|
+
if (cur.isBufferGeometry === true) {
|
|
25
|
+
const info = NEEDLE_progressive.getMeshLODInformation(cur);
|
|
26
|
+
const level = !info ? 0 : Math.min(currentDebugLodLevel, info.lods.length);
|
|
27
|
+
obj["DEBUG:LOD"] = level;
|
|
28
|
+
NEEDLE_progressive.assignMeshLOD(obj, level);
|
|
29
|
+
if (info)
|
|
30
|
+
maxLevel = Math.max(maxLevel, info.lods.length - 1);
|
|
31
|
+
}
|
|
32
|
+
else if (obj.isMaterial === true) {
|
|
33
|
+
obj["DEBUG:LOD"] = currentDebugLodLevel;
|
|
34
|
+
NEEDLE_progressive.assignTextureLOD(obj, currentDebugLodLevel);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
if (currentDebugLodLevel >= maxLevel) {
|
|
40
|
+
currentDebugLodLevel = -1;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
window.addEventListener("keyup", evt => {
|
|
44
|
+
if (evt.key === "p")
|
|
45
|
+
debugToggleProgressive();
|
|
46
|
+
if (evt.key === "w") {
|
|
47
|
+
wireframe = !wireframe;
|
|
48
|
+
if (debug_materials) {
|
|
49
|
+
debug_materials.forEach(mat => {
|
|
50
|
+
// we don't want to change the skybox material
|
|
51
|
+
if (mat.name == "BackgroundCubeMaterial")
|
|
52
|
+
return;
|
|
53
|
+
if ("wireframe" in mat)
|
|
54
|
+
mat.wireframe = wireframe;
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function registerDebug(obj, key, sourceId) {
|
|
61
|
+
if (!debug)
|
|
62
|
+
return;
|
|
63
|
+
if (!debug_toggle_maps.has(obj)) {
|
|
64
|
+
debug_toggle_maps.set(obj, { keys: [], sourceId });
|
|
65
|
+
}
|
|
66
|
+
const existing = debug_toggle_maps.get(obj);
|
|
67
|
+
if (existing?.keys?.includes(key) == false) {
|
|
68
|
+
existing.keys.push(key);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The NEEDLE_progressive extension for the GLTFLoader is responsible for loading progressive LODs for meshes and textures.
|
|
73
|
+
* This extension can be used to load different resolutions of a mesh or texture at runtime (e.g. for LODs or progressive textures).
|
|
74
|
+
* @example
|
|
75
|
+
* ```javascript
|
|
76
|
+
* const loader = new GLTFLoader();
|
|
77
|
+
* loader.register(new NEEDLE_progressive());
|
|
78
|
+
* loader.load("model.glb", (gltf) => {
|
|
79
|
+
* const mesh = gltf.scene.children[0] as Mesh;
|
|
80
|
+
* NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
|
|
81
|
+
* console.log("Mesh with LOD level 1 loaded", mesh);
|
|
82
|
+
* });
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
export class NEEDLE_progressive {
|
|
87
|
+
/** The name of the extension */
|
|
88
|
+
get name() {
|
|
89
|
+
return EXTENSION_NAME;
|
|
90
|
+
}
|
|
91
|
+
static getMeshLODInformation(geo) {
|
|
92
|
+
const info = this.getAssignedLODInformation(geo);
|
|
93
|
+
if (info?.key) {
|
|
94
|
+
return this.lodInfos.get(info.key);
|
|
95
|
+
}
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
/** Check if a LOD level is available for a mesh or a texture
|
|
99
|
+
* @param obj the mesh or texture to check
|
|
100
|
+
* @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
|
|
101
|
+
* @returns true if the LOD level is available (or if any LOD level is available if level is undefined)
|
|
102
|
+
*/
|
|
103
|
+
static hasLODLevelAvailable(obj, level) {
|
|
104
|
+
if (obj.isMaterial === true) {
|
|
105
|
+
for (const slot of Object.keys(obj)) {
|
|
106
|
+
const val = obj[slot];
|
|
107
|
+
if (val.isTexture) {
|
|
108
|
+
if (this.hasLODLevelAvailable(val, level))
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
else if (obj.isGroup === true) {
|
|
115
|
+
for (const child of obj.children) {
|
|
116
|
+
if (child.isMesh === true) {
|
|
117
|
+
if (this.hasLODLevelAvailable(child, level))
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
let lodObject;
|
|
123
|
+
let lodInformation;
|
|
124
|
+
if (obj.isMesh) {
|
|
125
|
+
lodObject = obj.geometry;
|
|
126
|
+
}
|
|
127
|
+
else if (obj.isBufferGeometry) {
|
|
128
|
+
lodObject = obj;
|
|
129
|
+
}
|
|
130
|
+
else if (obj.isTexture) {
|
|
131
|
+
lodObject = obj;
|
|
132
|
+
}
|
|
133
|
+
if (lodObject) {
|
|
134
|
+
if (lodObject?.userData?.LODS) {
|
|
135
|
+
const lods = lodObject.userData.LODS;
|
|
136
|
+
lodInformation = this.lodInfos.get(lods.key);
|
|
137
|
+
if (level === undefined)
|
|
138
|
+
return lodInformation != undefined;
|
|
139
|
+
if (lodInformation) {
|
|
140
|
+
if (Array.isArray(lodInformation.lods)) {
|
|
141
|
+
return level < lodInformation.lods.length;
|
|
142
|
+
}
|
|
143
|
+
return level === 0;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
/** Load a different resolution of a mesh (if available)
|
|
150
|
+
* @param context the context
|
|
151
|
+
* @param source the sourceid of the file from which the mesh is loaded (this is usually the component's sourceId)
|
|
152
|
+
* @param mesh the mesh to load the LOD for
|
|
153
|
+
* @param level the level of detail to load (0 is the highest resolution)
|
|
154
|
+
* @returns a promise that resolves to the mesh with the requested LOD level
|
|
155
|
+
* @example
|
|
156
|
+
* ```javascript
|
|
157
|
+
* const mesh = this.gameObject as Mesh;
|
|
158
|
+
* NEEDLE_progressive.assignMeshLOD(context, sourceId, mesh, 1).then(mesh => {
|
|
159
|
+
* console.log("Mesh with LOD level 1 loaded", mesh);
|
|
160
|
+
* });
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
static assignMeshLOD(mesh, level) {
|
|
164
|
+
if (!mesh)
|
|
165
|
+
return Promise.resolve(null);
|
|
166
|
+
if (mesh instanceof Mesh || mesh.isMesh === true) {
|
|
167
|
+
const currentGeometry = mesh.geometry;
|
|
168
|
+
const lodinfo = this.getAssignedLODInformation(currentGeometry);
|
|
169
|
+
if (!lodinfo) {
|
|
170
|
+
return Promise.resolve(null);
|
|
171
|
+
}
|
|
172
|
+
for (const plugin of plugins) {
|
|
173
|
+
plugin.onBeforeGetLODMesh?.(mesh, level);
|
|
174
|
+
}
|
|
175
|
+
// const info = this.onProgressiveLoadStart(context, source, mesh, null);
|
|
176
|
+
mesh["LOD:requested level"] = level;
|
|
177
|
+
return NEEDLE_progressive.getOrLoadLOD(currentGeometry, level).then(geo => {
|
|
178
|
+
if (mesh["LOD:requested level"] === level) {
|
|
179
|
+
delete mesh["LOD:requested level"];
|
|
180
|
+
if (Array.isArray(geo)) {
|
|
181
|
+
const index = lodinfo.index || 0;
|
|
182
|
+
geo = geo[index];
|
|
183
|
+
}
|
|
184
|
+
if (geo && currentGeometry != geo) {
|
|
185
|
+
// if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
|
|
186
|
+
if (geo instanceof BufferGeometry) {
|
|
187
|
+
mesh.geometry = geo;
|
|
188
|
+
if (debug)
|
|
189
|
+
registerDebug(mesh, "geometry", lodinfo.url);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// this.onProgressiveLoadEnd(info);
|
|
194
|
+
return geo;
|
|
195
|
+
}).catch(err => {
|
|
196
|
+
// this.onProgressiveLoadEnd(info);
|
|
197
|
+
console.error("Error loading mesh LOD", mesh, err);
|
|
198
|
+
return null;
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
else if (debug) {
|
|
202
|
+
console.error("Invalid call to assignMeshLOD: Request mesh LOD but the object is not a mesh", mesh);
|
|
203
|
+
}
|
|
204
|
+
return Promise.resolve(null);
|
|
205
|
+
}
|
|
206
|
+
/** Load a different resolution of a texture (if available)
|
|
207
|
+
* @param context the context
|
|
208
|
+
* @param source the sourceid of the file from which the texture is loaded (this is usually the component's sourceId)
|
|
209
|
+
* @param materialOrTexture the material or texture to load the LOD for (if passing in a material all textures in the material will be loaded)
|
|
210
|
+
* @param level the level of detail to load (0 is the highest resolution) - currently only 0 is supported
|
|
211
|
+
* @returns a promise that resolves to the material or texture with the requested LOD level
|
|
212
|
+
*/
|
|
213
|
+
static assignTextureLOD(materialOrTexture, level = 0) {
|
|
214
|
+
if (!materialOrTexture)
|
|
215
|
+
return Promise.resolve(null);
|
|
216
|
+
if (materialOrTexture instanceof Material || materialOrTexture.isMaterial === true) {
|
|
217
|
+
const material = materialOrTexture;
|
|
218
|
+
const promises = [];
|
|
219
|
+
const slots = new Array();
|
|
220
|
+
if (debug)
|
|
221
|
+
debug_materials.add(material);
|
|
222
|
+
if (material instanceof RawShaderMaterial) {
|
|
223
|
+
// iterate uniforms of custom shaders
|
|
224
|
+
for (const slot of Object.keys(material.uniforms)) {
|
|
225
|
+
const val = material.uniforms[slot].value;
|
|
226
|
+
if (val?.isTexture === true) {
|
|
227
|
+
const task = this.assignTextureLODForSlot(val, level, material, slot);
|
|
228
|
+
promises.push(task);
|
|
229
|
+
slots.push(slot);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else {
|
|
234
|
+
for (const slot of Object.keys(material)) {
|
|
235
|
+
const val = material[slot];
|
|
236
|
+
if (val?.isTexture === true) {
|
|
237
|
+
const task = this.assignTextureLODForSlot(val, level, material, slot);
|
|
238
|
+
promises.push(task);
|
|
239
|
+
slots.push(slot);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return Promise.all(promises).then(res => {
|
|
244
|
+
const textures = new Array();
|
|
245
|
+
for (let i = 0; i < res.length; i++) {
|
|
246
|
+
const tex = res[i];
|
|
247
|
+
const slot = slots[i];
|
|
248
|
+
if (tex && tex.isTexture === true) {
|
|
249
|
+
textures.push({ material, slot, texture: tex, level });
|
|
250
|
+
}
|
|
251
|
+
else {
|
|
252
|
+
textures.push({ material, slot, texture: null, level });
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return textures;
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
if (materialOrTexture instanceof Texture || materialOrTexture.isTexture === true) {
|
|
259
|
+
const texture = materialOrTexture;
|
|
260
|
+
return this.assignTextureLODForSlot(texture, level, null, null);
|
|
261
|
+
}
|
|
262
|
+
return Promise.resolve(null);
|
|
263
|
+
}
|
|
264
|
+
static assignTextureLODForSlot(current, level, material, slot) {
|
|
265
|
+
if (current?.isTexture !== true)
|
|
266
|
+
return Promise.resolve(null);
|
|
267
|
+
// if (debug) console.log("-----------\n", "FIND", material?.name, slot, current?.name, current?.userData, current, material);
|
|
268
|
+
// const info = this.onProgressiveLoadStart(context, source, material, slot);
|
|
269
|
+
return NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
|
|
270
|
+
// this can currently not happen
|
|
271
|
+
if (Array.isArray(tex))
|
|
272
|
+
return null;
|
|
273
|
+
if (tex?.isTexture === true) {
|
|
274
|
+
if (tex != current) {
|
|
275
|
+
// if (debug) console.warn("Assign LOD", material?.name, slot, tex.name, tex["guid"], material, "Prev:", current, "Now:", tex, "\n--------------");
|
|
276
|
+
// tex.needsUpdate = true;
|
|
277
|
+
if (material && slot) {
|
|
278
|
+
material[slot] = tex;
|
|
279
|
+
// material.needsUpdate = true;
|
|
280
|
+
}
|
|
281
|
+
if (debug && slot && material) {
|
|
282
|
+
const lodinfo = this.getAssignedLODInformation(current);
|
|
283
|
+
if (lodinfo)
|
|
284
|
+
registerDebug(material, slot, lodinfo.url);
|
|
285
|
+
}
|
|
286
|
+
// check if the old texture is still used by other objects
|
|
287
|
+
// if not we dispose it...
|
|
288
|
+
// this could also be handled elsewhere and not be done immediately
|
|
289
|
+
// const users = getResourceUserCount(current);
|
|
290
|
+
// if (!users) {
|
|
291
|
+
// if (debug) console.log("Progressive: Dispose texture", current.name, current.source.data, current.uuid);
|
|
292
|
+
// current?.dispose();
|
|
293
|
+
// }
|
|
294
|
+
}
|
|
295
|
+
// this.onProgressiveLoadEnd(info);
|
|
296
|
+
return tex;
|
|
297
|
+
}
|
|
298
|
+
else if (debug == "verbose") {
|
|
299
|
+
console.warn("No LOD found for", current, level);
|
|
300
|
+
}
|
|
301
|
+
// this.onProgressiveLoadEnd(info);
|
|
302
|
+
return null;
|
|
303
|
+
}).catch(err => {
|
|
304
|
+
// this.onProgressiveLoadEnd(info);
|
|
305
|
+
console.error("Error loading LOD", current, err);
|
|
306
|
+
return null;
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
parser;
|
|
310
|
+
url;
|
|
311
|
+
constructor(parser, url) {
|
|
312
|
+
if (debug)
|
|
313
|
+
console.log("Progressive extension registered for", url);
|
|
314
|
+
this.parser = parser;
|
|
315
|
+
this.url = url;
|
|
316
|
+
}
|
|
317
|
+
afterRoot(gltf) {
|
|
318
|
+
if (debug)
|
|
319
|
+
console.log("AFTER", this.url, gltf);
|
|
320
|
+
this.parser.json.textures?.forEach((textureInfo, index) => {
|
|
321
|
+
if (textureInfo?.extensions) {
|
|
322
|
+
const ext = textureInfo?.extensions[EXTENSION_NAME];
|
|
323
|
+
if (ext) {
|
|
324
|
+
let found = false;
|
|
325
|
+
// TODO: why are sometimes textures not in the associations array when using r3f glTF loader...
|
|
326
|
+
for (const key of this.parser.associations.keys()) {
|
|
327
|
+
if (key.isTexture === true) {
|
|
328
|
+
const val = this.parser.associations.get(key);
|
|
329
|
+
if (val.textures === index) {
|
|
330
|
+
found = true;
|
|
331
|
+
NEEDLE_progressive.registerTexture(this.url, key, index, ext);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
if (!found) {
|
|
336
|
+
// see TODO above, fallback for when textures are not in the associations array
|
|
337
|
+
this.parser.getDependency("texture", index).then(tex => {
|
|
338
|
+
if (tex) {
|
|
339
|
+
NEEDLE_progressive.registerTexture(this.url, tex, index, ext);
|
|
340
|
+
}
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
});
|
|
346
|
+
this.parser.json.meshes?.forEach((meshInfo, index) => {
|
|
347
|
+
if (meshInfo?.extensions) {
|
|
348
|
+
const ext = meshInfo?.extensions[EXTENSION_NAME];
|
|
349
|
+
if (ext && ext.lods) {
|
|
350
|
+
for (const entry of this.parser.associations.keys()) {
|
|
351
|
+
if (entry.isMesh) {
|
|
352
|
+
const val = this.parser.associations.get(entry);
|
|
353
|
+
if (val.meshes === index) {
|
|
354
|
+
NEEDLE_progressive.registerMesh(this.url, entry.uuid, entry, ext.lods.length, val.primitives, ext);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Register a texture with LOD information
|
|
365
|
+
*/
|
|
366
|
+
static registerTexture = (url, tex, index, ext) => {
|
|
367
|
+
if (debug)
|
|
368
|
+
console.log("> Progressive: register texture", index, tex.name, tex.uuid, tex, ext);
|
|
369
|
+
// 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)
|
|
370
|
+
// see https://github.com/needle-tools/needle-engine-support/issues/133
|
|
371
|
+
if (tex.source)
|
|
372
|
+
tex.source[$progressiveTextureExtension] = ext;
|
|
373
|
+
const LODKEY = tex.uuid;
|
|
374
|
+
NEEDLE_progressive.assignLODInformation(url, tex, LODKEY, 0, 0, undefined);
|
|
375
|
+
NEEDLE_progressive.lodInfos.set(LODKEY, ext);
|
|
376
|
+
NEEDLE_progressive.lowresCache.set(LODKEY, tex);
|
|
377
|
+
};
|
|
378
|
+
/**
|
|
379
|
+
* Register a mesh with LOD information
|
|
380
|
+
*/
|
|
381
|
+
static registerMesh = (url, key, mesh, level, index, ext) => {
|
|
382
|
+
if (debug)
|
|
383
|
+
console.log("> Progressive: register mesh", index, mesh.name, ext, mesh.uuid, mesh);
|
|
384
|
+
const geometry = mesh.geometry;
|
|
385
|
+
if (!geometry.userData)
|
|
386
|
+
geometry.userData = {};
|
|
387
|
+
NEEDLE_progressive.assignLODInformation(url, geometry, key, level, index, ext.density);
|
|
388
|
+
NEEDLE_progressive.lodInfos.set(key, ext);
|
|
389
|
+
let existing = NEEDLE_progressive.lowresCache.get(key);
|
|
390
|
+
if (existing)
|
|
391
|
+
existing.push(mesh.geometry);
|
|
392
|
+
else
|
|
393
|
+
existing = [mesh.geometry];
|
|
394
|
+
NEEDLE_progressive.lowresCache.set(key, existing);
|
|
395
|
+
for (const plugin of plugins) {
|
|
396
|
+
plugin.onRegisteredNewMesh?.(mesh, ext);
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
/** A map of key = asset uuid and value = LOD information */
|
|
400
|
+
static lodInfos = new Map();
|
|
401
|
+
/** cache of already loaded mesh lods */
|
|
402
|
+
static previouslyLoaded = new Map();
|
|
403
|
+
/** this contains the geometry/textures that were originally loaded */
|
|
404
|
+
static lowresCache = new Map();
|
|
405
|
+
static async getOrLoadLOD(current, level) {
|
|
406
|
+
const debugverbose = debug == "verbose";
|
|
407
|
+
/** this key is used to lookup the LOD information */
|
|
408
|
+
const LOD = current.userData.LODS;
|
|
409
|
+
if (!LOD) {
|
|
410
|
+
return null;
|
|
411
|
+
}
|
|
412
|
+
const LODKEY = LOD?.key;
|
|
413
|
+
let progressiveInfo;
|
|
414
|
+
// See https://github.com/needle-tools/needle-engine-support/issues/133
|
|
415
|
+
if (current.isTexture === true) {
|
|
416
|
+
const tex = current;
|
|
417
|
+
if (tex.source && tex.source[$progressiveTextureExtension])
|
|
418
|
+
progressiveInfo = tex.source[$progressiveTextureExtension];
|
|
419
|
+
}
|
|
420
|
+
if (!progressiveInfo)
|
|
421
|
+
progressiveInfo = NEEDLE_progressive.lodInfos.get(LODKEY);
|
|
422
|
+
if (progressiveInfo) {
|
|
423
|
+
if (level > 0) {
|
|
424
|
+
let useLowRes = false;
|
|
425
|
+
const hasMultipleLevels = Array.isArray(progressiveInfo.lods);
|
|
426
|
+
if (hasMultipleLevels && level >= progressiveInfo.lods.length) {
|
|
427
|
+
useLowRes = true;
|
|
428
|
+
}
|
|
429
|
+
else if (!hasMultipleLevels) {
|
|
430
|
+
useLowRes = true;
|
|
431
|
+
}
|
|
432
|
+
if (useLowRes) {
|
|
433
|
+
const lowres = this.lowresCache.get(LODKEY);
|
|
434
|
+
return lowres;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
/** the unresolved LOD url */
|
|
438
|
+
const unresolved_lod_url = Array.isArray(progressiveInfo.lods) ? progressiveInfo.lods[level].path : progressiveInfo.lods;
|
|
439
|
+
// check if we have a uri
|
|
440
|
+
if (!unresolved_lod_url) {
|
|
441
|
+
if (debug && !progressiveInfo["missing:uri"]) {
|
|
442
|
+
progressiveInfo["missing:uri"] = true;
|
|
443
|
+
console.warn("Missing uri for progressive asset for LOD " + level, progressiveInfo);
|
|
444
|
+
}
|
|
445
|
+
return null;
|
|
446
|
+
}
|
|
447
|
+
/** the resolved LOD url */
|
|
448
|
+
const lod_url = resolveUrl(LOD.url, unresolved_lod_url);
|
|
449
|
+
// check if the requested file needs to be loaded via a GLTFLoader
|
|
450
|
+
if (lod_url.endsWith(".glb") || lod_url.endsWith(".gltf")) {
|
|
451
|
+
if (!progressiveInfo.guid) {
|
|
452
|
+
console.warn("missing pointer for glb/gltf texture", progressiveInfo);
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
// check if the requested file has already been loaded
|
|
456
|
+
const KEY = lod_url + "_" + progressiveInfo.guid;
|
|
457
|
+
// check if the requested file is currently being loaded
|
|
458
|
+
const existing = this.previouslyLoaded.get(KEY);
|
|
459
|
+
if (existing !== undefined) {
|
|
460
|
+
if (debugverbose)
|
|
461
|
+
console.log(`LOD ${level} was already loading/loaded: ${KEY}`);
|
|
462
|
+
let res = await existing.catch(err => {
|
|
463
|
+
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
|
|
464
|
+
return null;
|
|
465
|
+
});
|
|
466
|
+
let resouceIsDisposed = false;
|
|
467
|
+
if (res == null) {
|
|
468
|
+
// if the resource is null the last loading result didnt succeed (maybe because the url doesnt exist)
|
|
469
|
+
// in which case we don't attempt to load it again
|
|
470
|
+
}
|
|
471
|
+
else if (res instanceof Texture && current instanceof Texture) {
|
|
472
|
+
// check if the texture has been disposed or not
|
|
473
|
+
if (res.image?.data || res.source?.data) {
|
|
474
|
+
res = this.copySettings(current, res);
|
|
475
|
+
}
|
|
476
|
+
// if it has been disposed we need to load it again
|
|
477
|
+
else {
|
|
478
|
+
resouceIsDisposed = true;
|
|
479
|
+
this.previouslyLoaded.delete(KEY);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
else if (res instanceof BufferGeometry && current instanceof BufferGeometry) {
|
|
483
|
+
if (res.attributes.position?.array) {
|
|
484
|
+
// the geometry is OK
|
|
485
|
+
}
|
|
486
|
+
else {
|
|
487
|
+
resouceIsDisposed = true;
|
|
488
|
+
this.previouslyLoaded.delete(KEY);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
if (!resouceIsDisposed) {
|
|
492
|
+
return res;
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const ext = progressiveInfo;
|
|
496
|
+
const request = new Promise(async (resolve, _) => {
|
|
497
|
+
const loader = new GLTFLoader();
|
|
498
|
+
addDracoAndKTX2Loaders(loader);
|
|
499
|
+
if (debug) {
|
|
500
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
501
|
+
if (debugverbose)
|
|
502
|
+
console.warn("Start loading (delayed) " + lod_url, ext.guid);
|
|
503
|
+
}
|
|
504
|
+
let url = lod_url;
|
|
505
|
+
if (ext && Array.isArray(ext.lods)) {
|
|
506
|
+
const lodinfo = ext.lods[level];
|
|
507
|
+
if (lodinfo.hash) {
|
|
508
|
+
url += "?v=" + lodinfo.hash;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const gltf = await loader.loadAsync(url).catch(err => {
|
|
512
|
+
console.error(`Error loading LOD ${level} from ${lod_url}\n`, err);
|
|
513
|
+
return null;
|
|
514
|
+
});
|
|
515
|
+
if (!gltf)
|
|
516
|
+
return null;
|
|
517
|
+
const parser = gltf.parser;
|
|
518
|
+
if (debugverbose)
|
|
519
|
+
console.log("Loading finished " + lod_url, ext.guid);
|
|
520
|
+
let index = 0;
|
|
521
|
+
if (gltf.parser.json.textures) {
|
|
522
|
+
let found = false;
|
|
523
|
+
for (const tex of gltf.parser.json.textures) {
|
|
524
|
+
// find the texture index
|
|
525
|
+
if (tex?.extensions) {
|
|
526
|
+
const other = tex?.extensions[EXTENSION_NAME];
|
|
527
|
+
if (other?.guid) {
|
|
528
|
+
if (other.guid === ext.guid) {
|
|
529
|
+
found = true;
|
|
530
|
+
break;
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
index++;
|
|
535
|
+
}
|
|
536
|
+
if (found) {
|
|
537
|
+
let tex = await parser.getDependency("texture", index);
|
|
538
|
+
if (debugverbose)
|
|
539
|
+
console.log("change \"" + current.name + "\" → \"" + tex.name + "\"", lod_url, index, tex, KEY);
|
|
540
|
+
if (current instanceof Texture)
|
|
541
|
+
tex = this.copySettings(current, tex);
|
|
542
|
+
if (tex) {
|
|
543
|
+
tex.guid = ext.guid;
|
|
544
|
+
}
|
|
545
|
+
return resolve(tex);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
index = 0;
|
|
549
|
+
if (gltf.parser.json.meshes) {
|
|
550
|
+
let found = false;
|
|
551
|
+
for (const mesh of gltf.parser.json.meshes) {
|
|
552
|
+
// find the mesh index
|
|
553
|
+
if (mesh?.extensions) {
|
|
554
|
+
const other = mesh?.extensions[EXTENSION_NAME];
|
|
555
|
+
if (other?.guid) {
|
|
556
|
+
if (other.guid === ext.guid) {
|
|
557
|
+
found = true;
|
|
558
|
+
break;
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
index++;
|
|
563
|
+
}
|
|
564
|
+
if (found) {
|
|
565
|
+
const mesh = await parser.getDependency("mesh", index);
|
|
566
|
+
const meshExt = ext;
|
|
567
|
+
if (debugverbose)
|
|
568
|
+
console.log(`Loaded Mesh \"${mesh.name}\"`, lod_url, index, mesh, KEY);
|
|
569
|
+
if (mesh.isMesh === true) {
|
|
570
|
+
const geo = mesh.geometry;
|
|
571
|
+
NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, undefined, meshExt.density);
|
|
572
|
+
return resolve(geo);
|
|
573
|
+
}
|
|
574
|
+
else {
|
|
575
|
+
const geometries = new Array();
|
|
576
|
+
for (let i = 0; i < mesh.children.length; i++) {
|
|
577
|
+
const child = mesh.children[i];
|
|
578
|
+
if (child instanceof Mesh) {
|
|
579
|
+
const geo = child.geometry;
|
|
580
|
+
NEEDLE_progressive.assignLODInformation(LOD.url, geo, LODKEY, level, i, meshExt.density);
|
|
581
|
+
geometries.push(geo);
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
return resolve(geometries);
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// we could not find a texture or mesh with the given guid
|
|
589
|
+
return resolve(null);
|
|
590
|
+
});
|
|
591
|
+
this.previouslyLoaded.set(KEY, request);
|
|
592
|
+
const res = await request;
|
|
593
|
+
return res;
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
if (current instanceof Texture) {
|
|
597
|
+
if (debugverbose)
|
|
598
|
+
console.log("Load texture from uri: " + lod_url);
|
|
599
|
+
const loader = new TextureLoader();
|
|
600
|
+
const tex = await loader.loadAsync(lod_url);
|
|
601
|
+
if (tex) {
|
|
602
|
+
tex.guid = progressiveInfo.guid;
|
|
603
|
+
tex.flipY = false;
|
|
604
|
+
tex.needsUpdate = true;
|
|
605
|
+
tex.colorSpace = current.colorSpace;
|
|
606
|
+
if (debugverbose)
|
|
607
|
+
console.log(progressiveInfo, tex);
|
|
608
|
+
}
|
|
609
|
+
else if (debug)
|
|
610
|
+
console.warn("failed loading", lod_url);
|
|
611
|
+
return tex;
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
else {
|
|
616
|
+
if (debug)
|
|
617
|
+
console.warn(`Can not load LOD ${level}: no LOD info found for \"${LODKEY}\" ${current.name}`, current.type);
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
static assignLODInformation(url, res, key, level, index, density) {
|
|
622
|
+
if (!res)
|
|
623
|
+
return;
|
|
624
|
+
if (!res.userData)
|
|
625
|
+
res.userData = {};
|
|
626
|
+
const info = new LODInformation(url, key, level, index, density);
|
|
627
|
+
res.userData.LODS = info;
|
|
628
|
+
res.userData.LOD = level;
|
|
629
|
+
}
|
|
630
|
+
static getAssignedLODInformation(res) {
|
|
631
|
+
return res?.userData?.LODS || null;
|
|
632
|
+
}
|
|
633
|
+
static _copiedTextures = new Map();
|
|
634
|
+
static copySettings(source, target) {
|
|
635
|
+
// don't copy again if the texture was processed before
|
|
636
|
+
const existingClone = this._copiedTextures.get(source);
|
|
637
|
+
if (existingClone) {
|
|
638
|
+
return existingClone;
|
|
639
|
+
}
|
|
640
|
+
// We need to clone e.g. when the same texture is used multiple times (but with e.g. different wrap settings)
|
|
641
|
+
// This is relatively cheap since it only stores settings
|
|
642
|
+
// This should only happen once ever for every texture
|
|
643
|
+
target = target.clone();
|
|
644
|
+
this._copiedTextures.set(source, target);
|
|
645
|
+
// we re-use the offset and repeat settings because it might be animated
|
|
646
|
+
target.offset = source.offset;
|
|
647
|
+
target.repeat = source.repeat;
|
|
648
|
+
target.colorSpace = source.colorSpace;
|
|
649
|
+
return target;
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
// declare type GetLODInformation = () => LODInformation | null;
|
|
653
|
+
class LODInformation {
|
|
654
|
+
url;
|
|
655
|
+
/** the key to lookup the LOD information */
|
|
656
|
+
key;
|
|
657
|
+
level;
|
|
658
|
+
/** For multi objects (e.g. a group of meshes) this is the index of the object */
|
|
659
|
+
index;
|
|
660
|
+
/** the mesh density */
|
|
661
|
+
density;
|
|
662
|
+
constructor(url, key, level, index, density) {
|
|
663
|
+
this.url = url;
|
|
664
|
+
this.key = key;
|
|
665
|
+
this.level = level;
|
|
666
|
+
if (index != undefined)
|
|
667
|
+
this.index = index;
|
|
668
|
+
if (density != undefined)
|
|
669
|
+
this.density = density;
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
;
|
|
673
|
+
//# sourceMappingURL=extension.js.map
|