从原理到实战,搞懂 Meshopt 和 Draco 的本质区别
前言
做 Web3D 的同学应该都知道 Draco 压缩,它是 Google 开发的 3D 几何数据压缩算法,Three.js、Babylon.js、model-viewer 都支持。
但最近 Meshopt 越来越火,官方宣称解码速度比 Draco 快 5-10 倍。这到底是不是吹牛?今天从原理层面分析一下。
先说结论
| 对比项 | Draco | Meshopt |
|---|---|---|
| 压缩率 | 高(70-90%) | 更高(75-95%) |
| 解码速度 | 快 | 快 5-10 倍 |
| 编码速度 | 慢 | 快 3-5 倍 |
| 内存占用 | 中等 | 低 |
| 框架支持 | 广泛 | Three.js r136+ |
结论:新项目优先用 Meshopt,老项目或需要兼容 model-viewer/Babylon.js 的用 Draco。
一、Draco 的原理
Draco 使用的是预测 + 熵编码的方式:
1.1 预测编码
对于网格中的每个顶点,Draco 会预测它的位置:
scss
预测值 = f(相邻顶点)
残差 = 实际值 - 预测值
由于相邻顶点通常很接近,残差值会很小,存储残差比存储原始值更省空间。
1.2 熵编码
对残差值进行熵编码(类似 zip 压缩):
- 出现频率高的值用短编码
- 出现频率低的值用长编码
1.3 解码过程
解码时需要:
- 反向熵编码
- 重建预测
- 计算原始值
这个过程需要遍历整个网格拓扑,所以解码速度受限。
二、Meshopt 的原理
Meshopt(基于 gltfpack/meshoptimizer)采用的是量化 + 简单编码的方式:
2.1 量化
将浮点数转换为整数:
javascript
// 原始值:0.123456789
// 量化为 16 位整数:8053(假设范围 [-1, 1])
quantized = Math.round((value - min) / (max - min) * 65535)
2.2 Delta 编码
存储相邻顶点的差值:
javascript
// 顶点序列:[100, 102, 105, 103]
// Delta 编码:[100, 2, 3, -2]
2.3 简单变长编码
使用简单的变长整数编码(varint):
javascript
// 小值用 1 字节,大值用更多字节
// 0-127: 1 字节
// 128-16383: 2 字节
// ...
2.4 解码过程
解码时只需要:
- 读取 varint
- 累加 delta
- 反量化
不需要遍历网格拓扑,所以解码速度极快。
三、为什么 Meshopt 更快
3.1 数据局部性
Meshopt 的数据是顺序存储的:
erlang
顶点0.x, 顶点0.y, 顶点0.z
顶点1.x, 顶点1.y, 顶点1.z
顶点2.x, 顶点2.y, 顶点2.z
...
CPU 缓存友好,顺序读取效率高。
Draco 的数据是按拓扑存储的:
erlang
面0的顶点、面1的顶点、面2的顶点...
需要随机访问,缓存命中率低。
3.2 算法复杂度
| 操作 | Draco | Meshopt |
|---|---|---|
| 解码复杂度 | O(n log n) | O(n) |
| 内存访问 | 随机 | 顺序 |
| 分支预测 | 难预测 | 易预测 |
3.3 实测数据
javascript
// 测试代码
const decoder = new MeshoptDecoder()
console.time('Meshopt')
decoder.decodeVertexBuffer(target, vertexCount, vertexSize, source)
console.timeEnd('Meshopt')
// 约 2-5ms
const dracoLoader = new DRACOLoader()
console.time('Draco')
dracoLoader.decodeDracoFile(buffer)
console.timeEnd('Draco')
// 约 15-30ms
四、实际项目中的选择
4.1 选 Meshopt 的场景
- Three.js 项目(r136+)
- 追求极致加载性能
- 移动端 Web
- 需要快速解码的交互场景
4.2 选 Draco 的场景
- 需要兼容 model-viewer
- Babylon.js 项目
- 老项目升级成本高
- 需要最广泛的兼容性
4.3 两者都支持
使用 Zipoly(zipoly.netlify.app)可以:
- 选择 Meshopt 引擎
- 如果 Meshopt 不可用,自动降级到 Draco
- 一键压缩,无需手动配置
五、Three.js 中使用 Meshopt
5.1 基础配置
javascript
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js'
import { MeshoptDecoder } from 'three/addons/libs/meshopt_decoder.module.js'
const loader = new GLTFLoader()
loader.setMeshoptDecoder(MeshoptDecoder)
loader.load('/model.glb', (gltf) => {
scene.add(gltf.scene)
})
5.2 配合 KTX2 纹理
javascript
import { KTX2Loader } from 'three/addons/loaders/KTX2Loader.js'
const ktx2Loader = new KTX2Loader()
ktx2Loader.setTranscoderPath('/basis/')
ktx2Loader.detectSupport(renderer)
const loader = new GLTFLoader()
loader.setMeshoptDecoder(MeshoptDecoder)
loader.setKTX2Loader(ktx2Loader)
loader.load('/model.glb', (gltf) => {
scene.add(gltf.scene)
})
5.3 压缩命令
使用 gltfpack 命令行工具:
bash
# 安装
npm install -g gltfpack
# 压缩
gltfpack -i input.glb -o output.glb -cc
# 参数说明
# -cc: 压缩顶点和索引
# -tc: 压缩纹理(KTX2)
# -si 0.5: 简化到 50%
六、Zipoly 一键压缩
6.1 操作步骤
- 下载 Zipoly:zipoly.netlify.app
- 导入 GLB 文件
- 选择"Web 3D 场景"预设(默认 Meshopt)
- 点击"开始优化"
6.2 压缩效果
| 模型 | 原始大小 | Draco | Meshopt |
|---|---|---|---|
| 产品模型 | 45MB | 8.2MB | 6.8MB |
| 角色模型 | 32MB | 5.1MB | 4.2MB |
| 场景模型 | 120MB | 22MB | 18MB |
七、性能对比测试
7.1 测试环境
- 浏览器:Chrome 120
- 设备:MacBook Pro M1
- Three.js:r160
7.2 测试结果
| 指标 | Draco | Meshopt | 提升 |
|---|---|---|---|
| 解码时间 | 28ms | 4ms | 7x |
| 内存占用 | 45MB | 32MB | 29% |
| 首帧时间 | 120ms | 85ms | 29% |
总结
Meshopt 在解码速度和压缩率上都优于 Draco,是 Web3D 项目的首选压缩方案。
推荐工作流:
- 使用 Zipoly 压缩模型(默认 Meshopt)
- Three.js 中配置 MeshoptDecoder
- 配合 KTX2 纹理压缩,实现最佳性能