用Three.js搞个炫酷3D地球

1.用canvas画一张地球贴图

1.地球geojson

js 复制代码
npm i  @surbowl/world-geo-json-zh 

2.画出地球贴图

  • 众所周知,经度范围[-180,180],纬度范围[-90,90]那么显而易见,经度是维度的两倍长度,所以画出的canvas也是2:1的图片,为了方便计算我这里将经纬度分别放大十倍画图,即宽度360*10,高度180*10

1.canvas样式设置

js 复制代码
 let canvas = document.createElement('canvas');

      canvas.width = 3600;
      canvas.height = 1800;

      let ctx = canvas.getContext('2d');
      //背景颜色
      ctx.fillStyle = that.bg;
      ctx.rect(0, 0, canvas.width, canvas.height);
      ctx.fill();
      
      
       //设置地图样式
      ctx.strokeStyle = that.borderColor;//描边颜色
      ctx.lineWidth = that.borderWidth;//描边线宽

      ctx.fillStyle = that.fillColor;//填充颜色
      if (that.blurWidth) {
        ctx.shadowBlur = that.blurWidth;//边界模糊范围
        ctx.shadowColor = that.blurColor;//边界模糊颜色
      }

2.遍历geojson画区块

  • geojson格式中features数组里面每个geometry包含了区块形状的坐标,对其进行遍历,分别画出区块就行
js 复制代码
 fetch('../node_modules/@surbowl/world-geo-json-zh/world.zh.json')
        .then((res) => res.json())
        .then((geojson) => {
          console.log(geojson);
          geojson.features.forEach((a) => {
            if (a.geometry.type == 'MultiPolygon') {//多个区块组成
              a.geometry.coordinates.forEach((b) => {
                b.forEach((c) => {
                  drawRegion(ctx, c);
                });
              });
            } else {//单个区块
              a.geometry.coordinates.forEach((c) => {
                drawRegion(ctx, c);
              });
            }
          });
          document.body.appendChild(canvas);
        });

画区块: 对每一组坐标点进行遍历,第一个点用moveTo,后面的点全用lineTo,为了保证每个区块形状都是独立的闭合形状,记得开始要用beginPath,结束要用closePath

注意:

  • 在canvas中坐标是以左上角的原点开始的,所以经纬度在canvas坐标是不适用的,需要转换,因为canvas的y轴正方向与纬度的方向是相反的,所以纬度需要取负值。-lat

  • 而经纬度有正负值,为了保证所有坐标都落在canvas可视范围内,要将坐标全部向canvas正轴方向偏移,经度偏移180度,纬度偏移90度,即lng+180,-lat+90

js 复制代码
function drawRegion(ctx, c, geoInfo) {
        ctx.beginPath();
        c.forEach((item, i) => {
        //转换经纬度坐标为canvas坐标点
          let pos = [(item[0] + 180)*10, (-item[1] + 90)*10];
          if (i == 0) {
            ctx.moveTo(pos[0], pos[1]);
          } else {
            ctx.lineTo(pos[0], pos[1]);
          }
        });
        ctx.closePath();
        ctx.fill();
        ctx.stroke();
      }

3.使用canvas画地球贴图

js 复制代码
 var that = {
        bg: '#000080',//背景色
        borderColor: '#1E90FF',//描边颜色
        blurColor: '#1E90FF',//边界模糊颜色
        borderWidth: 1,//描边宽度
        blurWidth: 5,//边界模糊范围
        fillColor: 'rgb(30 ,144 ,255,0.3)',//区块填充颜色       
      };

如图所见,虽然没了北冰洋但是还算个完整的地球贴图

2.添加一个地球

  • 用刚才画出来的canvas地球贴图作为球体的材质就能得到一个地球了
js 复制代码
//地球canvas贴图
const map = new THREE.CanvasTexture(canvas);
            map.wrapS = THREE.RepeatWrapping;
            map.wrapT = THREE.RepeatWrapping;
            //球体
            const geometry = new THREE.SphereGeometry(1, 32, 32);
            
            const material = new THREE.MeshBasicMaterial({ map: map, transparent: true });
            const sphere = new THREE.Mesh(geometry, material);
            this.scene.add(sphere);
            

