给模型减肥的第一课:顶点压缩三板斧
上一篇我们拆开了一个 GLB 文件,看到纹理吃掉 80% 的体积,顶点只占 10-20%。所以顶点压缩看起来是不是无关紧要?
恰恰相反。当一个模型的纹理已经压到 KTX2、顶点又密密麻麻时,剩下的 20% 就是顶点——而这 20% 可以再砍掉一半甚至 90%。更重要的是,顶点压缩是少数几种几乎零代价、立即生效的优化:加几行命令、换一个解码器,文件就瘦下来了。
这一篇讲清楚三件事:顶点数据到底长什么样;量化、MeshOpt、Draco 三者各自的脾气;以及一个让你少踩坑的结论——没有「最好的」方案,只有「最合适的」方案。
一个顶点到底有多大
先看一个顶点里装了什么。在 glTF 里,每个顶点由若干属性(attribute)组成:
| 属性 | 用途 | 默认精度 | 每顶点字节数 |
|---|---|---|---|
| position(位置) | 顶点在空间中的坐标 | 3 × float32 | 12 |
| normal(法线) | 决定光照方向 | 3 × float32 | 12 |
| tangent(切线) | 法线贴图计算 | 4 × float32 | 16 |
| texcoord_0(UV) | 贴图采样坐标 | 2 × float32 | 8 |
| color(顶点色) | 顶点级别的着色 | 4 × float32 | 16 |
一个带 PBR 完整属性的顶点,光几何数据就要 48-64 字节。一个 10 万顶点的模型,光顶点就是 5-6MB。
注意这里几乎全部用的是 float32(32 位浮点数)。这是默认配置,也是顶点压缩的突破口——因为绝大多数属性根本用不到 32 位的精度。
第一板斧:量化(Quantization)
量化是所有顶点压缩的底层原理,Draco 和 MeshOpt 内部也都在用它。
quantization(量化,把高精度的浮点数映射到低精度的整数) 的本质是:浮点数 3.14159265,你记住 3.14 就够用了。空间范围内的一组坐标,你不再用 32 位精确记录每个小数,而是用一个范围更小的整数来表示。
原始: position.x = 1.234567 (float32, 4字节)
量化后: position.x = 1234 (int16, 2字节) + 一个 scale/offset 还原
量化前后的对比:
| 属性 | float32 字节数 | 量化后(16 位) | 节省 |
|---|---|---|---|
| position | 12 | 6 | 50% |
| normal | 12 | 6(或 4,用 int8 + octahedral) | 50-67% |
| tangent | 16 | 4-8 | 50-75% |
| texcoord | 8 | 4 | 50% |
对刚才那个 48-64 字节的顶点,量化后基本能压到 16-24 字节,体积直接减半以上。
什么时候用量化
- 你只想减小体积、不需要极致压缩率
- 你希望零解码器依赖——量化后的 glTF 用标准的
KHR_mesh_quantization扩展,主流引擎原生支持,不需要额外引入解码库 - 目标平台对包体敏感(比如微信小程序,多带一个 Draco 解码器就是几十 KB)
什么时候别用
- 模型本身极小、细节是卖点(比如毫米级的工业零件)。量化对小尺寸模型特别容易露出马脚——纹理还行,但顶点位置偏移 0.1mm 在特写镜头下肉眼可见。
量化精度损失的真实坑:一个珠宝展示场景,把戒指模型量化到 16 位后,特写镜头下金属边缘出现了锯齿。原因不是顶点数不够,而是世界坐标系太小,16 位整数能表达的范围不够细。解法是降低量化范围(缩小
position的 bounding box)或对小模型改用更高位深。
第二板斧:MeshOpt
MeshOpt 是 glTF 官方扩展 EXT_meshopt_compression,定位是「压缩率不错、解码飞快」。
它的做法是先对属性做量化(和上面一样),再用一种叫 LISS(lossless entropy coding,无损熵编码) 的手段把量化后的整数进一步无损压一遍。换句话说:量化有损 + 熵编码无损 = 体积更小、画质和量化一致。
- 压缩率:比单纯量化再小 30-50%
- 解码速度:极快,纯 C/JS 实现,单线程每秒解几千万顶点
- 解码器体积:很小(gzip 后约 20-30KB)
- 兼容性:Three.js、Babylon.js 原生支持,是 Web 端事实标准之一
什么时候用 MeshOpt
- 需要更高压缩率,又不能接受 Draco 那种较慢的解码
- Web 端为主、移动端、WebXR——解码速度直接关系到首屏加载体验
- 模型需要频繁解压(比如动态加载的关卡)
什么时候别用
- 你的目标平台连
EXT_meshopt_compression都不认(极少数老旧引擎) - 你只需要「能跑」、不在乎 30% 的差距——那纯量化更简单,少一个依赖
第三板斧:Draco
Draco 是 Google 出的压缩方案,定位是「极限压缩率」。
它和前两者的根本区别:Draco 会改变顶点的连接关系(拓扑结构)。量化只改每个顶点的数值表示,MeshOpt 在此基础上做无损编码,而 Draco 会重新组织三角网格,用更紧凑的方式表达「哪些顶点连成三角形」。
- 压缩率:三者最高,顶点密集的模型常能做到 90% 以上的缩减
- 解码速度:三者最慢,但仍然很快(只是相对慢)
- 解码器体积:较大(约 100-200KB,且通常要单独加载 wasm)
- 画质:可调,但极端压缩率下会有可见形变
什么时候用 Draco
- 模型极大、顶点超密(百万级顶点扫描模型、地形)
- 一次性加载、解压后长时间复用(解码慢一点能接受)
- 包体不是瓶颈,下载速度才是
什么时候别用
- 移动端 + 需要快速首屏——解码器和模型本身都要下载,反而拖慢
- 小程序等对包体严苛的环境
- 模型需要做蒙皮动画、变形目标(morph targets)——Draco 对这些支持较弱,配置不当会出问题
三者放一起:一张表选型
以下压缩比参考自社区基准测试(DeepKolos 知乎测评 + Reddit r/threejs 讨论),不同模型会有出入,但相对关系基本稳定:
| 方案 | 压缩比(相对 float32) | 解码速度 | 解码器体积 | 有损吗 | glTF 扩展 |
|---|---|---|---|---|---|
| 纯量化 | ~50% | 原生,无需解码 | 0 | 是(精度) | KHR_mesh_quantization |
| MeshOpt | ~25-35% | 极快 | ~25KB | 是(精度) | EXT_meshopt_compression |
| Draco | ~10-20% | 快(三者中最慢) | ~100-200KB | 是(精度+拓扑) | KHR_draco_mesh_compression |
解码器与平台兼容性:
| 平台 | 纯量化 | MeshOpt | Draco |
|---|---|---|---|
| 桌面 Web | ✅ 原生 | ✅ 原生 | ✅ 需配解码器 |
| 移动 Web | ✅ 原生 | ✅ 原生 | ⚠️ 解码器偏重 |
| WebXR/VR | ✅ 原生 | ✅ 推荐 | ⚠️ 谨慎 |
| 微信小程序 | ✅ 推荐 | ✅ 推荐 | ❌ 尽量避免 |
一句话总结:要省心、零依赖 → 纯量化;要均衡 → MeshOpt;要极致压缩率且能等 → Draco。
实操:用 gltfpack 做量化和 MeshOpt
gltfpack 是 glTF 官方工具,一行命令搞定量化和 MeshOpt。
先安装(二进制可从 gltfpack release 下载):
# 把 model.glb 量化到 16 位,并加上 MeshOpt 压缩
gltfpack -i model.glb -o model-packed.glb -cc
# -cc = compress(在默认量化之上叠加 EXT_meshopt_compression)
常用参数:
# 只量化,不开 MeshOpt(最轻量、零解码器依赖)
# gltfpack 默认就对顶点做 16 位量化(KHR_mesh_quantization),无需额外参数即可
gltfpack -i model.glb -o model-quant.glb
# 量化并启用 MeshOpt
gltfpack -i model.glb -o model-meshopt.glb -cc
# 顶点非常多时,可顺手简化(减少顶点数,会改变模型)
gltfpack -i model.glb -o model-simplify.glb -cc -si 0.5
# -si 0.5 表示简化到约 50% 顶点
关于
-cc:它是「compress」开关,会额外叠加EXT_meshopt_compression。不写-cc时 gltfpack 默认就做量化——也就是说gltfpack -i in.glb -o out.glb这条命令本身已经是「纯量化、零解码器依赖」。(-v是 verbose 详细日志开关,不要混淆。)
典型效果(一个 5MB、12 万顶点的 PBR 模型,仅供参考):
| 处理 | 文件大小 | 说明 |
|---|---|---|
| 原始(float32) | 5.0MB | 基准 |
| 纯量化(默认) | 2.6MB | 减半,视觉无明显差异 |
MeshOpt(-cc) | 1.7MB | 再省 35%,加载稍快 |
注意:
-si简化是改变模型几何的有损操作,和压缩是两回事。压缩尽量保持视觉一致,简化则是主动减少细节。两者可以叠加,但要看场景是否允许。
常见踩坑
- 量化后法线方向变了:多半是用了过低精度。normal 至少用 16 位,或用八面体编码(octahedral)的 8 位。
- Draco 解码后材质丢失:Draco 只压缩网格,材质和纹理要单独处理。加载时需要同时配好 Draco 解码器和 KHR 扩展。
- 小程序里 Draco 加载不出来:解码器 wasm 在某些运行时环境里加载受限,换成 MeshOpt 通常就好了。
- 量化后模型「漂移」:模型离坐标原点太远时,16 位精度不足以表达大坐标 + 小细节。解法是把模型移到原点附近再量化,或加大位深。
下一步
顶点压完了,但别急着庆祝——前面说过,纹理占模型 80% 的体积。下一篇我们换个战场,看看为什么传统的 PNG/JPG 在 GPU 眼里是个「大胃王」,以及 GPU 原生纹理格式是怎么解决这个问题的。