
华为云官网是华为云的门户页面,对于页面的视觉与交互体验有较高的要求,结合华为云官网动态、强拟物、3D 影像等主流设计风格,我们在 Web 页面上使用了包含 js 动效、WebGL、WebGPU 等多种 Web 动效技术,极大地提升了门户页面的视觉与交互体验效果。
本文整理自华为云前端工程师杨鹏军在 QCon 2022 广州站的演讲分享,主题为“华为云官网 Web3D 和动效技术的应用与探索"。
这次分享主要分四个部分,分别是背景介绍、动效平台的构建和 Web3D 技术的应用实践、总结展望。
背景介绍
我们希望能实现目前比较流行的一些概念,比如大家在苹果官网产品页看到的炫酷动效,商超的裸眼 3D 屏幕,再比如一些厂家做的数字孪生能力,以及经常冒出头的 VR、AR 的概念。

在华为云内部,我们最近几年也需要实现一些真实的 3D 场景光影,符合物理运动规律的动效,以及符合用户直觉的交互方式。华为云的官网是面向 Web 用户的,更倾向于构建一些体验上的能力。从开发角度来说,前端动效,在整个前端技术里面属于比较小的分支,大家开发的时候并不是非常重视,我们没有实现统一的动效能力。随着近期设计的要求量增多,我们逐渐开始意识到,前端需要去构建 Web3D 的能力,要做一些技术沉淀。我们需要 3D 能力来展示华为云内部的监控页面、展示看板,以及看板上的复杂数据交互等内容。受限于二维层面无法清晰呈现数据节点间的复杂关系,我们需要再加一个层级,拓宽到三维的空间,提高呈现效果。

动效应用
前端动效基本概念
基于这个背景,华为云前端开发团队做了动态能力的构建。为了方便后续理解,我先简述几个基本概念。前端开发人员应该都比较清楚,动效基本上分为四种,CSS 动效、JS 动效、多媒体、SVG 动效。从我的角度来说,最简单的就是 CSS 动效,可以通过 Transition、Transform 或者 Animation 属性去做一些过渡效果,或者一些帧动画。JS 动效用得比较多,像华为官网的一些场景用的 scroll,就是监听用户的 scroll 事件,然后更新页面上的 Dom,也有围绕 requestAnimationFrame 全局函数去做的一些实现。多媒体用得也比较多,但它缺乏交互性,主要就是视频、gif。为了性能考虑,可以把 gif 换成 apng,或者 Webp 这种性能更高、体验效果更好的图片格式。这些和开发关系不大,开发考虑的是做一些视频、图片上的性能优化问题。最后是 SVG 动效。我认为 SVG 动效主要是做一些单独的动画,跟我们官网契合度不高。华为用的主要是 CSS 动效和 JS 动效,所以对这两个能力做了一些封装。
通用前端动效插件封装
CSS 动效和我们平常开发组件差不多,封装一些底层的 less 或 Mixin,在定义这些变量的基础上做动效封装。比如在一些渐变、旋转平移等场景,以及更上层的动效,例如如下图所示的华为线,各种元素悬浮的微动效,以及背景放缩等。

接下来介绍 JS 通用动效的封装。JS 通用动效的封装是围绕 requestAnimationFrame 建立最基本的底层能力。因为 requestAnimationFrame 它的调用方式跟 setTimeout 与 setInterval 不太一致,setInstance 和 setTimeout 是让用户去指定间隔的时间,跟屏幕的刷新率不太匹配。而 requestAnimationFrame 函数是在每次页面刷新的时候去调用传进去回调函数。我们是基于 requestAnimationFrame 传进来的默认参数(时间戳),计算两个时间戳之间的 costTime,再去做整个进度里面的时间占比,更新动画实例的状态。我们之前需要做很多额外工作才能实现这个简单的机制。比如要做的动画实例、动画对象不是简单的 Dom,有的时候还要去更新页面上的标签,更新 3D 物体 material 属性中的 Color,或者 position 属性里面的坐标,而这些对于底层插件来说都是不同的输入,需要做解析函数。前面提到,设计要求动效符合运动规律,就需要定义一些动效曲线。比如 ease-in 、ease-out 等不同类型的运动规律曲线。

