作者: vivo 互联网前端团队- Su Ning
本文从多个维度对比 Galacean 和 Three.js 两款Web3D 引擎的差异,并介绍拟我形象项目从Three.js 切换到 Galacean 以后带来的提升以及项目迁移的心得,为其他 Three.js 项目升级到 Galacean 提供参考。
1分钟看图掌握核心观点👇

一、背景
Web 3D 技术的发展日新月异,为我们带来了前所未有的沉浸式体验。从虚拟展示到游戏开发,从建筑可视化到教育模拟,Web 3D 技术的应用场景愈发广泛。而在这一领域,Three.js 作为一款广受欢迎的 JavaScript 3D 库,凭借其简洁易用的 API 和丰富的功能,帮助众多开发者实现了精彩的 3D 项目。
然而,随着项目复杂度的不断提升,以及用户对性能和体验要求的日益苛刻,Three.js 逐渐显露出一些局限性。比如在处理重负载时,很容易遇到性能瓶颈,出现卡顿、掉帧等问题。这就如同一位经验丰富的车手,驾驶着一辆曾经性能卓越的赛车,但在面对愈发复杂的赛道和激烈的竞争时,却发现车辆的动力和操控性渐渐力不从心。
二、Galacean:新一代 Web 3D 引擎
2.1 业务简介
拟我形象是 vivo 账号中的一个3D数字人功能,提供一种代表自由、个性、创新和时尚的虚拟形象,为用户提供更加生动、直观、有趣的交流方式。采用 Native+H5混合的开发方式,其中 3D 渲染的部分基于 Three.js 进行开发。
2.2 技术挑战与痛点
-
**性能瓶颈:**人物模型包含大量形态键以实现多样化面部特征,导致模型加载解析耗时过长。
-
**线程阻塞:**受限于JS单线程特性,模型解析过程会造成页面短暂无响应。
-
**多模型渲染:**套装切换等场景下,多个模型同时渲染时性能问题尤为突出。
-
**阴影优化:**Three.js 的阴影渲染性能消耗大,不得不通过局部阴影和限制捕捉范围等折中方案来平衡画质与性能。
2.3 Galacean 引擎核心优势
Galacean 是一款开源的 Web 游戏引擎,致力于打造一个开放、易用、高效的游戏开发工具,可以通过在线编辑器或者纯代码的形式进行使用。
针对现存的技术挑战与痛点,Galacean做了深度优化:
**多线程处理:**采用Worker避免主线程阻塞。
**移动端适配:**对大量常量进行近似取值优化,完美适配移动端。
**性能突破:**优化数据传输链路,创新缓存设计,显著降低重负载场景下的卡顿现象。

对比视频1:加载速度

