@needle-tools/gltf-progressive 3.6.0-alpha.1 → 3.6.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/extension.js CHANGED
@@ -209,7 +209,7 @@ export class NEEDLE_progressive {
209
209
  * });
210
210
  * ```
211
211
  */
212
- static assignMeshLOD(mesh, level) {
212
+ static assignMeshLOD(mesh, level, options) {
213
213
  if (!mesh)
214
214
  return Promise.resolve(null);
215
215
  if (mesh instanceof Mesh || mesh.isMesh === true) {
@@ -223,21 +223,32 @@ export class NEEDLE_progressive {
223
223
  }
224
224
  // const info = this.onProgressiveLoadStart(context, source, mesh, null);
225
225
  mesh["LOD:requested level"] = level;
226
- return NEEDLE_progressive.getOrLoadLOD(currentGeometry, level).then(geo => {
226
+ const shouldLoad = () => mesh["LOD:requested level"] === level || this.shouldApplyStaleMeshLOD(mesh, level);
227
+ return NEEDLE_progressive.getOrLoadLOD(currentGeometry, level, {
228
+ isCurrent: shouldLoad,
229
+ }).then(geo => {
227
230
  if (Array.isArray(geo)) {
228
231
  const index = lodinfo.index || 0;
229
232
  geo = geo[index];
230
233
  }
231
- if (mesh["LOD:requested level"] === level) {
232
- delete mesh["LOD:requested level"];
234
+ const isLatestRequest = mesh["LOD:requested level"] === level;
235
+ if (isLatestRequest || this.shouldApplyStaleMeshLOD(mesh, level)) {
236
+ if (isLatestRequest) {
237
+ delete mesh["LOD:requested level"];
238
+ }
233
239
  if (geo && currentGeometry != geo) {
234
240
  const isGeometry = geo?.isBufferGeometry;
235
241
  // if (debug == "verbose") console.log("Progressive Mesh " + mesh.name + " loaded", currentGeometry, "→", geo, "\n", mesh)
236
- if (isGeometry) {
237
- mesh.geometry = geo;
242
+ if (!isGeometry) {
243
+ if (debug) {
244
+ console.error("Invalid LOD geometry", geo);
245
+ }
238
246
  }
239
- else if (debug) {
240
- console.error("Invalid LOD geometry", geo);
247
+ else if (typeof options?.apply === "function") {
248
+ options.apply(geo, level, mesh);
249
+ }
250
+ else if (options?.apply !== false) {
251
+ mesh.geometry = geo;
241
252
  }
242
253
  }
243
254
  }
@@ -368,7 +379,14 @@ export class NEEDLE_progressive {
368
379
  return pending.promise;
369
380
  }
370
381
  }
371
- const promise = NEEDLE_progressive.getOrLoadLOD(current, level).then(tex => {
382
+ const requestId = material && slot ? this.nextTextureSlotRequestId(material, slot, level, force) : 0;
383
+ const isCurrentRequest = () => !material || !slot || this.getLatestTextureSlotRequest(material, slot)?.id === requestId;
384
+ const shouldLoad = () => isCurrentRequest() || this.shouldApplyStaleTextureSlotLOD(material, slot, level, force);
385
+ const promise = NEEDLE_progressive.getOrLoadLOD(current, level, {
386
+ isCurrent: shouldLoad,
387
+ }).then(tex => {
388
+ if (!isCurrentRequest() && !this.shouldApplyStaleTextureSlotLOD(material, slot, level, force))
389
+ return null;
372
390
  // this can currently not happen
373
391
  if (Array.isArray(tex)) {
374
392
  console.warn("Progressive: Got an array of textures for a texture slot, this should not happen...");
@@ -410,7 +428,7 @@ export class NEEDLE_progressive {
410
428
  return null;
411
429
  });
412
430
  if (material && slot) {
413
- this.setPendingTextureSlotRequest(material, slot, level, force, promise);
431
+ this.setPendingTextureSlotRequest(material, slot, level, force, requestId, promise);
414
432
  }
415
433
  return promise;
416
434
  }
@@ -418,6 +436,8 @@ export class NEEDLE_progressive {
418
436
  // referenced by many slots and should only be disposed after every slot moved away.
419
437
  static trackedTextureSlots = new WeakMap();
420
438
  static pendingTextureSlotRequests = new WeakMap();
439
+ static latestTextureSlotRequests = new WeakMap();
440
+ static textureSlotRequestId = 0;
421
441
  static trackCurrentMaterialTextureSlots(material) {
422
442
  if (material.uniforms && (material.isRawShaderMaterial || material.isShaderMaterial === true)) {
423
443
  const shaderMaterial = material;
@@ -439,17 +459,52 @@ export class NEEDLE_progressive {
439
459
  static getPendingTextureSlotRequest(material, slot) {
440
460
  return this.pendingTextureSlotRequests.get(material)?.get(slot);
441
461
  }
442
- static setPendingTextureSlotRequest(material, slot, level, force, promise) {
462
+ static nextTextureSlotRequestId(material, slot, level, force) {
463
+ let slots = this.latestTextureSlotRequests.get(material);
464
+ if (!slots) {
465
+ slots = new Map();
466
+ this.latestTextureSlotRequests.set(material, slots);
467
+ }
468
+ const id = ++this.textureSlotRequestId;
469
+ slots.set(slot, { id, level, force });
470
+ return id;
471
+ }
472
+ static getLatestTextureSlotRequest(material, slot) {
473
+ return this.latestTextureSlotRequests.get(material)?.get(slot);
474
+ }
475
+ static shouldApplyStaleTextureSlotLOD(material, slot, level, force) {
476
+ if (!material || !slot)
477
+ return false;
478
+ const latest = this.getLatestTextureSlotRequest(material, slot);
479
+ const assigned = this.getMaterialTextureSlot(material, slot);
480
+ const assignedLODLevel = this.getAssignedLODInformation(assigned)?.level ?? Infinity;
481
+ if (level >= assignedLODLevel)
482
+ return false;
483
+ if (force) {
484
+ if (!latest)
485
+ return false;
486
+ return level >= latest.level;
487
+ }
488
+ return true;
489
+ }
490
+ static shouldApplyStaleMeshLOD(mesh, level) {
491
+ const latestLevel = mesh["LOD:requested level"];
492
+ if (typeof latestLevel !== "number")
493
+ return false;
494
+ const assignedLODLevel = this.getAssignedLODInformation(mesh.geometry)?.level ?? Infinity;
495
+ return level < assignedLODLevel && level >= latestLevel;
496
+ }
497
+ static setPendingTextureSlotRequest(material, slot, level, force, id, promise) {
443
498
  let slots = this.pendingTextureSlotRequests.get(material);
444
499
  if (!slots) {
445
500
  slots = new Map();
446
501
  this.pendingTextureSlotRequests.set(material, slots);
447
502
  }
448
- const request = { level, force, promise };
503
+ const request = { level, force, id, promise };
449
504
  slots.set(slot, request);
450
505
  promise.finally(() => {
451
506
  const current = slots.get(slot);
452
- if (current === request) {
507
+ if (current?.id === id) {
453
508
  slots.delete(slot);
454
509
  }
455
510
  });
@@ -766,6 +821,8 @@ export class NEEDLE_progressive {
766
821
  this.textureRefCounts.clear();
767
822
  this.trackedTextureSlots = new WeakMap();
768
823
  this.pendingTextureSlotRequests = new WeakMap();
824
+ this.latestTextureSlotRequests = new WeakMap();
825
+ this.textureSlotRequestId = 0;
769
826
  }
770
827
  }
771
828
  /** Dispose a single cache entry's three.js resource(s) to free GPU memory. */
@@ -884,7 +941,7 @@ export class NEEDLE_progressive {
884
941
  }
885
942
  static workers = [];
886
943
  static _workersIndex = 0;
887
- static async getOrLoadLOD(current, level) {
944
+ static async getOrLoadLOD(current, level, options) {
888
945
  const debugverbose = debug == "verbose";
889
946
  /** this key is used to lookup the LOD information */
890
947
  const LOD = this.getAssignedLODInformation(current);
@@ -957,7 +1014,17 @@ export class NEEDLE_progressive {
957
1014
  const cached = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
958
1015
  if (cached.found)
959
1016
  return cached.value;
1017
+ if (options?.isCurrent?.() === false) {
1018
+ if (debugverbose)
1019
+ console.log(`Skipping stale LOD ${level} request before queue: ${lod_url}`);
1020
+ return null;
1021
+ }
960
1022
  const slot = await this.queue.slot(lod_url);
1023
+ if (options?.isCurrent?.() === false) {
1024
+ if (debugverbose)
1025
+ console.log(`Skipping stale LOD ${level} request after queue: ${lod_url}`);
1026
+ return null;
1027
+ }
961
1028
  // Another request can fill the cache while this one waits for a queue slot.
962
1029
  // Re-checking here avoids duplicate loads for heavily instanced assets.
963
1030
  const cachedAfterQueue = await this.tryResolveLODCacheEntry(this.cache.get(KEY), KEY, lod_url, current, level, debugverbose);
@@ -1138,10 +1205,19 @@ export class NEEDLE_progressive {
1138
1205
  }
1139
1206
  else {
1140
1207
  if (current instanceof Texture) {
1208
+ if (options?.isCurrent?.() === false) {
1209
+ if (debugverbose)
1210
+ console.log(`Skipping stale texture LOD ${level} request: ${lod_url}`);
1211
+ return null;
1212
+ }
1141
1213
  if (debugverbose)
1142
1214
  console.log("Load texture from uri: " + lod_url);
1143
1215
  const loader = new TextureLoader();
1144
1216
  const tex = await loader.loadAsync(lod_url);
1217
+ if (options?.isCurrent?.() === false) {
1218
+ tex?.dispose();
1219
+ return null;
1220
+ }
1145
1221
  if (tex) {
1146
1222
  tex.guid = lodInfo.guid;
1147
1223
  tex.flipY = false;
package/lib/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  export { version as VERSION } from "./version.js";
2
2
  export * from "./extension.js";
3
3
  export * from "./plugins/index.js";
4
- export { LODsManager, getLODColor, lodDebugColors, type LOD_Results } from "./lods.manager.js";
4
+ export { LODsManager, calculateMeshLODLevel, getLODColor, lodDebugColors, type LOD_Results, type MeshLODSelectionOptions, type MeshLODSelectionResult } from "./lods.manager.js";
5
5
  export { setDracoDecoderLocation, setKTX2TranscoderLocation, createLoaders, addDracoAndKTX2Loaders, configureLoader } from "./loaders.js";
6
6
  export { getRaycastMesh, registerRaycastMesh, useRaycastMeshes } from "./utils.js";
7
7
  import { WebGLRenderer } from "three";
package/lib/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  export { version as VERSION } from "./version.js";
2
2
  export * from "./extension.js";
3
3
  export * from "./plugins/index.js";
4
- export { LODsManager, getLODColor, lodDebugColors } from "./lods.manager.js";
4
+ export { LODsManager, calculateMeshLODLevel, getLODColor, lodDebugColors } from "./lods.manager.js";
5
5
  export { setDracoDecoderLocation, setKTX2TranscoderLocation, createLoaders, addDracoAndKTX2Loaders, configureLoader } from "./loaders.js";
6
6
  export { getRaycastMesh, registerRaycastMesh, useRaycastMeshes } from "./utils.js";
7
7
  import { addDracoAndKTX2Loaders, configureLoader, createLoaders } from "./loaders.js";
@@ -1,4 +1,4 @@
1
- import { Camera, Color, Material, Object3D, Scene, Texture, Vector3, WebGLRenderer } from "three";
1
+ import { Box3, BufferGeometry, Camera, Color, Material, Matrix4, Object3D, Scene, Texture, Vector3, WebGLRenderer } from "three";
2
2
  import { NEEDLE_progressive_plugin } from "./plugins/plugin.js";
3
3
  import { PromiseGroupOptions } from "./lods.promise.js";
4
4
  export type LODManagerContext = {
@@ -9,6 +9,28 @@ export declare type LOD_Results = {
9
9
  texture_lod: number;
10
10
  };
11
11
  export declare const lodDebugColors: number[];
12
+ export type MeshLODSelectionOptions = {
13
+ geometry: BufferGeometry;
14
+ matrixWorld: Matrix4;
15
+ camera: Camera;
16
+ projectionScreenMatrix: Matrix4;
17
+ desiredDensity: number;
18
+ canvasHeight?: number;
19
+ currentLevel?: number;
20
+ boundingBox?: Box3 | null;
21
+ xrEnabled?: boolean;
22
+ debugDrawLine?: (a: Vector3, b: Vector3, color: number) => void;
23
+ warnMissingPrimitiveDensities?: boolean;
24
+ target?: MeshLODSelectionResult;
25
+ };
26
+ export type MeshLODSelectionResult = {
27
+ level: number;
28
+ primitiveIndex: number;
29
+ screenCoverage: number;
30
+ screenspaceVolume: Vector3;
31
+ centrality: number;
32
+ };
33
+ export declare function calculateMeshLODLevel(options: MeshLODSelectionOptions): MeshLODSelectionResult;
12
34
  declare type LODChangedEventListener = (args: {
13
35
  type: "mesh" | "texture";
14
36
  level: number;
@@ -130,6 +152,8 @@ export declare class LODsManager {
130
152
  awaited_count: number;
131
153
  resolved_count: number;
132
154
  }>;
155
+ /** Track LOD work started outside this manager so {@link awaitLoading} waits for it too. */
156
+ trackLoadingPromise<T>(type: "mesh" | "texture", object: object, promise: Promise<T>): Promise<T>;
133
157
  private _postprocessPromiseGroups;
134
158
  private readonly _lodchangedlisteners;
135
159
  /**
@@ -199,18 +223,7 @@ export declare class LODsManager {
199
223
  */
200
224
  private loadProgressiveMeshes;
201
225
  private readonly _sphere;
202
- private readonly _tempBox;
203
- private readonly _tempBox2;
204
- private readonly tempMatrix;
205
226
  private readonly _tempWorldPosition;
206
- private readonly _tempBoxSize;
207
- private readonly _tempBox2Size;
208
- private static corner0;
209
- private static corner1;
210
- private static corner2;
211
- private static corner3;
212
- private static readonly _tempPtInside;
213
- private static isInside;
214
227
  private static skinnedMeshBoundsFrameOffsetCounter;
215
228
  private static $skinnedMeshBoundsOffset;
216
229
  private calculateLodLevel;
@@ -48,6 +48,131 @@ export const lodDebugColors = [
48
48
  0x4d908e,
49
49
  0x555555,
50
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
+ }
51
176
  /**
52
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.
53
178
  * It must be enabled by calling the `enable` method.
@@ -196,6 +321,11 @@ export class LODsManager {
196
321
  });
197
322
  return newGroup.ready;
198
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
+ }
199
329
  _postprocessPromiseGroups() {
200
330
  if (this._newPromiseGroups.length === 0)
201
331
  return;
@@ -545,25 +675,7 @@ export class LODsManager {
545
675
  }
546
676
  // private testIfLODLevelsAreAvailable() {
547
677
  _sphere = new Sphere();
548
- _tempBox = new Box3();
549
- _tempBox2 = new Box3();
550
- tempMatrix = new Matrix4();
551
678
  _tempWorldPosition = new Vector3();
552
- _tempBoxSize = new Vector3();
553
- _tempBox2Size = new Vector3();
554
- static corner0 = new Vector3();
555
- static corner1 = new Vector3();
556
- static corner2 = new Vector3();
557
- static corner3 = new Vector3();
558
- static _tempPtInside = new Vector3();
559
- static isInside(box, matrix) {
560
- const min = box.min;
561
- const max = box.max;
562
- const centerx = (min.x + max.x) * 0.5;
563
- const centery = (min.y + max.y) * 0.5;
564
- const pt1 = this._tempPtInside.set(centerx, centery, min.z).applyMatrix4(matrix);
565
- return pt1.z < 0;
566
- }
567
679
  static skinnedMeshBoundsFrameOffsetCounter = 0;
568
680
  static $skinnedMeshBoundsOffset = Symbol("gltf-progressive-skinnedMeshBoundsOffset");
569
681
  // #region calculateLodLevel
@@ -635,7 +747,6 @@ export class LODsManager {
635
747
  boundingBox = skinnedMesh.boundingBox;
636
748
  }
637
749
  if (boundingBox) {
638
- const cam = camera;
639
750
  // hack: if the mesh has vertex colors, has less than 100 vertices we always select the highest LOD
640
751
  if (mesh.geometry.attributes.color && mesh.geometry.attributes.color.count < 100) {
641
752
  if (mesh.geometry.boundingSphere) {
@@ -649,126 +760,30 @@ export class LODsManager {
649
760
  }
650
761
  }
651
762
  }
652
- // calculate size on screen
653
- this._tempBox.copy(boundingBox);
654
- this._tempBox.applyMatrix4(mesh.matrixWorld);
655
- // Converting into projection space has the disadvantage that objects further to the side
656
- // will have a much larger coverage, especially with high-field-of-view situations like in VR.
657
- // Alternatively, we could attempt to calculate angular coverage (some kind of polar coordinates maybe?)
658
- // or introduce a correction factor based on "expected distortion" of the object.
659
- // High distortions would lead to lower LOD levels.
660
- // "Centrality" of the calculated screen-space bounding box could be a factor here –
661
- // what's the distance of the bounding box to the center of the screen?
662
- if (cam.isPerspectiveCamera && LODsManager.isInside(this._tempBox, this.projectionScreenMatrix)) {
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) {
663
780
  result.mesh_lod = 0;
664
781
  result.texture_lod = 0;
665
782
  return;
666
783
  }
667
- this._tempBox.applyMatrix4(this.projectionScreenMatrix);
668
- // TODO might need to be adjusted for cameras that are rendered during an XR session but are
669
- // actually not XR cameras (e.g. a render texture)
670
- if (this.renderer.xr.enabled && (cam.isPerspectiveCamera) && cam.fov > 70) {
671
- // calculate centrality of the bounding box - how close is it to the screen center
672
- const min = this._tempBox.min;
673
- const max = this._tempBox.max;
674
- let minX = min.x;
675
- let minY = min.y;
676
- let maxX = max.x;
677
- let maxY = max.y;
678
- // enlarge
679
- const enlargementFactor = 2.0;
680
- const centerBoost = 1.5;
681
- const centerX = (min.x + max.x) * 0.5;
682
- const centerY = (min.y + max.y) * 0.5;
683
- minX = (minX - centerX) * enlargementFactor + centerX;
684
- minY = (minY - centerY) * enlargementFactor + centerY;
685
- maxX = (maxX - centerX) * enlargementFactor + centerX;
686
- maxY = (maxY - centerY) * enlargementFactor + centerY;
687
- const xCentrality = minX < 0 && maxX > 0 ? 0 : Math.min(Math.abs(min.x), Math.abs(max.x));
688
- const yCentrality = minY < 0 && maxY > 0 ? 0 : Math.min(Math.abs(min.y), Math.abs(max.y));
689
- const centrality = Math.max(xCentrality, yCentrality);
690
- // heuristically determined to lower quality for objects at the edges of vision
691
- state.lastCentrality = (centerBoost - centrality) * (centerBoost - centrality) * (centerBoost - centrality);
692
- }
693
- else {
694
- state.lastCentrality = 1;
695
- }
696
- const boxSize = this._tempBox.getSize(this._tempBoxSize);
697
- boxSize.multiplyScalar(0.5); // goes from -1..1, we want -0.5..0.5 for coverage in percent
698
- if (screen.availHeight > 0) {
699
- // correct for size of context on screen
700
- if (canvasHeight > 0)
701
- boxSize.multiplyScalar(canvasHeight / screen.availHeight);
702
- }
703
- if (camera.isPerspectiveCamera) {
704
- boxSize.x *= camera.aspect;
705
- }
706
- else if (camera.isOrthographicCamera) {
707
- // const cam = camera as OrthographicCamera;
708
- // boxSize.x *= cam.zoom * .01;
709
- }
710
- const matView = camera.matrixWorldInverse;
711
- const box2 = this._tempBox2;
712
- box2.copy(boundingBox);
713
- box2.applyMatrix4(mesh.matrixWorld);
714
- box2.applyMatrix4(matView);
715
- const boxSize2 = box2.getSize(this._tempBox2Size);
716
- // approximate depth coverage in relation to screenspace size
717
- const max2 = Math.max(boxSize2.x, boxSize2.y);
718
- const max1 = Math.max(boxSize.x, boxSize.y);
719
- if (max1 != 0 && max2 != 0)
720
- boxSize.z = boxSize2.z / Math.max(boxSize2.x, boxSize2.y) * Math.max(boxSize.x, boxSize.y);
721
- state.lastScreenCoverage = Math.max(boxSize.x, boxSize.y, boxSize.z);
722
- state.lastScreenspaceVolume.copy(boxSize);
723
- state.lastScreenCoverage *= state.lastCentrality;
724
- // draw screen size box
725
- if (debugProgressiveLoading && LODsManager.debugDrawLine) {
726
- const mat = this.tempMatrix.copy(this.projectionScreenMatrix);
727
- mat.invert();
728
- const corner0 = LODsManager.corner0;
729
- const corner1 = LODsManager.corner1;
730
- const corner2 = LODsManager.corner2;
731
- const corner3 = LODsManager.corner3;
732
- // get box corners, transform with camera space, and draw as quad lines
733
- corner0.copy(this._tempBox.min);
734
- corner1.copy(this._tempBox.max);
735
- corner1.x = corner0.x;
736
- corner2.copy(this._tempBox.max);
737
- corner2.y = corner0.y;
738
- corner3.copy(this._tempBox.max);
739
- // draw outlines at the center of the box
740
- const z = (corner0.z + corner3.z) * 0.5;
741
- // all outlines should have the same depth in screen space
742
- corner0.z = corner1.z = corner2.z = corner3.z = z;
743
- corner0.applyMatrix4(mat);
744
- corner1.applyMatrix4(mat);
745
- corner2.applyMatrix4(mat);
746
- corner3.applyMatrix4(mat);
747
- LODsManager.debugDrawLine(corner0, corner1, 0x0000ff);
748
- LODsManager.debugDrawLine(corner0, corner2, 0x0000ff);
749
- LODsManager.debugDrawLine(corner1, corner3, 0x0000ff);
750
- LODsManager.debugDrawLine(corner2, corner3, 0x0000ff);
751
- }
752
- let expectedLevel = 999;
753
- // const framerate = this.context.time.smoothedFps;
754
- if (mesh_lods && state.lastScreenCoverage > 0) {
755
- for (let l = 0; l < mesh_lods.length; l++) {
756
- const lod = mesh_lods[l];
757
- const densityForThisLevel = lod.densities?.[primitive_index] || lod.density || .00001;
758
- const resultingDensity = densityForThisLevel / state.lastScreenCoverage;
759
- if (primitive_index > 0 && isDevelopmentServer() && !lod.densities && !globalThis["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"]) {
760
- window["NEEDLE:MISSING_LOD_PRIMITIVE_DENSITIES"] = true;
761
- 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.`);
762
- }
763
- if (resultingDensity < desiredDensity) {
764
- expectedLevel = l;
765
- break;
766
- }
767
- }
768
- }
769
- const isLowerLod = expectedLevel < mesh_level;
784
+ const isLowerLod = selection.level >= 0 && selection.level < mesh_level;
770
785
  if (isLowerLod) {
771
- mesh_level = expectedLevel;
786
+ mesh_level = selection.level;
772
787
  mesh_level_calculated = true;
773
788
  }
774
789
  }
package/lib/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // replaced at build time
2
- export const version = "3.6.0-alpha.1";
2
+ export const version = "3.6.0-alpha.3";
3
3
  globalThis["GLTF_PROGRESSIVE_VERSION"] = version;
4
4
  console.debug(`[gltf-progressive] version ${version || "-"}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@needle-tools/gltf-progressive",
3
- "version": "3.6.0-alpha.1",
3
+ "version": "3.6.0-alpha.3",
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": {