纹理内存优化
内存过大导致的问题
游戏运行时占用的内存大小,是衡量游戏性能的一个非常重要的指标。游戏如果占用的内存过大,会导致以下的问题:
- 发热,发烫。过多的内存数据传输,是导致游戏发热的主要原因之一。
- 延迟、卡顿。像纹理的加载、传输都需要耗费时间,另外内存资源占用的越多,系统也更加容易触发 gc 操作,导致游戏非预期卡顿。
- 闪退。游戏超过一定的内存上限,操作系统会强制杀死进程。
而纹理占用的内存占整个运行内存的比重非常大,并且容易做优化,所以纹理内存占用的大小是需要重点关注的。
纹理加载流程
下面是一张图片加载的整个流程。
可以看出读取非压缩纹理与压缩纹理的主要区别是: 压缩纹理不需要解码 ,数据是可以直接被 gpu 读取;png 等格式图片,不能直接被 gpu 读取,需要解码成未压缩的位数据。不过实际运行中, 并不是所有阶段的数据都需要保存 。非压缩纹理在 cpu 内存里的编码数据(蓝色区域),在解码后,是不需要再保存的。
纹理管理方式
下面列举几种小游戏开发可以使用的纹理管理方案:
- 将所有资源都加载进 gpu 内存,那么对于非压缩纹理,只需要在 gpu 内存保存一份解码数据,压缩纹理只需要在 gpu 内存保存一份压缩纹理数据。也就是只需要红色区域的部分。
- 将所有资源都加载进 cpu 内存,然后在运行时,将所需要绘制的资源,加载进 gpu 内存,不需要绘制时,可以释放掉。
- 将部分资源加载进 cpu 内存,然后在运行时,再将所需要绘制的资源,加载进 gpu 内存。根据需要将资源加载、释放。
那么这三种方式各有什么优缺点呢?
第 1 种方式是运行性能最好的,因为所有的纹理数据都在 gpu 内存,可以直接绘画,无需从 cpu 内存 copy 数据,占用的内存比较大。但是要求你对 webgl 和所使用的引擎,有一定的了解,保证 gpu 内的纹理数据,没有被引擎或者自己释放。游戏引擎在绘制纹理的时候,如果发现 gpu 内存中没有纹理数据,就会从 cpu 内存中寻找数据并拷贝。但是由于这种方式,我们没有在 cpu 内存中放置纹理数据,如果 gpu 中的某些纹理数据被释放了,后面就无法绘制了。如果使用 three.js 这种不自动回收 gpu 资源的引擎,并且整个游戏纹理内存占用不大的情况下,可以用这种方法。
第 2 种方式占用的内存要比第 1 种方式大,并且性能不如第 1 种方式(如果你提前将需要的纹理加载进 gpu 内存,那性能就和 1 一样了)。大部分引擎会在绘制纹理时,负责将纹理数据从 cpu 内存 copy 进 gpu 内存,并且管理释放。如果引擎不释放资源的话,就需要自己去做。
第 3 种方式的优点是,占用的内存小,特别适合做多关卡、多场景游戏。缺点是,需要管理好资源的加载与释放问题。一般在加载关卡进度的时候,完成资源的加载。
纹理大小如何计算
要想优化纹理占用的内存,就要知道如何计算每张纹理的大小。下面用一个例子进行说明。
上面是脸萌的主场景图片,jpg 格式,尺寸为 512*512,颜色空间为 RGB888,大小 100kb。解码后占用的内存大小计算公式为:长 * 宽 * 通道个数 * 通道位数,如果解码成 RGB888 格式,那么占用的内存大小为 512*512*3*8bit = 0.75M。而对应的 etc2 RGB888 格式的压缩纹理占用的内存大小为 512*512*0.5byte = 0.125M(实际大小为 135kb,因为还包含纹理描述信息)。下面的表格列举出几种常用的压缩纹理格式每像素占用的字节数。
减小纹理占用内存的方法
那么减少单张图片占用的内存,有以下几个方案:
- 减小图片的尺寸,在满足视觉要求的情况下,尽可能地缩小图片尺寸,比如将一张 256256 图片缩小成 128128 尺寸,就减少了 75%的内存占用。
- 降低通道位数,比如 RGB888 格式的图片转换为 RGB565 格式。但是这也依赖于具体的解码方法,现在小游戏引擎,会把图片资源都解码成 RGBA8888 格式,那么即使将 RGB888 格式转换为 RGB565 格式,也不会降低解码后纹理数据占用的内存。
- 使用压缩纹理,但是有几个限制点:首先压缩纹理是有损压缩,会降低一些图片质量(一般不明显);其次各个平台、不同图形 api 支持的压缩纹理格式,也不尽相同。比如浏览器上可以支持 s3tc 纹理,手机端支持 etc2 纹理(需要支持 opengles3.0),ios 系统支持 pvrtc 纹理。
像 pngquant、tinypng 等采用 color reduction 方法压缩图片的工具,能够减小文件大小,加快加载速度,编码数据占用的内存也更小。但是在解码方式不变的情况下,并不能减小解码后的图片内存占用大小。