Stories

Detail Return Return

擁抱新一代 Web 3D 引擎,Three.js 項目快速升級 Galacean 指南 - Stories Detail

作者: 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避免主線程阻塞。
  • 移動端適配: 對大量常量進行近似取值優化,完美適配移動端。
  • 性能突破: 優化數據傳輸鏈路,創新緩存設計,顯著降低重負載場景下的卡頓現象。

<p align=center>對比視頻1:加載速度</p>

<p align=center>對比視頻2:套裝切換</p>

此外,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 = 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版本開始,默認的色彩空間改為線性,在代碼中需要手動轉換一下。

/** 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再放到各自的加載器中加載,這樣加載進度的獲取也可以進行自定義,不需要進行額外的改造。

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=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屬性對應下標的值。

/** 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擁有更加完善的面向組件的動畫系統,支持 狀態機、混合動畫、時長壓縮等,不同動畫之間的切換與播放更加簡單易維護。

/** 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的遷移成本不高,且整個過程平滑可控,能夠有效提升現有項目的畫面表現和性能。為未來複雜度更高的需求提供性能保障。

user avatar toopoo Avatar aqiongbei Avatar hard_heart_603dd717240e2 Avatar ligaai Avatar yangxiansheng_5a1b9b93a3a44 Avatar cynthia_59675eba1a2ee Avatar baidujiagoushi Avatar prosuoqi Avatar autohometech Avatar weidejianpan Avatar sheng_c Avatar jerryc Avatar
Favorites 33 users favorite the story!
Favorites

Add a new Comments

Some HTML is okay.