从0开始疫情3D地球 - 3D疫情地球VDEarth - 4 - 3D地球组件实现(2)


上一篇,实现到了地球的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);
    }
  }

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

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

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

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

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);
}

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

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连接,动态更新

相关链接