Web 3D 开发实践:3D 月球组件
最后更新于 2022-09-17 17:23:01
恰逢中秋节,有机会参与到一项中秋运营活动的项目中,负责为业务方提供一个移动端场景投放的 3D 月球组件。由于一直接触的是 2D 可视化开发,对于 3D 开发(WebGL)的技术未有深入了解和实践经验,基于对现有社区主流技术的简单了解和团队成员的技术背景,遂选定基于 Three.js 进行开发以降低风险。
这篇文章是基于 3D 月球组件的开发实践,记录 3D 开发实践中了解的一些 Web 3D 技术知识和一些典型问题场景的解决方案。
先看看最终线上效果:
为什么选择 three.js?
这里,先简单的聊一聊为什么会选择 three.js?
鉴于对 3D 开发技术不够了解和时间紧迫等原因,优先考虑借助工具库完成组件开发。开源社区主流的 3D 开发工具库可以根据 npm trends 提供的信息做个参考,其中 three.js 的下载量遥遥领先,可见 three.js 已经被应用的很广泛了,是一个足够成熟的工具库。另一方面,Web 3D 技术的应用场景大多还是集中在游戏领域,例如 Babylon.js 这个游戏开发框架,以及用来构建 AR 应用的 A-Frame,所以可选择的成熟方案并不多。
综合考虑,three.js 在社区应用广泛,并且资料丰富、官方文档也较为友好,是一个比较合适的方案,另一方面,three.js 属于是对底层进行封装抽象的 API 集合,轻量也不复杂,而对于一个偏向于框架的工具来说就引入了不必要的复杂度。与此同时,考虑到团队内成员的技术背景(部分人基于 three.js 实践过 3D 开发),基于 three.js 来开发组件技术风险也能低一些。顺带提及一下 three-globe 这个工具库,是用来专门构建 3D 地球可视化场景的开箱即用工具箱,也是后来开发过程中才了解到的,值得探究。
three.js 的应用程序结构
选定技术方案后,需要对此次的 3D 月球组件需求任务要进行一个分析和拆解,做一个简化的应用架构设计,方便后续编码实现和快速迭代。在设计 3D 月球组件之前,有必要了解一下 three.js 的应用程序结构,便于完成后续工作。
如上图所示,典型的 three.js 应用程序结构包含渲染器(Renderer)、场景(Scene)、相机(Camera)三大部分。相对于 2D 可视化开发来说,多了场景和相机这两个概念,前者要容易理解一些,可以简单的认为是绘图空间;而后者相机是一个新 的东西,对于屏幕来说永远都是一个二维平面,三维物体的展现也是在特定的距离、方位和视角下,可以等同于人眼来理解相机。
场景图是 three.js 的核心,用来组织所有的可视元素节点,与 2D 开发类似,都有一个组(Group) 的概念,其可以很方便将大型场景进行分组设计、布局计算。对于 3D 月球组件的场景图结构设计也是基于组的概念来完成。
下面以一个现实中的场景来举例说明组(Group)的概念重要性。如下图所示,太阳(黄色)、地球(蓝色)、月球(灰色)的分布,我们都知道地球绕着太阳转,月球绕着地球转,在全局空间中月球的位置将会变得难以计算(因为月球参照的中心点地球位置在移动,而月球还绕着地球在旋转)。此时,可以考虑将月球放在地球形成的组(Group)中,在这个组中的任何物体的布局计算都不再是基于全局空间的坐标原点,而是基于该组所在的坐标点,在这个“局部空间”中月球的坐标计算只需要考虑自身旋转运动即可,组的坐标点更新会自动同步到组中月球节点上。
对于此次要开发的 3D 月球组件来说,场景图结构设计要简单得多,如下所示:
3D 月球组件中的 3 大元素就是月球(Moon)、标签(Word)、飞线(FlyLine),其中标签通过一个组(Group)来实现,因其包含一个圆形节点和文本节点,可以统一位置布局;事实上飞线也是会存在多个的,也可将其放入一个组(Group)内,只不过这里仅仅是用来便于统一管理。按理来说,标签位于月球表面,而月球会自转,标签和月球放入一个组(Group)内不是更好?但由于这里的月球中心位于坐标原点处,球面标签的位置非常好计算,而且月球的自转动画的实现也借助了轨道控制器(后续会提到),所以不放在一个组内也是可以的。
参考官方文档,很快就能启动一个 3D 可视化项目,具体的代码不在这里举例说明。需要特别说明的是,对于三维场景中的坐标计算,通常利用一些简单的数学知识就能完成,而 three.js 提供了非常多的开箱即用的数学工具,例如线、球体、椭圆几何体的计算、向量的叉积与点积运算等、线之间的夹角计算等等,需要用到的时候先仔细翻翻官方文档,大概率是可以找到 api 的。
开始构建 3D 月球组件
现在,基于上文提到的场景图结构设计的参考图,再结合 2D 可视化开发中的组件化(模块化)思路,最终的项目结构呈现为:
src
├── assets
├── component
│ ├── FlyLine.ts
│ ├── Moon.ts
│ └── Word.ts
├── core
│ ├── Component.ts
│ └── Controller.ts
├── util
│ ├── common.ts
│ └── math.ts
└── index.ts
如上所示,Controller.ts 文件是整个组件程序控制流的实现,而 component/ 文件夹中的几个文件是业务组件的实现。
// src/core/Controller.ts
export default class Controller {
private __initialize() {
this.__initRenderer();
this.__initScene();
this.__initCamera();
this.__initLights();
this.__initControls();
this.__initInteractionManager();
this.___bindEvents();
this.__initialized = true;
this.__loop();
}
private __update() {
this.__getAllComponents().forEach((comm) => {
comm.update();
});
this.controls.update();
this._renderer.render(this.scene, this.camera);
}
private __loop() {
if (!this.__initialized) {
return;
}
this.__animationLoop = requestAnimationFrame((timestamp) => {
this.__update();
this.__loop();
});
}
}