3.在地球上添加柱体

1.计算柱体位置

  • 柱体要相对于地球表面进行旋转延伸,如果用球坐标系计算有点麻烦,而THREE有个很好的属性,对象是层级结构的,子级对象的位置和朝向都是相对于父级对象的。比如月亮绕着地球转(月亮是地球的子级),地球绕着太阳转的(地球是太阳的子级),只需要设置太阳系自转和地球自转,就可以做到。

  • 那么我们可以利用这个属性,添加几个层级结构的3D对象,作为辅助对象,通过操作3D对象的变换,获取其变换矩阵作为柱体变换矩阵即可。

js 复制代码
 const lonHelper = new THREE.Object3D();//经度旋转辅助对象
            this.scene.add(lonHelper); 
            const latHelper = new THREE.Object3D();//维度旋转辅助对象
            lonHelper.add(latHelper);

            const positionHelper = new THREE.Object3D();//最终位置辅助对象
            positionHelper.position.z = 1;//球体半径是1,让变换位置在球体表面,需z坐标向外偏移1
             latHelper.add(positionHelper);
  • 柱体在球面的经纬度的位置为,经度辅助对象先绕y轴旋转对应经度,然后维度辅助对象绕x轴旋转对应维度,即位置辅助对象当前变换矩阵就是柱体的最终位置的变换矩阵。

假设辅助对象变成了长方体,我们可以观察它的变化

js 复制代码
   //经度旋转辅助对象
          const lonHelper = new THREE.Mesh(
            new THREE.BoxGeometry(0.5, 0.5, 2),
            new THREE.MeshBasicMaterial({ color: '#FF0000' })
          );
          this.scene.add(lonHelper);
          //维度旋转辅助对象
          const latHelper = new THREE.Mesh(
            new THREE.BoxGeometry(0.5, 0.5, 1),
            new THREE.MeshBasicMaterial({ color: '#0000FF' })
          );
          lonHelper.add(latHelper);

//最终位置辅助对象
          const positionHelper = new THREE.Mesh(
            new THREE.BoxGeometry(0.5, 0.5, 0.5),
            new THREE.MeshBasicMaterial({ color: '#00FF00' })
          );
          positionHelper.position.z = 1;
          latHelper.add(positionHelper);
          const gui = new dat.GUI();
          gui.add(lonHelper.rotation, 'y', -Math.PI, Math.PI);
          gui.add(latHelper.rotation, 'x', -Math.PI * 0.5, Math.PI * 0.5);

绿色小正方体的位置就是柱体的位置

2.添加柱体

  • 默认柱体形状是1的单位长度,因为柱体的底面要贴着地球表面,因此要将柱体中心点往外偏移0.5,让中心点落在底面上。
js 复制代码
 const boxGeometry = new THREE.BoxGeometry(1, 1, 1);
 boxGeometry.applyMatrix4(new THREE.Matrix4().makeTranslation(0, 0, 0.5));

计算柱体热力颜色: 采用HSL颜色模式

  • 色相(hue):色轮上从 0 到 360 的度数。0 是红色,120 是绿色,240 是蓝色。

  • 饱和度(saturation):百分比,0% 表示灰色阴影,而 100% 是全色。

  • 亮度(lightness)百分比,0% 是黑色,50% 是既不明也不暗,100%是白色

通过MathUtils.lerp线性插值来计算对应值要取什么颜色值

js 复制代码
 const amount = (value - this.min) / (this.max-this.min);
 //色相
  const hue = THREE.MathUtils.lerp(this.that.barHueStart, this.that.barHueEnd, amount);
//饱和度
const saturation = 1;
//亮度
const lightness = THREE.MathUtils.lerp(
            this.that.barLightStart,
            this.that.barLightEnd,
            amount
          );
 material.color.setHSL(hue, saturation, lightness);
          

注意:HSL颜色到THREE.color中要转换成[0,1]范围的值

js 复制代码
const mesh = new THREE.Mesh(geometry, material);
          this.scene.add(mesh);
