引言

最近做T級互動,需要使用到3D模型。相信大家和我一樣,在開始著手的時候,一定會有這麼些問題:

  • 1.如何選擇3D模型的匯出格式
  • 2.如何對模型檔案進行優化
  • 3.在大流量的專案中相容性怎麼樣

讓我們通過這篇文章,進行細緻的探索、調研與沉澱。

一、什麼是 glTF 檔案

glTF 全稱 Graphics Language Transmission Format,是三維場景和模型的標準檔案格式。

glTF 核心是 JSON 檔案,描述了 3D 場景的整個內容。它由場景結構本身的描述組成,其由定義場景圖的節點的層次提供。

場景中出現的 3D 物件是使用連線到節點的 meshes(網格)定義的。Materials(材料)定義物件的外觀。Animations(動畫)描述 3D 物件如何隨著時間的推移轉換 3D 物件,並且 Skins(蒙皮)定義了對物體的幾何形狀的方式基於骨架姿勢變形。Cameras(相機)描述了渲染器的檢視配置。

除此以外,它還包括了帶有二進位制資料和影象檔案的連結,如下圖所示。

二、.gltf 與.glb

從 blender 檔案匯出中可以看出:

glTF 檔案有兩種拓展形式,.gltf(JSON / ASCII)或.glb(二進位制)。.gltf 檔案可能是自包含的,也可能引用外部二進位制和紋理資源,而 .glb 檔案則是完全自包含的(但使用外部工具可以將其緩衝區/紋理儲存為嵌入或單獨的檔案,後面會提到)。

2.1 .glb檔案產生原因

glTF 提供了兩個也可以一起使用的交付選項:

  • glTF JSON 指向外部二進位制資料(幾何、關鍵幀、面板)和影象。
  • glTF JSON 嵌入 base64 編碼的二進位制資料,並使用資料 URI 內聯影象。

對於這些資源,由於 base64 編碼,glTF 需要單獨的請求或額外的空間。Base64 編碼需要額外的處理來解碼並增加檔案大小(編碼資源增加約 33%)。雖然 gzip 減輕了檔案大小的增加,但解壓縮和解碼仍然會增加大量的載入時間。

為了解決這個問題,引入了一種容器格式 Binary glTF。在二進位制 glTF 中,glTF 資產(JSON、.bin 和影象)可以儲存在二進位制 blob 中,就是.glb 檔案

2.2 檔案對比

2.2.1 同一個glTF檔案,.glb格式要比.gltf小

  • 自包含的:

  • 引用外部二進位制和紋理資源的:

2.2.2 .gltf檔案預覽:

  • 自包含的:

  • 引用外部二進位制和紋理資源:

2.2.3 glb檔案預覽:

  • 自包含的:

  • 引用外部二進位制和紋理資源:

從圖中可以看到,當非自包含型的時候,請求glTF檔案時,會一同請求圖片檔案。

那麼,我們就可以利用這個特性,就可以實現一些效能優化,讓我們往下繼續。

三、glTF 檔案拆分

上文提到,glTF檔案可以拆分為.gltf/.glb檔案+二進位制檔案+紋理圖片,那麼,我們就可以將其拆分出來,並對紋理圖片進行單獨的壓縮,來進行效能的優化。

可以使用gltf pipeLine ,其具有以下功能:

  • glTF 與 glb 的相互轉換
  • 將緩衝區/紋理儲存為嵌入或單獨的檔案
  • 將 glTF 1.0 模型轉換為 glTF 2.0(使用KHR_techniques_webglKHR_blend)
  • 使用 Draco 進行網格壓縮

在這裡,我們是要使用“將緩衝區/紋理儲存為嵌入或單獨的檔案”這個功能。

讓我們來看看拆分出來的檔案

再回顧一下,.glb檔案是這麼引入外部單獨的紋理與二進位制檔案的

所以,只要將拆分出來的這幾個檔案,放入同一個路徑中,然後像之前那樣引入就好了。

  • 壓縮方式
gltf-pipeline -i male.glb -o male-processed.glb -s
  • 使用方式(在 Three.js 中)

    普普通通地用就好了,和不拆分的沒什麼區別
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

const loader = new GLTFLoader()
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
  • 效能對比

四、glTF 檔案壓縮

如上面介紹,glTF 檔案包括.gltf/.glb 檔案、.bin 檔案以及紋理資源。glTF2.0 相關的外掛主要有以下:

那麼我們從中取一些來分析一下。

4.1 網格壓縮

4.1.1 KHR_draco_mesh_compression

常見的一種網格壓縮方式,採用開源的Draco演算法,用於壓縮和解壓縮3D 網格和點雲,並且可能會改變網格中頂點的順序和數量。壓縮的使檔案小得多,但是在客戶端裝置上需要額外的解碼時間

  • 壓縮方式

可以使用gltf-pipelinegltf 檔案優化工具進行壓縮

gltf-pipeline -i male.glb -o male-processed.glb -d
  • 使用方式(在 Three.js 中)
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader' const loader = new GLTFLoader() // 建立解碼器例項
const dracoLoader = new DRACOLoader()
// 設定解壓庫檔案路徑
dracoLoader.setDecoderPath(DECODER_PATH)
// 載入解碼器例項
loader.setDRACOLoader(dracoLoader) loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
  • 效能分析對比

這個 glb 檔案原大小為 3.2M,draco 壓縮後為 1.8M,約為原檔案的56%

