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

赞助支持