diff --git a/packages/dev/core/src/Meshes/abstractMesh.ts b/packages/dev/core/src/Meshes/abstractMesh.ts index 3095e2786ac..df5beaaa13e 100644 --- a/packages/dev/core/src/Meshes/abstractMesh.ts +++ b/packages/dev/core/src/Meshes/abstractMesh.ts @@ -1538,27 +1538,6 @@ export abstract class AbstractMesh extends TransformNode implements IDisposable, // Do nothing } - /** - * Gets the current world matrix - * @returns a Matrix - */ - public override getWorldMatrix(): Matrix { - if (this._masterMesh && this.billboardMode === TransformNode.BILLBOARDMODE_NONE) { - return this._masterMesh.getWorldMatrix(); - } - - return super.getWorldMatrix(); - } - - /** @internal */ - public override _getWorldMatrixDeterminant(): number { - if (this._masterMesh) { - return this._masterMesh._getWorldMatrixDeterminant(); - } - - return super._getWorldMatrixDeterminant(); - } - /** * Gets a boolean indicating if this mesh is an instance or a regular mesh */ diff --git a/packages/dev/core/src/Meshes/mesh.ts b/packages/dev/core/src/Meshes/mesh.ts index df962ec91ae..282e131aa24 100644 --- a/packages/dev/core/src/Meshes/mesh.ts +++ b/packages/dev/core/src/Meshes/mesh.ts @@ -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"; @@ -1116,7 +1116,11 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { return this; } - const bSphere = boundingSphere || this.getBoundingInfo().boundingSphere; + let bSphere = boundingSphere; + 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; @@ -1152,6 +1156,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { } level.mesh._preActivate(); + level.mesh._updateSubMeshesBoundingInfo(this.worldMatrixFromCache); } @@ -1166,6 +1171,7 @@ export class Mesh extends AbstractMesh implements IGetSetVerticesData { if (this.onLODLevelSelection) { this.onLODLevelSelection(compareValue, this, this); } + return this; } diff --git a/packages/dev/core/src/scene.ts b/packages/dev/core/src/scene.ts index b80ad7ffb81..e0ecffc7577 100644 --- a/packages/dev/core/src/scene.ts +++ b/packages/dev/core/src/scene.ts @@ -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(); + 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; @@ -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) { @@ -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 ( diff --git a/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts b/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts index 86ce4c1beb0..8c1d8ba9d7f 100644 --- a/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts +++ b/packages/dev/loaders/src/glTF/2.0/Extensions/MSFT_lod.ts @@ -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"; @@ -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[], 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 */ @@ -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[] = []; + + for (let indexLOD = 0; indexLOD < nodeLODs.length; indexLOD++) { + transformNodes.push(null); + } for (let indexLOD = 0; indexLOD < nodeLODs.length; indexLOD++) { const nodeLOD = nodeLODs[indexLOD]; @@ -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] || []; diff --git a/packages/tools/tests/test/visualization/config.json b/packages/tools/tests/test/visualization/config.json index 1a9bf067cfc..ddda00391c1 100644 --- a/packages/tools/tests/test/visualization/config.json +++ b/packages/tools/tests/test/visualization/config.json @@ -1840,6 +1840,10 @@ "playgroundId": "#2YZFA0#422", "referenceImage": "gltfMSFTLOD.png" }, + { + "title": "GLTF ext MSFT_LOD for multi material mesh", + "playgroundId": "#SVEUE4#1" + }, { "title": "Reverse depth buffer and shadows", "renderCount": 10,