從上面的程式碼中可以看出,建立解碼器例項需要引入額外的庫來進行解碼,setDecoderPath會自動請求 wasm 檔案來進行解密操作。而這兩個 wasm 檔案同時也增加了請求時間和請求數量,那麼加上這兩個檔案,真實的壓縮率約為62.5%

所以,如果一個專案需要載入多個 glTF 檔案,那麼可以建立一個 DRACOLoader 例項並重復使用它。但如果專案只需要載入一個 glTF 檔案,那麼使用 draco 演算法是否具有“價效比”就值得考量了。

用 demo 進行一下效能對比:

可見 draco 演算法首次載入和解密時間,要大於原檔案。而在實際專案中,這個差距更加明顯,並且偶爾會出現解密堵塞的情況,需要重新進入頁面才能恢復功能。

除此以外,還有一個很直觀的問題,模型畫質的損失是肉眼可觀的。

如圖,分別是在 iPhone 12 和小米 MIX2 中的樣子:

總而言之,如果要將 draco 壓縮演算法運用到大規模專案中,需要結合實際專案進行以下對比:

  • (1) 請求兩個檔案+解密耗時,與本身 glb 檔案壓縮後的體積大小相比,真實效能對比;
  • (2) 畫質是否會出現設計師無法接受的損失。

4.1.2 KHR_mesh_quantization

頂點屬性通常使用FLOAT型別儲存,將原始始浮點值轉換為16位或8位儲存以適應統一的3D或2D網格,也就是我們所說的quantization向量化,該外掛主要就是將其向量化。

例如,靜態 PBR-ready 網格通常需要每個頂點POSITION(12 位元組)、TEXCOORD(8 位元組)、NORMAL(12 位元組)和TANGENT(16 位元組),總共 48 位元組。通過此擴充套件,可以用於SHORT儲存位置和紋理座標資料(分別為 8 和 4 位元組)以及BYTE儲存法線和切線資料(各 4 位元組),每個頂點總共 20 位元組。

  • 壓縮方式

可以使用gltfpack工具進行壓縮

gltfpack -i male.glb -o male-processed.glb
  • 使用方式(在 Three.js 中)

普普通通地用就好了,和不壓縮的沒什麼區別

import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'

const loader = new GLTFLoader()
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
  • 效能對比

原檔案3.2M,壓縮後1.9M,為原檔案的59.3%,比原模型載入速度也快上不少。

放到實際專案中,沒有畫質損失和載入時間過長的問題。

4.1.3 EXT_meshopt_compression

此外掛假定緩衝區檢視資料針對 GPU 效率進行了優化——使用量化並使用最佳資料順序進行 GPU 渲染——並在 bufferView 資料之上提供一個壓縮層。每個 bufferView 都是獨立壓縮的,這允許載入器最大程度地將資料直接解壓縮到 GPU 儲存中。

除了優化壓縮率之外,壓縮格式還具有兩個特性——非常快速的解碼(使用 WebAssembly SIMD,解碼器在現代桌面硬體上以約 1 GB/秒的速度執行),以及與通用壓縮相容的位元組儲存。也就是說,不是儘可能地減少編碼大小,而是以通用壓縮器可以進一步壓縮它的方式構建位元流。

  • 壓縮方式

可以使用gltfpack工具進行壓縮

gltfpack -i male.glb -o male-processed.glb -cc
  • 使用方式(在 Three.js 中)
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js' const loader = new GLTFLoader()
loader.setMeshoptDecoder(MeshoptDecoder)
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
  • 效能分析對比

原檔案3.2M,壓縮後1.1M,為原檔案的65.6%,首次載入時間比原模型快上不少。

放到實際專案中,沒有畫質損失和載入時間過長的問題。

五、多個機型裝置與優化對比結果

為了避免上文提到的“draco”壓縮使得模型受損的情況,找了幾臺iPhone、安卓的手機來進行了一下效能與相容的測試,讓我們看一下結果。

PS:公司網路在不同時間段內網速不同(如上午和下午),可能會對數字產生小部分影響,但不影響檔案優化橫向對比。

iPhone 12(iOS 14.4,自用)

Huawei Mate 40 pro (HarmonyOS,自用)

Xiaomi Mix2(Android 8.0,測試機)

iPhone 6sp (iOS 13.7,自用機)

5.1 總結

可見,對於小部分需要使用模型的,並且只需要載入一個模型的業務,採用KHR_mesh_quantizationEXT_meshopt_compression進行網格壓縮,再使用gltf-pipeline進行模組區分並對紋理圖片壓縮,是目前找到的較好的優化方案。

六、其他

其實還有很多效能優化的外掛,目前正在進行除錯和調查,等後續迭代或有什麼新進展,會繼續更新:

網格優化的:

還有一些紋理優化的外掛:

七、參考資料

  1. The Basic Structure of glTF

  2. GLB File Format Specification

  3. Extensions for glTF 2.0

  4. KHR_draco_mesh_compression

  5. DRACOLoader – three.js docs

  6. CesiumGS/gltf-pipeline: Content pipeline tools for optimizing glTF assets.

  7. KHR_mesh_quantization

  8. gltfpack | meshoptimizer

  9. GLTFLoader

  10. EXT_meshopt_compression

  11. 【網格壓縮測評】MeshQuan、MeshOpt、Draco


歡迎關注凹凸實驗室部落格:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章: