上一篇,實現到了地球的3D可視化的展現,本篇在以前的基礎上增長一些數據標註html
爲了美觀,先給地球加上一些點綴前端
太空中的視角,星星就是一個個的兩點,這裏藉助threejs的Geometry實現npm
修改material.js, 在材質類中新增一個星星的材質建立方法json
class material { . . . createStarMat() { var starsMaterial = new PointsMaterial({ color: 0x8e8e8e }); return starsMaterial; } . . . }
修改material.js, 在材質類中新增一個星星的模型建立方法,建立2000個geometry數組
class vdGeom { . . . createStarGeom() { let starsGeometry = new Geometry(); for (let i = 0; i < 2000; i++) { let starVector = new Vector3( Math.randFloatSpread(2000), Math.randFloatSpread(2000), Math.randFloatSpread(2000) ); starsGeometry.vertices.push(starVector); } return starsGeometry; } . . . }
修改model.js ,新增建立星星模型的方法瀏覽器
class mesh { . . . createStars() { let vdGeom = this.vdGeom.createStarGeom(); let vdMaterial = this.vdMaterial.createStarMat(); return new Points(vdGeom, vdMaterial); } . . . }
修改VDEarth.js ,修改以前的initObj方法,新增調用星星建立的方法socket
// 建立模型 function initObj() { let self = this; let fontloader = new FontLoader(); // 建立地球模型組 this.baseGroup = new Group(); // 建立地球 this.radius = minSize(this.contentWidth, this.contentHeight) * 0.2; let globalMesh = new model().createGlobe(this.radius, this.textrue); this.baseGroup.add(globalMesh); // 建立星點 let stars = new model().createStars(); this.baseGroup.add(stars); this.scene.add(this.baseGroup); }
npm run start 打開瀏覽器就能夠看到動態建立的地球和散列的星星ide
有了基本的地球模型,咱們要在地球模型上實現數據的展現,這裏使用3d柱形圖進行數據的展現,以各個地區的疫情數據做爲基礎,這裏只涉及到前端展現,先定義一個數據格式以下:字體
定義一個數組對象,定義兩個國家的數據,包含名稱,經度,維度,累計確診總數動畫
[{ name:'中國', lng:'116.46', lat: '39.92', total:'80000' }, { name:'美國', lng:'-77.02', lat: '39.91', total:'890000' }]
接下來修改以前的marker.js,建立一個marker類
import geometry from './geometry'; import material from './material'; class marker { constructor() { this.vdGeom = new geometry(); this.vdMaterial = new material(); } } export default marker;
在marker類中,新增建立柱形圖marker 的方法
addBoxMarkers(group, radius, items) { for (let i = 0; i < items.length; i++) { // 獲取場景下組內已有的柱形圖標記模型 let boxMarker = _.find(group.children, (model) => { return ( model.userData.type == 'bar' && model.userData.name == items[i].name ); }); // 不存在就建立 if (!boxMarker) { // 柱形圖的深度,即顯示出來的高度 // 防止數據大小差異很大,這裏用log方法對數據進行歸一化處理 let depth = window.Math.log2(items[i].total) * 5; let boxMarkerGeom = this.vdGeom.createBoxMarkerGeom(depth); let boxMarkerMat = this.vdMaterial.createBoxMakerMat(); boxMarker = new Mesh(boxMarkerGeom, boxMarkerMat); // 定位 let position = getPosition( parseFloat(items[i].lng), parseFloat(items[i].lat), radius ); boxMarker.position.set(position.x, position.y, position.z); // 設置柱形圖的自定義數據爲遍歷的數據項 boxMarker.userData = Object.assign({ type: 'bar' }, items[i]); // 標記垂直於圓心 boxMarker.lookAt(new Vector3(0, 0, 0)); group.add(boxMarker); } // 縮放0.1倍,爲後續柱形圖動畫預留 boxMarker.scale.set(1, 1, 0.1); boxMarkAnimate(boxMarker); } }
裏面定義了mesh的自定義數據,數據爲前面定義的數據格式,
其中調用了獲取數據在場景中定位的方法getPosition,這個定義在utils.js文件
// 經緯度轉圖形position export function getPosition(lng, lat, radius) { let phi = (90 - lat) * (window.Math.PI / 180), theta = (lng + 180) * (window.Math.PI / 180), x = -(radius * window.Math.sin(phi) * window.Math.cos(theta)), z = radius * window.Math.sin(phi) * window.Math.sin(theta), y = radius * window.Math.cos(phi); return { x: x, y: y, z: z }; }
能夠看到這裏建立的仍是一個mesh對象,響應的要建立一個geometry和material方法
修改 geomtry.js, 在類中新增一個建立柱形圖的geometry方法,createBoxMarkerGeom
class vdGeom { ... createBoxMarkerGeom(depth) { let boxGeometry = new BoxGeometry(2, 2, depth); return boxGeometry; } ... }
修改material.js , 在類中新增一個建立材質的方法createBoxMakerMat
class material { ... createBoxMakerMat() { var boxMarkerMaterial = new MeshPhongMaterial({ color: '#6dc3ec', side: DoubleSide, depthTest: true, }); return boxMarkerMaterial; } ... }
修改VDEarth.js, 修改initObj 方法,新增調用建立柱形圖的方法
// 建立模型 function initObj() { ... // 定義好的數據格式 var data = [{ name:'中國', lng:'116.46', lat: '39.92', total:'80000' },{ name:'美國', lng:'-77.02', lat: '39.91', total:'890000' }] var self = this // 建立一個標記組 self.markerGroup = new Group(); // 建立標記 var myMarkers = new marker(); // 柱形 myMarkers.addBoxMarkers(self.markerGroup, self.radius, data); // 添加到地球模型組裏 self.baseGroup.add(self.markerGroup); ... }
這樣柱形圖標註就建立完成了
除了柱形圖的標註的建立,繼續建立一個名稱標註,這裏用和柱形圖一樣的數據格式
同上一節,增長建立名稱標註的方法
addNameMarkers(group, radius, items, font) { for (let i = 0; i < items.length; i++) { let nameMarker = _.find(group.children, (model) => { return ( model.userData.type == 'name' && model.userData.name == items[i].name ); }); if (!nameMarker) { let geometry = this.vdGeom.createNameMarkerGeom( items[i].name + ':' + items[i].total, font ); let material = this.vdMaterial.createNameMarkerMat(); nameMarker = new Mesh(geometry, material); // 定位 let position = getPosition( parseFloat(items[i].lng), parseFloat(items[i].lat), radius + window.Math.log2(items[i].total) * 5 ); nameMarker.position.set(position.x, position.y, position.z); // 標記垂直於圓心 nameMarker.lookAt( new Vector3(position.x * 1.1, position.y * 1.1, position.z * 1.1) ); group.add(nameMarker); } nameMarker.visible = false; nameMarker.userData = Object.assign({ type: 'name' }, items[i]); } }
建立geometry
createNameMarkerGeom(name, font) { let textGeo = new TextGeometry(name, { font: font, size: 4, height: 0.5, curveSegments: 0.1, bevelEnabled: false, }); // 文字居中 textGeo.center(); return textGeo; }
建立材質
createNameMarkerMat() { var nameMarkerMaterial = new MeshBasicMaterial({ color: '#fff', transparent: true, }); return nameMarkerMaterial; }
修改VDEarth.js, 修改initObj 方法,新增調用建立柱形圖的方法,這裏的字體使用了json文件的字體,具體能夠參考threejs官網字體的說明
// 建立模型 function initObj() { let self = this; let fontloader = new FontLoader(); // 建立地球模型組 this.baseGroup = new Group(); // 建立地球 this.radius = minSize(this.contentWidth, this.contentHeight) * 0.2; let globalMesh = new model().createGlobe(this.radius, this.textrue); this.baseGroup.add(globalMesh); // 建立星點 let stars = new model().createStars(); this.baseGroup.add(stars); var data = [{ name:'中國', lng:'116.46', lat: '39.92', total:'80000' }, { name:'美國', lng:'-77.02', lat: '39.91', total:'890000' }] // 建立一個標記組 self.markerGroup = new Group(); // 建立標記 var myMarkers = new marker(); // 柱形 myMarkers.addBoxMarkers(self.markerGroup, self.radius, data); // 加載字體 if (self.font) { myMarkers.addNameMarkers(self.markerGroup, self.radius, data, self.font); } else { fontloader.load('./fonts/SimHei_Regular.json', function (font) { self.font = font; myMarkers.addNameMarkers( self.markerGroup, self.radius, data, self.font ); }); } self.baseGroup.add(self.markerGroup); this.scene.add(this.baseGroup); }
因爲名稱在地球上顯示,數據過多會相互遮擋,因此前面的名稱標註的建立都是隱藏的狀態,使用點擊柱形圖觸發名稱和數據的顯示
建立和修改eventLister.js
import {Vector2,Raycaster} from 'three' // 獲取mesh function getAllMeshes(obj, meshArr) { obj.children.forEach(element => { if (element.type == 'Mesh') { meshArr.push(element); } else if (element.children.length > 0) { getAllMeshes(element, meshArr); } }); } // 柱狀標記點擊事件 export function createModelClick(){ var self = this //點擊事件 window.addEventListener('click',function (e) { // 重置選擇的柱形標註的顏色 if(self.seledModel){ self.seledModel.object.material.color.set('#6dc3ec'); } // 重置名稱的可見 if(self.nameModel ){ self.nameModel.visible = false; } var mouse = new Vector2(); mouse.x =((e.clientX - window.innerWidth + self.contentWidth) / self.contentWidth) * 2 - 1; mouse.y =-((e.clientY - window.innerHeight + self.contentHeight) / self.contentHeight) *2 + 1; var raycaster = new Raycaster(); // update the picking ray with the camera and mouse position raycaster.setFromCamera(mouse, self.camera); var objects = []; // 獲取標記組下全部的mesh getAllMeshes(self.markerGroup, objects); //射線和模型求交,選中一系列直線 var intersects = raycaster.intersectObjects(objects); if (intersects.length > 0) { for (var i = 0; i < intersects.length; i++) { var ele = intersects[i]; if (ele.object.userData && ele.object.userData.type === 'bar') { // 選中的柱形圖顏色修改 self.seledModel = ele; ele.object.material.color.set('#e0ef08'); // 選中的柱形標註對應的文本的可見 self.nameModel = _.find(self.markerGroup.children,model=>{ return model.userData.type == 'name' && model.userData.name == ele.object.userData.name }) if(self.nameModel){ self.nameModel.visible = true; } return ; } self.seledModel = null self.nameModel = null } }else{ self.seledModel = null self.nameModel = null } }, false); }
修改VDEarth.js ,修改VDEarth類內的init方法
init(opt = {}) { var self = this; // 合併用戶配置屬性 _.merge(this.options, opt); // 獲取容器的寬高 this.contentWidth = this.options.container.offsetWidth; this.contentHeight = this.options.container.offsetHeight; // 加載貼圖 let globeTextureLoader = new TextureLoader(); globeTextureLoader.load('./images/world.jpg', function (textrue) { self.textrue = textrue; // 初始化渲染器 initRenderer.call(self); // 初始化舞臺 initScene.call(self); // 初始化相機 initCamera.call(self); // 初始化燈光 initLight.call(self); // 初始化控制器 initControls.call(self); // 初始化模型 initObj.call(self); // 初始化點擊事件 createModelClick.call(self); // 初始化實時更新方法 animate.call(self); }); }
柱形圖的動畫藉助tween.js進行實現,首先保證已經npm install @tweenjs/tween.js 組件
建立和修改animation.js, 定義一個加單的動畫效果,柱形圖在Z週上的縮放效果,具體以下
import TWEEN from '@tweenjs/tween.js' export function boxMarkAnimate(model){ let tw = new TWEEN.Tween(model.scale) tw.to({ z:1 },2000) tw.easing(TWEEN.Easing.Sinusoidal.InOut) tw.start() }
修改markers.js,引入
import { boxMarkAnimate } from './animation';
在addBoxMarkers方法內增長
boxMarkAnimate(boxMarker);
至此就實現了3D地球的可視化,後續會在爬蟲完成後新增socket鏈接,動態更新
相關連接
從0開始疫情3D地球 - 3D疫情地球VDEarth - 1- 引言
從0開始疫情3D地球 - 3D疫情地球VDEarth - 2 - 前端代碼構建
從0開始疫情3D地球 - 3D疫情地球VDEarth - 3 - 3D地球組件實現(1)
從0開始疫情3D地球 - 3D疫情地球VDEarth - 4 - 3D地球組件實現(2)