Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
222 changes: 222 additions & 0 deletions Playground/scenes/msft-lod.gltf

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions dist/preview release/what's new.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
- Added support for more uv sets to glTF loader. ([bghgary](https://github.com/bghgary))
- Added support for KHR_materials_volume for glTF loader. ([MiiBond](https://github.com/MiiBond/))
- Added support for custom timeout in WebRequest. ([jamidwyer](https://github.com/jamidwyer/))
- Improved support for MSFT_lod, now LOD levels are loaded and accurately displayed according to screen coverage ([CraigFeldspar](https://github.com/CraigFeldspar))
- 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))

### Navigation
Expand Down
120 changes: 80 additions & 40 deletions loaders/src/glTF/2.0/Extensions/MSFT_lod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import { Deferred } from "babylonjs/Misc/deferred";
import { Material } from "babylonjs/Materials/material";
import { TransformNode } from "babylonjs/Meshes/transformNode";
import { Mesh } from "babylonjs/Meshes/mesh";
import { BaseTexture } from 'babylonjs/Materials/Textures/baseTexture';
import { BaseTexture } from "babylonjs/Materials/Textures/baseTexture";
import { INode, IMaterial, IBuffer, IScene } from "../glTFLoaderInterfaces";
import { IGLTFLoaderExtension } from "../glTFLoaderExtension";
import { GLTFLoader, ArrayItem } from "../glTFLoader";
import { IProperty, IMSFTLOD } from 'babylonjs-gltf2interface';
import { IProperty, IMSFTLOD } from "babylonjs-gltf2interface";

const NAME = "MSFT_lod";

Expand Down Expand Up @@ -153,6 +153,11 @@ export class MSFT_lod implements IGLTFLoaderExtension {

const nodeLODs = this._getLODs(extensionContext, node, this._loader.gltf.nodes, extension.ids);
this._loader.logOpen(`${extensionContext}`);
const transformNodes: Nullable<TransformNode>[] = [];

for (let indexLOD = 0; indexLOD < nodeLODs.length; indexLOD++) {
transformNodes.push(null);
}

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

const assign = (babylonTransformNode: TransformNode) => { babylonTransformNode.setEnabled(false); };
const promise = this._loader.loadNodeAsync(`/nodes/${nodeLOD.index}`, nodeLOD, assign).then((babylonMesh) => {
if (indexLOD !== 0) {
const assign = (babylonTransformNode: TransformNode, index: number) => {
babylonTransformNode.setEnabled(false);
transformNodes[index] = babylonTransformNode;

let fullArray = true;
for (let i = 0; i < transformNodes.length; i++) {
if (!transformNodes[i]) {
fullArray = false;
}
}

const lod0 = transformNodes[transformNodes.length - 1];
if (fullArray && lod0) {
const screenCoverages = lod0.metadata?.gltf?.extras?.MSFT_screencoverage as any[];

if (screenCoverages && screenCoverages.length) {
screenCoverages.reverse();
(lod0 as Mesh).useLODScreenCoverage = true;
for (let i = 0; i < transformNodes.length - 1; i++) {
(lod0 as Mesh).addLODLevel(screenCoverages[i + 1], transformNodes[i] as Mesh);
}
if (screenCoverages[0] > 0) {
// Adding empty LOD
(lod0 as Mesh).addLODLevel(screenCoverages[0], null);
}
}
}
};

const promise = this._loader.loadNodeAsync(`/nodes/${nodeLOD.index}`, nodeLOD, (node: TransformNode) => assign(node, indexLOD)).then((babylonMesh) => {
const screenCoverages = (nodeLODs[nodeLODs.length - 1]._babylonTransformNode as Mesh).metadata?.gltf?.extras?.MSFT_screencoverage;

if (indexLOD !== 0 && !screenCoverages) {
// TODO: should not rely on _babylonTransformNode
const previousNodeLOD = nodeLODs[indexLOD - 1];
if (previousNodeLOD._babylonTransformNode) {
Expand All @@ -181,8 +216,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {

if (indexLOD === 0) {
firstPromise = promise;
}
else {
} else {
this._nodeIndexLOD = null;
this._nodePromiseLODs[indexLOD].push(promise);
}
Expand All @@ -194,7 +228,13 @@ export class MSFT_lod implements IGLTFLoaderExtension {
}

/** @hidden */
public _loadMaterialAsync(context: string, material: IMaterial, babylonMesh: Nullable<Mesh>, babylonDrawMode: number, assign: (babylonMaterial: Material) => void): Nullable<Promise<Material>> {
public _loadMaterialAsync(
context: string,
material: IMaterial,
babylonMesh: Nullable<Mesh>,
babylonDrawMode: number,
assign: (babylonMaterial: Material) => void
): Nullable<Promise<Material>> {
// Don't load material LODs if already loading a node LOD.
if (this._nodeIndexLOD) {
return null;
Expand All @@ -213,31 +253,32 @@ export class MSFT_lod implements IGLTFLoaderExtension {
this._materialIndexLOD = indexLOD;
}

const promise = this._loader._loadMaterialAsync(`/materials/${materialLOD.index}`, materialLOD, babylonMesh, babylonDrawMode, (babylonMaterial) => {
if (indexLOD === 0) {
assign(babylonMaterial);
}
}).then((babylonMaterial) => {
if (indexLOD !== 0) {
assign(babylonMaterial);

// TODO: should not rely on _data
const previousDataLOD = materialLODs[indexLOD - 1]._data!;
if (previousDataLOD[babylonDrawMode]) {
this._disposeMaterials([previousDataLOD[babylonDrawMode].babylonMaterial]);
delete previousDataLOD[babylonDrawMode];
const promise = this._loader
._loadMaterialAsync(`/materials/${materialLOD.index}`, materialLOD, babylonMesh, babylonDrawMode, (babylonMaterial) => {
if (indexLOD === 0) {
assign(babylonMaterial);
}
})
.then((babylonMaterial) => {
if (indexLOD !== 0) {
assign(babylonMaterial);

// TODO: should not rely on _data
const previousDataLOD = materialLODs[indexLOD - 1]._data!;
if (previousDataLOD[babylonDrawMode]) {
this._disposeMaterials([previousDataLOD[babylonDrawMode].babylonMaterial]);
delete previousDataLOD[babylonDrawMode];
}
}
}

return babylonMaterial;
});
return babylonMaterial;
});

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

if (indexLOD === 0) {
firstPromise = promise;
}
else {
} else {
this._materialIndexLOD = null;
this._materialPromiseLODs[indexLOD].push(promise);
}
Expand All @@ -258,8 +299,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {
return this._nodeSignalLODs[this._nodeIndexLOD - 1].promise.then(() => {
return this._loader.loadUriAsync(context, property, uri);
});
}
else if (this._materialIndexLOD !== null) {
} else if (this._materialIndexLOD !== null) {
this._loader.log(`deferred`);
const previousIndexLOD = this._materialIndexLOD - 1;
this._materialSignalLODs[previousIndexLOD] = this._materialSignalLODs[previousIndexLOD] || new Deferred<void>();
Expand All @@ -285,8 +325,7 @@ export class MSFT_lod implements IGLTFLoaderExtension {
if (bufferLOD) {
bufferLOD.start = Math.min(bufferLOD.start, start);
bufferLOD.end = Math.max(bufferLOD.end, end);
}
else {
} else {
bufferLOD = { start: start, end: end, loaded: new Deferred() };
bufferLODs[indexLOD] = bufferLOD;
}
Expand All @@ -300,11 +339,9 @@ export class MSFT_lod implements IGLTFLoaderExtension {

if (this._nodeIndexLOD !== null) {
return loadAsync(this._nodeBufferLODs, this._nodeIndexLOD);
}
else if (this._materialIndexLOD !== null) {
} else if (this._materialIndexLOD !== null) {
return loadAsync(this._materialBufferLODs, this._materialIndexLOD);
}
else {
} else {
return loadAsync(this._bufferLODs, 0);
}
}
Expand All @@ -316,11 +353,14 @@ export class MSFT_lod implements IGLTFLoaderExtension {
const bufferLOD = bufferLODs[indexLOD];
if (bufferLOD) {
this._loader.log(`Loading buffer range [${bufferLOD.start}-${bufferLOD.end}]`);
this._loader.bin!.readAsync(bufferLOD.start, bufferLOD.end - bufferLOD.start + 1).then((data) => {
bufferLOD.loaded.resolve(data);
}, (error) => {
bufferLOD.loaded.reject(error);
});
this._loader.bin!.readAsync(bufferLOD.start, bufferLOD.end - bufferLOD.start + 1).then(
(data) => {
bufferLOD.loaded.resolve(data);
},
(error) => {
bufferLOD.loaded.reject(error);
}
);
}
}

Expand Down Expand Up @@ -388,4 +428,4 @@ export class MSFT_lod implements IGLTFLoaderExtension {
}
}

GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_lod(loader));
GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_lod(loader));
25 changes: 25 additions & 0 deletions src/Cameras/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,31 @@ export class Camera extends Node {
return this._upVector;
}

/**
* The screen area in scene units squared
*/
public get screenArea(): number {
let x = 0;
let y = 0;
if (this.mode === Camera.PERSPECTIVE_CAMERA) {
if (this.fovMode === Camera.FOVMODE_VERTICAL_FIXED) {
y = this.minZ * 2 * Math.tan(this.fov / 2);
x = this.getEngine().getAspectRatio(this) * y;
} else {
x = this.minZ * 2 * Math.tan(this.fov / 2);
y = x / this.getEngine().getAspectRatio(this);
}
} else {
const halfWidth = this.getEngine().getRenderWidth() / 2.0;
const halfHeight = this.getEngine().getRenderHeight() / 2.0;

x = (this.orthoRight ?? halfWidth) - (this.orthoLeft ?? -halfWidth);
y = (this.orthoTop ?? halfHeight) - (this.orthoBottom ?? -halfHeight);
}

return x * y;
}

/**
* Define the current limit on the left side for an orthographic camera
* In scene unit
Expand Down
53 changes: 39 additions & 14 deletions src/Meshes/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,8 @@ class _InternalMeshDataInfo {

public _preActivateId: number = -1;
public _LODLevels = new Array<MeshLODLevel>();

/** Alternative definition of LOD level, using screen coverage instead of distance */
public _useLODScreenCoverage: boolean = false;
public _checkReadinessObserver: Nullable<Observer<Scene>>;

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

/**
* Determines if the LOD levels are intended to be calculated using screen coverage (surface area ratio) instead of distance
*/
public get useLODScreenCoverage() {
return this._internalMeshDataInfo._useLODScreenCoverage;
}

public set useLODScreenCoverage(value: boolean) {
this._internalMeshDataInfo._useLODScreenCoverage = value;
}

/**
* Will notify when the mesh is completely ready, including materials.
* Observers added to this observable will be removed once triggered
Expand Down Expand Up @@ -784,12 +796,13 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
}

private _sortLODLevels(): void {
const sortingOrderFactor = this._internalMeshDataInfo._useLODScreenCoverage ? -1 : 1;
this._internalMeshDataInfo._LODLevels.sort((a, b) => {
if (a.distance < b.distance) {
return 1;
if (a.distanceOrScreenCoverage < b.distanceOrScreenCoverage) {
return sortingOrderFactor;
}
if (a.distance > b.distance) {
return -1;
if (a.distanceOrScreenCoverage > b.distanceOrScreenCoverage) {
return -sortingOrderFactor;
}

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

var level = new MeshLODLevel(distance, mesh);
var level = new MeshLODLevel(distanceOrScreenCoverage, mesh);
this._internalMeshDataInfo._LODLevels.push(level);

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

if (level.distance === distance) {
if (level.distanceOrScreenCoverage === distance) {
return level.mesh;
}
}
Expand Down Expand Up @@ -884,18 +898,29 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
}

var distanceToCamera = bSphere.centerWorld.subtract(camera.globalPosition).length();
const useScreenCoverage = internalDataInfo._useLODScreenCoverage;
let compareValue = distanceToCamera;
let compareSign = 1;

if (useScreenCoverage) {
const screenArea = camera.screenArea;
let meshArea = bSphere.radiusWorld * camera.minZ / distanceToCamera;
meshArea = meshArea * meshArea * Math.PI;
compareValue = meshArea / screenArea;
compareSign = -1;
}

if (internalDataInfo._LODLevels[internalDataInfo._LODLevels.length - 1].distance > distanceToCamera) {
if (compareSign * internalDataInfo._LODLevels[internalDataInfo._LODLevels.length - 1].distanceOrScreenCoverage > compareSign * compareValue) {
if (this.onLODLevelSelection) {
this.onLODLevelSelection(distanceToCamera, this, this);
this.onLODLevelSelection(compareValue, this, this);
}
return this;
}

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

if (level.distance < distanceToCamera) {
if (compareSign * level.distanceOrScreenCoverage < compareSign * compareValue) {
if (level.mesh) {
if (level.mesh.delayLoadState === Constants.DELAYLOADSTATE_NOTLOADED) {
level.mesh._checkDelayState();
Expand All @@ -911,15 +936,15 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
}

if (this.onLODLevelSelection) {
this.onLODLevelSelection(distanceToCamera, this, level.mesh);
this.onLODLevelSelection(compareValue, this, level.mesh);
}

return level.mesh;
}
}

if (this.onLODLevelSelection) {
this.onLODLevelSelection(distanceToCamera, this, this);
this.onLODLevelSelection(compareValue, this, this);
}
return this;
}
Expand Down
6 changes: 3 additions & 3 deletions src/Meshes/meshLODLevel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import { Nullable } from '../types';
export class MeshLODLevel {
/**
* Creates a new LOD level
* @param distance defines the distance where this level should star being displayed
* @param distanceOrScreenCoverage defines either the distance or the screen coverage where this level should start being displayed
* @param mesh defines the mesh to use to render this level
*/
constructor(
/** Defines the distance where this level should start being displayed */
public distance: number,
/** 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*/
public distanceOrScreenCoverage: number,
/** Defines the mesh to use to render this level */
public mesh: Nullable<Mesh>) {
}
Expand Down
Binary file added tests/validation/ReferenceImages/gltfMSFTLOD.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading