前段时间流行的3D地球是怎么做的?

1,853 阅读5分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第6天,点击查看活动详情

年前突然发现Azure华为云 这两个页面上都出现了3D地球,它是怎么实现的?来听听我们的约稿作者——鹏军同学的介绍,上一秒还是他的知识,下一秒就是你的了,快来get吧

一、需求背景

华为云官网首页全球站点楼层需要实现一个可交互的3D地球,这个地球具备以下几点功能:

  1. 球体能正常的展示世界地图
  2. 可自转,可拖动旋转,鼠标悬浮球体,球体停止自转
  3. 城市标记可悬浮交互,旋转到背面会隐藏
  4. 城市坐标内容可动态添加

我们用Three.js来开发

image-20210525190050601_1644394877093.png

二、技术要点

通过Three.js基础可以知道,要实现这一效果,需要用到以下几点技术:

  1. 基本的几何体绘制与贴图
  2. orbitControls相关的轨道控制交互
  3. 如何在3D场景中添加2D可交互dom
  4. 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 地球贴图

image-20210627215323844_1644394930632.png

纹理贴图的具体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,由于球体的表面是曲面,南极和北极会出现贴图拉伸而变模糊的情况,所以我们通过增加这两篇区域的像素来达到更清晰的效果,市面上大多数地球贴图都有这个毛病:

image-20210627220139405_1644394901125.png

上图是滴滴官网首页的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球体的效果了:

c44b0d621c6ed9b0ca97de36dace3b28_982x822_1644395120895.gif

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投影计算

光是有一个光秃秃的地球不足以支撑官网的内容表达,我们需要赋予它足够的信息,就是一个个确定了位置的城市标签及其文字面板,城市的标签具体波浪的效果,这里其实涉及到技术选型了:

  1. 用3D平面几何体和文字几何体绘制

    优点:自带3维场景投影,可以在城市转到背面后自动被遮住

    缺点:动画效果实现麻烦;文字几何体生成极其耗费性能,加载时间慢

  2. 用canvas绘制

    优点:性能耗费比1小

    缺点:动画效果实现麻烦,投影需自己计算

  3. 用html绘制

    优点:动画效果实现简单,性能耗费最低

    缺点:3维投影关系需自己计算

最终我们选用了第三种:用html绘制那最难的就是要搞定3维场景投影关系,实时计算每一个城市是否转到了地球后面,再用css做隐藏

很幸运,我在外网找到了一篇关于这种绘制场景的文章

image-20210627224328242_1644394995561.png 从这个图我们就可以发现这里面的几何原理,算夹角!!

球体的位置和摄像机的位置都是已知的,我们只需要知道每个城市的位置后,算出来两条线的夹角小于90°,那么label就一定是隐藏状态:

2.3.1 根据经纬度算坐标

image-20210627225825231_1644395011073.png

参考此图能得出位置根据经纬度的算法:

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

完整代码较长,就不贴了,详情可以查看文章

三、最终效果

b23984bf07280892c21b376da377c9b6_852x666.gif@900-0-90-f_1644395065067.gif

配上阴影与光晕等细节,我们最终实现了上图中的效果。

再回顾一下技术点:

  • Three.js 3D场景与几何体绘制
  • OrbitControls轨道控制
  • RayCaster光线投影
  • 根据经纬度算3维坐标并通过夹角计算投影规则