图形渲染性能优化之压缩纹理

PNG 是我们非常熟悉的一种无损压缩图片格式,常用于网络图片传输。
而在图形渲染场景,对于 PNG 素材,我们一般要先将其解码为位图,然后转换为 GPU 纹理,处理完之后再释放内存中的位图对象。
(iOS 有 GLKTextureLoader 帮我们封装了这一过程,但其实内部还是会做转换)
在这一过程中,解码消耗 CPU,存储位图消耗内存;如果需要处理多帧纹理,则不断分配和释放内存还会造成内存“抖动”,频繁 GC。

有没有办法解决这里的性能消耗呢?答案是肯定的。
PNG 虽然优秀,但对 GPU 并不友好;我们只需要改用纹理格式,就可以免除解码的过程。

那么,为什么 PNG 就一定需要解码才能给 GPU 使用呢?

为什么纹理格式更适合渲染

决定素材能否被 GPU 直接使用的关键一点,就是能否快速访问像素颜色和坐标

我们先看看 PNG 的编码格式:

PNG 本质上是一种存储格式,是为了尽可能较少文件体积,便于存储和网络传输,所以内部采用了 zlib 压缩和 CRC 循环冗余校验等算法;
但 GPU 渲染的时候并不 care 这些,它没法快速访问像素颜色和坐标。

我们再来看看两种常见的纹理格式 ETCPVRTC:


他们虽然有区别,但是都有一个共同特点:

  • 以 block 为单位编码,能充分发挥 GPU 的并行处理能力;
  • 每个 block 前半部分为 RGBA 颜色,后半部分为坐标,便于快速渲染;

iOS 使用纹理格式

PVRTC 是 iOS 上普遍使用的纹理格式,支持目前所有主流机型。

而且 Apple 官方提供了 TextureTool 命令行工具进行纹理格式转换:

texturetool -e PVRTC --channel-weighting-linear --bits-per-pixel-4 -o ImageL4.pvrtc Image.png

当然也可以使用 GUI 工具 PVRTEXTOOL

由于 PVRTC 天生支持 alpha 通道,而且又有 GLKTextureLoader,所以加载纹理格式素材仍然和普通 PNG 一样:

NSLog(@"GL Error = %u", glGetError());
GLKTextureInfo* textureInfo = [GLKTextureLoader textureWithContentsOfFile:imagePath options:nil error:&error];

这里需要注意的一点就是,必须要先调用 glGetError,否则后面的 GLKTextureInfo 可能会为 nil
至于原因,貌似是系统的一个 bug,可参考 这篇文章

Android 使用纹理格式

Android 上目前兼容性最好的纹理格式是 ETC1,但它有个缺点就是不支持 alpha 通道。
但要解决这个问题并不难:

首先将 alpha 数据单独写入素材文件(可以通过 ARM Mali Tool 等工具);

然后在 vertex shader 里定义两个纹理坐标,分别用于读取上半部分 RGB 和 下半部分 alpha:

attribute vec4 a_Position;
attribute vec2 a_TextureCoordinate;
varying vec2 v_rgbCoordinate;
varying vec2 v_alphaCoordinate;
uniform mat4 u_projectionMatrix;
uniform mat4 u_cameraMatrix;
uniform mat4 u_modelMatrix;

void main() {
    v_rgbCoordinate = a_TextureCoordinate * vec2(1.0, 0.5);
    v_alphaCoordinate = v_rgbCoordinate + vec2(0.0, 0.5);
}

最后在 fragment shader 分别读取为两个 texture2D,合成 gl_FragColor

precision mediump float;
uniform sampler2D u_Texture;
varying vec2 v_rgbCoordinate;
varying vec2 v_alphaCoordinate;

void main() {
    vec4 color = texture2D(u_Texture, v_rgbCoordinate);
    color.a = texture2D(u_Texture, v_alphaCoordinate).r;
    gl_FragColor = color;
}

最后 Android 应用层加载 ETC1 纹理可以使用官方提供的 ETC1Util.createTexture()

参考