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

简介: 地球人怎么可以不会画地球!从canvas画地球贴图开始,用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

functiondrawRegion(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 = newTHREE.CanvasTexture(canvas);            map.wrapS = THREE.RepeatWrapping;            map.wrapT = THREE.RepeatWrapping;            //球体            const geometry = newTHREE.SphereGeometry(1, 32, 32);                        const material = newTHREE.MeshBasicMaterial({ map: map, transparent: true });            const sphere = newTHREE.Mesh(geometry, material);            this.scene.add(sphere);            

3.在地球上添加柱体

1.计算柱体位置

  • 柱体要相对于地球表面进行旋转延伸,如果用球坐标系计算有点麻烦,而THREE有个很好的属性,对象是层级结构的,子级对象的位置和朝向都是相对于父级对象的。比如月亮绕着地球转(月亮是地球的子级),地球绕着太阳转的(地球是太阳的子级),只需要设置太阳系自转和地球自转,就可以做到。
  • 那么我们可以利用这个属性,添加几个层级结构的3D对象,作为辅助对象,通过操作3D对象的变换,获取其变换矩阵作为柱体变换矩阵即可。

js

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

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

js

   //经度旋转辅助对象          const lonHelper = newTHREE.Mesh(            newTHREE.BoxGeometry(0.5, 0.5, 2),            newTHREE.MeshBasicMaterial({ color: '#FF0000' })          );          this.scene.add(lonHelper);          //维度旋转辅助对象          const latHelper = newTHREE.Mesh(            newTHREE.BoxGeometry(0.5, 0.5, 1),            newTHREE.MeshBasicMaterial({ color: '#0000FF' })          );          lonHelper.add(latHelper);//最终位置辅助对象          const positionHelper = newTHREE.Mesh(            newTHREE.BoxGeometry(0.5, 0.5, 0.5),            newTHREE.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 = newTHREE.BoxGeometry(1, 1, 1); boxGeometry.applyMatrix4(newTHREE.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 = newTHREE.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 = newMyEarth();      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 = newTHREE.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 = newTHREE.Color();          color.setHSL(hue, saturation, lightness);//柱体形状          const geometry = newTHREE.BoxGeometry(1, 1, 1);          const rgb = color.toArray().map((v) => v * 255);          //颜色数组等于顶点数*3          const colors = newUint8Array(3 * geometry.getAttribute('position').count);          // 将颜色赋值到每个顶点的颜色数组中          colors.forEach((v, ndx) => {            colors[ndx] = rgb[ndx % 3];          });          geometry.setAttribute('color', newTHREE.BufferAttribute(colors, 3, true));          //....柱体的位置和大小                    //添加到合并形状数组中           this.geometries.push(geometry);                      //合并形状            const mergedGeometry = BufferGeometryUtils.mergeGeometries(this.geometries, false);                const boxmesh = newTHREE.Mesh(                  mergedGeometry,                  newTHREE.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 = newTHREE.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 = newTWEEN.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(                  //柱体动画                    newTWEEN.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

参考:

相关文章
|
4月前
|
JavaScript
js实现图片3D轮播效果(收藏)
js实现图片3D轮播效果(收藏)
55 0
|
4月前
three.js的3D模型渲染主要构成
three.js的3D模型渲染主要构成
87 0
基于three.js的牛逼轰轰的3D编辑器nunuStudio!
这是一款基于Three.js的3D编辑器,我之前一直喊错,叫人家"牛牛",因为我觉得它真的好牛,其实人家正确拼音喊“努努”! 可以发布web的运行包,直接可以网页端二次开发,真的不要太方便了!
基于three.js的牛逼轰轰的3D编辑器nunuStudio!
|
1月前
|
编解码 缓存 算法
Three.js如何降低3D模型的大小以便更快加载
为加快600MB的3D模型在Three.js中的加载速度,可采用多种压缩方法:1) 减少顶点数,使用简化工具或LOD技术;2) 压缩纹理,降低分辨率或转为KTX2等格式;3) 采用高效文件格式如glTF 2.0及draco压缩;4) 合并材质减少数量;5) 利用Three.js内置优化如BufferGeometry;6) 按需分批加载模型;7) Web Workers后台处理;8) 多模型合并减少绘制;9) 使用Texture Atlas及专业优化工具。示例代码展示了使用GLTFLoader加载优化后的模型。
179 12
|
1月前
|
存储 JavaScript 前端开发
小白实战!用JS实现一个3D翻书效果,附上代码
小白实战!用JS实现一个3D翻书效果,附上代码
|
1月前
|
存储 JavaScript 前端开发
使用JS创造一个3D粒子化星空,十分酷炫,大家快进来看看吧
使用JS创造一个3D粒子化星空,十分酷炫,大家快进来看看吧
|
3月前
|
Web App开发 JavaScript 前端开发
程序员必知:【three.js练习程序】创建地球贴图
程序员必知:【three.js练习程序】创建地球贴图
30 0
|
4月前
|
JavaScript 开发工具 git
Three.js第1篇,Three.js新手教学,如何在项目中使用Three.js(three.js使用流程详细,three.js的使用方式,three.js创建3d物体)
Three.js封装了WebGL的底层细节,是一款运行在浏览器中的 3D 引擎,可以用它创建各种三维场景,包括了摄影机、光影、材质等各种对象,目前在Git上已经拥有90k+的star。
79 0
Three.js第1篇,Three.js新手教学,如何在项目中使用Three.js(three.js使用流程详细,three.js的使用方式,three.js创建3d物体)
|
JavaScript 前端开发 CDN
JavaScript 实现 3D 模型
JavaScript 实现 3D 模型
133 0
|
11月前
|
存储 数据可视化 JavaScript
Three.js:打造独一无二的3D模型可视化编辑神器!
因为之前工作过的可视化大屏项目开发3d大屏组件模块需要用到Three.js来完成,其主功能是实现对3d模型的材质,灯光,背景,动画。等属性进行可视化的编辑操作以及模型编辑数据的存储和模型在大屏上面的拖拽显示
504 1
Three.js:打造独一无二的3D模型可视化编辑神器!