发布于: 2023.12.15
最近在一个官网项目上,需要展示一个3D效果的地球,并用亮点呈现该企业在全球的分布情况。本文主要记录了,使用 Three.js
绘制3D地球的“流水账”过程。
经过一些搜索,绘制3D地球的基础库有: Echarts
, Three.js
, CesiumJS
。
还有两个组件库 globe.gl
和 three-globe
是基于Three.js/WebGL
的实现。
Echarts
有一些3D地球示例,其中 Hello World 示例,代码实现比较简单和期望效果比较接近,但是没有示例绘制地球上的亮点,也看不懂API。待定吧。three-globe
的 Basic 示例比较接近设计稿,但也看不懂API,也待定吧。globe.gl
的 World Cities 示例也比较接近设计稿。使用 geojson
数据画的陆地,这数据和国家地理信息有关,需要谨慎使用(果然是有问题的)。同类的格式还有 TopoJSON
。这种 geojson
数据文件不好找。放弃了。CesiumJS
的 Quickstart
,怎么还要注册和创建 access_token
,这么麻烦。放弃了。只需要画个地球,没有其他复杂的动效,three-globe
就没必要了,还是研究一下基础库 Three.js
。于是最简版的 TODO LIST 就有了:
再看看 Echarts
的 Hello World 示例吧。地球表面居然是一个矩形图片?它是怎么变成球面的?
设计稿上的地球是黑白两种颜色,还得需要设计师PS图。要不先看看 GeoJSON
吧,可以自定义颜色。
TopoJSON
的 world-atlas 这个库有比较全的地理数据。搜搜我们的台湾在哪儿,果然有妖。国内的 GeoJSON
数据也不好找。只需要显示陆地,不需要国家的信息。先放弃了。
在 YouTube 上搜搜 three.js+globe
的视频。啊哈,原来这类图片叫“纹理Texture
”。在planetpixelemporium.com 里,有很多清晰的地球纹理图片。
并且,three-globe
的 basic 示例里,也使用的是地球图片。这下可以确定使用 Tree.js
+Texture
纹理图片的方案,绘制3D地球。
Three.js
对象模型先抄个 Three.js
的入门教程 Creating a scene
。太简单了,但 PerspectiveCamera
, BoxGeometry
, MeshBasicMaterial
, Mesh
这些API名词怎么理解和使用?
首先,需要理解一个基础概念 视锥体(Viewing Frustum)
,你确定这是“初等”几何?看这个图帮助理解 PerspectiveCamera
对象模型概念,
再找找其他视频和Blog:
这下理解了,原来在 Three.js
里,
Scene
,Camera
和 Renderer
这三个对象构建三维空间Mesh
)、几何体(Geometry
)形状和材质(Material
) ,三个对象组合而成的。画一个3D地球,需要使用 球形几何体 SphereGeometry
对象,加载地球纹理图片和设置 MeshBasicMaterial
的 map
属性。修改一下代码,再调整颜色和旋转等参数。搞定,见 globe-01 。
一下子就完成了2个Task,还剩最后一个Task:“用亮点显示城市位置”,估计再有一天就能做完。
问题来了,globe.rotation.y -= 0.005
的意思是地球沿Y轴方向旋转,那么地球的旋转速度是多少呢?XYZ坐标轴分别指向哪个方向?
在 latlong.net 网站可以搜到城市的经纬度数据。接下来,怎么将经纬度值转换成球面上的一个点坐标呢?
看这篇Blog“Three.js 地理坐标和三维空间坐标的转换”,理解了:“经纬度与地理坐标关系”,“球面坐标参数” 和 “地理坐标转换” 等理论知识。
最终,地理经纬度坐标转换三维坐标的算法,如下:
function lglt2xyz(lng, lat, radius) {
const phi = (180 + lng) * (Math.PI / 180)
const theta = (90 - lat) * (Math.PI / 180)
return {
x: -radius * Math.sin(theta) * Math.cos(phi),
y: radius * Math.cos(theta),
z: radius * Math.sin(theta) * Math.sin(phi),
}
}
已经忘了 sin
,cos
,Math.PI/180
,“弧度”和“角度”这些三角函数的知识,赶紧恶补一下。
效果图上的城市亮点是一个带有光晕的圆形。
先简单使用 圆形几何体CircleGeometry
画出来,再解决光晕效果。很快解决完以下问题:
Mesh
对象和城市Mesh
集合,也Group
起来,一起旋转。OrbitControls
对象困难的问题来了,需要圆形亮点要贴在地球上显示,也就是:
Three.js
的 example,ParametricGeometry
这个差不多可以用。然而第一个回调方法参数 u
和 v
是什么东西?怎么算?劝退了。rotation
参数就行,简单。依次调试处理:
rotation.x
和 rotation.y
弧度值。看下最终结果。圆形平面亮点为什么没有和球面相切?
看到文档有其他旋转接口 setRotationFromEuler(euler: Euler)
。Euler
这不就是“数学之神”欧拉么。还有,setRotationFromQuaternion(q: Quaternion)
,四元数(Quaternion)又是什么?
赶紧恶补欧拉角和四元数知识。学到了,四元数可以解决欧拉角旋转的“万向锁(Gimbal lock)”问题,但太难理解了。劝退了。 用欧拉角就足够了,调整一下旋转顺序就解决了相切的问题。核心代码如下,
function toEuler(latlng) {
const { lat, lng } = latlng
const x = -lat * (Math.PI / 180)
const y = (lng + 90) * (Math.PI / 180)
return new THREE.Euler(x, y, 0, 'YXZ') // 重点: 先旋转Y轴再旋转X轴
}
这是此时的效果 globe-02。之前评估再来一天就能画完地球上的亮点,太乐观了。还剩最后一个Task,怎么绘制光晕效果?
先使用png图片看一下,结果光晕会重叠在一起,不行。
那就尝试用着色器ShaderMaterial
实现,继续恶补WebGL
知识和GLSL
语法。终于知道到了“uv坐标”,“世界坐标”,“模型矩阵”等3D模型知识,还有,“顶点着色器Vertex Shader
”,“片元着色器Fragment Shader
” 等底层“着色器”的知识和用法。这又是一个全新而又复杂的领域。
几天后,客户说:“就用图片吧”。这一切终于可以结束了。