@needle-tools/gltf-progressive 4.0.0-alpha.1 → 4.0.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/LICENSE +21 -0
- package/README.md +47 -11
- package/examples/modelviewer-multiple.html +4 -4
- package/examples/modelviewer.html +4 -4
- package/examples/offscreen/index.html +15 -0
- package/examples/offscreen/main.js +98 -0
- package/examples/react-three-fiber/index.html +2 -2
- package/examples/react-three-fiber/package-lock.json +482 -484
- package/examples/react-three-fiber/package.json +4 -4
- package/examples/react-three-fiber/src/App.tsx +76 -21
- package/examples/react-three-fiber/src/styles.css +2 -4
- package/examples/react-three-fiber/vite.config.js +2 -2
- package/examples/shared/example-utils.js +297 -0
- package/examples/shared/example.css +44 -0
- package/examples/shared/runtime.js +34 -0
- package/examples/threejs/index.html +6 -10
- package/examples/threejs/main.js +5 -23
- package/examples/webgpu/index.html +15 -0
- package/examples/webgpu/main.js +105 -0
- package/examples/worker-rendering/index.html +15 -0
- package/examples/worker-rendering/main.js +109 -0
- package/examples/worker-rendering/worker.js +166 -0
- package/gltf-progressive.js +1025 -669
- package/gltf-progressive.min.js +9 -9
- package/gltf-progressive.umd.cjs +9 -9
- package/lib/extension.d.ts +73 -7
- package/lib/extension.js +351 -111
- package/lib/index.d.ts +1 -1
- package/lib/index.js +1 -1
- package/lib/loaders.js +17 -3
- package/lib/lods.debug.js +2 -2
- package/lib/lods.manager.d.ts +93 -15
- package/lib/lods.manager.js +331 -157
- package/lib/lods.promise.js +1 -1
- package/lib/plugins/modelviewer.js +1 -1
- package/lib/utils.internal.js +20 -6
- package/lib/version.js +1 -1
- package/lib/worker/loader.mainthread.js +4 -1
- package/package.json +10 -3
package/lib/lods.manager.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
import { Box3, Matrix4, Mesh,
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import { Box3, Color, Matrix4, Mesh, Sphere, Vector3 } from "three";
|
|
3
3
|
import { NEEDLE_progressive } from "./extension.js";
|
|
4
4
|
import { createLoaders } from "./loaders.js";
|
|
5
5
|
import { getParam, isDevelopmentServer, isMobileDevice } from "./utils.internal.js";
|
|
@@ -8,11 +8,181 @@ import { getRaycastMesh } from "./utils.js";
|
|
|
8
8
|
import { applyDebugSettings, debug, debug_OverrideLodLevel } from "./lods.debug.js";
|
|
9
9
|
import { PromiseGroup } from "./lods.promise.js";
|
|
10
10
|
const debugProgressiveLoading = getParam("debugprogressive");
|
|
11
|
+
const debugProgressiveLODColors = debugProgressiveLoading === "colors";
|
|
11
12
|
const suppressProgressiveLoading = getParam("noprogressive");
|
|
12
13
|
const $lodsManager = Symbol("Needle:LODSManager");
|
|
13
14
|
const $lodstate = Symbol("Needle:LODState");
|
|
14
15
|
const $currentLOD = Symbol("Needle:CurrentLOD");
|
|
16
|
+
const ThreeRuntime = THREE;
|
|
15
17
|
const levels = { mesh_lod: -1, texture_lod: -1 };
|
|
18
|
+
const debugLODColor = new Color();
|
|
19
|
+
export const lodDebugColors = [
|
|
20
|
+
0x35d05f,
|
|
21
|
+
0xa8d83a,
|
|
22
|
+
0xf3d13b,
|
|
23
|
+
0xf29332,
|
|
24
|
+
0xf0523b,
|
|
25
|
+
0xa856f0,
|
|
26
|
+
0x49a7f2,
|
|
27
|
+
0x32d7c4,
|
|
28
|
+
0xff6b9d,
|
|
29
|
+
0x6f7df7,
|
|
30
|
+
0xd66fd2,
|
|
31
|
+
0x35a853,
|
|
32
|
+
0xb7a51f,
|
|
33
|
+
0xe05d2f,
|
|
34
|
+
0x3c78d8,
|
|
35
|
+
0x00a6a6,
|
|
36
|
+
0xd7263d,
|
|
37
|
+
0x7f52ff,
|
|
38
|
+
0x46b450,
|
|
39
|
+
0xf7a531,
|
|
40
|
+
0x2f9be0,
|
|
41
|
+
0xb84592,
|
|
42
|
+
0x8a9a2a,
|
|
43
|
+
0x1f6f8b,
|
|
44
|
+
0xf05a9d,
|
|
45
|
+
0x9b5de5,
|
|
46
|
+
0x00bbf9,
|
|
47
|
+
0x00f5d4,
|
|
48
|
+
0xfee440,
|
|
49
|
+
0xf15bb5,
|
|
50
|
+
0x4d908e,
|
|
51
|
+
0x555555,
|
|
52
|
+
];
|
|
53
|
+
function createLODTimer() {
|
|
54
|
+
const Timer = ThreeRuntime.Timer || ThreeRuntime.Clock;
|
|
55
|
+
return new Timer();
|
|
56
|
+
}
|
|
57
|
+
const _meshLODWorldBox = new Box3();
|
|
58
|
+
const _meshLODProjectedBox = new Box3();
|
|
59
|
+
const _meshLODCameraSpaceBox = new Box3();
|
|
60
|
+
const _meshLODBoxSize = new Vector3();
|
|
61
|
+
const _meshLODCameraSpaceBoxSize = new Vector3();
|
|
62
|
+
const _meshLODProjectionInverse = new Matrix4();
|
|
63
|
+
const _meshLODCorner0 = new Vector3();
|
|
64
|
+
const _meshLODCorner1 = new Vector3();
|
|
65
|
+
const _meshLODCorner2 = new Vector3();
|
|
66
|
+
const _meshLODCorner3 = new Vector3();
|
|
67
|
+
function isInsideProjectedBox(box, projectionScreenMatrix) {
|
|
68
|
+
const min = box.min;
|
|
69
|
+
const max = box.max;
|
|
70
|
+
const centerx = (min.x + max.x) * 0.5;
|
|
71
|
+
const centery = (min.y + max.y) * 0.5;
|
|
72
|
+
const point = _meshLODCorner0.set(centerx, centery, min.z).applyMatrix4(projectionScreenMatrix);
|
|
73
|
+
return point.z < 0;
|
|
74
|
+
}
|
|
75
|
+
export function calculateMeshLODLevel(options) {
|
|
76
|
+
const { geometry, matrixWorld, camera, projectionScreenMatrix, desiredDensity, canvasHeight = 0, currentLevel = -1, xrEnabled = false, debugDrawLine, warnMissingPrimitiveDensities = false, } = options;
|
|
77
|
+
const meshLods = NEEDLE_progressive.getMeshLODExtension(geometry)?.lods;
|
|
78
|
+
const primitiveIndex = NEEDLE_progressive.getPrimitiveIndex(geometry);
|
|
79
|
+
const result = options.target ?? {
|
|
80
|
+
level: currentLevel,
|
|
81
|
+
primitiveIndex,
|
|
82
|
+
screenCoverage: 0,
|
|
83
|
+
screenspaceVolume: new Vector3(),
|
|
84
|
+
centrality: 1,
|
|
85
|
+
};
|
|
86
|
+
result.level = currentLevel;
|
|
87
|
+
result.primitiveIndex = primitiveIndex;
|
|
88
|
+
result.screenCoverage = 0;
|
|
89
|
+
result.screenspaceVolume.set(0, 0, 0);
|
|
90
|
+
result.centrality = 1;
|
|
91
|
+
// Note: we intentionally do NOT early-return when there are no mesh LODs.
|
|
92
|
+
// The screen coverage / screenspace volume computed below is also consumed by
|
|
93
|
+
// the texture LOD selection, which must keep working for meshes that only have
|
|
94
|
+
// texture LODs (no mesh LODs). Only the mesh LOD-level selection loop is skipped.
|
|
95
|
+
let boundingBox = options.boundingBox ?? geometry.boundingBox;
|
|
96
|
+
if (!boundingBox) {
|
|
97
|
+
geometry.computeBoundingBox();
|
|
98
|
+
boundingBox = geometry.boundingBox;
|
|
99
|
+
}
|
|
100
|
+
if (!boundingBox)
|
|
101
|
+
return result;
|
|
102
|
+
_meshLODWorldBox.copy(boundingBox).applyMatrix4(matrixWorld);
|
|
103
|
+
if (camera.isPerspectiveCamera && isInsideProjectedBox(_meshLODWorldBox, projectionScreenMatrix)) {
|
|
104
|
+
result.level = 0;
|
|
105
|
+
result.screenCoverage = Infinity;
|
|
106
|
+
result.screenspaceVolume.set(Infinity, Infinity, Infinity);
|
|
107
|
+
return result;
|
|
108
|
+
}
|
|
109
|
+
_meshLODProjectedBox.copy(_meshLODWorldBox).applyMatrix4(projectionScreenMatrix);
|
|
110
|
+
if (xrEnabled && camera.isPerspectiveCamera && camera.fov > 70) {
|
|
111
|
+
const min = _meshLODProjectedBox.min;
|
|
112
|
+
const max = _meshLODProjectedBox.max;
|
|
113
|
+
let minX = min.x;
|
|
114
|
+
let minY = min.y;
|
|
115
|
+
let maxX = max.x;
|
|
116
|
+
let maxY = max.y;
|
|
117
|
+
const enlargementFactor = 2.0;
|
|
118
|
+
const centerBoost = 1.5;
|
|
119
|
+
const centerX = (min.x + max.x) * 0.5;
|
|
120
|
+
const centerY = (min.y + max.y) * 0.5;
|
|
121
|
+
minX = (minX - centerX) * enlargementFactor + centerX;
|
|
122
|
+
minY = (minY - centerY) * enlargementFactor + centerY;
|
|
123
|
+
maxX = (maxX - centerX) * enlargementFactor + centerX;
|
|
124
|
+
maxY = (maxY - centerY) * enlargementFactor + centerY;
|
|
125
|
+
const xCentrality = minX < 0 && maxX > 0 ? 0 : Math.min(Math.abs(min.x), Math.abs(max.x));
|
|
126
|
+
const yCentrality = minY < 0 && maxY > 0 ? 0 : Math.min(Math.abs(min.y), Math.abs(max.y));
|
|
127
|
+
const centrality = Math.max(xCentrality, yCentrality);
|
|
128
|
+
result.centrality = (centerBoost - centrality) * (centerBoost - centrality) * (centerBoost - centrality);
|
|
129
|
+
}
|
|
130
|
+
const boxSize = _meshLODProjectedBox.getSize(_meshLODBoxSize);
|
|
131
|
+
boxSize.multiplyScalar(0.5);
|
|
132
|
+
if (globalThis.screen?.availHeight > 0 && canvasHeight > 0) {
|
|
133
|
+
boxSize.multiplyScalar(canvasHeight / globalThis.screen.availHeight);
|
|
134
|
+
}
|
|
135
|
+
if (camera.isPerspectiveCamera) {
|
|
136
|
+
boxSize.x *= camera.aspect;
|
|
137
|
+
}
|
|
138
|
+
_meshLODCameraSpaceBox.copy(boundingBox).applyMatrix4(matrixWorld).applyMatrix4(camera.matrixWorldInverse);
|
|
139
|
+
const cameraSpaceSize = _meshLODCameraSpaceBox.getSize(_meshLODCameraSpaceBoxSize);
|
|
140
|
+
const screenMax = Math.max(boxSize.x, boxSize.y);
|
|
141
|
+
const cameraSpaceMax = Math.max(cameraSpaceSize.x, cameraSpaceSize.y);
|
|
142
|
+
if (screenMax !== 0 && cameraSpaceMax !== 0) {
|
|
143
|
+
boxSize.z = cameraSpaceSize.z / cameraSpaceMax * screenMax;
|
|
144
|
+
}
|
|
145
|
+
const screenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z) * result.centrality;
|
|
146
|
+
result.screenCoverage = screenCoverage;
|
|
147
|
+
result.screenspaceVolume.copy(boxSize);
|
|
148
|
+
if (screenCoverage <= 0)
|
|
149
|
+
return result;
|
|
150
|
+
if (debugDrawLine) {
|
|
151
|
+
const mat = _meshLODProjectionInverse.copy(projectionScreenMatrix);
|
|
152
|
+
mat.invert();
|
|
153
|
+
_meshLODCorner0.copy(_meshLODProjectedBox.min);
|
|
154
|
+
_meshLODCorner1.copy(_meshLODProjectedBox.max);
|
|
155
|
+
_meshLODCorner1.x = _meshLODCorner0.x;
|
|
156
|
+
_meshLODCorner2.copy(_meshLODProjectedBox.max);
|
|
157
|
+
_meshLODCorner2.y = _meshLODCorner0.y;
|
|
158
|
+
_meshLODCorner3.copy(_meshLODProjectedBox.max);
|
|
159
|
+
const z = (_meshLODCorner0.z + _meshLODCorner3.z) * 0.5;
|
|
160
|
+
_meshLODCorner0.z = _meshLODCorner1.z = _meshLODCorner2.z = _meshLODCorner3.z = z;
|
|
161
|
+
_meshLODCorner0.applyMatrix4(mat);
|
|
162
|
+
_meshLODCorner1.applyMatrix4(mat);
|
|
163
|
+
_meshLODCorner2.applyMatrix4(mat);
|
|
164
|
+
_meshLODCorner3.applyMatrix4(mat);
|
|
165
|
+
debugDrawLine(_meshLODCorner0, _meshLODCorner1, 0x0000ff);
|
|
166
|
+
debugDrawLine(_meshLODCorner0, _meshLODCorner2, 0x0000ff);
|
|
167
|
+
debugDrawLine(_meshLODCorner1, _meshLODCorner3, 0x0000ff);
|
|
168
|
+
debugDrawLine(_meshLODCorner2, _meshLODCorner3, 0x0000ff);
|
|
169
|
+
}
|
|
170
|
+
if (meshLods?.length) {
|
|
171
|
+
for (let i = 0; i < meshLods.length; i++) {
|
|
172
|
+
const lod = meshLods[i];
|
|
173
|
+
const density = lod.densities?.[primitiveIndex] || lod.density || .00001;
|
|
174
|
+
if (primitiveIndex > 0 && warnMissingPrimitiveDensities && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
|
|
175
|
+
globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
|
|
176
|
+
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.`);
|
|
177
|
+
}
|
|
178
|
+
if (density / screenCoverage < desiredDensity) {
|
|
179
|
+
result.level = i;
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return result;
|
|
185
|
+
}
|
|
16
186
|
/**
|
|
17
187
|
* 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.
|
|
18
188
|
* It must be enabled by calling the `enable` method.
|
|
@@ -59,6 +229,10 @@ export class LODsManager {
|
|
|
59
229
|
if (index >= 0)
|
|
60
230
|
plugins.splice(index, 1);
|
|
61
231
|
}
|
|
232
|
+
/** Read-only snapshot of the currently registered plugins, for inspection. Use {@link addPlugin} / {@link removePlugin} to modify the registry. */
|
|
233
|
+
static getPlugins() {
|
|
234
|
+
return plugins;
|
|
235
|
+
}
|
|
62
236
|
/**
|
|
63
237
|
* Gets the LODsManager for the given renderer. If the LODsManager does not exist yet, it will be created.
|
|
64
238
|
* @param renderer The renderer to get the LODsManager for.
|
|
@@ -117,7 +291,31 @@ export class LODsManager {
|
|
|
117
291
|
_newPromiseGroups = [];
|
|
118
292
|
_promiseGroupIds = 0;
|
|
119
293
|
/**
|
|
120
|
-
*
|
|
294
|
+
* Returns a promise that resolves once all LOD requests initiated during the next render cycles have finished loading.
|
|
295
|
+
* This is useful for hiding low-resolution placeholders (e.g. with a loading overlay or CSS blur) until high-quality assets are ready.
|
|
296
|
+
*
|
|
297
|
+
* By default, the returned promise captures LOD loading requests for 2 frames and resolves when all of them complete.
|
|
298
|
+
* Use `waitForFirstCapture` if no LOD requests may happen immediately (e.g. after a scene switch).
|
|
299
|
+
*
|
|
300
|
+
* @param opts - Optional configuration for how long to capture and what to wait for. See {@link PromiseGroupOptions}.
|
|
301
|
+
* @returns A promise that resolves with `{ cancelled, awaited_count, resolved_count }` once all captured LOD loads complete (or the signal aborts).
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```ts
|
|
305
|
+
* // Wait for initial LODs to finish loading, then remove a blur overlay
|
|
306
|
+
* const result = await lodsManager.awaitLoading({
|
|
307
|
+
* frames: 5,
|
|
308
|
+
* signal: AbortSignal.timeout(10_000),
|
|
309
|
+
* });
|
|
310
|
+
* console.log(`Loaded ${result.resolved_count} of ${result.awaited_count} LODs`);
|
|
311
|
+
* document.querySelector('.blur-overlay')?.remove();
|
|
312
|
+
* ```
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* ```ts
|
|
316
|
+
* // Wait until at least one LOD starts loading before resolving
|
|
317
|
+
* await lodsManager.awaitLoading({ waitForFirstCapture: true });
|
|
318
|
+
* ```
|
|
121
319
|
*/
|
|
122
320
|
awaitLoading(opts) {
|
|
123
321
|
const id = this._promiseGroupIds++;
|
|
@@ -137,6 +335,11 @@ export class LODsManager {
|
|
|
137
335
|
});
|
|
138
336
|
return newGroup.ready;
|
|
139
337
|
}
|
|
338
|
+
/** Track LOD work started outside this manager so {@link awaitLoading} waits for it too. */
|
|
339
|
+
trackLoadingPromise(type, object, promise) {
|
|
340
|
+
PromiseGroup.addPromise(type, object, promise, this._newPromiseGroups);
|
|
341
|
+
return promise;
|
|
342
|
+
}
|
|
140
343
|
_postprocessPromiseGroups() {
|
|
141
344
|
if (this._newPromiseGroups.length === 0)
|
|
142
345
|
return;
|
|
@@ -146,17 +349,46 @@ export class LODsManager {
|
|
|
146
349
|
}
|
|
147
350
|
}
|
|
148
351
|
_lodchangedlisteners = [];
|
|
352
|
+
/**
|
|
353
|
+
* Register a listener that is called whenever a mesh or texture LOD level has finished loading and has been applied.
|
|
354
|
+
* The listener receives the type of asset (`"mesh"` or `"texture"`), the new LOD level, and the affected object.
|
|
355
|
+
*
|
|
356
|
+
* @param evt - The event type. Currently only `"changed"` is supported.
|
|
357
|
+
* @param listener - Callback invoked after a LOD swap completes.
|
|
358
|
+
* @return A function to unregister the listener.
|
|
359
|
+
*
|
|
360
|
+
* @example
|
|
361
|
+
* ```ts
|
|
362
|
+
* lodsManager.addEventListener("changed", ({ type, level, object }) => {
|
|
363
|
+
* console.log(`${type} LOD changed to level ${level}`, object);
|
|
364
|
+
* });
|
|
365
|
+
* ```
|
|
366
|
+
*/
|
|
149
367
|
addEventListener(evt, listener) {
|
|
150
368
|
if (evt === "changed") {
|
|
151
369
|
this._lodchangedlisteners.push(listener);
|
|
370
|
+
return () => {
|
|
371
|
+
this.removeEventListener(evt, listener);
|
|
372
|
+
};
|
|
152
373
|
}
|
|
374
|
+
return () => { };
|
|
153
375
|
}
|
|
376
|
+
/**
|
|
377
|
+
* Remove a previously registered `"changed"` event listener.
|
|
378
|
+
* @param evt - The event type (`"changed"`).
|
|
379
|
+
* @param listener - The listener to remove.
|
|
380
|
+
* @return `true` if the listener was found and removed, `false` otherwise.
|
|
381
|
+
*/
|
|
154
382
|
removeEventListener(evt, listener) {
|
|
383
|
+
let removed = false;
|
|
155
384
|
if (evt === "changed") {
|
|
156
385
|
const index = this._lodchangedlisteners.indexOf(listener);
|
|
157
|
-
if (index >= 0)
|
|
386
|
+
if (index >= 0) {
|
|
158
387
|
this._lodchangedlisteners.splice(index, 1);
|
|
388
|
+
removed = true;
|
|
389
|
+
}
|
|
159
390
|
}
|
|
391
|
+
return removed;
|
|
160
392
|
}
|
|
161
393
|
// readonly plugins: NEEDLE_progressive_plugin[] = [];
|
|
162
394
|
constructor(renderer, context) {
|
|
@@ -172,11 +404,11 @@ export class LODsManager {
|
|
|
172
404
|
// })
|
|
173
405
|
}
|
|
174
406
|
#originalRender;
|
|
175
|
-
#clock = new Timer();
|
|
176
407
|
#frame = 0;
|
|
177
408
|
#delta = 0;
|
|
178
409
|
#time = 0;
|
|
179
410
|
#fps = 0;
|
|
411
|
+
#clock = createLODTimer();
|
|
180
412
|
_fpsBuffer = [60, 60, 60, 60, 60];
|
|
181
413
|
/**
|
|
182
414
|
* Enable the LODsManager. This will replace the render method of the renderer with a method that updates the LODs.
|
|
@@ -198,8 +430,8 @@ export class LODsManager {
|
|
|
198
430
|
if (renderTarget == null || ("isXRRenderTarget" in renderTarget && renderTarget.isXRRenderTarget)) {
|
|
199
431
|
stack = 0;
|
|
200
432
|
self.#frame += 1;
|
|
201
|
-
self.#clock.update();
|
|
202
|
-
self.#delta = self.#clock.getDelta();
|
|
433
|
+
self.#clock.update?.();
|
|
434
|
+
self.#delta = Math.max(self.#clock.getDelta(), 1 / 1000);
|
|
203
435
|
self.#time += self.#delta;
|
|
204
436
|
self._fpsBuffer.shift();
|
|
205
437
|
self._fpsBuffer.push(1 / self.#delta);
|
|
@@ -219,13 +451,30 @@ export class LODsManager {
|
|
|
219
451
|
this.renderer.render = this.#originalRender;
|
|
220
452
|
this.#originalRender = undefined;
|
|
221
453
|
}
|
|
454
|
+
/**
|
|
455
|
+
* Manually trigger a LOD update for a scene and camera.
|
|
456
|
+
* Only needed when {@link manual} is set to `true` — otherwise LOD updates happen automatically on each render call.
|
|
457
|
+
*
|
|
458
|
+
* @param scene - The scene containing objects with progressive LODs.
|
|
459
|
+
* @param camera - The camera used to determine screen coverage and LOD levels.
|
|
460
|
+
*
|
|
461
|
+
* @example
|
|
462
|
+
* ```ts
|
|
463
|
+
* const lodsManager = LODsManager.get(renderer);
|
|
464
|
+
* lodsManager.manual = true;
|
|
465
|
+
* // ... later, trigger an update at a specific point:
|
|
466
|
+
* lodsManager.update(scene, camera);
|
|
467
|
+
* ```
|
|
468
|
+
*/
|
|
222
469
|
update(scene, camera) {
|
|
223
470
|
this.internalUpdate(scene, camera);
|
|
224
471
|
}
|
|
225
472
|
onAfterRender(scene, camera, _stack) {
|
|
226
473
|
if (this.pause)
|
|
227
474
|
return;
|
|
228
|
-
const renderList = this.
|
|
475
|
+
const renderList = this.getRenderList(scene, camera, _stack);
|
|
476
|
+
if (!renderList)
|
|
477
|
+
return;
|
|
229
478
|
const opaque = renderList.opaque;
|
|
230
479
|
let updateLODs = true;
|
|
231
480
|
// check if we're rendering a postprocessing pass
|
|
@@ -282,7 +531,9 @@ export class LODsManager {
|
|
|
282
531
|
* Update LODs in a scene
|
|
283
532
|
*/
|
|
284
533
|
internalUpdate(scene, camera) {
|
|
285
|
-
const renderList = this.
|
|
534
|
+
const renderList = this.getRenderList(scene, camera, 0);
|
|
535
|
+
if (!renderList)
|
|
536
|
+
return;
|
|
286
537
|
const opaque = renderList.opaque;
|
|
287
538
|
this.projectionScreenMatrix.multiplyMatrices(camera.projectionMatrix, camera.matrixWorldInverse);
|
|
288
539
|
const desiredDensity = this.targetTriangleDensity;
|
|
@@ -308,16 +559,6 @@ export class LODsManager {
|
|
|
308
559
|
case "MeshDepthMaterial":
|
|
309
560
|
continue;
|
|
310
561
|
}
|
|
311
|
-
if (debugProgressiveLoading === "color") {
|
|
312
|
-
if (entry.material) {
|
|
313
|
-
if (!entry.object["progressive_debug_color"]) {
|
|
314
|
-
entry.object["progressive_debug_color"] = true;
|
|
315
|
-
const randomColor = Math.random() * 0xffffff;
|
|
316
|
-
const newMaterial = new MeshStandardMaterial({ color: randomColor });
|
|
317
|
-
entry.object.material = newMaterial;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
562
|
const object = entry.object;
|
|
322
563
|
if (object instanceof Mesh || (object.isMesh)) {
|
|
323
564
|
this.updateLODs(scene, camera, object, desiredDensity);
|
|
@@ -338,6 +579,30 @@ export class LODsManager {
|
|
|
338
579
|
}
|
|
339
580
|
}
|
|
340
581
|
}
|
|
582
|
+
getRenderList(scene, camera, stack) {
|
|
583
|
+
const renderer = this.renderer;
|
|
584
|
+
let renderList = null;
|
|
585
|
+
if (renderer.isWebGPURenderer === true) {
|
|
586
|
+
const renderLists = renderer._renderLists;
|
|
587
|
+
if (!renderLists)
|
|
588
|
+
return null;
|
|
589
|
+
renderList = renderLists.get(scene, camera);
|
|
590
|
+
}
|
|
591
|
+
else if (renderer.isWebGLRenderer === true) {
|
|
592
|
+
const renderLists = renderer.renderLists;
|
|
593
|
+
if (!renderLists)
|
|
594
|
+
return null;
|
|
595
|
+
renderList = renderLists.get(scene, stack);
|
|
596
|
+
}
|
|
597
|
+
if (!renderList)
|
|
598
|
+
return null;
|
|
599
|
+
return {
|
|
600
|
+
opaque: renderList.opaque || [],
|
|
601
|
+
transparent: renderList.transparent || [],
|
|
602
|
+
transmissive: renderList.transmissive || renderList.transparentDoublePass || [],
|
|
603
|
+
transparentDoublePass: renderList.transparentDoublePass || [],
|
|
604
|
+
};
|
|
605
|
+
}
|
|
341
606
|
/** Update the LOD levels for the renderer. */
|
|
342
607
|
updateLODs(scene, camera, object, desiredDensity) {
|
|
343
608
|
if (!object.userData) {
|
|
@@ -376,6 +641,9 @@ export class LODsManager {
|
|
|
376
641
|
if (debug && object.material && !object["isGizmo"]) {
|
|
377
642
|
applyDebugSettings(object.material);
|
|
378
643
|
}
|
|
644
|
+
if (debugProgressiveLODColors && object.material && !object["isGizmo"] && !object["isBatchedMesh"]) {
|
|
645
|
+
applyLODColor(object.material, levels.mesh_lod);
|
|
646
|
+
}
|
|
379
647
|
for (const plugin of plugins) {
|
|
380
648
|
plugin.onAfterUpdatedLOD?.(this.renderer, scene, camera, object, levels);
|
|
381
649
|
}
|
|
@@ -392,7 +660,7 @@ export class LODsManager {
|
|
|
392
660
|
return;
|
|
393
661
|
if (Array.isArray(material)) {
|
|
394
662
|
for (const mat of material) {
|
|
395
|
-
this.loadProgressiveTextures(mat, level);
|
|
663
|
+
this.loadProgressiveTextures(mat, level, overrideLodLevel);
|
|
396
664
|
}
|
|
397
665
|
return;
|
|
398
666
|
}
|
|
@@ -405,13 +673,15 @@ export class LODsManager {
|
|
|
405
673
|
else if (level < material[$currentLOD]) {
|
|
406
674
|
update = true;
|
|
407
675
|
}
|
|
408
|
-
|
|
676
|
+
const forceExactTextureLOD = overrideLodLevel !== undefined && overrideLodLevel >= 0;
|
|
677
|
+
if (forceExactTextureLOD) {
|
|
409
678
|
update = material[$currentLOD] != overrideLodLevel;
|
|
410
679
|
level = overrideLodLevel;
|
|
411
680
|
}
|
|
412
681
|
if (update) {
|
|
413
682
|
material[$currentLOD] = level;
|
|
414
|
-
const
|
|
683
|
+
const options = forceExactTextureLOD ? { force: true } : undefined;
|
|
684
|
+
const promise = NEEDLE_progressive.assignTextureLOD(material, level, options).then(_ => {
|
|
415
685
|
this._lodchangedlisteners.forEach(l => l({ type: "texture", level, object: material }));
|
|
416
686
|
});
|
|
417
687
|
PromiseGroup.addPromise("texture", material, promise, this._newPromiseGroups);
|
|
@@ -448,25 +718,7 @@ export class LODsManager {
|
|
|
448
718
|
}
|
|
449
719
|
// private testIfLODLevelsAreAvailable() {
|
|
450
720
|
_sphere = new Sphere();
|
|
451
|
-
_tempBox = new Box3();
|
|
452
|
-
_tempBox2 = new Box3();
|
|
453
|
-
tempMatrix = new Matrix4();
|
|
454
721
|
_tempWorldPosition = new Vector3();
|
|
455
|
-
_tempBoxSize = new Vector3();
|
|
456
|
-
_tempBox2Size = new Vector3();
|
|
457
|
-
static corner0 = new Vector3();
|
|
458
|
-
static corner1 = new Vector3();
|
|
459
|
-
static corner2 = new Vector3();
|
|
460
|
-
static corner3 = new Vector3();
|
|
461
|
-
static _tempPtInside = new Vector3();
|
|
462
|
-
static isInside(box, matrix) {
|
|
463
|
-
const min = box.min;
|
|
464
|
-
const max = box.max;
|
|
465
|
-
const centerx = (min.x + max.x) * 0.5;
|
|
466
|
-
const centery = (min.y + max.y) * 0.5;
|
|
467
|
-
const pt1 = this._tempPtInside.set(centerx, centery, min.z).applyMatrix4(matrix);
|
|
468
|
-
return pt1.z < 0;
|
|
469
|
-
}
|
|
470
722
|
static skinnedMeshBoundsFrameOffsetCounter = 0;
|
|
471
723
|
static $skinnedMeshBoundsOffset = Symbol("gltf-progressive-skinnedMeshBoundsOffset");
|
|
472
724
|
// #region calculateLodLevel
|
|
@@ -538,7 +790,6 @@ export class LODsManager {
|
|
|
538
790
|
boundingBox = skinnedMesh.boundingBox;
|
|
539
791
|
}
|
|
540
792
|
if (boundingBox) {
|
|
541
|
-
const cam = camera;
|
|
542
793
|
// hack: if the mesh has vertex colors, has less than 100 vertices we always select the highest LOD
|
|
543
794
|
if (mesh.geometry.attributes.color && mesh.geometry.attributes.color.count < 100) {
|
|
544
795
|
if (mesh.geometry.boundingSphere) {
|
|
@@ -552,126 +803,30 @@ export class LODsManager {
|
|
|
552
803
|
}
|
|
553
804
|
}
|
|
554
805
|
}
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
806
|
+
const selection = calculateMeshLODLevel({
|
|
807
|
+
geometry: mesh.geometry,
|
|
808
|
+
matrixWorld: mesh.matrixWorld,
|
|
809
|
+
camera,
|
|
810
|
+
projectionScreenMatrix: this.projectionScreenMatrix,
|
|
811
|
+
desiredDensity,
|
|
812
|
+
canvasHeight,
|
|
813
|
+
currentLevel: state.lastLodLevel_Mesh,
|
|
814
|
+
boundingBox,
|
|
815
|
+
xrEnabled: this.renderer.xr.enabled,
|
|
816
|
+
debugDrawLine: debugProgressiveLoading ? LODsManager.debugDrawLine : undefined,
|
|
817
|
+
warnMissingPrimitiveDensities: true,
|
|
818
|
+
});
|
|
819
|
+
state.lastCentrality = selection.centrality;
|
|
820
|
+
state.lastScreenCoverage = selection.screenCoverage;
|
|
821
|
+
state.lastScreenspaceVolume.copy(selection.screenspaceVolume);
|
|
822
|
+
if (selection.screenCoverage === Infinity) {
|
|
566
823
|
result.mesh_lod = 0;
|
|
567
824
|
result.texture_lod = 0;
|
|
568
825
|
return;
|
|
569
826
|
}
|
|
570
|
-
|
|
571
|
-
// TODO might need to be adjusted for cameras that are rendered during an XR session but are
|
|
572
|
-
// actually not XR cameras (e.g. a render texture)
|
|
573
|
-
if (this.renderer.xr.enabled && (cam.isPerspectiveCamera) && cam.fov > 70) {
|
|
574
|
-
// calculate centrality of the bounding box - how close is it to the screen center
|
|
575
|
-
const min = this._tempBox.min;
|
|
576
|
-
const max = this._tempBox.max;
|
|
577
|
-
let minX = min.x;
|
|
578
|
-
let minY = min.y;
|
|
579
|
-
let maxX = max.x;
|
|
580
|
-
let maxY = max.y;
|
|
581
|
-
// enlarge
|
|
582
|
-
const enlargementFactor = 2.0;
|
|
583
|
-
const centerBoost = 1.5;
|
|
584
|
-
const centerX = (min.x + max.x) * 0.5;
|
|
585
|
-
const centerY = (min.y + max.y) * 0.5;
|
|
586
|
-
minX = (minX - centerX) * enlargementFactor + centerX;
|
|
587
|
-
minY = (minY - centerY) * enlargementFactor + centerY;
|
|
588
|
-
maxX = (maxX - centerX) * enlargementFactor + centerX;
|
|
589
|
-
maxY = (maxY - centerY) * enlargementFactor + centerY;
|
|
590
|
-
const xCentrality = minX < 0 && maxX > 0 ? 0 : Math.min(Math.abs(min.x), Math.abs(max.x));
|
|
591
|
-
const yCentrality = minY < 0 && maxY > 0 ? 0 : Math.min(Math.abs(min.y), Math.abs(max.y));
|
|
592
|
-
const centrality = Math.max(xCentrality, yCentrality);
|
|
593
|
-
// heuristically determined to lower quality for objects at the edges of vision
|
|
594
|
-
state.lastCentrality = (centerBoost - centrality) * (centerBoost - centrality) * (centerBoost - centrality);
|
|
595
|
-
}
|
|
596
|
-
else {
|
|
597
|
-
state.lastCentrality = 1;
|
|
598
|
-
}
|
|
599
|
-
const boxSize = this._tempBox.getSize(this._tempBoxSize);
|
|
600
|
-
boxSize.multiplyScalar(0.5); // goes from -1..1, we want -0.5..0.5 for coverage in percent
|
|
601
|
-
if (screen.availHeight > 0) {
|
|
602
|
-
// correct for size of context on screen
|
|
603
|
-
if (canvasHeight > 0)
|
|
604
|
-
boxSize.multiplyScalar(canvasHeight / screen.availHeight);
|
|
605
|
-
}
|
|
606
|
-
if (camera.isPerspectiveCamera) {
|
|
607
|
-
boxSize.x *= camera.aspect;
|
|
608
|
-
}
|
|
609
|
-
else if (camera.isOrthographicCamera) {
|
|
610
|
-
// const cam = camera as OrthographicCamera;
|
|
611
|
-
// boxSize.x *= cam.zoom * .01;
|
|
612
|
-
}
|
|
613
|
-
const matView = camera.matrixWorldInverse;
|
|
614
|
-
const box2 = this._tempBox2;
|
|
615
|
-
box2.copy(boundingBox);
|
|
616
|
-
box2.applyMatrix4(mesh.matrixWorld);
|
|
617
|
-
box2.applyMatrix4(matView);
|
|
618
|
-
const boxSize2 = box2.getSize(this._tempBox2Size);
|
|
619
|
-
// approximate depth coverage in relation to screenspace size
|
|
620
|
-
const max2 = Math.max(boxSize2.x, boxSize2.y);
|
|
621
|
-
const max1 = Math.max(boxSize.x, boxSize.y);
|
|
622
|
-
if (max1 != 0 && max2 != 0)
|
|
623
|
-
boxSize.z = boxSize2.z / Math.max(boxSize2.x, boxSize2.y) * Math.max(boxSize.x, boxSize.y);
|
|
624
|
-
state.lastScreenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z);
|
|
625
|
-
state.lastScreenspaceVolume.copy(boxSize);
|
|
626
|
-
state.lastScreenCoverage *= state.lastCentrality;
|
|
627
|
-
// draw screen size box
|
|
628
|
-
if (debugProgressiveLoading && LODsManager.debugDrawLine) {
|
|
629
|
-
const mat = this.tempMatrix.copy(this.projectionScreenMatrix);
|
|
630
|
-
mat.invert();
|
|
631
|
-
const corner0 = LODsManager.corner0;
|
|
632
|
-
const corner1 = LODsManager.corner1;
|
|
633
|
-
const corner2 = LODsManager.corner2;
|
|
634
|
-
const corner3 = LODsManager.corner3;
|
|
635
|
-
// get box corners, transform with camera space, and draw as quad lines
|
|
636
|
-
corner0.copy(this._tempBox.min);
|
|
637
|
-
corner1.copy(this._tempBox.max);
|
|
638
|
-
corner1.x = corner0.x;
|
|
639
|
-
corner2.copy(this._tempBox.max);
|
|
640
|
-
corner2.y = corner0.y;
|
|
641
|
-
corner3.copy(this._tempBox.max);
|
|
642
|
-
// draw outlines at the center of the box
|
|
643
|
-
const z = (corner0.z + corner3.z) * 0.5;
|
|
644
|
-
// all outlines should have the same depth in screen space
|
|
645
|
-
corner0.z = corner1.z = corner2.z = corner3.z = z;
|
|
646
|
-
corner0.applyMatrix4(mat);
|
|
647
|
-
corner1.applyMatrix4(mat);
|
|
648
|
-
corner2.applyMatrix4(mat);
|
|
649
|
-
corner3.applyMatrix4(mat);
|
|
650
|
-
LODsManager.debugDrawLine(corner0, corner1, 0x0000ff);
|
|
651
|
-
LODsManager.debugDrawLine(corner0, corner2, 0x0000ff);
|
|
652
|
-
LODsManager.debugDrawLine(corner1, corner3, 0x0000ff);
|
|
653
|
-
LODsManager.debugDrawLine(corner2, corner3, 0x0000ff);
|
|
654
|
-
}
|
|
655
|
-
let expectedLevel = 999;
|
|
656
|
-
// const framerate = this.context.time.smoothedFps;
|
|
657
|
-
if (mesh_lods && state.lastScreenCoverage > 0) {
|
|
658
|
-
for (let l = 0; l < mesh_lods.length; l++) {
|
|
659
|
-
const lod = mesh_lods[l];
|
|
660
|
-
const densityForThisLevel = lod.densities?.[primitive_index] || lod.density || .00001;
|
|
661
|
-
const resultingDensity = densityForThisLevel / state.lastScreenCoverage;
|
|
662
|
-
if (primitive_index > 0 && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
|
|
663
|
-
window["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
|
|
664
|
-
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.`);
|
|
665
|
-
}
|
|
666
|
-
if (resultingDensity < desiredDensity) {
|
|
667
|
-
expectedLevel = l;
|
|
668
|
-
break;
|
|
669
|
-
}
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
const isLowerLod = expectedLevel < mesh_level;
|
|
827
|
+
const isLowerLod = selection.level >= 0 && selection.level < mesh_level;
|
|
673
828
|
if (isLowerLod) {
|
|
674
|
-
mesh_level =
|
|
829
|
+
mesh_level = selection.level;
|
|
675
830
|
mesh_level_calculated = true;
|
|
676
831
|
}
|
|
677
832
|
}
|
|
@@ -708,7 +863,8 @@ export class LODsManager {
|
|
|
708
863
|
if (this.context?.engine === "model-viewer") {
|
|
709
864
|
factor *= 1.5;
|
|
710
865
|
}
|
|
711
|
-
const
|
|
866
|
+
const devicePixelRatio = this.renderer.getPixelRatio?.() || globalThis.devicePixelRatio || 1;
|
|
867
|
+
const screenSize = canvasHeight / devicePixelRatio;
|
|
712
868
|
const pixelSizeOnScreen = screenSize * factor;
|
|
713
869
|
let foundLod = false;
|
|
714
870
|
for (let i = texture_lods_minmax.lods.length - 1; i >= 0; i--) {
|
|
@@ -747,3 +903,21 @@ class LOD_state {
|
|
|
747
903
|
lastScreenspaceVolume = new Vector3();
|
|
748
904
|
lastCentrality = 0;
|
|
749
905
|
}
|
|
906
|
+
function applyLODColor(material, level) {
|
|
907
|
+
if (level < 0)
|
|
908
|
+
return;
|
|
909
|
+
if (Array.isArray(material)) {
|
|
910
|
+
for (const mat of material) {
|
|
911
|
+
applyLODColor(mat, level);
|
|
912
|
+
}
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
if ("color" in material && material.color instanceof Color) {
|
|
916
|
+
material.color.copy(getLODColor(level, debugLODColor));
|
|
917
|
+
material.needsUpdate = true;
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
export function getLODColor(level, target) {
|
|
921
|
+
const index = Math.max(0, Math.min(lodDebugColors.length - 1, Math.floor(level)));
|
|
922
|
+
return target.setHex(lodDebugColors[index]);
|
|
923
|
+
}
|
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;
|