zoukankan      html  css  js  c++  java
  • 从0开始疫情3D地球

    上一篇,实现到了地球的3D可视化的展示,本篇在之前的基础上增加一些数据标注

    为了美观,先给地球加上一些点缀

    1 创建星星

    太空中的视角,星星就是一个个的两点,这里借助threejs的Geometry实现

    修改material.js, 在材质类中新增一个星星的材质创建方法

    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方法,新增调用星星创建的方法

    // 创建模型
    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 打开浏览器就可以看到动态创建的地球和散列的星星

    2 创建柱形图标注

    有了基本的地球模型,我们要在地球模型上实现数据的展示,这里使用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);
        }
      }
    View Code

    里面定义了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);
       ...
    }
    View Code

    这样柱形图标注就创建完成了

    3 创建名称标注

    除了柱形图的标注的创建,继续创建一个名称标注,这里用和柱形图同样的数据格式

    同上一节,增加创建名称标注的方法

    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]);
        }
      }
    View Code

    创建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);
    }
    View Code

    4 柱形图交互

    由于名称在地球上显示,数据过多会相互遮挡,所以前面的名称标注的创建都是隐藏的状态,使用点击柱形图触发名称和数据的显示

    创建和修改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);
    }
    View Code

    修改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);
        });
      }
    View Code

    5 柱形图动画

    柱形图的动画借助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) 

    从0开始疫情3D地球 - 3D疫情地球VDEarth - 5 - 疫情数据爬虫 

    从0开始疫情3D地球 - 3D疫情地球VDEarth - 6 - 数据推送  

  • 相关阅读:
    【html】【21】高级篇--搜索框
    【html】【20】高级篇--轮播图[聚焦]
    【html】【19】高级篇--大事件时间轴
    【html】【18】高级篇--下拉列表[竖向手风琴]
    【html】【17】高级篇--loading加载
    【html】【16】高级篇--毛玻璃效果[模糊]
    【html】【15】特效篇--分页
    【html】【14】特效篇--侧边栏客服
    【mysql】【分组】后取每组的top2
    【html】【13】特效篇--下拉导航
  • 原文地址:https://www.cnblogs.com/dpwow/p/12738109.html
Copyright © 2011-2022 走看看