Any3DAny3D
·Any3D Team

從 Blender 到上線:端到端壓縮實戰

3d-compressionpipelinetexture-compressionvertex-compressiongltf

系列到這裡,工具、原理、選型都講過了。最後這一篇,我們把所有東西串成一條能跑的流水線:從一個真實的 Blender 模型出發,一步步壓,每一步記錄檔案大小、顯存、載入時間的變化,最後看它能不能從一個 50MB 的胖子,瘦成一個能在手機上秒開的 5MB 模型。

目標讀者:已經讀完前 5 篇、準備真正動手的人。這篇不放新概念,只放能複製的流程、命令和腳本。

起點:一個真實的 PBR 模型

用一個非常典型的電商展示模型做樣本:一個高精度產品模型,帶完整 PBR 貼圖。

初始指標數值
Blender 原始檔~120MB(含未匯出的高模)
匯出 GLB(float32 + PNG)~50MB
頂點數約 18 萬
貼圖6 張 4096×4096(albedo、normal、roughness、metallic、AO、emissive)
顯存佔用(6 張全解壓縮)~520MB
目標檔案 ≤ 5MB,顯存可控,行動端秒開

50MB 的檔案、520MB 的顯存——這個模型直接上行動端必崩。我們一步步來。

第 0 步:從 Blender 正確匯出

壓縮的第一道關其實是匯出,很多人在這步就漏了血。

Blender 匯出 glTF 時的關鍵設定:

  • 格式glTF Binary (.glb)(單一檔案,便於傳輸)
  • 幾何:勾選 NormalsTangents(PBR 法線貼圖需要切線)
  • UV:確保匯出(預設開)
  • 貼圖AutomaticJPEG(這步的貼圖格式無所謂,後面會重壓,但要確保匯出了)
  • 壓縮先不要勾 Blender 自帶的 Mesh 壓縮,我們用更專業的工具
  • 變換+Y Up(glTF 標準)
  • 資料:只勾選需要的(動畫、相機、燈光不需要就不匯出,減小體積)

匯出後的 model.glb50MB,6 張 PNG 貼圖,float32 頂點。這就是我們的基準。

第一個常見坑就在這:Blender 預設會把沒用的 mesh、隱藏的輔助物體一起匯出。匯出前 File > Clean Up > Purge Orphans,並在大綱裡只選中要匯出的物體。

端到端 pipeline 全景

把整條流水線畫出來,心裡有個全景:

Blender 原始檔
   │  匯出 .glb(float32 + PNG)            50MB
   ▼
[1] 去冗餘 + 焊接重複頂點(gltf-transform)   ~45MB
   │
[2] 頂點壓縮:MeshOpt(gltfpack / gltf-transform) ~30MB
   │
[3] 貼圖壓縮:PNG → KTX2(ETC1S/UASTC)     ~6MB
   │
[4] (可選) 幾何簡化 LOD(simplify)         ~4-5MB
   ▼
最終 model-final.glb                          ~5MB
   │
引擎載入(Three.js / Babylon.js)→ 執行時轉碼 → 上線

每一步的數字會在下面表格裡即時追蹤。

工具鏈:選哪個

壓縮工具有好幾個,先做個對比,免得選錯:

工具強項弱項適合
gltf-transform全能,貼圖+頂點一把梭,API 化、可腳本化極致壓縮率不如專門工具推薦主力,絕大多數場景
gltfpack頂點壓縮專業,MeshOpt 原生支援貼圖壓縮能力弱頂點密集、想要 MeshOpt 細控
toktx貼圖壓縮最專業、參數最全只處理貼圖,不能處理整模型單張貼圖精細調優
gltf-pipeline老牌,支援 Draco維護不活躍,功能少已有 Draco 舊專案
線上工具(gltf.report)零安裝不適合自動化、大批量試驗、一次性任務

主線推薦gltf-transform 走完整個流程,需要時用 gltfpack 補頂點、用 toktx 調單張貼圖。下面所有步驟都基於 gltf-transform。

第 1 步:去冗餘 + 焊接

模型裡常有重複頂點、未使用的節點和材質。先清一遍。

gltf-transform optimize model.glb step1.glb --weld --prune
階段檔案大小顯存變化
基準50MB~520MB
Step 1 去冗餘45MB~520MB-5MB(顯存沒變,因為貼圖還在)

顯存幾乎沒動,這是預期的——去冗餘主要省的是頂點和結構,貼圖才是顯存大頭。

第 2 步:頂點壓縮 MeshOpt

gltf-transform optimize step1.glb step2.glb --meshopt --weld --prune

--meshopt 會把頂點量化到 16 位並用 MeshOpt 無損編碼,自動帶上 EXT_meshopt_compression 擴展。

階段檔案大小顯存變化
Step 145MB~520MB
Step 2 + MeshOpt30MB~520MB-15MB(頂點部分)

