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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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.2";
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.2",
4
4
  "description": "three.js support for loading glTF or GLB files that contain progressive loading data",
5
5
  "homepage": "https://needle.tools",
6
6
  "author": {