@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.
Files changed (40) hide show
  1. package/CHANGELOG.md +21 -1
  2. package/LICENSE +21 -0
  3. package/README.md +47 -11
  4. package/examples/modelviewer-multiple.html +4 -4
  5. package/examples/modelviewer.html +4 -4
  6. package/examples/offscreen/index.html +15 -0
  7. package/examples/offscreen/main.js +98 -0
  8. package/examples/react-three-fiber/index.html +2 -2
  9. package/examples/react-three-fiber/package-lock.json +482 -484
  10. package/examples/react-three-fiber/package.json +4 -4
  11. package/examples/react-three-fiber/src/App.tsx +76 -21
  12. package/examples/react-three-fiber/src/styles.css +2 -4
  13. package/examples/react-three-fiber/vite.config.js +2 -2
  14. package/examples/shared/example-utils.js +297 -0
  15. package/examples/shared/example.css +44 -0
  16. package/examples/shared/runtime.js +34 -0
  17. package/examples/threejs/index.html +6 -10
  18. package/examples/threejs/main.js +5 -23
  19. package/examples/webgpu/index.html +15 -0
  20. package/examples/webgpu/main.js +105 -0
  21. package/examples/worker-rendering/index.html +15 -0
  22. package/examples/worker-rendering/main.js +109 -0
  23. package/examples/worker-rendering/worker.js +166 -0
  24. package/gltf-progressive.js +1025 -669
  25. package/gltf-progressive.min.js +9 -9
  26. package/gltf-progressive.umd.cjs +9 -9
  27. package/lib/extension.d.ts +73 -7
  28. package/lib/extension.js +351 -111
  29. package/lib/index.d.ts +1 -1
  30. package/lib/index.js +1 -1
  31. package/lib/loaders.js +17 -3
  32. package/lib/lods.debug.js +2 -2
  33. package/lib/lods.manager.d.ts +93 -15
  34. package/lib/lods.manager.js +331 -157
  35. package/lib/lods.promise.js +1 -1
  36. package/lib/plugins/modelviewer.js +1 -1
  37. package/lib/utils.internal.js +20 -6
  38. package/lib/version.js +1 -1
  39. package/lib/worker/loader.mainthread.js +4 -1
  40. package/package.json +10 -3
@@ -1,5 +1,5 @@
1
- // @ts-ignore Timer exists at runtime in r183 but @types/three doesn't export it from main entry yet
2
- import { Box3, Matrix4, Mesh, MeshStandardMaterial, Sphere, Timer, Vector3 } from "three";
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
- * Call to await LODs loading during the next render cycle.
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.renderer.renderLists.get(scene, 0);
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.renderer.renderLists.get(scene, 0);
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
- if (overrideLodLevel !== undefined && overrideLodLevel >= 0) {
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 promise = NEEDLE_progressive.assignTextureLOD(material, level).then(_ => {
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
- // calculate size on screen
556
- this._tempBox.copy(boundingBox);
557
- this._tempBox.applyMatrix4(mesh.matrixWorld);
558
- // Converting into projection space has the disadvantage that objects further to the side
559
- // will have a much larger coverage, especially with high-field-of-view situations like in VR.
560
- // Alternatively, we could attempt to calculate angular coverage (some kind of polar coordinates maybe?)
561
- // or introduce a correction factor based on "expected distortion" of the object.
562
- // High distortions would lead to lower LOD levels.
563
- // "Centrality" of the calculated screen-space bounding box could be a factor here –
564
- // what's the distance of the bounding box to the center of the screen?
565
- if (cam.isPerspectiveCamera && LODsManager.isInside(this._tempBox, this.projectionScreenMatrix)) {
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
- this._tempBox.applyMatrix4(this.projectionScreenMatrix);
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 = expectedLevel;
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 screenSize = canvasHeight / window.devicePixelRatio;
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
+ }
@@ -77,7 +77,7 @@ export class PromiseGroup {
77
77
  }
78
78
  if (this._maxPromisesPerObject >= 1) {
79
79
  if (this._seen.has(object)) {
80
- let count = this._seen.get(object);
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
- let interval = setInterval(() => {
111
+ const interval = setInterval(() => {
112
112
  if (forcedFrames++ > 5) {
113
113
  clearInterval(interval);
114
114
  return;