我们的游戏在 UI 界面使用了过多的图片,以至于挤占了安装包的体积。
1. 其实可以不要用硬件支持的压缩格式
为了节省运行期的内存,这些图片都用了 ASTC_4x4 压缩格式。
但其实,这是不必要的。
因为大多数的界面都是打开之后短时间内关闭,不会一直挤占内存,
所以即便在内存中是非压缩的 RGBA,也不要紧。
常驻内存的少量图片仍然可以用 ASTC_4x4 来压缩。但这只是少数,不需要重点讨论了。
2. 各种图片格式的调查
之前的图片打包,不外乎是在硬件支持的各种压缩纹理格式之中,选择其一。
要么 ASTC,要么 ETC1、ETC2、PVRTC。
现在既然内存里可以直接用 RGBA,那选择就增加了一些。
各个格式的简单介绍如下:
名称 | 内存占用(字节/像素) | 质量 | 优势 | 缺点 |
---|---|---|---|---|
ASTC | 0.11 ~ 1 | 有损(质量可调) | 硬件支持,加载速度最快,占用内存很少。画质可调 | |
ETC1 | 0.5 ~ 1 | 有损(质量中等) | 硬件支持,加载速度最快,占用内存很少 | |
ETC2 | 0.5 ~ 1 | 有损(质量中等) | 硬件支持,加载速度最快,占用内存很少 | |
PNG | 4 | 无损 | 网络常见格式,对应软件多。画质无损 | |
JPG | 4 | 有损(质量可调) | 网络常见格式,对应软件多。画质可调,同等画质情况下文件体积小 | |
GIF | 4 | 较差,颜色数量有限 | 网络常见格式,对应软件多。支持动画(游戏里不常用) | 画质差 |
FLIF | 4 | 无损 | 画质无损,且比 PNG 更小 | 解码速度很慢 |
WEBP | 4 | 有损(质量可调)、或者无损 | 新一代格式(1),内部使用 VP8 编码。同等文件体积时,画质比 JPG 更高 | VP8 诸多小毛病,实际上已经过时 |
BPG | 4 | 有损(质量可调)、或者无损 | 新一代格式(2),内部使用 HEVC 编码。同等文件体积时,画质比 JPG 更高 | HEVC 带来法务风险 |
HEIF | 4 | 有损(质量可调)、或者无损 | 新一代格式(2),内部使用 HEVC 编码。同等文件体积时,画质比 JPG 更高 | HEVC 带来法务风险 |
AVIF | 4 | 有损(质量可调)、或者无损 | 新一代格式(3),内部使用 AV1 编码。同等文件体积时,画质比 JPG 更高 |
注释:
- 按编码格式的出现顺序,是 AV1 > HEVC > VP8,其中 AV1 是最新的。越新的格式,质量就更高,同时也意味着复杂、计算量大、速度慢。
- 图片加载的耗时,大致分为两部分。一是读取数据到内存,二是解码。如今的手机设备,读取数据很快,主要的耗时在解码。硬件支持的格式无需解码、所以极快。而硬件不支持的格式,解码速度往往就等同于加载速度。
诸多格式,各有特点。一些格式明显存在不能用的理由:
- BPG, HEIF 由于 HEVC 编码的法务风险,还是不要用了
- WEBP 由于画质问题被淘汰。虽然数据上胜过了 JPG,但我的主观感觉其实是不如 JPG 的,它压缩得到的 UI 图片往往很糊,损失了一些锐利的细节
- GIF 由于画质问题被淘汰。因为只能支持 256 色,对于现代 UI 来说,往往不够
- FLIF 由于解码速度太慢被淘汰(后面有数据、比 PNG 慢约 300 倍)
硬件支持的 ASTC, ETC1, 等等格式,虽然加载速度最高,内存也最省,但相同画质情况下,文件体积比 JPG 大。综合考虑的话,不一定合适。像我们之前,所有图片全都用 ASTC_4x4 其实不太可取。
于是剩下的只有:PNG, JPG, AVIF
AVIF 的画质是比 JPG 好很多的(相同文件体积时),具体可以参考这里:https://netflixtechblog.com/avif-for-next-generation-image-coding-b1d75675fe4
按这个文章的数据,在 0.4 bits/pixel 的情况下(一张 1024x1024 的图片只需 52.5k 字节):
名称 | 颜色格式 | SSIM(从图表里肉眼观测、数值越高说明画质损失越小) |
---|---|---|
JPG | YUV420 | 0.957 |
HEIF(HEVC) | YUV420 | 0.981 |
AVIF-MSE | YUV420 | 0.984 |
AVIF-SSIM | YUV420 | 0.988 |
JPG | YUV444 | 0.927 |
HEIF(HEVC) | YUV444 | 0.977 |
AVIF-MSE | YUV444 | 0.979 |
AVIF-SSIM | YUV444 | 0.983 |
看来从数值上,AVIF 的画质是明显好于 JPG 的。
但是解码速度也明显的慢了。因为 AVIF 是基于 AV1 编码,这个编码目前还不成熟,编码解码的速度并不理想。我看了另一篇帖子:https://github.com/joedrago/avif/issues/11,用的是一张 512x512 的图片,
<Decode> [PNG]: Time: 1.43 ms
<Decode> [JPEG]: Time: 0.39 ms
<Decode> [HEIC]: Time: 1.47 ms
<Decode> [HEIF]: Time: 102.06 ms
<Decode> [WebP]: Time: 24.00 ms
<Decode> [BPG]: Time: 39.60 ms
<Decode> [FLIF]: Time: 364.84 ms
<Decode> [AVIF]: Time: 154.54 ms
<Encode> [PNG]: Time: 39.63 ms
<Encode> [JPEG]: Time: 2.46 ms
<Encode> [HEIC]: Time: 65.51 ms
<Encode> [HEIF]: Time: 1799.63 ms
<Encode> [WebP]: Time: 168.42 ms
<Encode> [BPG]: Time: 2761.79 ms
<Encode> [FLIF]: Time: 6160.68 ms
<Encode> [AVIF]: Time: 346592.24 ms"HEIC" means using Apple's built-in HEVC codec, "HEIF" means using libheif
可以看到 AVIF 的解码速度比 JPG 慢 100 倍,而编码速度更是骇人听闻的慢了 10 万倍,于是也只能说一句“告辞”。等它慢慢去优化吧。
逛了一圈回来,发现还是 JPG 和 PNG 最为靠谱。其中 PNG 体积略大,但好在画质无损。在 UI 实际只有极少图片需要真正的无损画质。而大多数图片都用 JPG 更为合适。
3. 关于 JPG
3.1 JPG 的 alpha
JPG 有一个问题是“不支持 alpha”,这可以通过两种办法来解决。
一是在引擎层面,用两张 jpg 图片,一张保存 RGB,另一张保存 alpha,到运行之时把它们合并为 RGBA。这又细分为两种:(1) 加载时,在内存中合并为 RGBA;(2) 渲染时,采样两张图片然后在 shader 里合并为 RGBA。两种做法各有优劣。前者性能略高一点,后者可以支持更多用途(例如把 RGB 和 alpha 各用一张 ETC1,因为 ETC1 是无法在内存中合并为 RGBA 的,但在 shader 就可以)。
二是从 JPG 格式层面。注意到 JPG 格式本来支持三种颜色格式,分别是 Gray, RGB, CMYK,对应了 1 通道、3 通道、4 通道。暴雪公司的著名游戏《魔兽争霸3》中,有一种用法。把 RGBA 四个通道的数据当作 CMYK 数据(注意是“当作”CMYK,不是“转换颜色空间到”CMYK),然后保存为 CMYK 格式的 JPG(他们换了文件头部、然后重新起了个名字叫做 BLP)。这个做法看起来是可行的。正常的 JPG 编码会把 RGB 转为 YCbCr,于是还有了 YUV420 和 YUV444 等等的区别(前者通常肉眼感觉更好,而后者的数值损失更小)。而 CMYK 是否也会转为 YCbCr?我没有再去调查。
3.2 JPG 的解码速度
一般而言,JPG 的解码应该是极快的,比 PNG 更快。(前文引用的数据中指出,一张 512x512 的图片耗时为 0.39 毫秒)
如果测试发现 JPG 解码比 PNG 慢,可能是用错了库。一般老的游戏引擎可能搭载了 libjpeg,而后来有了 libjpeg-turbo 是使用 SIMD 的加速版本,它宣称是提速了 2~6 倍。有文章说“Most web browsers use the open source libjpeg-turbo for decoding JPEG files”,意味着今天的 libjpeg-turbo 已经开始取代原来的 libjpeg 了。
另外,JPG 压缩时,画质是可调的。越高的画质,不仅意味着文件体积的增大,而且也意味着解码速度变慢。最高画质、较差画质,解码速度可能会有成倍的差距。
还有一个方案是使用 GPU(而非 CPU)来做解码。但这都只是一些尝试,没看到太多人这样用。
- https://github.com/negge/jpeg_gpu 这个项目尝试使用 OpenGL 的 shader 来做 JPG 解码。
- https://developer.nvidia.com/blog/leveraging-hardware-jpeg-decoder-and-nvjpeg-on-a100/ 这是 nVidia 提供的一个库,使用 GPU 来做 JPG 的编码和解码。