一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情。
年前突然发现Azure 和华为云 这两个页面上都出现了3D地球,它是怎么实现的?来听听我们的约稿作者——鹏军同学的介绍,上一秒还是他的知识,下一秒就是你的了,快来get吧
一、需求背景
华为云官网首页全球站点楼层需要实现一个可交互的3D地球,这个地球具备以下几点功能:
- 球体能正常的展示世界地图
- 可自转,可拖动旋转,鼠标悬浮球体,球体停止自转
- 城市标记可悬浮交互,旋转到背面会隐藏
- 城市坐标内容可动态添加
我们用Three.js来开发
二、技术要点
通过Three.js基础可以知道,要实现这一效果,需要用到以下几点技术:
- 基本的几何体绘制与贴图
- orbitControls相关的轨道控制交互
- 如何在3D场景中添加2D可交互dom
- 3D投影计算
其中3,4两点是本次需求的难点,我们一步步学习并实现相关功能。
2.1 球形几何体绘制与贴图
2.1.1 球形几何体
从上一章几何体一节可知,球体的绘制需要调用SphereGeometry(球体)这一函数:
const geometry = new THREE.SphereGeometry(1, 64, 64);
let material = new THREE.MeshBasicMaterial({
color: 0xffffff,
transparent: true,
});
let mesh = new THREE.Mesh(geometry, material);
this.scene.add(mesh);
老三样,非常简单:
- 创建几何体
- 创建材质
- 几何体+材质 生成物体并加入场景
2.1.2 地球贴图
纹理贴图的具体API我们直接参考官方文档
const texture = new THREE.TextureLoader().load(
this.$el.getAttribute('data-map') ||
'https://res.hc-cdn.com/cpage-pep-home-page/2.0.10/images/global-site-3d/%E5%9C%B0%E5%9B%BE.jpg',
); // 创建纹理贴图
texture.anisotropy = 10;
const geometry = new THREE.SphereGeometry(1, 64, 64);
let material = new THREE.MeshBasicMaterial({
color: 0xffffff,
map: texture,
transparent: true,
});
注意这个属性texture.anisotropy = 10,由于球体的表面是曲面,南极和北极会出现贴图拉伸而变模糊的情况,所以我们通过增加这两篇区域的像素来达到更清晰的效果,市面上大多数地球贴图都有这个毛病:
上图是滴滴官网首页的3d地球,就存在这个问题。
到这一步我们就完成了一个基本的地球绘制,先不急着看效果,我们把鼠标的拖动和自转也加上!
2.2 轨道控制
2.2.1 OrbitControls实现基本交互
轨道控制上节也学习过,我们就直接贴代码了:
// 初始化轨道控制器
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
this.controls.enableZoom = false; // 禁止缩放
this.controls.enableDamping = true; // 增加阻尼感
this.controls.dampingFactor = 0.05; // 阻尼系数
this.controls.autoRotate = true; // 轨道视角是否自转
this.controls.autoRotateSpeed = 0.1;// 自转速度
this.controls.enablePan = false;// 禁止平移
出了基础的new一个OrbitControls实例之外,我们增加了很多配置项,当然OrbitControls还支持许多其它配置,这个大家可以上官网查看文档
ok,到这里我就实现了很多网站上3D球体的效果了:
2.2.2 Raycaster实现悬浮响应
这个类用于进行raycasting(光线投射)。 光线投射用于进行鼠标拾取(在三维空间中计算出鼠标移过了什么物体)。
public render() {
this.controls.autoRotate = true;
this.renderRequested = undefined;
// 在Render函数中实时跟踪鼠标的位置以及摄像机位置
this.raycaster.setFromCamera(this.mouse, this.camera);
// intersectObjects函数会返回场景中被鼠标碰到的3d物体
const intersects = this.raycaster.intersectObjects(this.scene.children);
// 因为场景中的3D物体只有球体,所以只要有被碰到的3d物体,就是地球,此时把控制器自转关掉
for (let i = 0; i < intersects.length; i++) {
this.controls.autoRotate = false;
}
if (this.resizeRendererToDisplaySize(this.renderer)) {
const canvas = this.renderer.domElement;
this.camera.aspect = canvas.clientWidth / canvas.clientHeight;
this.camera.updateProjectionMatrix();
}
this.controls.update();
this.updateLabels();
this.renderer.render(this.scene, this.camera);
requestAnimationFrame(() => this.render());
}
2.3 创建城市DOM与3D投影计算
光是有一个光秃秃的地球不足以支撑官网的内容表达,我们需要赋予它足够的信息,就是一个个确定了位置的城市标签及其文字面板,城市的标签具体波浪的效果,这里其实涉及到技术选型了:
-
用3D平面几何体和文字几何体绘制
优点:自带3维场景投影,可以在城市转到背面后自动被遮住
缺点:动画效果实现麻烦;文字几何体生成极其耗费性能,加载时间慢
-
用canvas绘制
优点:性能耗费比1小
缺点:动画效果实现麻烦,投影需自己计算
-
用html绘制
优点:动画效果实现简单,性能耗费最低
缺点:3维投影关系需自己计算
最终我们选用了第三种:用html绘制,那最难的就是要搞定3维场景投影关系,实时计算每一个城市是否转到了地球后面,再用css做隐藏。
很幸运,我在外网找到了一篇关于这种绘制场景的文章:
从这个图我们就可以发现这里面的几何原理,算夹角!! 。
球体的位置和摄像机的位置都是已知的,我们只需要知道每个城市的位置后,算出来两条线的夹角小于90°,那么label就一定是隐藏状态:
2.3.1 根据经纬度算坐标
参考此图能得出位置根据经纬度的算法:
y=OE=OD sin∠DOB
y=OE=OD sin∠DOB
OB=OD cos∠DOB
OB=OD cos∠DOB
z=OA=OB cos∠AOB
z=OA=OB cos∠AOB
x=OC=OB sin∠AOB
对应到代码中就是:
function getPosition(longitude, latitude, radius) {
var lg = THREE.Math.degToRad(longitude);
var lt = THREE.Math.degToRad(latitude);
var temp = radius * Math.cos(lt);
var x = temp * Math.sin(lg);
var y = radius * Math.sin(lt);
var z = temp * Math.cos(lg);
return {
x: x,
y: y,
z: z
}
}
可参考此文:blog.csdn.net/oneKnow/art…
2.3.2 计算夹角
得到了3个点的坐标自己就可以计算出夹角了:
// get a matrix that represents a relative orientation of the camera
normalMatrix.getNormalMatrix(camera.matrixWorldInverse);
// get the camera's position
camera.getWorldPosition(cameraPosition);
for (const countryInfo of countryInfos) {
...
// if the orientation is not facing us hide it.
if (dot > settings.maxVisibleDot) {
elem.style.display = 'none';
continue;
}
完整代码较长,就不贴了,详情可以查看文章
三、最终效果
配上阴影与光晕等细节,我们最终实现了上图中的效果。
再回顾一下技术点:
- Three.js 3D场景与几何体绘制
- OrbitControls轨道控制
- RayCaster光线投影
- 根据经纬度算3维坐标并通过夹角计算投影规则