//旋转经度
          lonHelper.rotation.y = THREE.MathUtils.degToRad(lon);
          //旋转维度

          latHelper.rotation.x = THREE.MathUtils.degToRad(lat);
//最终坐标位置
          positionHelper.updateWorldMatrix(true, false);
          mesh.applyMatrix4(positionHelper.matrixWorld);
//柱体高度跟着数值变化
          mesh.scale.set(0.01, 0.01, THREE.MathUtils.lerp(0.01, 0.5, amount));

数据来源于antV L7示例数据:全球地震热力分布

如图所见,经纬度位置与地球贴图位置对不上,那么需要进行位置调整。

  • 纬度跟canvas坐标一个问题,都是要取反才正确
  • 经度跟贴图位置偏差Math.PI*0.5
js 复制代码
lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + Math.PI * 0.5;
latHelper.rotation.x = THREE.MathUtils.degToRad(-lat);

4.地球柱体的使用

js 复制代码
   var myEarth = new MyEarth();
      window.myEarth = myEarth;
      myEarth.initThree(document.getElementById('canvas'));
      myEarth.createChart({ 
        bg: '#000080',//背景色
        borderColor: '#1E90FF',//描边颜色
        blurColor: '#1E90FF',//边界模糊颜色
        borderWidth: 1,//描边宽度
        blurWidth: 5,//边界模糊范围
        fillColor: 'rgb(30 ,144 ,255,0.3)',//区块填充颜色

        barHueStart: 0.2,//开始色相
        barHueEnd: 0.5,//结束色相
        barLightStart: 0.5,//开始亮度
        barLightEnd: 0.78//结束亮度
      });

5.优化大量柱体

柱体数量太多会让THREE渲染有压力,可以采用以下两种常用方式进行优化

1.mergeGeometries优化

  • 将所有柱体合并成一个整的形状,减少形状数量。

计算每个柱体的形状: 因为柱体不再是一个单独的Mesh不能用scale来缩放,达到高度随数值变化,而是要在生成图形时把缩放高度也加到变化矩阵中。

这时候需要在positionHelper位置辅助对象的基础上再加上一个originHelper缩放辅助对象。缩放辅助对象是针对柱体大小的,柱体原始大小是1,所以将变换位置往外偏移0.5,相对于地球表面缩放。

js 复制代码
 this.originHelper = new THREE.Object3D();
            this.originHelper.position.z = 0.5;
            this.positionHelper.add(this.originHelper);

最终柱体的位置和大小 位置辅助对象根据数值缩放,缩放辅助对象跟着改变,得到最终柱体矩阵

js 复制代码
  lonHelper.rotation.y = THREE.MathUtils.degToRad(lon) + Math.PI * 0.5;
          latHelper.rotation.x = THREE.MathUtils.degToRad(-lat);

          positionHelper.updateWorldMatrix(true, false);
          //位置辅助对象根据数值缩放
          positionHelper.scale.set(0.01, 0.01, THREE.MathUtils.lerp(0.01, 0.5, amount));
          //最终柱体矩阵
          originHelper.updateWorldMatrix(true, false);
          geometry.applyMatrix4(originHelper.matrixWorld);

多个柱体合并成一个形状

js 复制代码
this.geometries=[]


//柱体颜色
 const color = new THREE.Color();
          color.setHSL(hue, saturation, lightness);
//柱体形状
          const geometry = new THREE.BoxGeometry(1, 1, 1);

          const rgb = color.toArray().map((v) => v * 255);
          //颜色数组等于顶点数*3
          const colors = new Uint8Array(3 * geometry.getAttribute('position').count);

          // 将颜色赋值到每个顶点的颜色数组中
          colors.forEach((v, ndx) => {
            colors[ndx] = rgb[ndx % 3];
          });

          geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3, true));
          //....柱体的位置和大小
          
          //添加到合并形状数组中
           this.geometries.push(geometry);
           
           //合并形状
            const mergedGeometry = BufferGeometryUtils.mergeGeometries(this.geometries, false);
                const boxmesh = new THREE.Mesh(
                  mergedGeometry,
                  new THREE.MeshBasicMaterial({
                  //颜色使用顶点颜色
                    vertexColors: true
                  })
                );
                this.scene.add(boxmesh);

