@needle-tools/gltf-progressive 3.5.0-rc → 3.6.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -1
- package/LICENSE +21 -0
- package/gltf-progressive.js +896 -636
- package/gltf-progressive.min.js +9 -9
- package/gltf-progressive.umd.cjs +9 -9
- package/lib/extension.d.ts +65 -6
- package/lib/extension.js +278 -110
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/lods.debug.js +1 -1
- package/lib/lods.manager.d.ts +90 -15
- package/lib/lods.manager.js +282 -150
- package/lib/lods.promise.js +1 -1
- package/lib/plugins/modelviewer.js +1 -1
- package/lib/version.js +1 -1
- package/package.json +4 -2
package/lib/lods.manager.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Box3, Clock, Matrix4, Mesh,
|
|
1
|
+
import { Box3, Clock, Color, Matrix4, Mesh, Sphere, Vector3 } from "three";
|
|
2
2
|
import { NEEDLE_progressive } from "./extension.js";
|
|
3
3
|
import { createLoaders } from "./loaders.js";
|
|
4
4
|
import { getParam, isDevelopmentServer, isMobileDevice } from "./utils.internal.js";
|
|
@@ -7,11 +7,172 @@ import { getRaycastMesh } from "./utils.js";
|
|
|
7
7
|
import { applyDebugSettings, debug, debug_OverrideLodLevel } from "./lods.debug.js";
|
|
8
8
|
import { PromiseGroup } from "./lods.promise.js";
|
|
9
9
|
const debugProgressiveLoading = getParam("debugprogressive");
|
|
10
|
+
const debugProgressiveLODColors = debugProgressiveLoading === "colors";
|
|
10
11
|
const suppressProgressiveLoading = getParam("noprogressive");
|
|
11
12
|
const $lodsManager = Symbol("Needle:LODSManager");
|
|
12
13
|
const $lodstate = Symbol("Needle:LODState");
|
|
13
14
|
const $currentLOD = Symbol("Needle:CurrentLOD");
|
|
14
15
|
const levels = { mesh_lod: -1, texture_lod: -1 };
|
|
16
|
+
const debugLODColor = new Color();
|
|
17
|
+
export const lodDebugColors = [
|
|
18
|
+
0x35d05f,
|
|
19
|
+
0xa8d83a,
|
|
20
|
+
0xf3d13b,
|
|
21
|
+
0xf29332,
|
|
22
|
+
0xf0523b,
|
|
23
|
+
0xa856f0,
|
|
24
|
+
0x49a7f2,
|
|
25
|
+
0x32d7c4,
|
|
26
|
+
0xff6b9d,
|
|
27
|
+
0x6f7df7,
|
|
28
|
+
0xd66fd2,
|
|
29
|
+
0x35a853,
|
|
30
|
+
0xb7a51f,
|
|
31
|
+
0xe05d2f,
|
|
32
|
+
0x3c78d8,
|
|
33
|
+
0x00a6a6,
|
|
34
|
+
0xd7263d,
|
|
35
|
+
0x7f52ff,
|
|
36
|
+
0x46b450,
|
|
37
|
+
0xf7a531,
|
|
38
|
+
0x2f9be0,
|
|
39
|
+
0xb84592,
|
|
40
|
+
0x8a9a2a,
|
|
41
|
+
0x1f6f8b,
|
|
42
|
+
0xf05a9d,
|
|
43
|
+
0x9b5de5,
|
|
44
|
+
0x00bbf9,
|
|
45
|
+
0x00f5d4,
|
|
46
|
+
0xfee440,
|
|
47
|
+
0xf15bb5,
|
|
48
|
+
0x4d908e,
|
|
49
|
+
0x555555,
|
|
50
|
+
];
|
|
51
|
+
const _meshLODWorldBox = new Box3();
|
|
52
|
+
const _meshLODProjectedBox = new Box3();
|
|
53
|
+
const _meshLODCameraSpaceBox = new Box3();
|
|
54
|
+
const _meshLODBoxSize = new Vector3();
|
|
55
|
+
const _meshLODCameraSpaceBoxSize = new Vector3();
|
|
56
|
+
const _meshLODProjectionInverse = new Matrix4();
|
|
57
|
+
const _meshLODCorner0 = new Vector3();
|
|
58
|
+
const _meshLODCorner1 = new Vector3();
|
|
59
|
+
const _meshLODCorner2 = new Vector3();
|
|
60
|
+
const _meshLODCorner3 = new Vector3();
|
|
61
|
+
function isInsideProjectedBox(box, projectionScreenMatrix) {
|
|
62
|
+
const min = box.min;
|
|
63
|
+
const max = box.max;
|
|
64
|
+
const centerx = (min.x + max.x) * 0.5;
|
|
65
|
+
const centery = (min.y + max.y) * 0.5;
|
|
66
|
+
const point = _meshLODCorner0.set(centerx, centery, min.z).applyMatrix4(projectionScreenMatrix);
|
|
67
|
+
return point.z < 0;
|
|
68
|
+
}
|
|
69
|
+
export function calculateMeshLODLevel(options) {
|
|
70
|
+
const { geometry, matrixWorld, camera, projectionScreenMatrix, desiredDensity, canvasHeight = 0, currentLevel = -1, xrEnabled = false, debugDrawLine, warnMissingPrimitiveDensities = false, } = options;
|
|
71
|
+
const meshLods = NEEDLE_progressive.getMeshLODExtension(geometry)?.lods;
|
|
72
|
+
const primitiveIndex = NEEDLE_progressive.getPrimitiveIndex(geometry);
|
|
73
|
+
const result = options.target ?? {
|
|
74
|
+
level: currentLevel,
|
|
75
|
+
primitiveIndex,
|
|
76
|
+
screenCoverage: 0,
|
|
77
|
+
screenspaceVolume: new Vector3(),
|
|
78
|
+
centrality: 1,
|
|
79
|
+
};
|
|
80
|
+
result.level = currentLevel;
|
|
81
|
+
result.primitiveIndex = primitiveIndex;
|
|
82
|
+
result.screenCoverage = 0;
|
|
83
|
+
result.screenspaceVolume.set(0, 0, 0);
|
|
84
|
+
result.centrality = 1;
|
|
85
|
+
if (!meshLods?.length)
|
|
86
|
+
return result;
|
|
87
|
+
let boundingBox = options.boundingBox ?? geometry.boundingBox;
|
|
88
|
+
if (!boundingBox) {
|
|
89
|
+
geometry.computeBoundingBox();
|
|
90
|
+
boundingBox = geometry.boundingBox;
|
|
91
|
+
}
|
|
92
|
+
if (!boundingBox)
|
|
93
|
+
return result;
|
|
94
|
+
_meshLODWorldBox.copy(boundingBox).applyMatrix4(matrixWorld);
|
|
95
|
+
if (camera.isPerspectiveCamera && isInsideProjectedBox(_meshLODWorldBox, projectionScreenMatrix)) {
|
|
96
|
+
result.level = 0;
|
|
97
|
+
result.screenCoverage = Infinity;
|
|
98
|
+
result.screenspaceVolume.set(Infinity, Infinity, Infinity);
|
|
99
|
+
return result;
|
|
100
|
+
}
|
|
101
|
+
_meshLODProjectedBox.copy(_meshLODWorldBox).applyMatrix4(projectionScreenMatrix);
|
|
102
|
+
if (xrEnabled && camera.isPerspectiveCamera && camera.fov > 70) {
|
|
103
|
+
const min = _meshLODProjectedBox.min;
|
|
104
|
+
const max = _meshLODProjectedBox.max;
|
|
105
|
+
let minX = min.x;
|
|
106
|
+
let minY = min.y;
|
|
107
|
+
let maxX = max.x;
|
|
108
|
+
let maxY = max.y;
|
|
109
|
+
const enlargementFactor = 2.0;
|
|
110
|
+
const centerBoost = 1.5;
|
|
111
|
+
const centerX = (min.x + max.x) * 0.5;
|
|
112
|
+
const centerY = (min.y + max.y) * 0.5;
|
|
113
|
+
minX = (minX - centerX) * enlargementFactor + centerX;
|
|
114
|
+
minY = (minY - centerY) * enlargementFactor + centerY;
|
|
115
|
+
maxX = (maxX - centerX) * enlargementFactor + centerX;
|
|
116
|
+
maxY = (maxY - centerY) * enlargementFactor + centerY;
|
|
117
|
+
const xCentrality = minX < 0 && maxX > 0 ? 0 : Math.min(Math.abs(min.x), Math.abs(max.x));
|
|
118
|
+
const yCentrality = minY < 0 && maxY > 0 ? 0 : Math.min(Math.abs(min.y), Math.abs(max.y));
|
|
119
|
+
const centrality = Math.max(xCentrality, yCentrality);
|
|
120
|
+
result.centrality = (centerBoost - centrality) * (centerBoost - centrality) * (centerBoost - centrality);
|
|
121
|
+
}
|
|
122
|
+
const boxSize = _meshLODProjectedBox.getSize(_meshLODBoxSize);
|
|
123
|
+
boxSize.multiplyScalar(0.5);
|
|
124
|
+
if (globalThis.screen?.availHeight > 0 && canvasHeight > 0) {
|
|
125
|
+
boxSize.multiplyScalar(canvasHeight / globalThis.screen.availHeight);
|
|
126
|
+
}
|
|
127
|
+
if (camera.isPerspectiveCamera) {
|
|
128
|
+
boxSize.x *= camera.aspect;
|
|
129
|
+
}
|
|
130
|
+
_meshLODCameraSpaceBox.copy(boundingBox).applyMatrix4(matrixWorld).applyMatrix4(camera.matrixWorldInverse);
|
|
131
|
+
const cameraSpaceSize = _meshLODCameraSpaceBox.getSize(_meshLODCameraSpaceBoxSize);
|
|
132
|
+
const screenMax = Math.max(boxSize.x, boxSize.y);
|
|
133
|
+
const cameraSpaceMax = Math.max(cameraSpaceSize.x, cameraSpaceSize.y);
|
|
134
|
+
if (screenMax !== 0 && cameraSpaceMax !== 0) {
|
|
135
|
+
boxSize.z = cameraSpaceSize.z / cameraSpaceMax * screenMax;
|
|
136
|
+
}
|
|
137
|
+
const screenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z) * result.centrality;
|
|
138
|
+
result.screenCoverage = screenCoverage;
|
|
139
|
+
result.screenspaceVolume.copy(boxSize);
|
|
140
|
+
if (screenCoverage <= 0)
|
|
141
|
+
return result;
|
|
142
|
+
if (debugDrawLine) {
|
|
143
|
+
const mat = _meshLODProjectionInverse.copy(projectionScreenMatrix);
|
|
144
|
+
mat.invert();
|
|
145
|
+
_meshLODCorner0.copy(_meshLODProjectedBox.min);
|
|
146
|
+
_meshLODCorner1.copy(_meshLODProjectedBox.max);
|
|
147
|
+
_meshLODCorner1.x = _meshLODCorner0.x;
|
|
148
|
+
_meshLODCorner2.copy(_meshLODProjectedBox.max);
|
|
149
|
+
_meshLODCorner2.y = _meshLODCorner0.y;
|
|
150
|
+
_meshLODCorner3.copy(_meshLODProjectedBox.max);
|
|
151
|
+
const z = (_meshLODCorner0.z + _meshLODCorner3.z) * 0.5;
|
|
152
|
+
_meshLODCorner0.z = _meshLODCorner1.z = _meshLODCorner2.z = _meshLODCorner3.z = z;
|
|
153
|
+
_meshLODCorner0.applyMatrix4(mat);
|
|
154
|
+
_meshLODCorner1.applyMatrix4(mat);
|
|
155
|
+
_meshLODCorner2.applyMatrix4(mat);
|
|
156
|
+
_meshLODCorner3.applyMatrix4(mat);
|
|
157
|
+
debugDrawLine(_meshLODCorner0, _meshLODCorner1, 0x0000ff);
|
|
158
|
+
debugDrawLine(_meshLODCorner0, _meshLODCorner2, 0x0000ff);
|
|
159
|
+
debugDrawLine(_meshLODCorner1, _meshLODCorner3, 0x0000ff);
|
|
160
|
+
debugDrawLine(_meshLODCorner2, _meshLODCorner3, 0x0000ff);
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < meshLods.length; i++) {
|
|
163
|
+
const lod = meshLods[i];
|
|
164
|
+
const density = lod.densities?.[primitiveIndex] || lod.density || .00001;
|
|
165
|
+
if (primitiveIndex > 0 && warnMissingPrimitiveDensities && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
|
|
166
|
+
globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
|
|
167
|
+
console.warn(`[Needle Progressive] Detected usage of mesh without primitive densities. This might cause incorrect LOD level selection: Consider re-optimizing your model by updating your Needle Integration, Needle glTF Pipeline or running optimization again on Needle Cloud.`);
|
|
168
|
+
}
|
|
169
|
+
if (density / screenCoverage < desiredDensity) {
|
|
170
|
+
result.level = i;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return result;
|
|
175
|
+
}
|
|
15
176
|
/**
|
|
16
177
|
* The LODsManager class is responsible for managing the LODs and progressive assets in the scene. It will automatically update the LODs based on the camera position, screen coverage and mesh density of the objects.
|
|
17
178
|
* It must be enabled by calling the `enable` method.
|
|
@@ -116,7 +277,31 @@ export class LODsManager {
|
|
|
116
277
|
_newPromiseGroups = [];
|
|
117
278
|
_promiseGroupIds = 0;
|
|
118
279
|
/**
|
|
119
|
-
*
|
|
280
|
+
* Returns a promise that resolves once all LOD requests initiated during the next render cycles have finished loading.
|
|
281
|
+
* This is useful for hiding low-resolution placeholders (e.g. with a loading overlay or CSS blur) until high-quality assets are ready.
|
|
282
|
+
*
|
|
283
|
+
* By default, the returned promise captures LOD loading requests for 2 frames and resolves when all of them complete.
|
|
284
|
+
* Use `waitForFirstCapture` if no LOD requests may happen immediately (e.g. after a scene switch).
|
|
285
|
+
*
|
|
286
|
+
* @param opts - Optional configuration for how long to capture and what to wait for. See {@link PromiseGroupOptions}.
|
|
287
|
+
* @returns A promise that resolves with `{ cancelled, awaited_count, resolved_count }` once all captured LOD loads complete (or the signal aborts).
|
|
288
|
+
*
|
|
289
|
+
* @example
|
|
290
|
+
* ```ts
|
|
291
|
+
* // Wait for initial LODs to finish loading, then remove a blur overlay
|
|
292
|
+
* const result = await lodsManager.awaitLoading({
|
|
293
|
+
* frames: 5,
|
|
294
|
+
* signal: AbortSignal.timeout(10_000),
|
|
295
|
+
* });
|
|
296
|
+
* console.log(`Loaded ${result.resolved_count} of ${result.awaited_count} LODs`);
|
|
297
|
+
* document.querySelector('.blur-overlay')?.remove();
|
|
298
|
+
* ```
|
|
299
|
+
*
|
|
300
|
+
* @example
|
|
301
|
+
* ```ts
|
|
302
|
+
* // Wait until at least one LOD starts loading before resolving
|
|
303
|
+
* await lodsManager.awaitLoading({ waitForFirstCapture: true });
|
|
304
|
+
* ```
|
|
120
305
|
*/
|
|
121
306
|
awaitLoading(opts) {
|
|
122
307
|
const id = this._promiseGroupIds++;
|
|
@@ -136,6 +321,11 @@ export class LODsManager {
|
|
|
136
321
|
});
|
|
137
322
|
return newGroup.ready;
|
|
138
323
|
}
|
|
324
|
+
/** Track LOD work started outside this manager so {@link awaitLoading} waits for it too. */
|
|
325
|
+
trackLoadingPromise(type, object, promise) {
|
|
326
|
+
PromiseGroup.addPromise(type, object, promise, this._newPromiseGroups);
|
|
327
|
+
return promise;
|
|
328
|
+
}
|
|
139
329
|
_postprocessPromiseGroups() {
|
|
140
330
|
if (this._newPromiseGroups.length === 0)
|
|
141
331
|
return;
|
|
@@ -145,17 +335,46 @@ export class LODsManager {
|
|
|
145
335
|
}
|
|
146
336
|
}
|
|
147
337
|
_lodchangedlisteners = [];
|
|
338
|
+
/**
|
|
339
|
+
* Register a listener that is called whenever a mesh or texture LOD level has finished loading and has been applied.
|
|
340
|
+
* The listener receives the type of asset (`"mesh"` or `"texture"`), the new LOD level, and the affected object.
|
|
341
|
+
*
|
|
342
|
+
* @param evt - The event type. Currently only `"changed"` is supported.
|
|
343
|
+
* @param listener - Callback invoked after a LOD swap completes.
|
|
344
|
+
* @return A function to unregister the listener.
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```ts
|
|
348
|
+
* lodsManager.addEventListener("changed", ({ type, level, object }) => {
|
|
349
|
+
* console.log(`${type} LOD changed to level ${level}`, object);
|
|
350
|
+
* });
|
|
351
|
+
* ```
|
|
352
|
+
*/
|
|
148
353
|
addEventListener(evt, listener) {
|
|
149
354
|
if (evt === "changed") {
|
|
150
355
|
this._lodchangedlisteners.push(listener);
|
|
356
|
+
return () => {
|
|
357
|
+
this.removeEventListener(evt, listener);
|
|
358
|
+
};
|
|
151
359
|
}
|
|
360
|
+
return () => { };
|
|
152
361
|
}
|
|
362
|
+
/**
|
|
363
|
+
* Remove a previously registered `"changed"` event listener.
|
|
364
|
+
* @param evt - The event type (`"changed"`).
|
|
365
|
+
* @param listener - The listener to remove.
|
|
366
|
+
* @return `true` if the listener was found and removed, `false` otherwise.
|
|
367
|
+
*/
|
|
153
368
|
removeEventListener(evt, listener) {
|
|
369
|
+
let removed = false;
|
|
154
370
|
if (evt === "changed") {
|
|
155
371
|
const index = this._lodchangedlisteners.indexOf(listener);
|
|
156
|
-
if (index >= 0)
|
|
372
|
+
if (index >= 0) {
|
|
157
373
|
this._lodchangedlisteners.splice(index, 1);
|
|
374
|
+
removed = true;
|
|
375
|
+
}
|
|
158
376
|
}
|
|
377
|
+
return removed;
|
|
159
378
|
}
|
|
160
379
|
// readonly plugins: NEEDLE_progressive_plugin[] = [];
|
|
161
380
|
constructor(renderer, context) {
|
|
@@ -217,6 +436,21 @@ export class LODsManager {
|
|
|
217
436
|
this.renderer.render = this.#originalRender;
|
|
218
437
|
this.#originalRender = undefined;
|
|
219
438
|
}
|
|
439
|
+
/**
|
|
440
|
+
* Manually trigger a LOD update for a scene and camera.
|
|
441
|
+
* Only needed when {@link manual} is set to `true` — otherwise LOD updates happen automatically on each render call.
|
|
442
|
+
*
|
|
443
|
+
* @param scene - The scene containing objects with progressive LODs.
|
|
444
|
+
* @param camera - The camera used to determine screen coverage and LOD levels.
|
|
445
|
+
*
|
|
446
|
+
* @example
|
|
447
|
+
* ```ts
|
|
448
|
+
* const lodsManager = LODsManager.get(renderer);
|
|
449
|
+
* lodsManager.manual = true;
|
|
450
|
+
* // ... later, trigger an update at a specific point:
|
|
451
|
+
* lodsManager.update(scene, camera);
|
|
452
|
+
* ```
|
|
453
|
+
*/
|
|
220
454
|
update(scene, camera) {
|
|
221
455
|
this.internalUpdate(scene, camera);
|
|
222
456
|
}
|
|
@@ -306,16 +540,6 @@ export class LODsManager {
|
|
|
306
540
|
case "MeshDepthMaterial":
|
|
307
541
|
continue;
|
|
308
542
|
}
|
|
309
|
-
if (debugProgressiveLoading === "color") {
|
|
310
|
-
if (entry.material) {
|
|
311
|
-
if (!entry.object["progressive_debug_color"]) {
|
|
312
|
-
entry.object["progressive_debug_color"] = true;
|
|
313
|
-
const randomColor = Math.random() * 0xffffff;
|
|
314
|
-
const newMaterial = new MeshStandardMaterial({ color: randomColor });
|
|
315
|
-
entry.object.material = newMaterial;
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
543
|
const object = entry.object;
|
|
320
544
|
if (object instanceof Mesh || (object.isMesh)) {
|
|
321
545
|
this.updateLODs(scene, camera, object, desiredDensity);
|
|
@@ -374,6 +598,9 @@ export class LODsManager {
|
|
|
374
598
|
if (debug && object.material && !object["isGizmo"]) {
|
|
375
599
|
applyDebugSettings(object.material);
|
|
376
600
|
}
|
|
601
|
+
if (debugProgressiveLODColors && object.material && !object["isGizmo"] && !object["isBatchedMesh"]) {
|
|
602
|
+
applyLODColor(object.material, levels.mesh_lod);
|
|
603
|
+
}
|
|
377
604
|
for (const plugin of plugins) {
|
|
378
605
|
plugin.onAfterUpdatedLOD?.(this.renderer, scene, camera, object, levels);
|
|
379
606
|
}
|
|
@@ -390,7 +617,7 @@ export class LODsManager {
|
|
|
390
617
|
return;
|
|
391
618
|
if (Array.isArray(material)) {
|
|
392
619
|
for (const mat of material) {
|
|
393
|
-
this.loadProgressiveTextures(mat, level);
|
|
620
|
+
this.loadProgressiveTextures(mat, level, overrideLodLevel);
|
|
394
621
|
}
|
|
395
622
|
return;
|
|
396
623
|
}
|
|
@@ -403,13 +630,15 @@ export class LODsManager {
|
|
|
403
630
|
else if (level < material[$currentLOD]) {
|
|
404
631
|
update = true;
|
|
405
632
|
}
|
|
406
|
-
|
|
633
|
+
const forceExactTextureLOD = overrideLodLevel !== undefined && overrideLodLevel >= 0;
|
|
634
|
+
if (forceExactTextureLOD) {
|
|
407
635
|
update = material[$currentLOD] != overrideLodLevel;
|
|
408
636
|
level = overrideLodLevel;
|
|
409
637
|
}
|
|
410
638
|
if (update) {
|
|
411
639
|
material[$currentLOD] = level;
|
|
412
|
-
const
|
|
640
|
+
const options = forceExactTextureLOD ? { force: true } : undefined;
|
|
641
|
+
const promise = NEEDLE_progressive.assignTextureLOD(material, level, options).then(_ => {
|
|
413
642
|
this._lodchangedlisteners.forEach(l => l({ type: "texture", level, object: material }));
|
|
414
643
|
});
|
|
415
644
|
PromiseGroup.addPromise("texture", material, promise, this._newPromiseGroups);
|
|
@@ -446,25 +675,7 @@ export class LODsManager {
|
|
|
446
675
|
}
|
|
447
676
|
// private testIfLODLevelsAreAvailable() {
|
|
448
677
|
_sphere = new Sphere();
|
|
449
|
-
_tempBox = new Box3();
|
|
450
|
-
_tempBox2 = new Box3();
|
|
451
|
-
tempMatrix = new Matrix4();
|
|
452
678
|
_tempWorldPosition = new Vector3();
|
|
453
|
-
_tempBoxSize = new Vector3();
|
|
454
|
-
_tempBox2Size = new Vector3();
|
|
455
|
-
static corner0 = new Vector3();
|
|
456
|
-
static corner1 = new Vector3();
|
|
457
|
-
static corner2 = new Vector3();
|
|
458
|
-
static corner3 = new Vector3();
|
|
459
|
-
static _tempPtInside = new Vector3();
|
|
460
|
-
static isInside(box, matrix) {
|
|
461
|
-
const min = box.min;
|
|
462
|
-
const max = box.max;
|
|
463
|
-
const centerx = (min.x + max.x) * 0.5;
|
|
464
|
-
const centery = (min.y + max.y) * 0.5;
|
|
465
|
-
const pt1 = this._tempPtInside.set(centerx, centery, min.z).applyMatrix4(matrix);
|
|
466
|
-
return pt1.z < 0;
|
|
467
|
-
}
|
|
468
679
|
static skinnedMeshBoundsFrameOffsetCounter = 0;
|
|
469
680
|
static $skinnedMeshBoundsOffset = Symbol("gltf-progressive-skinnedMeshBoundsOffset");
|
|
470
681
|
// #region calculateLodLevel
|
|
@@ -536,7 +747,6 @@ export class LODsManager {
|
|
|
536
747
|
boundingBox = skinnedMesh.boundingBox;
|
|
537
748
|
}
|
|
538
749
|
if (boundingBox) {
|
|
539
|
-
const cam = camera;
|
|
540
750
|
// hack: if the mesh has vertex colors, has less than 100 vertices we always select the highest LOD
|
|
541
751
|
if (mesh.geometry.attributes.color && mesh.geometry.attributes.color.count < 100) {
|
|
542
752
|
if (mesh.geometry.boundingSphere) {
|
|
@@ -550,126 +760,30 @@ export class LODsManager {
|
|
|
550
760
|
}
|
|
551
761
|
}
|
|
552
762
|
}
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
763
|
+
const selection = calculateMeshLODLevel({
|
|
764
|
+
geometry: mesh.geometry,
|
|
765
|
+
matrixWorld: mesh.matrixWorld,
|
|
766
|
+
camera,
|
|
767
|
+
projectionScreenMatrix: this.projectionScreenMatrix,
|
|
768
|
+
desiredDensity,
|
|
769
|
+
canvasHeight,
|
|
770
|
+
currentLevel: state.lastLodLevel_Mesh,
|
|
771
|
+
boundingBox,
|
|
772
|
+
xrEnabled: this.renderer.xr.enabled,
|
|
773
|
+
debugDrawLine: debugProgressiveLoading ? LODsManager.debugDrawLine : undefined,
|
|
774
|
+
warnMissingPrimitiveDensities: true,
|
|
775
|
+
});
|
|
776
|
+
state.lastCentrality = selection.centrality;
|
|
777
|
+
state.lastScreenCoverage = selection.screenCoverage;
|
|
778
|
+
state.lastScreenspaceVolume.copy(selection.screenspaceVolume);
|
|
779
|
+
if (selection.screenCoverage === Infinity) {
|
|
564
780
|
result.mesh_lod = 0;
|
|
565
781
|
result.texture_lod = 0;
|
|
566
782
|
return;
|
|
567
783
|
}
|
|
568
|
-
|
|
569
|
-
// TODO might need to be adjusted for cameras that are rendered during an XR session but are
|
|
570
|
-
// actually not XR cameras (e.g. a render texture)
|
|
571
|
-
if (this.renderer.xr.enabled && (cam.isPerspectiveCamera) && cam.fov > 70) {
|
|
572
|
-
// calculate centrality of the bounding box - how close is it to the screen center
|
|
573
|
-
const min = this._tempBox.min;
|
|
574
|
-
const max = this._tempBox.max;
|
|
575
|
-
let minX = min.x;
|
|
576
|
-
let minY = min.y;
|
|
577
|
-
let maxX = max.x;
|
|
578
|
-
let maxY = max.y;
|
|
579
|
-
// enlarge
|
|
580
|
-
const enlargementFactor = 2.0;
|
|
581
|
-
const centerBoost = 1.5;
|
|
582
|
-
const centerX = (min.x + max.x) * 0.5;
|
|
583
|
-
const centerY = (min.y + max.y) * 0.5;
|
|
584
|
-
minX = (minX - centerX) * enlargementFactor + centerX;
|
|
585
|
-
minY = (minY - centerY) * enlargementFactor + centerY;
|
|
586
|
-
maxX = (maxX - centerX) * enlargementFactor + centerX;
|
|
587
|
-
maxY = (maxY - centerY) * enlargementFactor + centerY;
|
|
588
|
-
const xCentrality = minX < 0 && maxX > 0 ? 0 : Math.min(Math.abs(min.x), Math.abs(max.x));
|
|
589
|
-
const yCentrality = minY < 0 && maxY > 0 ? 0 : Math.min(Math.abs(min.y), Math.abs(max.y));
|
|
590
|
-
const centrality = Math.max(xCentrality, yCentrality);
|
|
591
|
-
// heuristically determined to lower quality for objects at the edges of vision
|
|
592
|
-
state.lastCentrality = (centerBoost - centrality) * (centerBoost - centrality) * (centerBoost - centrality);
|
|
593
|
-
}
|
|
594
|
-
else {
|
|
595
|
-
state.lastCentrality = 1;
|
|
596
|
-
}
|
|
597
|
-
const boxSize = this._tempBox.getSize(this._tempBoxSize);
|
|
598
|
-
boxSize.multiplyScalar(0.5); // goes from -1..1, we want -0.5..0.5 for coverage in percent
|
|
599
|
-
if (screen.availHeight > 0) {
|
|
600
|
-
// correct for size of context on screen
|
|
601
|
-
if (canvasHeight > 0)
|
|
602
|
-
boxSize.multiplyScalar(canvasHeight / screen.availHeight);
|
|
603
|
-
}
|
|
604
|
-
if (camera.isPerspectiveCamera) {
|
|
605
|
-
boxSize.x *= camera.aspect;
|
|
606
|
-
}
|
|
607
|
-
else if (camera.isOrthographicCamera) {
|
|
608
|
-
// const cam = camera as OrthographicCamera;
|
|
609
|
-
// boxSize.x *= cam.zoom * .01;
|
|
610
|
-
}
|
|
611
|
-
const matView = camera.matrixWorldInverse;
|
|
612
|
-
const box2 = this._tempBox2;
|
|
613
|
-
box2.copy(boundingBox);
|
|
614
|
-
box2.applyMatrix4(mesh.matrixWorld);
|
|
615
|
-
box2.applyMatrix4(matView);
|
|
616
|
-
const boxSize2 = box2.getSize(this._tempBox2Size);
|
|
617
|
-
// approximate depth coverage in relation to screenspace size
|
|
618
|
-
const max2 = Math.max(boxSize2.x, boxSize2.y);
|
|
619
|
-
const max1 = Math.max(boxSize.x, boxSize.y);
|
|
620
|
-
if (max1 != 0 && max2 != 0)
|
|
621
|
-
boxSize.z = boxSize2.z / Math.max(boxSize2.x, boxSize2.y) * Math.max(boxSize.x, boxSize.y);
|
|
622
|
-
state.lastScreenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z);
|
|
623
|
-
state.lastScreenspaceVolume.copy(boxSize);
|
|
624
|
-
state.lastScreenCoverage *= state.lastCentrality;
|
|
625
|
-
// draw screen size box
|
|
626
|
-
if (debugProgressiveLoading && LODsManager.debugDrawLine) {
|
|
627
|
-
const mat = this.tempMatrix.copy(this.projectionScreenMatrix);
|
|
628
|
-
mat.invert();
|
|
629
|
-
const corner0 = LODsManager.corner0;
|
|
630
|
-
const corner1 = LODsManager.corner1;
|
|
631
|
-
const corner2 = LODsManager.corner2;
|
|
632
|
-
const corner3 = LODsManager.corner3;
|
|
633
|
-
// get box corners, transform with camera space, and draw as quad lines
|
|
634
|
-
corner0.copy(this._tempBox.min);
|
|
635
|
-
corner1.copy(this._tempBox.max);
|
|
636
|
-
corner1.x = corner0.x;
|
|
637
|
-
corner2.copy(this._tempBox.max);
|
|
638
|
-
corner2.y = corner0.y;
|
|
639
|
-
corner3.copy(this._tempBox.max);
|
|
640
|
-
// draw outlines at the center of the box
|
|
641
|
-
const z = (corner0.z + corner3.z) * 0.5;
|
|
642
|
-
// all outlines should have the same depth in screen space
|
|
643
|
-
corner0.z = corner1.z = corner2.z = corner3.z = z;
|
|
644
|
-
corner0.applyMatrix4(mat);
|
|
645
|
-
corner1.applyMatrix4(mat);
|
|
646
|
-
corner2.applyMatrix4(mat);
|
|
647
|
-
corner3.applyMatrix4(mat);
|
|
648
|
-
LODsManager.debugDrawLine(corner0, corner1, 0x0000ff);
|
|
649
|
-
LODsManager.debugDrawLine(corner0, corner2, 0x0000ff);
|
|
650
|
-
LODsManager.debugDrawLine(corner1, corner3, 0x0000ff);
|
|
651
|
-
LODsManager.debugDrawLine(corner2, corner3, 0x0000ff);
|
|
652
|
-
}
|
|
653
|
-
let expectedLevel = 999;
|
|
654
|
-
// const framerate = this.context.time.smoothedFps;
|
|
655
|
-
if (mesh_lods && state.lastScreenCoverage > 0) {
|
|
656
|
-
for (let l = 0; l < mesh_lods.length; l++) {
|
|
657
|
-
const lod = mesh_lods[l];
|
|
658
|
-
const densityForThisLevel = lod.densities?.[primitive_index] || lod.density || .00001;
|
|
659
|
-
const resultingDensity = densityForThisLevel / state.lastScreenCoverage;
|
|
660
|
-
if (primitive_index > 0 && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
|
|
661
|
-
window["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
|
|
662
|
-
console.warn(`[Needle Progressive] Detected usage of mesh without primitive densities. This might cause incorrect LOD level selection: Consider re-optimizing your model by updating your Needle Integration, Needle glTF Pipeline or running optimization again on Needle Cloud.`);
|
|
663
|
-
}
|
|
664
|
-
if (resultingDensity < desiredDensity) {
|
|
665
|
-
expectedLevel = l;
|
|
666
|
-
break;
|
|
667
|
-
}
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
const isLowerLod = expectedLevel < mesh_level;
|
|
784
|
+
const isLowerLod = selection.level >= 0 && selection.level < mesh_level;
|
|
671
785
|
if (isLowerLod) {
|
|
672
|
-
mesh_level =
|
|
786
|
+
mesh_level = selection.level;
|
|
673
787
|
mesh_level_calculated = true;
|
|
674
788
|
}
|
|
675
789
|
}
|
|
@@ -745,3 +859,21 @@ class LOD_state {
|
|
|
745
859
|
lastScreenspaceVolume = new Vector3();
|
|
746
860
|
lastCentrality = 0;
|
|
747
861
|
}
|
|
862
|
+
function applyLODColor(material, level) {
|
|
863
|
+
if (level < 0)
|
|
864
|
+
return;
|
|
865
|
+
if (Array.isArray(material)) {
|
|
866
|
+
for (const mat of material) {
|
|
867
|
+
applyLODColor(mat, level);
|
|
868
|
+
}
|
|
869
|
+
return;
|
|
870
|
+
}
|
|
871
|
+
if ("color" in material && material.color instanceof Color) {
|
|
872
|
+
material.color.copy(getLODColor(level, debugLODColor));
|
|
873
|
+
material.needsUpdate = true;
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
export function getLODColor(level, target) {
|
|
877
|
+
const index = Math.max(0, Math.min(lodDebugColors.length - 1, Math.floor(level)));
|
|
878
|
+
return target.setHex(lodDebugColors[index]);
|
|
879
|
+
}
|
package/lib/lods.promise.js
CHANGED
|
@@ -77,7 +77,7 @@ export class PromiseGroup {
|
|
|
77
77
|
}
|
|
78
78
|
if (this._maxPromisesPerObject >= 1) {
|
|
79
79
|
if (this._seen.has(object)) {
|
|
80
|
-
|
|
80
|
+
const count = this._seen.get(object);
|
|
81
81
|
if (count >= this._maxPromisesPerObject) {
|
|
82
82
|
if (debug)
|
|
83
83
|
console.warn(`PromiseGroup: Already awaiting object ignoring new promise for it.`);
|
|
@@ -108,7 +108,7 @@ function _patchModelViewer(modelviewer) {
|
|
|
108
108
|
function renderFrames() {
|
|
109
109
|
if (needsRender) {
|
|
110
110
|
let forcedFrames = 0;
|
|
111
|
-
|
|
111
|
+
const interval = setInterval(() => {
|
|
112
112
|
if (forcedFrames++ > 5) {
|
|
113
113
|
clearInterval(interval);
|
|
114
114
|
return;
|
package/lib/version.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@needle-tools/gltf-progressive",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.6.0-alpha.2",
|
|
4
4
|
"description": "three.js support for loading glTF or GLB files that contain progressive loading data",
|
|
5
5
|
"homepage": "https://needle.tools",
|
|
6
6
|
"author": {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
"type": "git",
|
|
13
13
|
"url": "git+https://github.com/needle-tools/gltf-progressive"
|
|
14
14
|
},
|
|
15
|
+
"license": "MIT",
|
|
15
16
|
"readme": "README.md",
|
|
16
17
|
"keywords": [
|
|
17
18
|
"three.js",
|
|
@@ -41,7 +42,8 @@
|
|
|
41
42
|
"gltf-progressive.js",
|
|
42
43
|
"gltf-progressive.min.js",
|
|
43
44
|
"gltf-progressive.umd.cjs",
|
|
44
|
-
"NEEDLE_progressive"
|
|
45
|
+
"NEEDLE_progressive",
|
|
46
|
+
"LICENSE"
|
|
45
47
|
],
|
|
46
48
|
"watch": {
|
|
47
49
|
"build:lib": {
|