顯存還是 520MB 左右?對——因為頂點在顯存裡佔比小(10-20%),砍頂點對顯存影響有限。真正的顯存巨獸是貼圖,下一步解決。

第 3 步:貼圖壓縮 PNG → KTX2

這一步是性價比之王。

gltf-transform optimize step2.glb step3.glb \
  --texture-compress basisu \
  --meshopt --weld --prune

--texture-compress basisu 會自動判斷每張貼圖:顏色貼圖(albedo、emissive)用 ETC1S,資料貼圖(normal、roughness、metallic、AO)用 UASTC。

階段檔案大小顯存變化
Step 230MB~520MB
Step 3 + KTX26MB~70MB-24MB 檔案 / -450MB 顯存

這一步是整個 pipeline 的轉折點:

  • 檔案從 30MB 掉到 6MB
  • 顯存從 520MB 掉到約 70MB——因為 6 張 4096 貼圖從「解壓縮後原始像素」變成了「塊壓縮」,每張從 ~87MB 降到 ~11-14MB

顯存降了一個數量級,這才是行動端能不能跑的關鍵。

第 4 步:(可選)幾何簡化

如果還想要更小,且場景允許降低頂點精度,可以加幾何簡化。

gltf-transform optimize step3.glb final.glb \
  --texture-compress basisu \
  --meshopt \
  --simplify --simplify-ratio 0.5 \
  --weld --prune

--simplify-ratio 0.5 表示保留約 50% 的頂點。

階段檔案大小顯存變化
Step 36MB~70MB
Step 4 + 簡化 0.54.5MB~70MB-1.5MB(顯存幾乎不變)

簡化主要省檔案大小,對顯存影響不大。代價是模型細節降低——近距離觀看會察覺。電商產品頁通常不建議過度簡化,建築/大場景則很合適。

效果追蹤總表

把四步疊在一起看全貌(基於上述樣本,數字僅供說明量級):

步驟檔案大小顯存累計降幅
基準(float32 + PNG)50MB~520MB
+ 去冗餘焊接45MB~520MB-10%
+ MeshOpt 頂點30MB~520MB-40%
+ KTX2 貼圖6MB~70MB-88% 檔案 / -87% 顯存
+ 幾何簡化(0.5)4.5MB~70MB-91% 檔案

結論:貼圖壓縮貢獻了絕大部分的體積和顯存收益。頂點壓縮是錦上添花,貼圖壓縮是雪中送炭。這和第 1 篇的論斷完全吻合——貼圖佔 80% 體積,最佳化它回報最高。

一條命令版:懶人一鍵壓

如果不想分步看,把所有最佳化一次到位:

gltf-transform optimize model.glb model-final.glb \
  --texture-compress basisu \
  --meshopt \
  --simplify --simplify-ratio 0.5 \
  --weld --prune

這一條命令 = 去冗餘 + 焊接 + 頂點 MeshOpt + 貼圖 KTX2 + 幾何簡化。90% 的場景它就夠了,分步主要是為了理解和調參。

自動化腳本:可重用

把上面的流程封裝成腳本,整合到構建裡。這個腳本支援按目標平台產出不同版本,並列印每步的效果。

// scripts/compress-model.mjs
import { optimize } from "@gltf-transform/functions";
import { NodeIO } from "@gltf-transform/core";
import { KHRONOS_EXTENSIONS } from "@gltf-transform/extensions";
import { filesize } from "filesize";

const io = new NodeIO().registerExtensions(KHRONOS_EXTENSIONS);

// 按平台定義壓縮策略
const PROFILES = {
  mobile: {
    textureCompression: "basisu",
    meshCompression: "meshopt",
    simplify: { ratio: 0.5 },
    weld: true,
    prune: true,
  },
  vr: {
    textureCompression: "basisu",
    meshCompression: "meshopt",
    // VR 近距離觀看,不簡化
    simplify: null,
    weld: true,
    prune: true,
  },
  desktop: {
    textureCompression: "webp",
    meshCompression: "meshopt",
    simplify: null,
    weld: true,
    prune: true,
  },
};

async function compress(inputPath, profileName) {
  const cfg = PROFILES[profileName];
  const doc = await io.read(inputPath);
  const before = Buffer.byteLength(await io.writeBinary(doc), "utf8");

  await optimize(doc, {
    textureCompression: cfg.textureCompression,
    meshCompression: cfg.meshCompression,
    simplify: cfg.simplify ?? undefined,
    weld: cfg.weld,
    prune: cfg.prune,
  });

  const bytes = await io.writeBinary(doc);
  const outPath = inputPath.replace(/\.glb$/, `-${profileName}.glb`);
  await io.write(outPath, doc);
  const after = bytes.byteLength;

  console.log(
    `${profileName.padEnd(8)} ${filesize(before)} → ${filesize(after)} ` +
      `(${Math.round((1 - after / before) * 100)}% smaller) → ${outPath}`
  );
}

