从 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,应该是一条可复制的路。剩下的就是动手了。