完成上述两种动效封装后我们已经可以实现很多种动效方式了。一般动效可以设置动画指令的初始值,再设置它的结束时间。你还可以指定延时、动效曲线的变换方式。这个通用的动效应用能够做一些常用的数字动画、导航,锚点定位,以及帧动画。
我们内部之前都是各自独立开发各自的功效的。这个动画组件发布后内部开发就可以直接接入使用了。我们基于这两个底层能力,还做了一些更复杂的封装。比如下图的这个模拟的 3D 鼠标跟随效果,其实不是用 3D 做的,而是用底层的帧动画去做的。监听鼠标运动在屏幕区域位置去对整个图形做上下定位,中间再加一个过渡效果,就可以实现模拟 3D 空间的鼠标跟随效果。这个动画组件还可以呈现苹果官网上常见的视差动画,就是大家去滚动页面的时候,各种不同的 Dom 去做视差平移,实现错位的效果。

我们做这个组件的目的是统一内部的动效能力。之前大家开发的时候会去使用各种不同的动效插件。把这个底层能力封装后,实现各自的动效会节省很多工作量。基于这个底层封装,大家还可以再在上层封装自己的动效应用。这有两种实现方式,一个在动效平台上做个表单,让大家手动去填。另一种是接入到内部的 Devops 平台,在发布的结点上注册一个钩子函数,钩子函数在发布时会调这个组件,把它内部的 Readme.md,或是 package.json 里的配置信息通过接口传到动画平台的数据库里面,平台会把组件信息展示出来。

有了这个平台后,我们用它建立了大概 30~40 个基础 / 上层的动效应用,官网目前有 200 多万个页面用到了这个动效组件通用能力,大概占到一半以上了。

Web3D 技术的探索与应用
Web3D 应用与动效组件的关联性不大,但 Web3D 里的动效使用了一些动效平台的通用插件。
在背景介绍里我们说了业务上的痛点,对于 Web 来说,全球的云服务布局,对外展示的一些解决方案的展厅,用 3D 的效果实现体验会更好。内部的接口调用链、全球性能监控,以及整个组网里面各种网云的可用性监控,定单的数据流走向,也可以用 3D 数据可视化提升使用体验。

需求确认之后,接下来就是技术选型。我们查了市面上大家用得比较多的一些 3D 能力库,第一个 High topo。High topo 相对来说功能比较强大,比 three.js 封装做得好,它把一些效果,比如文字标签、后期粒子光效等都已经封装好了,不需要自己再做;但是它不好的地方就是不开源,社区太封闭了,想要做一些更上层的封装就比较难。ECharts 也比较简单易用,但灵活性扩展性比较差,而我们需要做一些自己定制化的东西。three.js 上手难度稍微高一点,但是社区非常活跃,可以找到很多不同的效果,遇到问题,去网站搜一搜,大部分都能解决。

我们研发工作流很简单,设计和产品去画原形图,提供给我们面板的设计稿,如果有复杂的模型,就输出 GLTF 格式的模型,或者是 obj 格式的模型。模型导入后需要对材质信息调优、完成底层 3D 场景,元素构建。效果做完之后就是数据通用配置化工作。之后借助内部的平台把 3D 数据图表的通用插件发布出来,让各个业务去对接。

这里重点介绍开发的工作。如图所示,three.js 提供一些底层的场景。最基础的点、线、面元素一定要有,还有就是辅助元素。接下来是交互方法的封装。最上层实现一些动效、特效,提高视觉体验。

基本元素点、线、面的开发占用了我们大量时间。它不像写前端代码,做一些标签就行。节点(node)的展示形式非常多,除了通过 three.js 提供的 API 去画一些基本的 3D 元素外,还需要根据自己的展示做一些组合,或者通过 group 统一控制。node 有时候会比较复杂,自己画图形很难实现,这部分需要去跟设计师沟通,请设计师输出各种不同格式的 Model。如果不强求节点在页面的三维空间属性,为了性能也可以用 HTML DOM 去做。edge 就是根据图里是否带流量,或者根据粗细做不同的实现。如果没有粗细限制,就直接调 three.js 的 Line 函数实现。根据它是实线还是虚线,使用 material shader 自定义样式。如果是有粗细要求,就用管道去做。面的实现方式很简单,目前识别的有网格、平面和球面。