// 用法: node scripts/compress-model.mjs path/to/model.glb
const input = process.argv[2];
for (const profile of Object.keys(PROFILES)) {
  await compress(input, profile);
}

放進專案裡:

node scripts/compress-model.mjs public/models/model.glb
# mobile   50MB → 4.5MB (91% smaller) → model-mobile.glb
# vr       50MB → 6.2MB (87% smaller) → model-vr.glb
# desktop  50MB → 11MB (78% smaller)  → model-desktop.glb

前端執行時按裝置載入對應版本即可。

不想自己搭這套流水線?Any3D 的線上工具可以一鍵完成上述所有最佳化——上傳 GLB,自動跑貼圖 KTX2 + 頂點 MeshOpt,按平台輸出壓縮版本,省去本地裝工具鏈的麻煩。

常見踩坑 FAQ

壓縮後模型變黑 / 貼圖不顯示

  • 99% 是色彩空間:顏色貼圖漏設 sRGB。Three.js 裡 texture.colorSpace = THREE.SRGBColorSpace
  • toktx 時顏色貼圖忘加 --srgb

法線貼圖壓完光照不對

  • 法線貼圖用了 ETC1S,換 UASTC。
  • 法線貼圖是 DirectX 風格(綠通道朝下),引擎要 OpenGL 風格,需翻轉 G 通道。

行動端載入卡在首屏

  • 檢查是否在載入 Draco 解碼器 wasm(額外請求)。行動端優先 MeshOpt。
  • KTX2 transcoder 路徑配錯,轉碼失敗 fallback 到 CPU 解壓縮。

壓縮後檔案反而變大

  • 貼圖太小(< 128px)壓 KTX2 不划算,塊壓縮有固定開銷。
  • 模型已經壓過一遍,再壓沒有收益(甚至負收益)。

簡化後模型破面

  • --simplify-ratio 調太低,降到 0.7-0.8。
  • 簡化對硬表面(機械、建築)友好,對有機曲面(角色)容易破面。

KTX2 在某些瀏覽器載入失敗

  • 舊版 Safari / 舊 WebView 不支援。準備 PNG/WebP 的 fallback,或用 KHR_texture_basisufallback 欄位提供兜底貼圖。

系列速查表

整個 6 篇的精華濃縮成一張表,建議收藏。

體積構成

組成佔比最佳化工具
貼圖70-85%KTX2(最大收益)
頂點資料10-20%MeshOpt / 量化 / Draco
動畫資料0-15%減少關鍵幀 / 壓縮
其他< 2%去冗餘

顯存公式

傳統格式顯存 = 寬 * 高 * 4位元組 * 1.333(含mipmap)
KTX2 塊壓縮顯存 ≈ 上式 / 4 (ETC1S) 或 / 2 (UASTC)

頂點壓縮選型

場景推薦
零依賴、最簡單純量化(KHR_mesh_quantization
Web 均衡首選MeshOpt
極限壓縮率、能等解碼Draco
小程式 / 包體敏感純量化 / MeshOpt,避免 Draco

貼圖壓縮選型

貼圖類型推薦編碼
albedo / emissive(顏色)KTX2 ETC1S
normal / roughness / metallic / AO(資料)KTX2 UASTC
桌面 Web、追求下載速度WebP / AVIF
小貼圖(< 128px)保持 PNG,別壓 KTX2

一鍵命令

# 全套最佳化(貼圖 + 頂點 + 簡化)
gltf-transform optimize model.glb model-final.glb \
  --texture-compress basisu --meshopt \
  --simplify --simplify-ratio 0.5 --weld --prune

平台速查

平台貼圖頂點
桌面 WebWebP / KTX2MeshOpt
行動 WebKTX2 必上MeshOpt
VRKTX2 必上MeshOpt + LOD
小程式KTX2 / WebPMeshOpt / 量化
大場景KTX2 必上MeshOpt + Draco + LOD

系列回顧

六篇走完,串起來是一條完整鏈路:

  1. 為什麼這麼大:搞清楚體積構成和顯存真相
  2. 頂點壓縮三板斧:量化、MeshOpt、Draco 的原理與選擇
  3. 貼圖顯存問題:為什麼 PNG/JPG 在 GPU 眼裡有原罪
  4. KTX2 實戰:ETC1S/UASTC 選型、工具鏈、引擎載入
  5. 選型指南:按平台/場景選方案的決策框架
  6. 本篇:端到端流水線,從 Blender 到上線

核心只有一句話:先想清楚瓶頸(下載/顯存/幀率),再選工具;貼圖壓縮收益最大,頂點壓縮是錦上添花;不同平台不同命,別一刀切。

照著這篇的腳本和速查表,你的模型從 50MB 壓到 5MB、顯存從 520MB 降到 70MB,應該是一條可複製的路。剩下的就是動手了。

贊助支持