Skip to content

Add MSFT_screencoverage support for MSFT_lod #16736

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
10 changes: 8 additions & 2 deletions packages/dev/core/src/Meshes/mesh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { Geometry } from "./geometry";
import type { IMeshDataOptions } from "./abstractMesh";
import { AbstractMesh } from "./abstractMesh";
import { SubMesh } from "./subMesh";
import type { BoundingSphere } from "../Culling/boundingSphere";
import { BoundingSphere } from "../Culling/boundingSphere";
import type { Effect } from "../Materials/effect";
import { Material } from "../Materials/material";
import { MultiMaterial } from "../Materials/multiMaterial";
Expand Down Expand Up @@ -1116,7 +1116,11 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
return this;
}

const bSphere = boundingSphere || this.getBoundingInfo().boundingSphere;
let bSphere = boundingSphere;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks like a potential breaking change I am not sure we should create the bounding sphere here.

if (!bSphere) {
const { min, max } = this.getHierarchyBoundingVectors();
bSphere = new BoundingSphere(min, max);
}

const distanceToCamera = camera.mode === Camera.ORTHOGRAPHIC_CAMERA ? camera.minZ : bSphere.centerWorld.subtract(camera.globalPosition).length();
let compareValue = distanceToCamera;
Expand Down Expand Up @@ -1152,6 +1156,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
}

level.mesh._preActivate();

level.mesh._updateSubMeshesBoundingInfo(this.worldMatrixFromCache);
}

Expand All @@ -1166,6 +1171,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData {
if (this.onLODLevelSelection) {
this.onLODLevelSelection(compareValue, this, this);
}

return this;
}

Expand Down
79 changes: 77 additions & 2 deletions packages/dev/core/src/scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4323,12 +4323,44 @@ export class Scene implements IAnimatable, IClipPlanesHolder, IAssetContainer {
}

// Determine mesh candidates
const meshes = this.getActiveMeshCandidates();
const meshes = this.getActiveMeshCandidates().data;

// Check if any mesh has LOD levels to avoid unnecessary processing
let hasLODMeshes = false;
for (let i = 0; i < meshes.length; i++) {
if ((meshes[i] as Mesh).hasLODLevels) {
hasLODMeshes = true;
break;
}
}

// Move meshes with LOD levels to the front of the list for priority processing
if (hasLODMeshes) {
meshes.sort((a, b) => ((a as Mesh).hasLODLevels ? -1 : (b as Mesh).hasLODLevels ? 1 : 0));
}

// Build LOD parent map to cache LOD parent relationships
const lodParentMap = new Map<AbstractMesh, AbstractMesh>();
if (hasLODMeshes) {
for (let i = 0; i < meshes.length; i++) {
const mesh = meshes[i];
if ((mesh as Mesh).hasLODLevels) {
const lodLevels = (mesh as Mesh).getLODLevels();
// Map each LOD mesh to its parent
for (const lodLevel of lodLevels) {
if (lodLevel.mesh) {
lodParentMap.set(lodLevel.mesh, mesh);
}
}
}
}
}

