写点什么

拥抱新一代 Web 3D 引擎,Three.js 项目快速升级 Galacean 指南

  • 2025-09-18
    广东
  • 本文字数:3831 字

    阅读完需:约 13 分钟

作者: 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 待上传】

对比视频 1:加载速度


【视频 2 待上传】

对比视频 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 即可。

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 则采用更为统一的设定,所有角度相关单位均为角度。

/** Three.js */camera.fov = 15item.rotation.y = 15 * Math.PI/180
/** Galacean */camera.fieldOfView = 15item.rotation.y = 15
复制代码


在 Three.js 中颜色的设置更加灵活,可以使用 16 进制或者 RGB 值来进行赋值,但是在 Galacean 中只能通过 RGB 来进行赋值,且有别于 0-255 的取值范围,Galacean 中的颜色范围是 0-1。从 Galacean1.5 版本开始,默认的色彩空间改为线性,在代码中需要手动转换一下。

/** Three.js */directLight.color=0xffffffdirectLight.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 再放到各自的加载器中加载,这样加载进度的获取也可以进行自定义,不需要进行额外的改造。

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,再进行赋值。

/** Three.js */material.map=canvasmaterial.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 属性对应下标的值。

/** Three.js */const index = morphTargetDictionary[keyName]
if (index !== undefined) {  mesh.morphTargetInfluences[index] = value}
/** Galacean */const blendShapes = skinMeshRenderer.mesh.blendShapesconst index = blendShapes.findIndex(i=>i.name===keyName)if (index > -1){  skinMeshRenderer.blendShapeWeights[index] = value}
复制代码


3.4 动画

相较于 Three.js 的 AnimationMixer 和 AnimationClip,Galacean 拥有更加完善的面向组件的动画系统,支持 状态机、混合动画、时长压缩等,不同动画之间的切换与播放更加简单易维护。

/** 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 = 1transition.offset = 0transition.exitTime = 1transition.destinationState = idleStatleanimationState.addTransition(transition)animator.play('action')
复制代码


四、结语

Galacean 的出现,无疑为 Web 3D 开发领域带来了新的活力。它不仅解决了 Three.js 等传统技术在性能和功能上的诸多痛点,还以其卓越的性能、丰富的功能和易用性,为开发者打开了一扇通往更广阔创意空间的大门。


需要注意的是,Galacean 不同版本之间的 API 差异较大,需要进行甄别,同时开发文档及相关的案例也需要进一步完善。


对于全新的项目,Galacean 提供编码或在线编辑器两种方式保障创意的高效落地,详细的文档和案例也便于接触 Web3D 开发的新人快速上手。


对于存量的项目,Galacean 的迁移成本不高,且整个过程平滑可控,能够有效提升现有项目的画面表现和性能。为未来复杂度更高的需求提供性能保障。

发布于: 刚刚阅读数: 5
用户头像

官方公众号:vivo互联网技术,ID:vivoVMIC 2020-07-10 加入

分享 vivo 互联网技术干货与沙龙活动,推荐最新行业动态与热门会议。

评论

发布
暂无评论
拥抱新一代 Web 3D 引擎,Three.js 项目快速升级 Galacean 指南_前端_vivo互联网技术_InfoQ写作社区