最近做T级互动,需要使用到3D模型。
相信大家和我一样,在开始着手的时候,一定会有这么些问题:
- 1.如何选择3D模型的导出格式
- 2.如何对模型文件进行优化
- 3.在大流量的项目中兼容性怎么样
让我们通过这篇文章,进行细致的探索、调研与沉淀。
一、什么是 glTF 文件
glTF 全称 Graphics Language Transmission Format
,是三维场景和模型的标准文件格式。
glTF 核心是 JSON 文件,描述了 3D 场景的整个内容。
它由场景结构本身的描述组成,其由定义场景图的节点的层次提供。
场景中出现的 3D 对象是使用连接到节点的 meshes(网格)定义的。
Materials(材料)定义对象的外观。
Animations(动画)描述 3D 对象如何随着时间的推移转换 3D 对象,并且 Skins(蒙皮)定义了对物体的几何形状的方式基于骨架姿势变形。
Cameras(相机)描述了渲染器的视图配置。
除此以外,它还包括了带有二进制数据和图像文件的链接,如下图所示。
二、.gltf 与.glb
从 blender 文件导出中可以看出:
glTF 文件有两种拓展形式,.gltf(JSON / ASCII)或.glb(二进制)。
.gltf 文件可能是自包含的,也可能引用外部二进制和纹理资源,而 .glb 文件则是完全自包含的(但使用外部工具可以将其缓冲区/纹理保存为嵌入或单独的文件,后面会提到)。
glTF 提供了两个也可以一起使用的交付选项:
- glTF JSON 指向外部二进制数据(几何、关键帧、皮肤)和图像。
- glTF JSON 嵌入 base64 编码的二进制数据,并使用数据 URI 内联图像。
对于这些资源,由于 base64 编码,glTF 需要单独的请求或额外的空间。
Base64 编码需要额外的处理来解码并增加文件大小(编码资源增加约 33%)。
虽然 gzip 减轻了文件大小的增加,但解压缩和解码仍然会增加大量的加载时间。
为了解决这个问题,引入了一种容器格式 Binary glTF。
在二进制 glTF 中,glTF 资产(JSON、.bin 和图像)可以存储在二进制 blob 中,就是.glb 文件。
- 自包含的:
- 引用外部二进制和纹理资源的:
- 自包含的:
- 引用外部二进制和纹理资源:
- 自包含的:
- 引用外部二进制和纹理资源:
从图中可以看到,当非自包含型的时候,请求glTF文件时,会一同请求图片文件。
那么,我们就可以利用这个特性,就可以实现一些性能优化,让我们往下继续。
三、glTF 文件拆分
上文提到,glTF文件可以拆分为.gltf/.glb文件+二进制文件+纹理图片,那么,我们就可以将其拆分出来,并对纹理图片进行单独的压缩,来进行性能的优化。
可以使用gltf pipeLine
,其具有以下功能:
- glTF 与 glb 的相互转换
- 将缓冲区/纹理保存为嵌入或单独的文件
- 将 glTF 1.0 模型转换为 glTF 2.0(使用
KHR_techniques_webgl
和KHR_blend
) - 使用 Draco 进行网格压缩
在这里,我们是要使用“将缓冲区/纹理保存为嵌入或单独的文件”这个功能。
让我们来看看拆分出来的文件
再回顾一下,.glb文件是这么引入外部单独的纹理与二进制文件的
所以,只要将拆分出来的这几个文件,放入同一个路径中,然后像之前那样引入就好了。
- 压缩方式
gltf-pipeline -i male.glb -o male-processed.glb -s
- 使用方式(在 Three.js 中)
普普通通地用就好了,和不拆分的没什么区别
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
const loader = new GLTFLoader()
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
- 性能对比
四、glTF 文件压缩
如上面介绍,glTF 文件包括.gltf/.glb 文件、.bin 文件以及纹理资源。
glTF2.0 相关的插件主要有以下:
那么我们从中取一些来分析一下。
最常见的一种网格压缩方式,采用开源的Draco算法,用于压缩和解压缩3D 网格和点云,并且可能会改变网格中顶点的顺序和数量。
压缩的使文件小得多,但是在客户端设备上需要额外的解码时间。
- 压缩方式
可以使用gltf-pipeline
gltf 文件优化工具进行压缩
gltf-pipeline -i male.glb -o male-processed.glb -d
- 使用方式(在 Three.js 中)
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { DRACOLoader } from 'three/examples/jsm/loaders/DRACOLoader'
const loader = new GLTFLoader()
// 创建解码器实例
const dracoLoader = new DRACOLoader()
// 设置解压库文件路径
dracoLoader.setDecoderPath(DECODER_PATH)
// 加载解码器实例
loader.setDRACOLoader(dracoLoader)
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
- 性能分析对比
这个 glb 文件原大小为 3.2M,draco 压缩后为 1.8M,约为原文件的56%。
从上面的代码中可以看出,创建解码器实例需要引入额外的库来进行解码,setDecoderPath
会自动请求 wasm 文件来进行解密 *** 作。
而这两个 wasm 文件同时也增加了请求时间和请求数量,那么加上这两个文件,真实的压缩率约为62.5%。
所以,如果一个项目需要加载多个 glTF 文件,那么可以创建一个 DRACOLoader 实例并重复使用它。
但如果项目只需要加载一个 glTF 文件,那么使用 draco 算法是否具有“性价比”就值得考量了。
用 demo 进行一下性能对比:
可见 draco 算法首次加载和解密时间,要大于原文件。
而在实际项目中,这个差距更加明显,并且偶尔会出现解密堵塞的情况,需要重新进入页面才能恢复功能。
除此以外,还有一个很直观的问题,模型画质的损失是肉眼可观的。
如图,分别是在 iPhone 12 和小米 MIX2 中的样子:
总而言之,如果要将 draco 压缩算法运用到大规模项目中,需要结合实际项目进行以下对比:
- (1) 请求两个文件+解密耗时,与本身 glb 文件压缩后的体积大小相比,真实性能对比;
- (2) 画质是否会出现设计师无法接受的损失。
顶点属性通常使用FLOAT
类型存储,将原始始浮点值转换为16位或8位存储以适应统一的3D或2D网格,也就是我们所说的quantization向量化,该插件主要就是将其向量化。
例如,静态 PBR-ready 网格通常需要每个顶点POSITION
(12 字节)、TEXCOORD
(8 字节)、NORMAL
(12 字节)和TANGENT
(16 字节),总共 48 字节。
通过此扩展,可以用于SHORT
存储位置和纹理坐标数据(分别为 8 和 4 字节)以及BYTE
存储法线和切线数据(各 4 字节),每个顶点总共 20 字节。
- 压缩方式
可以使用gltfpack
工具进行压缩
gltfpack -i male.glb -o male-processed.glb
- 使用方式(在 Three.js 中)
普普通通地用就好了,和不压缩的没什么区别
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
const loader = new GLTFLoader()
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
- 性能对比
原文件3.2M,压缩后1.9M,为原文件的59.3%,比原模型加载速度也快上不少。
放到实际项目中,没有画质损失和加载时间过长的问题。
此插件假定缓冲区视图数据针对 GPU 效率进行了优化——使用量化并使用最佳数据顺序进行 GPU 渲染——并在 bufferView 数据之上提供一个压缩层。
每个 bufferView 都是独立压缩的,这允许加载器最大程度地将数据直接解压缩到 GPU 存储中。
除了优化压缩率之外,压缩格式还具有两个特性——非常快速的解码(使用 WebAssembly SIMD,解码器在现代桌面硬件上以约 1 GB/秒的速度运行),以及与通用压缩兼容的字节存储。
也就是说,不是尽可能地减少编码大小,而是以通用压缩器可以进一步压缩它的方式构建比特流。
- 压缩方式
可以使用gltfpack
工具进行压缩
gltfpack -i male.glb -o male-processed.glb -cc
- 使用方式(在 Three.js 中)
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader'
import { MeshoptDecoder } from 'three/examples/jsm/libs/meshopt_decoder.module.js'
const loader = new GLTFLoader()
loader.setMeshoptDecoder(MeshoptDecoder)
loader.load(MODEL_FILE_PATH, (gltf) => {
// ....
})
- 性能分析对比
原文件3.2M,压缩后1.1M,为原文件的65.6%,首次加载时间比原模型快上不少。
放到实际项目中,没有画质损失和加载时间过长的问题。
五、多个机型设备与优化对比结果
为了避免上文提到的“draco”压缩使得模型受损的情况,找了几台iPhone、安卓的手机来进行了一下性能与兼容的测试,让我们看一下结果。
PS:公司网络在不同时间段内网速不同(如上午和下午),可能会对数字产生小部分影响,但不影响文件优化横向对比。
可见,对于小部分需要使用模型的,并且只需要加载一个模型的业务,采用KHR_mesh_quantization
或EXT_meshopt_compression
进行网格压缩,再使用gltf-pipeline
进行模块区分并对纹理图片压缩,是目前找到的较好的优化方案。
六、其他
其实还有很多性能优化的插件,目前正在进行调试和调查,等后续迭代或有什么新进展,会继续更新:
网格优化的:
EXT_mesh_gpu_instancing
现 Three.js 的 GLTFLoader 尚未支持,Babylon.js 的BABYLON.GLTF2.Loader.Extensions 支持
还有一些纹理优化的插件:
KHR_texture_basisu
EXT_texture_webp
七、参考资料
The Basic Structure of glTF
GLB File Format Specification
Extensions for glTF 2.0
KHR_draco_mesh_compression
DRACOLoader – three.js docs
CesiumGS/gltf-pipeline: Content pipeline tools for optimizing glTF assets.
KHR_mesh_quantization
gltfpack | meshoptimizer
GLTFLoader
EXT_meshopt_compression
【网格压缩测评】MeshQuan、MeshOpt、Draco
欢迎关注凹凸实验室博客:aotu.io
或者关注凹凸实验室公众号(AOTULabs),不定时推送文章:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)