注意:BufferGeometryUtils.mergeGeometries方法在three的0.124.0版本不存在,在目前最新版本0.156.1存在,不同版本之间的图元Geometry的格式有所不同,即便复制高版本的BufferGeometryUtils到低版本也不能完全兼容,所以要注意three的版本

2.InstancedMesh优化

如果必须渲染大量具有相同几何体和材质但具有不同世界变换的对象,用InstancedMeshInstancedMesh适用于大量复用,它能减少绘制调用的次数,从而提高应用程序的整体渲染性能。

InstancedMesh能被Raycaster监测到instanceId实例索引,进而可以设置交互

js 复制代码
 this.mesh = new THREE.InstancedMesh(boxGeometry, boxMaterial, res.length);
   this.scene.add(this.mesh);
   
   //设置柱体变换矩阵和颜色
   res.forEach((a, i) => {
                 ///....计算柱体颜色,位置大小
                 
                 //设置实例颜色
                  this.mesh.setColorAt(i, color);
                  //设置实例变换矩阵
                  this.mesh.setMatrixAt(i, originHelper.matrixWorld);
                });
                //更新柱体网格颜色
                this.mesh.instanceColor.needsUpdate = true;
                //更新柱体网格位置
                this.mesh.instanceMatrix.needsUpdate = true;

6.地球出场动画

  • 实现效果:地球从小变大,并且边旋转,视角边转到对应位置,柱体从短慢慢往外延伸到对应高度
js 复制代码
this.objGroup.scale.set(0.1, 0.1, 0.1);
        //开始视角
                let orgCamera = this.camera.position;
                let orgControl = this.controls.target;
                const { cameraPos, controlPos } = this.that;
                //视角,缩放,旋转的出场动画
                let tween = new TWEEN.Tween({
                
                  scale: 0.1,
                  rotate: 0,
          
                  cameraX: orgCamera.x,
                  cameraY: orgCamera.y,
                  cameraZ: orgCamera.z,
                  controlsX: orgControl.x,
                  controlsY: orgControl.y,
                  controlsZ: orgControl.z
                })
                  .to(
                    {
                      scale: 1,
                      rotate: Math.PI,
                      //目标视角
                      cameraX: cameraPos.x,
                      cameraY: cameraPos.y,
                      cameraZ: cameraPos.z,
                      controlsX: controlPos.x,
                      controlsY: controlPos.y,
                      controlsZ: controlPos.z
                    },
                    2000//持续时间
                  )
                  //时间变化方法
                  .easing(TWEEN.Easing.Quadratic.Out)
                  //动画更新
                  .onUpdate((obj) => {
                    this.objGroup.scale.set(obj.scale, obj.scale, obj.scale);
                    this.objGroup.rotation.y = obj.rotate;
                    this.camera.position.set(obj.cameraX, obj.cameraY, obj.cameraZ);
                    this.controls.target.set(obj.controlsX, obj.controlsY, obj.controlsZ);
                  })
                  //链式执行下一个动画
                  .chain(
                  //柱体动画
                    new TWEEN.Tween({ h: this.barMin })
                      .to({ h: this.barMax }, 2000)
                      .easing(TWEEN.Easing.Quadratic.Out)
                      .onUpdate((obj) => {//更新柱体高度
                        this.currentBarH = obj.h;
                        this.addBars(res);
                      })
                  )
                  .start();
                TWEEN.add(tween);

Github

https://github.com/xiaolidan00/my-earth

参考:

相关推荐
小白学习日记38 分钟前
【复习】HTML常用标签<table>
前端·html
john_hjy41 分钟前
11. 异步编程
运维·服务器·javascript
风清扬_jd1 小时前
Chromium 中JavaScript Fetch API接口c++代码实现(二)
javascript·c++·chrome
丁总学Java1 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele1 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
It'sMyGo1 小时前
Javascript数组研究09_Array.prototype[Symbol.unscopables]
开发语言·javascript·原型模式
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
xgq2 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
李是啥也不会2 小时前
数组的概念
javascript
用户3157476081352 小时前
前端之路-了解原型和原型链
前端