辅助元素开发也很花时间,主要是标签文字。我们不建议用 TextGeometry 实现标签文字,因为国内需要展示的一般都是中文文字,中文转换成 3D 几何体在页面上渲染,会有非常多的网格和面片。中文我们要么通过 Canvas 模拟,要么直接用 HTML DOM。一般只有在比较大的面板上,需要显示有质感的英文标签时,才会用 TextGeometry。Canvas 是把文字作为纹理贴图,画到 3D 元素属性上面。我们基于文字的一些基本属性做了一些封装,把文字背景色、边框、颜色、字体大小,统一封装到 planeText、SpriteText 类里面。辅助面板是整个 3D 界面里底部的操作提示。点击某个节点会显示一些辅助说明文字,基本上用 HTML 就能解决,这里不再赘述。

接下来讲交互。交互的原理是监听 camera 和鼠标放在屏幕上面的位置,中间发出一条射线,Raycaster 监听射线和 3D 场景里 Object 的交点,把焦点里面的物体全部存储起来,之后可以为 Object 绑定各种不同的事件。

接下来讲特效和动效的封装。动效主要是摄像机动画和元素动画。摄像机动画我们用了 GitHub 上比较常用的插件 CameraControl,它比 three.js 的 OrbitControl 提供的动画效果更好,可以呈现阻尼变化效果,在视角切换的时候也更加顺滑。元素动画总结起来就是一些贴图动画,用 canvas 可以实现动态纹理,更新 Mesh 属性也能实现一些动画。有一些效果我们会使用自定义的 Material 去做,定义它的顶点着色器和片段着色器,通过修改 JS 里的 Time 属性,跟顶点位置绑定做着色渲染。

下面分享我们的两个实践。第一个就是全球布局。一开始它就是个 Table,设计师要求用三维空间的球体去展示它。归结到底层其实就是点、线、面的能力。如下面 2 张图所示。


页面的功能主要包括标签点定位、2G、3G 切换和视角聚焦。这里的技术重点是球体表面的标签定位,还有 HTML DOM 显示和隐藏的处理。定位的问题就是,因为运营人员给到的是城市的经纬度信息,无法提供出球体上面三维空间里的 position。这需要我们去建立经纬度辅助器,把经纬度转换成三维空间上面的 position,然后再通过矩阵变换器定位到三维空间里。这里还涉及文字选型的问题。之前也说到,文字样式比较复杂,需要呈现悬浮动效。内容变更用三维方式去写会比较麻烦,所以我们就把它做成 HTML Dom。但是 HTML Dom 就带来一个问题,标签元素如何隐藏。我们需要把标签的 Z 轴坐标映射成 HTML 里的 CSS z-index 值,Z 轴坐标越靠近前面它的 z-index 值就越高。标签在球体上面转动时,要确定它是否被球体盖住。因为球体是三维的,但是 HTML 是二维的,两边本质上来说是互不影响的,如果不去做处理,球体转到后面,HTML 标签是能够被看到的。看下面的示意图,我们监听 dom 和球体中心的边以及 dom 和摄像机的边的夹角,如果小于 90 度就隐藏 dom。

这个实践还用在了官网和其他一些内部页面。因为这个 3D 地球在华为内部应用很广,很多页面都有用到,所以我们就做了通用性封装。比如通过不同贴图去表示一些主题,并且做了多端的兼容,还为标签单独做了配置化。
另一个实践应用是内部数据流监控。这个监控原来是用数据流图表示的。如果只是几个节点还好,但是应用到具体的业务里面,数据流的节点非常多,可能有几百个节点。每个节点里面是一个实例,下面还有一些子节点。在主流程上面点击子节点,子节点再跳到子流程,这样体验上就会有割裂感。于是我们对这个图表进行了 3D 化改造。

如下图所示,左边是整体布局的监控图,右边是每个云服务内部的一些具体流程。运维人员在看主流程的监控时,如果碰到某个云服务故障,就在界面上通过光学效果或数据流走向标注出来。点击节点可以进入下一个层级。子流程可以通过点击收起或展开。

