/// module BABYLON.GLTF2.Loader.Extensions { const NAME = "MSFT_lod"; interface IMSFTLOD { ids: number[]; } /** * [Specification](https://github.com/KhronosGroup/glTF/tree/master/extensions/2.0/Vendor/MSFT_lod) */ export class MSFT_lod implements IGLTFLoaderExtension { /** The name of this extension. */ public readonly name = NAME; /** Defines whether this extension is enabled. */ public enabled = true; /** * Maximum number of LODs to load, starting from the lowest LOD. */ public maxLODsToLoad = Number.MAX_VALUE; /** * Observable raised when all node LODs of one level are loaded. * The event data is the index of the loaded LOD starting from zero. * Dispose the loader to cancel the loading of the next level of LODs. */ public onNodeLODsLoadedObservable = new Observable(); /** * Observable raised when all material LODs of one level are loaded. * The event data is the index of the loaded LOD starting from zero. * Dispose the loader to cancel the loading of the next level of LODs. */ public onMaterialLODsLoadedObservable = new Observable(); private _loader: GLTFLoader; private _nodeIndexLOD: Nullable = null; private _nodeSignalLODs = new Array>(); private _nodePromiseLODs = new Array>>(); private _materialIndexLOD: Nullable = null; private _materialSignalLODs = new Array>(); private _materialPromiseLODs = new Array>>(); /** @hidden */ constructor(loader: GLTFLoader) { this._loader = loader; } /** @hidden */ public dispose() { delete this._loader; this._nodeIndexLOD = null; this._nodeSignalLODs.length = 0; this._nodePromiseLODs.length = 0; this._materialIndexLOD = null; this._materialSignalLODs.length = 0; this._materialPromiseLODs.length = 0; this.onMaterialLODsLoadedObservable.clear(); this.onNodeLODsLoadedObservable.clear(); } /** @hidden */ public onReady(): void { for (let indexLOD = 0; indexLOD < this._nodePromiseLODs.length; indexLOD++) { const promise = Promise.all(this._nodePromiseLODs[indexLOD]).then(() => { if (indexLOD !== 0) { this._loader.endPerformanceCounter(`Node LOD ${indexLOD}`); } this._loader.log(`Loaded node LOD ${indexLOD}`); this.onNodeLODsLoadedObservable.notifyObservers(indexLOD); if (indexLOD !== this._nodePromiseLODs.length - 1) { this._loader.startPerformanceCounter(`Node LOD ${indexLOD + 1}`); if (this._nodeSignalLODs[indexLOD]) { this._nodeSignalLODs[indexLOD].resolve(); } } }); this._loader._completePromises.push(promise); } for (let indexLOD = 0; indexLOD < this._materialPromiseLODs.length; indexLOD++) { const promise = Promise.all(this._materialPromiseLODs[indexLOD]).then(() => { if (indexLOD !== 0) { this._loader.endPerformanceCounter(`Material LOD ${indexLOD}`); } this._loader.log(`Loaded material LOD ${indexLOD}`); this.onMaterialLODsLoadedObservable.notifyObservers(indexLOD); if (indexLOD !== this._materialPromiseLODs.length - 1) { this._loader.startPerformanceCounter(`Material LOD ${indexLOD + 1}`); if (this._materialSignalLODs[indexLOD]) { this._materialSignalLODs[indexLOD].resolve(); } } }); this._loader._completePromises.push(promise); } } /** @hidden */ public loadNodeAsync(context: string, node: INode, assign: (babylonTransformNode: TransformNode) => void): Nullable> { return GLTFLoader.LoadExtensionAsync(context, node, this.name, (extensionContext, extension) => { let firstPromise: Promise; const nodeLODs = this._getLODs(extensionContext, node, this._loader.gltf.nodes, extension.ids); this._loader.logOpen(`${extensionContext}`); for (let indexLOD = 0; indexLOD < nodeLODs.length; indexLOD++) { const nodeLOD = nodeLODs[indexLOD]; if (indexLOD !== 0) { this._nodeIndexLOD = indexLOD; 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) { // TODO: should not rely on _babylonMesh const previousNodeLOD = nodeLODs[indexLOD - 1]; if (previousNodeLOD._babylonTransformNode) { previousNodeLOD._babylonTransformNode.dispose(); delete previousNodeLOD._babylonTransformNode; this._disposeUnusedMaterials(); } } babylonMesh.setEnabled(true); return babylonMesh; }); if (indexLOD === 0) { firstPromise = promise; } else { this._nodeIndexLOD = null; } this._nodePromiseLODs[indexLOD] = this._nodePromiseLODs[indexLOD] || []; this._nodePromiseLODs[indexLOD].push(promise); } this._loader.logClose(); return firstPromise!; }); } /** @hidden */ public _loadMaterialAsync(context: string, material: IMaterial, babylonMesh: Mesh, babylonDrawMode: number, assign: (babylonMaterial: Material) => void): Nullable> { // Don't load material LODs if already loading a node LOD. if (this._nodeIndexLOD) { return null; } return GLTFLoader.LoadExtensionAsync(context, material, this.name, (extensionContext, extension) => { let firstPromise: Promise; const materialLODs = this._getLODs(extensionContext, material, this._loader.gltf.materials, extension.ids); this._loader.logOpen(`${extensionContext}`); for (let indexLOD = 0; indexLOD < materialLODs.length; indexLOD++) { const materialLOD = materialLODs[indexLOD]; if (indexLOD !== 0) { 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]) { previousDataLOD[babylonDrawMode].babylonMaterial.dispose(); delete previousDataLOD[babylonDrawMode]; } } return babylonMaterial; }); if (indexLOD === 0) { firstPromise = promise; } else { this._materialIndexLOD = null; } this._materialPromiseLODs[indexLOD] = this._materialPromiseLODs[indexLOD] || []; this._materialPromiseLODs[indexLOD].push(promise); } this._loader.logClose(); return firstPromise!; }); } /** @hidden */ public _loadUriAsync(context: string, uri: string): Nullable> { // Defer the loading of uris if loading a material or node LOD. if (this._materialIndexLOD !== null) { this._loader.log(`deferred`); const previousIndexLOD = this._materialIndexLOD - 1; this._materialSignalLODs[previousIndexLOD] = this._materialSignalLODs[previousIndexLOD] || new Deferred(); return this._materialSignalLODs[previousIndexLOD].promise.then(() => { return this._loader.loadUriAsync(context, uri); }); } else if (this._nodeIndexLOD !== null) { this._loader.log(`deferred`); const previousIndexLOD = this._nodeIndexLOD - 1; this._nodeSignalLODs[previousIndexLOD] = this._nodeSignalLODs[previousIndexLOD] || new Deferred(); return this._nodeSignalLODs[this._nodeIndexLOD - 1].promise.then(() => { return this._loader.loadUriAsync(context, uri); }); } return null; } /** * Gets an array of LOD properties from lowest to highest. */ private _getLODs(context: string, property: T, array: ArrayLike | undefined, ids: number[]): T[] { if (this.maxLODsToLoad <= 0) { throw new Error("maxLODsToLoad must be greater than zero"); } const properties = new Array(); for (let i = ids.length - 1; i >= 0; i--) { properties.push(ArrayItem.Get(`${context}/ids/${ids[i]}`, array, ids[i])); if (properties.length === this.maxLODsToLoad) { return properties; } } properties.push(property); return properties; } private _disposeUnusedMaterials(): void { // TODO: should not rely on _data const materials = this._loader.gltf.materials; if (materials) { for (const material of materials) { if (material._data) { for (const drawMode in material._data) { const data = material._data[drawMode]; if (data.babylonMeshes.length === 0) { data.babylonMaterial.dispose(false, true); delete material._data[drawMode]; } } } } } } } GLTFLoader.RegisterExtension(NAME, (loader) => new MSFT_lod(loader)); }