对比视频2:套装切换
此外,Galacean 基于 EC(Entity-Component)架构设计,而非 Three.js 的面向对象,大幅提升了开发的灵活性。
近期我们将渲染引擎由 Three.js 切换为 Galacean。这一举措不仅解决了页面卡顿问题,还提升了浏览器兼容性(可支持到 chrome82),帧率表现更出色,画面质感也得到显著改善。整体切换过程较为平滑,但也遇到了一些问题。接下来,将与大家分享此次整体升级的相关经验。
三、调优过程
任务拆解:
作为一个数字人项目,涉及到引擎升级的模块大致有
①环境初始化(场景、相机、光线、引擎设置)
② 模型加载
-
骨架获取
-
材质获取
-
动画获取
③妆容、穿搭还原
-
形态键修改
-
贴图、颜色修改
-
模型替换
-
头像(静态头像、动态头像)导出
-
壁纸(静态壁纸、动态壁纸、视差壁纸)导出
经过梳理,可以大致分为四类:
初始化
模型加载
素材替换
动画状态
接下来我们对这几个部分进行分别的处理
3.1 初始化
有别于 Three.js 的渲染器创建,Galacean 的 engine 初始化是异步方法,所以后续用到用到engine的地方需要考虑加载的时序,以及engine存在状态的判断。另外,Three.js 中 renderer 的渲染行为需要手动调用,一般是使用requestAnimationFrame循环调用,而Galacean则不需要,引擎开始渲染只需要调用一次 engine.run 即可。
javascript
const renderer=new THREE.WebGLRenderer({
alpha: true,
antialias: true,
})
document.body.appendChild(renderer.domElement)
const scene = new THREE.Scene()
const camera = new THREE.PerspectiveCamera(15, window.innerWidth/window.innerHeight, 0.1, 100)
requestAnimationFrame(function render() {
renderer.render(scene, camera)
requestAnimationFrame(render)
})
const engine = await WebGLEngine.create({
canvas,
physics: new LitePhysics()
})
engine.run()
在 Three.js 中,尺寸单位统一以米为基准,无需额外进行特殊处理。不过在角度单位的使用上存在差异:Three.js 里,仅相机的 fov(视场角)采用角度单位,其他涉及角度的参数均以弧度计量;而 Galacean 则采用更为统一的设定,所有角度相关单位均为角度。
ini
/** Three.js */
camera.fov = 15
item.rotation.y = 15 * Math.PI/180
/** Galacean */
camera.fieldOfView = 15
item.rotation.y = 15
在Three.js中颜色的设置更加灵活,可以使用16进制或者RGB值来进行赋值,但是在Galacean中只能通过RGB来进行赋值,且有别于0-255的取值范围,Galacean中的颜色范围是0-1。从Galacean1.5版本开始,默认的色彩空间改为线性,在代码中需要手动转换一下。
ini
/** Three.js */
directLight.color=0xffffff
directLight.intensity=0.9
/** Galacean */
const color = new Color(0.9, 0.9, 0.9, 1)
color.toLinear(color)
directLight.color = color
3.2 模型加载
对于包含大量形态键和动画的模型,将模型打成zip包可以有效的压缩模型的体积,不论是Three.js还是Galacean都不支持加载zip包,但是我们可以自行扩展模型加载的链路,将zip下载后解压出的模型获取ObjectUrl再放到各自的加载器中加载,这样加载进度的获取也可以进行自定义,不需要进行额外的改造。
typescript
exportclassModelLoader {
engine: WebGLEngine
constructor(engine: WebGLEngine){
this.engine = engine
}
async load(src: string) {
const url = await fileLoader(src)
returnthis.engine.resourceManager.load<GLTFResource>({
url,
type: AssetType.GLTF
})
}
}
Three.js 解析 glTF 模型输出的数据结构较为简单,主要使用模型的场景和动画片段。由于后续需针对特定材质进行替换,所以要根据节点名获取特定节点,再取出节点中的材质信息,模型的骨架也通过这种方式获取。而 Galacean 输出的数据更为全面,除动画片段和实体信息外,模型中使用的材质、贴图、蒙皮和网格信息也会分门别类展示,需要对应内容时直接获取即可,相比 Three.js 更加方便。
3.3 素材替换
素材替换如上文总结分为四种,分别是颜色、贴图、形态键和模型的替换,颜色设置我们在初始化中已经讲解,而模型加载和展示也没有特别的内容,无非是节点/实体的添加和移除,这里我们讲下贴图和形态键修改的一些tips。
在Three.js中修改材质贴图map可以直接直接使用canvas或者image,修改后需要将材质needsUpdate属性设置为true。而在Galacean需要先将图片加载为texture,再进行赋值。
ini
/** Three.js */
material.map=canvas
material.needsUpdate = true
/** Galacean */
const texture: Texture2D = await engine.resourceManager.load({
url,
type: AssetType.Texture2D
})
material.baseTexture = texture
在Three.js中修改形态键,可以先通过网格中的morphTargetDictionary属性获取到需要修改的形态键的索引,然后修改morphTargetInfluences中对应索引的值即可。
在Galacean中网格渲染器中没有存储形态键的索引信息,而是存储在MeshRenderer下的mesh属性下的blendShapes属性中,通过获取对应名称的形态键在数组中的索引,修改网格渲染器中blendShapeWeights属性对应下标的值。
ini
/** Three.js */
const index = morphTargetDictionary[keyName]
if (index !== undefined) {
mesh.morphTargetInfluences[index] = value
}
/** Galacean */
const blendShapes = skinMeshRenderer.mesh.blendShapes
const index = blendShapes.findIndex(i=>i.name===keyName)
if (index > -1){
skinMeshRenderer.blendShapeWeights[index] = value
}
3.4 动画
相较于Three.js的AnimationMixer和AnimationClip,Galacean拥有更加完善的面向组件的动画系统,支持 状态机、混合动画、时长压缩等,不同动画之间的切换与播放更加简单易维护。
ini
/** Three.js 播放动画片段 */
const mixer = new THREE.AnimationMixer(scene)
const action=mixer.clipAction(avatarClip)
action.play()
ticker.addEvent(delta => {
mixer.update(delta)
})
/** Galacean 添加状态机,播放完成回到待机状态 */
const animationState = animator.findAnimatorState('action')
const idleStatle = animator.findAnimatorState('idle')
const transition = new AnimatorStateTransition()
transition.duration = 1
transition.offset = 0
transition.exitTime = 1
transition.destinationState = idleStatle
animationState.addTransition(transition)
animator.play('action')
四、结语
Galacean 的出现,无疑为 Web 3D 开发领域带来了新的活力。它不仅解决了 Three.js 等传统技术在性能和功能上的诸多痛点,还以其卓越的性能、丰富的功能和易用性,为开发者打开了一扇通往更广阔创意空间的大门。
需要注意的是,Galacean不同版本之间的API差异较大,需要进行甄别,同时开发文档及相关的案例也需要进一步完善。
对于全新的项目,Galacean提供编码或在线编辑器两种方式保障创意的高效落地,详细的文档和案例也便于接触 Web3D 开发的新人快速上手。
对于存量的项目,Galacean的迁移成本不高,且整个过程平滑可控,能够有效提升现有项目的画面表现和性能。为未来复杂度更高的需求提供性能保障。