從 Blender 到上線:端到端壓縮實戰
系列到這裡,工具、原理、選型都講過了。最後這一篇,我們把所有東西串成一條能跑的流水線:從一個真實的 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)(單一檔案,便於傳輸) - 幾何:勾選
Normals、Tangents(PBR 法線貼圖需要切線) - UV:確保匯出(預設開)
- 貼圖:
Automatic或JPEG(這步的貼圖格式無所謂,後面會重壓,但要確保匯出了) - 壓縮:先不要勾 Blender 自帶的 Mesh 壓縮,我們用更專業的工具
- 變換:
+Y Up(glTF 標準) - 資料:只勾選需要的(動畫、相機、燈光不需要就不匯出,減小體積)
匯出後的 model.glb:50MB,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 1 | 45MB | ~520MB | — |
| Step 2 + MeshOpt | 30MB | ~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 2 | 30MB | ~520MB | — |
| Step 3 + KTX2 | 6MB | ~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 3 | 6MB | ~70MB | — |
| Step 4 + 簡化 0.5 | 4.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_basisu的fallback欄位提供兜底貼圖。
系列速查表
整個 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
平台速查
| 平台 | 貼圖 | 頂點 |
|---|---|---|
| 桌面 Web | WebP / KTX2 | MeshOpt |
| 行動 Web | KTX2 必上 | MeshOpt |
| VR | KTX2 必上 | MeshOpt + LOD |
| 小程式 | KTX2 / WebP | MeshOpt / 量化 |
| 大場景 | KTX2 必上 | MeshOpt + Draco + LOD |
系列回顧
六篇走完,串起來是一條完整鏈路:
- 為什麼這麼大:搞清楚體積構成和顯存真相
- 頂點壓縮三板斧:量化、MeshOpt、Draco 的原理與選擇
- 貼圖顯存問題:為什麼 PNG/JPG 在 GPU 眼裡有原罪
- KTX2 實戰:ETC1S/UASTC 選型、工具鏈、引擎載入
- 選型指南:按平台/場景選方案的決策框架
- 本篇:端到端流水線,從 Blender 到上線
核心只有一句話:先想清楚瓶頸(下載/顯存/幀率),再選工具;貼圖壓縮收益最大,頂點壓縮是錦上添花;不同平台不同命,別一刀切。
照著這篇的腳本和速查表,你的模型從 50MB 壓到 5MB、顯存從 520MB 降到 70MB,應該是一條可複製的路。剩下的就是動手了。