diff --git a/src/Layer/C3DTilesLayer.js b/src/Layer/C3DTilesLayer.js index 27ff08da8c..62a2a92c11 100644 --- a/src/Layer/C3DTilesLayer.js +++ b/src/Layer/C3DTilesLayer.js @@ -31,6 +31,16 @@ class C3DTilesLayer extends GeometryLayer { * @param {C3TilesSource} config.source The source of 3d Tiles. * * name. + * @param {Number} [config.sseThreshold=16] The [Screen Space Error](https://github.com/CesiumGS/3d-tiles/blob/main/specification/README.md#geometric-error) + * threshold at which child nodes of the current node will be loaded and added to the scene. + * @param {Number} [config.cleanupDelay=1000] The time (in ms) after which a tile content (and its children) are + * removed from the scene. + * @param {Function} [config.onTileContentLoaded] Callback executed when the content of a tile is loaded. + * @param {Boolean|Material} [config.overrideMaterials='false'] option to override the materials of all the layer's + * objects. If true, a threejs [MeshBasicMaterial](https://threejs.org/docs/index.html?q=meshbasic#api/en/materials/MeshBasicMaterial) + * is set up. config.overrideMaterials can also be a threejs [Material](https://threejs.org/docs/index.html?q=material#api/en/materials/Material) + * in which case it will be used as the material for all objects of the layer. + * @param {C3DTExtensions} [config.registeredExtensions] 3D Tiles extensions managers registered for this tileset. * @param {View} view The view */ constructor(id, config, view) { @@ -40,8 +50,7 @@ class C3DTilesLayer extends GeometryLayer { this.cleanupDelay = config.cleanupDelay || 1000; this.onTileContentLoaded = config.onTileContentLoaded || (() => {}); this.protocol = '3d-tiles'; - // custom cesium shaders are not functional; - this.overrideMaterials = config.overrideMaterials !== undefined ? config.overrideMaterials : true; + this.overrideMaterials = config.overrideMaterials ?? false; this.name = config.name; this.registeredExtensions = config.registeredExtensions || new C3DTExtensions(); diff --git a/src/Parser/B3dmParser.js b/src/Parser/B3dmParser.js index 42f4719415..3a54fc8a7d 100644 --- a/src/Parser/B3dmParser.js +++ b/src/Parser/B3dmParser.js @@ -7,6 +7,8 @@ import shaderUtils from 'Renderer/Shader/ShaderUtils'; import utf8Decoder from 'Utils/Utf8Decoder'; import C3DTBatchTable from 'Core/3DTiles/C3DTBatchTable'; import ReferLayerProperties from 'Layer/ReferencingLayerProperties'; +import { MeshBasicMaterial } from 'three'; +import disposeThreeMaterial from 'Utils/ThreeUtils'; const matrixChangeUpVectorZtoY = (new THREE.Matrix4()).makeRotationX(Math.PI / 2); // For gltf rotation @@ -78,7 +80,10 @@ export default { * @param {string} options.urlBase - the base url of the b3dm file (used to fetch textures for the embedded glTF model). * @param {boolean=} [options.doNotPatchMaterial='false'] - disable patching material with logarithmic depth buffer support. * @param {float} [options.opacity=1.0] - the b3dm opacity. - * @param {boolean|Material=} [options.overrideMaterials='false'] - override b3dm's embedded glTF materials. If overrideMaterials is a three.js material, it will be the material used to override. + * @param {boolean|Material=} [options.overrideMaterials='false'] - override b3dm's embedded glTF materials. If + * true, a threejs [MeshBasicMaterial](https://threejs.org/docs/index.html?q=meshbasic#api/en/materials/MeshBasicMaterial) + * is set up. config.overrideMaterials can also be a threejs [Material](https://threejs.org/docs/index.html?q=material#api/en/materials/Material) + * in which case it will be the material used to override. * @return {Promise} - a promise that resolves with an object containig a THREE.Scene (gltf) and a batch table (batchTable). * */ @@ -184,19 +189,15 @@ export default { mesh.frustumCulled = false; if (mesh.material) { if (options.overrideMaterials) { - if (Array.isArray(mesh.material)) { - for (const material of mesh.material) { - material.dispose(); - } - } else { - mesh.material.dispose(); - } + const oldMat = mesh.material; + // Set up new material if (typeof (options.overrideMaterials) === 'object' && - options.overrideMaterials.isMaterial) { + options.overrideMaterials.isMaterial) { mesh.material = options.overrideMaterials; } else { - mesh.material.depthWrite = true; + mesh.material = new MeshBasicMaterial(); } + disposeThreeMaterial(oldMat); } else if (Capabilities.isLogDepthBufferSupported() && mesh.material.isRawShaderMaterial && !options.doNotPatchMaterial) { diff --git a/src/Utils/ThreeUtils.js b/src/Utils/ThreeUtils.js new file mode 100644 index 0000000000..a471f9c26b --- /dev/null +++ b/src/Utils/ThreeUtils.js @@ -0,0 +1,67 @@ +/** + * Find threejs textures from a material + * @param {Material} material the threejs material holding textures + * @returns {Array} an array of textures in the material + */ +function findTextures(material) { + const textures = []; + if (material.alphaMap) { + textures.push(material.map); + } + if (material.aoMap) { + textures.push(material.map); + } + if (material.bumpMap) { + textures.push(material.bumpMap); + } + if (material.displacementMap) { + textures.push(material.bumpMap); + } + if (material.emissiveMap) { + textures.push(material.emissiveMap); + } + if (material.envMap) { + textures.push(material.envMap); + } + if (material.lightMap) { + textures.push(material.envMap); + } + if (material.map) { + textures.push(material.map); + } + if (material.metalnessMap) { + textures.push(material.map); + } + if (material.normalMap) { + textures.push(material.map); + } + if (material.roughnessMap) { + textures.push(material.map); + } + if (material.specularMap) { + textures.push(material.specularMap); + } + return textures; +} + +/** + * Removes a material and its textures, memory will be freed. + * IMPORTANT NOTE: the material and the texture must not be referenced by other threejs objects, otherwise the memory + * won't be freed. + * @param {Material} material the material to remove + */ +export default function disposeThreeMaterial(material) { + const textures = findTextures(material); + // Remove material + if (Array.isArray(material)) { + for (const m of material) { + m.dispose(); + } + } else { + material.dispose(); + } + // Remove textures + for (let i = 0; i < textures.length; i++) { + textures[i].dispose(); + } +} diff --git a/test/unit/threeutils.js b/test/unit/threeutils.js new file mode 100644 index 0000000000..a0ea1159b6 --- /dev/null +++ b/test/unit/threeutils.js @@ -0,0 +1,62 @@ +import assert from 'assert'; +import * as THREE from 'three'; +import disposeThreeMaterial from 'Utils/ThreeUtils'; + +describe('ThreeJS Utils', function () { + it('Should dispose material and textures', function () { + // Testing if material and textures are correctly disposed is not easy since dispose dispatches an event to + // indicate that some WebGLRenderer GPU resources should be remove. A way to test that is to check the + // WebGLRenderer.info value which lists used GPU resources but WebGLRenderer is not available for unit tests. + // Another way, that is implemented here, is to create a material and a all possible textures and to count the + // number of dispatched 'dispose' events + var disposeCounter = { + nb: 0, + }; + const increment = function () { + this.nb++; + }; + + const material = new THREE.MeshStandardMaterial(); + material.addEventListener('dispose', increment.bind(disposeCounter)); + + material.alphaMap = new THREE.Texture(); + material.alphaMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.aoMap = new THREE.Texture(); + material.aoMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.bumpMap = new THREE.Texture(); + material.bumpMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.displacementMap = new THREE.Texture(); + material.displacementMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.emissiveMap = new THREE.Texture(); + material.emissiveMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.envMap = new THREE.Texture(); + material.envMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.lightMap = new THREE.Texture(); + material.lightMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.map = new THREE.Texture(); + material.map.addEventListener('dispose', increment.bind(disposeCounter)); + + material.metalnessMap = new THREE.Texture(); + material.metalnessMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.normalMap = new THREE.Texture(); + material.normalMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.roughnessMap = new THREE.Texture(); + material.roughnessMap.addEventListener('dispose', increment.bind(disposeCounter)); + + material.specularMap = new THREE.Texture(); + material.specularMap.addEventListener('dispose', increment.bind(disposeCounter)); + + disposeThreeMaterial(material); + + assert.equal(disposeCounter.nb, 13); + }); +});