Any3DAny3D
·Any3D Team

给模型减肥的第一课:顶点压缩三板斧

3d-compressionvertex-compressionmeshoptdracogltf

上一篇我们拆开了一个 GLB 文件,看到纹理吃掉 80% 的体积,顶点只占 10-20%。所以顶点压缩看起来是不是无关紧要?

恰恰相反。当一个模型的纹理已经压到 KTX2、顶点又密密麻麻时,剩下的 20% 就是顶点——而这 20% 可以再砍掉一半甚至 90%。更重要的是,顶点压缩是少数几种几乎零代价、立即生效的优化:加几行命令、换一个解码器,文件就瘦下来了。

这一篇讲清楚三件事:顶点数据到底长什么样;量化、MeshOpt、Draco 三者各自的脾气;以及一个让你少踩坑的结论——没有「最好的」方案,只有「最合适的」方案

一个顶点到底有多大

先看一个顶点里装了什么。在 glTF 里,每个顶点由若干属性(attribute)组成:

属性用途默认精度每顶点字节数
position(位置)顶点在空间中的坐标3 × float3212
normal(法线)决定光照方向3 × float3212
tangent(切线)法线贴图计算4 × float3216
texcoord_0(UV)贴图采样坐标2 × float328
color(顶点色)顶点级别的着色4 × float3216

一个带 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 位)节省
position12650%
normal126(或 4,用 int8 + octahedral)50-67%
tangent164-850-75%
texcoord8450%

对刚才那个 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

解码器与平台兼容性:

平台纯量化MeshOptDraco
桌面 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(-cc1.7MB再省 35%,加载稍快

注意:-si 简化是改变模型几何的有损操作,和压缩是两回事。压缩尽量保持视觉一致,简化则是主动减少细节。两者可以叠加,但要看场景是否允许。

常见踩坑

  • 量化后法线方向变了:多半是用了过低精度。normal 至少用 16 位,或用八面体编码(octahedral)的 8 位。
  • Draco 解码后材质丢失:Draco 只压缩网格,材质和纹理要单独处理。加载时需要同时配好 Draco 解码器和 KHR 扩展。
  • 小程序里 Draco 加载不出来:解码器 wasm 在某些运行时环境里加载受限,换成 MeshOpt 通常就好了。
  • 量化后模型「漂移」:模型离坐标原点太远时,16 位精度不足以表达大坐标 + 小细节。解法是把模型移到原点附近再量化,或加大位深。

下一步

顶点压完了,但别急着庆祝——前面说过,纹理占模型 80% 的体积。下一篇我们换个战场,看看为什么传统的 PNG/JPG 在 GPU 眼里是个「大胃王」,以及 GPU 原生纹理格式是怎么解决这个问题的。

赞助支持