// Check each mesh
const len = meshes.length;
const skippedLODMeshes = [];
for (let i = 0; i < len; i++) {
const mesh = meshes.data[i];
const mesh = meshes[i];
let currentLOD = mesh._internalAbstractMeshDataInfo._currentLOD.get(this.activeCamera);
if (currentLOD) {
currentLOD[1] = -1;
Expand All @@ -4355,6 +4387,7 @@ export class Scene implements IAnimatable, IClipPlanesHolder, IAssetContainer {

// Switch to current LOD
let meshToRender = this.customLODSelector ? this.customLODSelector(mesh, this.activeCamera) : mesh.getLOD(this.activeCamera);

currentLOD[0] = meshToRender;
currentLOD[1] = this._frameId;
if (meshToRender === undefined || meshToRender === null) {
Expand All @@ -4366,6 +4399,48 @@ export class Scene implements IAnimatable, IClipPlanesHolder, IAssetContainer {
meshToRender.computeWorldMatrix();
}

// If the mesh has LOD levels, add its LOD levels that are not the current LOD to the skipped list
const lodLevelMeshes = (mesh as Mesh).hasLODLevels
? (mesh as Mesh)
.getLODLevels()
.map((level) => level.mesh as Mesh)
.concat(mesh as Mesh)
: [];
for (let levelIndex = 0; levelIndex < lodLevelMeshes.length; levelIndex++) {
const levelMesh = lodLevelMeshes[levelIndex];
if (levelMesh !== meshToRender) {
skippedLODMeshes.push(levelMesh);
}
}

// Skip meshes that are not the current LOD level
// First, check if this mesh is part of a LOD system via the cache
const lodParent = lodParentMap.get(mesh);
if (lodParent) {
// This mesh is a LOD child, check if it's the current LOD
const parentLOD = this.customLODSelector ? this.customLODSelector(lodParent, this.activeCamera) : lodParent.getLOD(this.activeCamera);
if (mesh !== parentLOD) {
skippedLODMeshes.push(mesh);
continue;
}
}

// Check if any ancestor is in the skipped list
let shouldSkip = false;
let ancestor = mesh.parent;
while (ancestor) {
if (skippedLODMeshes.includes(ancestor as Mesh)) {
shouldSkip = true;
break;
}
ancestor = ancestor.parent;
}

if (shouldSkip) {
skippedLODMeshes.push(mesh);
continue;
}

mesh._preActivate();

if (
Expand Down
132 changes: 119 additions & 13 deletions packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { Observable } from "core/Misc/observable";
import { Deferred } from "core/Misc/deferred";
import type { Material } from "core/Materials/material";
import type { TransformNode } from "core/Meshes/transformNode";
import type { Mesh } from "core/Meshes/mesh";
import { Mesh } from "core/Meshes/mesh";
import type { BaseTexture } from "core/Materials/Textures/baseTexture";
import type { INode, IMaterial, IBuffer, IScene } from "../glTFLoaderInterfaces";
import type { IGLTFLoaderExtension } from "../glTFLoaderExtension";
Expand Down Expand Up @@ -173,6 +173,86 @@ export class MSFT_lod implements IGLTFLoaderExtension {
return promise;
}

/**
* Converts a TransformNode to a Mesh, preserving its properties and parent-child relationships
* @param node The TransformNode to convert
* @returns The converted Mesh
*/
private _convertTransformNodeToMesh(node: TransformNode): Mesh {
const mesh = new Mesh(node.name, node.getScene());

mesh.parent = node.parent;

// Copy transform properties
mesh.position = node.position.clone();
mesh.rotation = node.rotation.clone();
mesh.scaling = node.scaling.clone();

// Copy metadata
mesh.metadata = node.metadata;

// Move all children from TransformNode to the new Mesh
const children = node.getChildren();
for (const child of children) {
child.parent = mesh;
}

// Dispose the original TransformNode
node.dispose();

return mesh;
}

/**
* Sets up screen coverage LOD levels for the given meshes
* @param transformNodes Array of nodes to set up as LOD levels
* @param screenCoverages Screen coverage values from metadata
* @returns The LOD0 mesh with all LOD levels configured
*/
private _setupScreenCoverageLOD(transformNodes: Nullable<TransformNode>[], screenCoverages: number[]): Mesh {
const lod0 = transformNodes[transformNodes.length - 1];
if (!lod0) {
throw new Error("LOD0 node is missing");
}

// Convert lod0 to Mesh if it's a TransformNode
let lod0Mesh: Mesh;
if (lod0 instanceof Mesh) {
lod0Mesh = lod0;
} else {
lod0Mesh = this._convertTransformNodeToMesh(lod0);
transformNodes[transformNodes.length - 1] = lod0Mesh;
}

screenCoverages.reverse();
lod0Mesh.useLODScreenCoverage = true;

for (let i = 0; i < transformNodes.length - 1; i++) {
const node = transformNodes[i];
if (!node) {
continue;
}

let meshToAdd: Mesh;

if (node instanceof Mesh) {
meshToAdd = node;
} else {
meshToAdd = this._convertTransformNodeToMesh(node);
transformNodes[i] = meshToAdd;
}

lod0Mesh.addLODLevel(screenCoverages[i + 1], meshToAdd);
}

if (screenCoverages[0] > 0) {
// Adding empty LOD
lod0Mesh.addLODLevel(screenCoverages[0], null);
}

return lod0Mesh;
}

/**
* @internal
*/
Expand All @@ -183,6 +263,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 @@ -192,24 +277,45 @@ export class MSFT_lod implements IGLTFLoaderExtension {
this._nodeSignalLODs[indexLOD] = this._nodeSignalLODs[indexLOD] || new Deferred();
}

const assignWrap = (babylonTransformNode: TransformNode) => {
const _assign = (babylonTransformNode: TransformNode, index: number): void => {
assign(babylonTransformNode);
babylonTransformNode.setEnabled(false);
};
transformNodes[index] = babylonTransformNode;

const promise = this._loader.loadNodeAsync(`/nodes/${nodeLOD.index}`, nodeLOD, assignWrap).then((babylonMesh) => {
if (indexLOD !== 0) {
// TODO: should not rely on _babylonTransformNode
const previousNodeLOD = nodeLODs[indexLOD - 1];
if (previousNodeLOD._babylonTransformNode) {
this._disposeTransformNode(previousNodeLOD._babylonTransformNode);
delete previousNodeLOD._babylonTransformNode;
let fullArray = true;
for (let i = 0; i < transformNodes.length; i++) {
if (!transformNodes[i]) {
fullArray = false;
}
}

babylonMesh.setEnabled(true);
return babylonMesh;
});
const lod0 = transformNodes[transformNodes.length - 1];
if (fullArray && lod0) {
const screenCoverages = lod0.metadata?.gltf?.extras?.MSFT_screencoverage as number[] | undefined;
if (screenCoverages?.length) {
this._setupScreenCoverageLOD(transformNodes, screenCoverages);
}
}
};

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

if (indexLOD !== 0 && !screenCoverages) {
// TODO: should not rely on _babylonTransformNode
const previousNodeLOD = nodeLODs[indexLOD - 1];
if (previousNodeLOD._babylonTransformNode) {
this._disposeTransformNode(previousNodeLOD._babylonTransformNode);
delete previousNodeLOD._babylonTransformNode;
}
}

babylonMesh.setEnabled(true);
return babylonMesh;
});

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

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 10 additions & 0 deletions packages/tools/tests/test/visualization/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1840,6 +1840,16 @@
"playgroundId": "#2YZFA0#422",
"referenceImage": "gltfMSFTLOD.png"
},
{
"title": "GLTF ext MSFT_LOD for multi material mesh",
"playgroundId": "#SVEUE4#2",
"referenceImage": "gltfMSFTLOD-multi-material.png"
},
{
"title": "GLTF ext MSFT_LOD for complex hierarchical mesh",
"playgroundId": "#ZSTNSI#3",
"referenceImage": "gltfMSFTLOD-complex-hierarchy.png"
},
{
"title": "Reverse depth buffer and shadows",
"renderCount": 10,
Expand Down
Loading