Skip to content

Commit 677ca4c

Browse files
authored
Merge pull request #10509 from CraigFeldspar/lod-screen-coverage
LOD screen coverage proposal
2 parents 95398ee + 89834bc commit 677ca4c

File tree

8 files changed

+375
-57
lines changed

8 files changed

+375
-57
lines changed

Playground/scenes/msft-lod.gltf

Lines changed: 222 additions & 0 deletions
Large diffs are not rendered by default.

dist/preview release/what's new.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
- Added support for more uv sets to glTF loader. ([bghgary](https://github.com/bghgary))
5454
- Added support for KHR_materials_volume for glTF loader. ([MiiBond](https://github.com/MiiBond/))
5555
- Added support for custom timeout in WebRequest. ([jamidwyer](https://github.com/jamidwyer/))
56+
- Improved support for MSFT_lod, now LOD levels are loaded and accurately displayed according to screen coverage ([CraigFeldspar](https://github.com/CraigFeldspar))
5657
- Added support for direct loading [base64 data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) for all loader ([CoPrez](https://github.com/CoPrez))
5758

5859
### Navigation

loaders/src/glTF/2.0/Extensions/MSFT_lod.ts

Lines changed: 80 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@ import { Deferred } from "babylonjs/Misc/deferred";
44
import { Material } from "babylonjs/Materials/material";
55
import { TransformNode } from "babylonjs/Meshes/transformNode";
66
import { Mesh } from "babylonjs/Meshes/mesh";
7-
import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
7+
import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
88
import { INode, IMaterial, IBuffer, IScene } from "../glTFLoaderInterfaces";
99
import { IGLTFLoaderExtension } from "../glTFLoaderExtension";
1010
import { GLTFLoader, ArrayItem } from "../glTFLoader";
11-
import { IProperty, IMSFTLOD } from 'babylonjs-gltf2interface';
11+
import { IProperty, IMSFTLOD } from "babylonjs-gltf2interface";
1212

1313
const NAME = "MSFT_lod";
1414

@@ -153,6 +153,11 @@ export class MSFT_lod implements IGLTFLoaderExtension {
153153

154154
const nodeLODs = this._getLODs(extensionContext, node, this._loader.gltf.nodes, extension.ids);
155155
this._loader.logOpen(`${extensionContext}`);
156+
const transformNodes: Nullable<TransformNode>[] = [];
157+
158+
for (let indexLOD = 0; indexLOD < nodeLODs.length; indexLOD++) {
159+
transformNodes.push(null);
160+
}
156161

157162
for (let indexLOD = 0; indexLOD < nodeLODs.length; indexLOD++) {
158163
const nodeLOD = nodeLODs[indexLOD];
@@ -162,9 +167,39 @@ export class MSFT_lod implements IGLTFLoaderExtension {
162167
this._nodeSignalLODs[indexLOD] = this._nodeSignalLODs[indexLOD] || new Deferred();
163168
}
164169

165-
const assign = (babylonTransformNode: TransformNode) => { babylonTransformNode.setEnabled(false); };
166-
const promise = this._loader.loadNodeAsync(`/nodes/${nodeLOD.index}`, nodeLOD, assign).then((babylonMesh) => {
167-
if (indexLOD !== 0) {
170+
const assign = (babylonTransformNode: TransformNode, index: number) => {
171+
babylonTransformNode.setEnabled(false);
172+
transformNodes[index] = babylonTransformNode;
173+
174+
let fullArray = true;
175+
for (let i = 0; i < transformNodes.length; i++) {
176+
if (!transformNodes[i]) {
177+
fullArray = false;
178+
}
179+
}
180+
181+
const lod0 = transformNodes[transformNodes.length - 1];
182+
if (fullArray && lod0) {
183+
const screenCoverages = lod0.metadata?.gltf?.extras?.MSFT_screencoverage as any[];
184+
185+
if (screenCoverages && screenCoverages.length) {
186+
screenCoverages.reverse();
187+
(lod0 as Mesh).useLODScreenCoverage = true;
188+
for (let i = 0; i < transformNodes.length - 1; i++) {
189+
(lod0 as Mesh).addLODLevel(screenCoverages[i + 1], transformNodes[i] as Mesh);
190+
}
191+
if (screenCoverages[0] > 0) {
192+
// Adding empty LOD
193+
(lod0 as Mesh).addLODLevel(screenCoverages[0], null);
194+
}
195+
}
196+
}
197+
};
198+
199+
const promise = this._loader.loadNodeAsync(`/nodes/${nodeLOD.index}`, nodeLOD, (node: TransformNode) => assign(node, indexLOD)).then((babylonMesh) => {
200+
const screenCoverages = (nodeLODs[nodeLODs.length - 1]._babylonTransformNode as Mesh).metadata?.gltf?.extras?.MSFT_screencoverage;
201+
202+
if (indexLOD !== 0 && !screenCoverages) {
168203
// TODO: should not rely on _babylonTransformNode
169204
const previousNodeLOD = nodeLODs[indexLOD - 1];
170205
if (previousNodeLOD._babylonTransformNode) {
@@ -181,8 +216,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {
181216

182217
if (indexLOD === 0) {
183218
firstPromise = promise;
184-
}
185-
else {
219+
} else {
186220
this._nodeIndexLOD = null;
187221
this._nodePromiseLODs[indexLOD].push(promise);
188222
}
@@ -194,7 +228,13 @@ export class MSFT_lod implements IGLTFLoaderExtension {
194228
}
195229

196230
/** @hidden */
197-
public _loadMaterialAsync(context: string, material: IMaterial, babylonMesh: Nullable<Mesh>, babylonDrawMode: number, assign: (babylonMaterial: Material) => void): Nullable<Promise<Material>> {
231+
public _loadMaterialAsync(
232+
context: string,
233+
material: IMaterial,
234+
babylonMesh: Nullable<Mesh>,
235+
babylonDrawMode: number,
236+
assign: (babylonMaterial: Material) => void
237+
): Nullable<Promise<Material>> {
198238
// Don't load material LODs if already loading a node LOD.
199239
if (this._nodeIndexLOD) {
200240
return null;
@@ -213,31 +253,32 @@ export class MSFT_lod implements IGLTFLoaderExtension {
213253
this._materialIndexLOD = indexLOD;
214254
}
215255

216-
const promise = this._loader._loadMaterialAsync(`/materials/${materialLOD.index}`, materialLOD, babylonMesh, babylonDrawMode, (babylonMaterial) => {
217-
if (indexLOD === 0) {
218-
assign(babylonMaterial);
219-
}
220-
}).then((babylonMaterial) => {
221-
if (indexLOD !== 0) {
222-
assign(babylonMaterial);
223-
224-
// TODO: should not rely on _data
225-
const previousDataLOD = materialLODs[indexLOD - 1]._data!;
226-
if (previousDataLOD[babylonDrawMode]) {
227-
this._disposeMaterials([previousDataLOD[babylonDrawMode].babylonMaterial]);
228-
delete previousDataLOD[babylonDrawMode];
256+
const promise = this._loader
257+
._loadMaterialAsync(`/materials/${materialLOD.index}`, materialLOD, babylonMesh, babylonDrawMode, (babylonMaterial) => {
258+
if (indexLOD === 0) {
259+
assign(babylonMaterial);
260+
}
261+
})
262+
.then((babylonMaterial) => {
263+
if (indexLOD !== 0) {
264+
assign(babylonMaterial);
265+
266+
// TODO: should not rely on _data
267+
const previousDataLOD = materialLODs[indexLOD - 1]._data!;
268+
if (previousDataLOD[babylonDrawMode]) {
269+
this._disposeMaterials([previousDataLOD[babylonDrawMode].babylonMaterial]);
270+
delete previousDataLOD[babylonDrawMode];
271+
}
229272
}
230-
}
231273

232-
return babylonMaterial;
233-
});
274+
return babylonMaterial;
275+
});
234276

235277
this._materialPromiseLODs[indexLOD] = this._materialPromiseLODs[indexLOD] || [];
236278

237279
if (indexLOD === 0) {
238280
firstPromise = promise;
239-
}
240-
else {
281+
} else {
241282
this._materialIndexLOD = null;
242283
this._materialPromiseLODs[indexLOD].push(promise);
243284
}
@@ -258,8 +299,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {
258299
return this._nodeSignalLODs[this._nodeIndexLOD - 1].promise.then(() => {
259300
return this._loader.loadUriAsync(context, property, uri);
260301
});
261-
}
262-
else if (this._materialIndexLOD !== null) {
302+
} else if (this._materialIndexLOD !== null) {
263303
this._loader.log(`deferred`);
264304
const previousIndexLOD = this._materialIndexLOD - 1;
265305
this._materialSignalLODs[previousIndexLOD] = this._materialSignalLODs[previousIndexLOD] || new Deferred<void>();
@@ -285,8 +325,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {
285325
if (bufferLOD) {
286326
bufferLOD.start = Math.min(bufferLOD.start, start);
287327
bufferLOD.end = Math.max(bufferLOD.end, end);
288-
}
289-
else {
328+
} else {
290329
bufferLOD = { start: start, end: end, loaded: new Deferred() };
291330
bufferLODs[indexLOD] = bufferLOD;
292331
}
@@ -300,11 +339,9 @@ export class MSFT_lod implements IGLTFLoaderExtension {
300339

301340
if (this._nodeIndexLOD !== null) {
302341
return loadAsync(this._nodeBufferLODs, this._nodeIndexLOD);
303-
}
304-
else if (this._materialIndexLOD !== null) {
342+
} else if (this._materialIndexLOD !== null) {
305343
return loadAsync(this._materialBufferLODs, this._materialIndexLOD);
306-
}
307-
else {
344+
} else {
308345
return loadAsync(this._bufferLODs, 0);
309346
}
310347
}
@@ -316,11 +353,14 @@ export class MSFT_lod implements IGLTFLoaderExtension {
316353
const bufferLOD = bufferLODs[indexLOD];
317354
if (bufferLOD) {
318355
this._loader.log(`Loading buffer range [${bufferLOD.start}-${bufferLOD.end}]`);
319-
this._loader.bin!.readAsync(bufferLOD.start, bufferLOD.end - bufferLOD.start + 1).then((data) => {
320-
bufferLOD.loaded.resolve(data);
321-
}, (error) => {
322-
bufferLOD.loaded.reject(error);
323-
});
356+
this._loader.bin!.readAsync(bufferLOD.start, bufferLOD.end - bufferLOD.start + 1).then(
357+
(data) => {
358+
bufferLOD.loaded.resolve(data);
359+
},
360+
(error) => {
361+
bufferLOD.loaded.reject(error);
362+
}
363+
);
324364
}
325365
}
326366

@@ -388,4 +428,4 @@ export class MSFT_lod implements IGLTFLoaderExtension {
388428
}
389429
}
390430

391-
GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_lod(loader));
431+
GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_lod(loader));

src/Cameras/camera.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,31 @@ export class Camera extends Node {
135135
return this._upVector;
136136
}
137137

138+
/**
139+
* The screen area in scene units squared
140+
*/
141+
public get screenArea(): number {
142+
let x = 0;
143+
let y = 0;
144+
if (this.mode === Camera.PERSPECTIVE_CAMERA) {
145+
if (this.fovMode === Camera.FOVMODE_VERTICAL_FIXED) {
146+
y = this.minZ * 2 * Math.tan(this.fov / 2);
147+
x = this.getEngine().getAspectRatio(this) * y;
148+
} else {
149+
x = this.minZ * 2 * Math.tan(this.fov / 2);
150+
y = x / this.getEngine().getAspectRatio(this);
151+
}
152+
} else {
153+
const halfWidth = this.getEngine().getRenderWidth() / 2.0;
154+
const halfHeight = this.getEngine().getRenderHeight() / 2.0;
155+
156+
x = (this.orthoRight ?? halfWidth) - (this.orthoLeft ?? -halfWidth);
157+
y = (this.orthoTop ?? halfHeight) - (this.orthoBottom ?? -halfHeight);
158+
}
159+
160+
return x * y;
161+
}
162+
138163
/**
139164
* Define the current limit on the left side for an orthographic camera
140165
* In scene unit

src/Meshes/mesh.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,8 @@ class _InternalMeshDataInfo {
130130

131131
public _preActivateId: number = -1;
132132
public _LODLevels = new Array<MeshLODLevel>();
133-
133+
/** Alternative definition of LOD level, using screen coverage instead of distance */
134+
public _useLODScreenCoverage: boolean = false;
134135
public _checkReadinessObserver: Nullable<Observer<Scene>>;
135136

136137
public _onMeshReadyObserverAdded: (observer: Observer<Mesh>) => void;
@@ -246,6 +247,17 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
246247
// Internal data
247248
private _internalMeshDataInfo = new _InternalMeshDataInfo();
248249

250+
/**
251+
* Determines if the LOD levels are intended to be calculated using screen coverage (surface area ratio) instead of distance
252+
*/
253+
public get useLODScreenCoverage() {
254+
return this._internalMeshDataInfo._useLODScreenCoverage;
255+
}
256+
257+
public set useLODScreenCoverage(value: boolean) {
258+
this._internalMeshDataInfo._useLODScreenCoverage = value;
259+
}
260+
249261
/**
250262
* Will notify when the mesh is completely ready, including materials.
251263
* Observers added to this observable will be removed once triggered
@@ -784,12 +796,13 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
784796
}
785797

786798
private _sortLODLevels(): void {
799+
const sortingOrderFactor = this._internalMeshDataInfo._useLODScreenCoverage ? -1 : 1;
787800
this._internalMeshDataInfo._LODLevels.sort((a, b) => {
788-
if (a.distance < b.distance) {
789-
return 1;
801+
if (a.distanceOrScreenCoverage < b.distanceOrScreenCoverage) {
802+
return sortingOrderFactor;
790803
}
791-
if (a.distance > b.distance) {
792-
return -1;
804+
if (a.distanceOrScreenCoverage > b.distanceOrScreenCoverage) {
805+
return -sortingOrderFactor;
793806
}
794807

795808
return 0;
@@ -799,17 +812,18 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
799812
/**
800813
* Add a mesh as LOD level triggered at the given distance.
801814
* @see https://doc.babylonjs.com/how_to/how_to_use_lod
802-
* @param distance The distance from the center of the object to show this level
815+
* @param distanceOrScreenCoverage Either distance from the center of the object to show this level or the screen coverage if `useScreenCoverage` is set to `true`.
816+
* If screen coverage, value is a fraction of the screen's total surface, between 0 and 1.
803817
* @param mesh The mesh to be added as LOD level (can be null)
804818
* @return This mesh (for chaining)
805819
*/
806-
public addLODLevel(distance: number, mesh: Nullable<Mesh>): Mesh {
820+
public addLODLevel(distanceOrScreenCoverage: number, mesh: Nullable<Mesh>): Mesh {
807821
if (mesh && mesh._masterMesh) {
808822
Logger.Warn("You cannot use a mesh as LOD level twice");
809823
return this;
810824
}
811825

812-
var level = new MeshLODLevel(distance, mesh);
826+
var level = new MeshLODLevel(distanceOrScreenCoverage, mesh);
813827
this._internalMeshDataInfo._LODLevels.push(level);
814828

815829
if (mesh) {
@@ -832,7 +846,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
832846
for (var index = 0; index < internalDataInfo._LODLevels.length; index++) {
833847
var level = internalDataInfo._LODLevels[index];
834848

835-
if (level.distance === distance) {
849+
if (level.distanceOrScreenCoverage === distance) {
836850
return level.mesh;
837851
}
838852
}
@@ -884,18 +898,29 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
884898
}
885899

886900
var distanceToCamera = bSphere.centerWorld.subtract(camera.globalPosition).length();
901+
const useScreenCoverage = internalDataInfo._useLODScreenCoverage;
902+
let compareValue = distanceToCamera;
903+
let compareSign = 1;
904+
905+
if (useScreenCoverage) {
906+
const screenArea = camera.screenArea;
907+
let meshArea = bSphere.radiusWorld * camera.minZ / distanceToCamera;
908+
meshArea = meshArea * meshArea * Math.PI;
909+
compareValue = meshArea / screenArea;
910+
compareSign = -1;
911+
}
887912

888-
if (internalDataInfo._LODLevels[internalDataInfo._LODLevels.length - 1].distance > distanceToCamera) {
913+
if (compareSign * internalDataInfo._LODLevels[internalDataInfo._LODLevels.length - 1].distanceOrScreenCoverage > compareSign * compareValue) {
889914
if (this.onLODLevelSelection) {
890-
this.onLODLevelSelection(distanceToCamera, this, this);
915+
this.onLODLevelSelection(compareValue, this, this);
891916
}
892917
return this;
893918
}
894919

895920
for (var index = 0; index < internalDataInfo._LODLevels.length; index++) {
896921
var level = internalDataInfo._LODLevels[index];
897922

898-
if (level.distance < distanceToCamera) {
923+
if (compareSign * level.distanceOrScreenCoverage < compareSign * compareValue) {
899924
if (level.mesh) {
900925
if (level.mesh.delayLoadState === Constants.DELAYLOADSTATE_NOTLOADED) {
901926
level.mesh._checkDelayState();
@@ -911,15 +936,15 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
911936
}
912937

913938
if (this.onLODLevelSelection) {
914-
this.onLODLevelSelection(distanceToCamera, this, level.mesh);
939+
this.onLODLevelSelection(compareValue, this, level.mesh);
915940
}
916941

917942
return level.mesh;
918943
}
919944
}
920945

921946
if (this.onLODLevelSelection) {
922-
this.onLODLevelSelection(distanceToCamera, this, this);
947+
this.onLODLevelSelection(compareValue, this, this);
923948
}
924949
return this;
925950
}

src/Meshes/meshLODLevel.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ import { Nullable } from '../types';
88
export class MeshLODLevel {
99
/**
1010
* Creates a new LOD level
11-
* @param distance defines the distance where this level should star being displayed
11+
* @param distanceOrScreenCoverage defines either the distance or the screen coverage where this level should start being displayed
1212
* @param mesh defines the mesh to use to render this level
1313
*/
1414
constructor(
15-
/** Defines the distance where this level should start being displayed */
16-
public distance: number,
15+
/** Either distance from the center of the object to show this level or the screen coverage if `useLODScreenCoverage` is set to `true` on the mesh*/
16+
public distanceOrScreenCoverage: number,
1717
/** Defines the mesh to use to render this level */
1818
public mesh: Nullable<Mesh>) {
1919
}
Loading

0 commit comments

Comments
 (0)