const THREE = require('three'); const updateMorphColorAttribute = require("../utilities").updateMorphColorAttribute; const toBufferGeometry = require('../utilities').toBufferGeometry; /** * Provides an object which stores meshes at different levels based * on specified distance. * This object is ued by zincObject to provide mesh at different LODs. * A layer is displayed when the distance from the camera is greater * than its specified distance and closest compared to other layers. * This is intended to be an internal object used only by Zinc Object. * * This object assumes the centroid and bounding box are consistent between * different level of layers. * * @class * @author Alan Wu * @return {LOD} */ const LOD = function (parent) { this.levels = []; this._currentLevel = 0; this._renderOrder = 1; this._material = undefined; this._secondaryMaterial = undefined; this._loader = undefined; //The owning Zinc Object this._parent = parent; /* * Add a level of LOD at the specified distance */ this.addLevel = (object, distanceIn) => { if (object) { const distance = Math.abs(distanceIn); let l; for (l = 0; l < this.levels.length; l++) { if (distance < this.levels[l].distance) { break; } } const levelObject = { distance: distance, morph: object, loaded: true, loading: false, url: "", }; this.levels.splice(l, 0, levelObject); object.renderOrder = this._renderOrder; //this.add( object ); } } /* * This is called once an ondemand level is loaded */ this.levelLoaded = (object, distanceIn) => { if (object) { const distance = Math.abs(distanceIn); for (let l = 0; l < this.levels.length; l++) { if (distance === this.levels[l].distance) { this._parent.group.add(object); this.levels[l].morph = object; this.levels[l].loaded = true; this.levels[l].loading = false; break; } } this.checkTransparentMesh(); } } this.addLevelFromURL = (loader, level, url, index, preload) => { this._loader = loader; const distance = this.calculateDistance(level); const levelObject = { distance: distance, morph: undefined, loaded: false, loading: false, url: url, index: index, }; let l; for (l = 0; l < this.levels.length; l++) { if (distance < this.levels[l].distance) { break; } } this.levels.splice(l, 0, levelObject); if (preload) { this.loadLevel(l); } } //load the mesh at index, return true if morph is not ready this.loadLevel = (index) => { const level = this.levels[index]; if (!level.morph && !level.loaded && !level.loading) { level.loading = true; this._loader.load(level.url, this.lodLoader(level.distance), undefined, undefined, {index: level.index}); } return (level.morph === undefined); } this.calculateDistance = function (level) { this._parent.getBoundingBox(); const radius = this._parent.radius; let distance = 0; if (level === "far") { distance = radius * 4.5; } else if (level === "medium") { distance = radius * 2.5; } else if (level === "close") { distance = 0; } return distance; } /** * Check if there are multiple levels. */ this.containsLevels = () => { if (this.levels && this.levels.length > 1) { return true; } return false; } /** * Check if material is transparent, create secondary mesh * for better rendering if required. */ this.checkTransparentMesh = () => { const level = this.levels[this._currentLevel]; if (this._material) { if (this._material.transparent) { if (!this._secondaryMaterial) { this._secondaryMaterial = this._material.clone(); this._secondaryMaterial.side = THREE.FrontSide; } this._secondaryMaterial.opacity = this._material.opacity; if (this._secondaryMaterial.emissive) { this._secondaryMaterial.emissive.copy(this._material.emissive); } this._secondaryMaterial.needsUpdate = true; // THREE.Mesh - for utilities purpose such as rendering // transparent surfaces - one for front face and one for back face. if (!level.secondaryMesh) { level.secondaryMesh = new THREE.Mesh(level.morph.geometry, this._secondaryMaterial); level.secondaryMesh.renderOrder = level.morph.renderOrder + 1; level.secondaryMesh.userData = level.morph.userData; level.secondaryMesh.name = level.morph.name; } this._material.side = THREE.BackSide; this._material.needsUpdate = true; if (!level.secondaryMesh.parent) { level.morph.add(level.secondaryMesh); if (this._parent.animationGroup) { this._parent.animationGroup.add(level.secondaryMesh); } } } else { if (level.secondaryMesh) { //Do not delete this mesh, remove it from //rendering and animation group instead level.morph.remove(level.secondaryMesh); if (this._parent.animationGroup) { this._parent.animationGroup.uncache(level.secondaryMesh); this._parent.animationGroup.remove(level.secondaryMesh); } } this._material.side = THREE.DoubleSide; this._material.needsUpdate = true; } } } this.dispose = () => { this.levels.forEach((level) => { if (level.morph && level.morph.geometry) { level.morph.geometry.dispose(); } }); if (this._material) { this._material.dispose(); } if (this._secondaryMaterial) { this._secondaryMaterial.dispose(); } } this.getCurrentLevel = () => { return this._currentLevel; } this.getCurrentMorph = () => { const level = this.levels[this._currentLevel]; if (level && level.morph) { return level.morph; } return this._parent.morph; } /** * Loader for lod object */ this.lodLoader = function (distance) { return (geometryIn) => { const material = this._material; const options = { localTimeEnabled: this._parent.timeEnabled, localMorphColour: this._parent.morphColour, } const geometry = toBufferGeometry(geometryIn, options); let mesh = undefined; if (this._parent.isGeometry) { mesh = new THREE.Mesh(geometry, material); } else if (this._parent.isLines) { mesh = new (require("../three/line/LineSegments").LineSegments)(geometry, material); } mesh.userData = this._parent; mesh.renderOrder = this._renderOrder; geometryIn.dispose(); this.levelLoaded(mesh, distance); }; } this.updateMorphColorAttribute = (currentOnly) => { //Multilayers - set all if (this._material) { if ((this._material.vertexColors == THREE.VertexColors) || (this._material.vertexColors == true)) { if (currentOnly) { const morph = this.getCurrentMorph(); updateMorphColorAttribute(morph.geometry, morph); } else { this.levels.forEach((level) => { if (level.morph && level.morph.geometry) { updateMorphColorAttribute(level.morph.geometry, level.morph); } }); } } } } this.setColour = (colour) => { this._material.color = colour; if (this._secondaryMaterial) { this._secondaryMaterial.color = colour; } updateGeometryColour(); } this.setFrustumCulled = (flag) => { this.levels.forEach((level) => { if (level.morph) { level.morph.frustumCulled = flag; } if (level.secondaryMesh) { level.secondaryMesh.frustumCulled = flag; } }); } this.setMaterial = (material) => { if (material) { if (!this._material || (this._material.id !== material.id)) { this._material = material; if (this._secondaryMaterial) { this._secondaryMaterial.dispose(); } this._secondaryMaterial = material.clone() this._secondaryMaterial.side = THREE.FrontSide; this._secondaryMaterial.transparent = true; this.levels.forEach((level) => { if (level.morph) { level.morph.material = this._material; if (level.morph.geometry) { level.morph.geometry.colorsNeedUpdate = true; } } if (level.secondaryMesh) { level.secondaryMesh.material = this._secondaryMaterial; } }); } } } this.setName = (name) => { this.levels.forEach((level) => { if (level.morph) { level.morph.name = name; } if (level.secondaryMesh) { level.secondaryMesh.name = name; } }); } this.setRenderOrder = (order) => { this._renderOrder = order; this.levels.forEach((level) => { if (level.morph) { level.morph.renderOrder = order; } if (level.secondaryMesh) { level.secondaryMesh.renderOrder = order; } }); } this.setVertexColors = (vertexColors) => { this._material.vertexColors = vertexColors; updateGeometryColour(); if (this._secondaryMaterial) { this._secondaryMaterial.vertexColors = vertexColors; } } /* Update layers based on the */ this.update = (camera, center) => { const levels = this.levels; if (levels.length > 1) { const distance = camera.cameraObject.position.distanceTo(center); let visibleIndex = -1; let optimalIndex = -1; let i, l; //Found a visible index that is within range of the LOD for (i = 0, l = levels.length; i < l; i++) { if (distance >= levels[i].distance) { //Check if a level is loading if (levels[i].morph) { if (visibleIndex > -1 && levels[visibleIndex].morph) { levels[visibleIndex].morph.visible = false; } visibleIndex = i; levels[i].morph.visible = true; optimalIndex = -1; } else { optimalIndex = i; } } else { break; } } if (optimalIndex > -1) { this.loadLevel(optimalIndex); } for (; i < l; i++) { if (levels[i].morph) { //Set visibility of other morph to false //and set the closest lod to true if //none is found if (visibleIndex > -1) { levels[i].morph.visible = false; } else { levels[i].morph.visible = true; visibleIndex = i; } } } if (this._currentLevel != visibleIndex) { this._currentLevel = visibleIndex; this.checkTransparentMesh(); } } } this.toggleMarker = (marker, flag) => { this.levels.forEach((level) => { if (level.morph) { if (flag) { level.morph.add(marker); } else { level.morph.remove(marker); } } }); } const updateGeometryColour = () => { this.levels.forEach((level) => { if (level.morph && level.morph.geometry) { level.morph.geometry.colorsNeedUpdate = true; } }); } } exports.LOD = LOD;