数据流监控比 3D 地球用的模型要多,因为设计和产品要求这个 3D 的模型需要一些流化处理,所以我们还做了效果调优,包括通过 canvas 的动态纹理实现玻璃、辉光和能量球等高质量的 3D 材质效果。

材质主要是实现金属效果和玻璃效果。之前教导版本的 three.js 没有开放 MeshPhysicalMaterial 的 transmission 属性,要通过构建 shader 去实现。新版本的 three.js 提供了修改玻璃属性的函数后,就可以调整 transmission、thickness 属性设置透光度、反射度。金属材质可以通过调整表面的粗糙度,加上光照的环境贴图来实现更加逼真的效果。
性能优化
我们在性能优化的过程中也做了很多工作。如下图所示,图里有很多类似的云服务的节点,需要做一些网格的组合和 Mesh 克隆。我们建议尽量少去加载 obj 或 fbx 文件模型文件,推荐统一都用 GLTF 格式模型载入。实际的场景区分文字实现,其中部分面板文字用了 TextGeometry,其他大部分文字是用 HTML Dom 和 SpriteText 做的。

接下来具体讲讲大数据量对象渲染的优化。优化是一个渐进的过程。一开始,我们把网格用 Group 组合在一起,但每个 Group 都是单独渲染的。节点数量变多的时候,整体帧数就降低了,节点一多就变得非常卡。后面我们采用了网格组合方式,虽然性能提升了,但也有问题,因为它不是分片渲染的,所以材质都是一样的。我们的 3D 物体需要一些点缀性的装饰物,有不同的颜色等。虽然材质没办法改变,但是可以通过顶点转换的方式设置颜色,用 position 设置对应的颜色属性。
下面介绍关于资源的释放问题。在 3D 物体里面,包括几何体、纹理、材质都可以通过 dispose 方法把资源释放出去。我们不能每次想要释放资源的时候,都手动去 dispose,所以就做了资源跟踪类 ResourceTracker,类里封装了一些方法。在声明 three.js 对象时,把它用类提供的方法包一层,把资源注册到追踪器里。在需要释放整个场景时,统一调用追踪器的 dispose 方法。它做了两个方法,一个是把资源从场景中移除,另一个是一次性释放所有资源。

总结
最后是总结展望。第一点是技术沉淀和平台的重要性。我们一开始做能力构建时,并没有做那么多底层的东西,只是完成一些单一的设计要求。做得多了之后发现很多东西是可以去复用的,包括动效的一些能力。我们的 3D 图表就是点、线、面,对应一些贴图动效的结合。需求来了后,先根据业务去实现它,然后再去做技术去封装。除了这个之外,我们在 AR、VR,还有 WebGPU 上面也做了一些简单的探索。但目前业务应用的还不是特别广泛。
AR 在内部做了简单的探索,能够扫描地板,检测出地板的平面,完后在手机的屏幕上添加一些 3D 的模型。和华为云业务结合起来的场景是在大促活动里面,让用户去扫描一些能够识别的物体,完后领取促销品等。我们的 VR 从能力上来说不能算真实的 VR,它是基于第一人称视角用 VR 场景模拟了战机看板,也只是内部的一些实验性探索,用在了我们内部的一些看板的登录界面上面。

WebGPU 是我们下一代要用来渲染浏览器 3D 场景的一个 API,比 WebGL 的性能好很多,目前在浏览器的某些实验版本上可以用。我们下载了一个 Google Chrome 的 Canaries 版本(金丝雀版本),它的某个实验标签可以开启 WebGPU。开启后就可以跑 three.js,three.js 已经封装了一些 WebGPU 能力,可以通过 three.js 实现 WebGPU 场景。根据谷歌开发者文档所述,这个功能可能在 2023 年或 2025 年才会开放出来,技术会持续更迭,我们要紧跟潮流。

以上就是分享的全部内容。
演讲嘉宾介绍
杨鹏军,华为云官网前端工程师,毕业于东南大学,2018 年加入华为云,负责华为云官网首页及公共组件等的开发,多次主导华为云官网整体设计改版的开发工作,提升官网整体视觉与交互体验,对前端动效、Web3D 及前端性能优化有较深入的研究。
评论