图形渲染性能优化之压缩纹理
PNG 是我们非常熟悉的一种无损压缩图片格式,常用于网络图片传输。
而在图形渲染场景,对于 PNG 素材,我们一般要先将其解码为位图,然后转换为 GPU 纹理,处理完之后再释放内存中的位图对象。
(iOS 有 GLKTextureLoader
帮我们封装了这一过程,但其实内部还是会做转换)
在这一过程中,解码消耗 CPU,存储位图消耗内存;如果需要处理多帧纹理,则不断分配和释放内存还会造成内存“抖动”,频繁 GC。
有没有办法解决这里的性能消耗呢?答案是肯定的。
PNG 虽然优秀,但对 GPU 并不友好;我们只需要改用纹理格式,就可以免除解码的过程。
那么,为什么 PNG 就一定需要解码才能给 GPU 使用呢?
为什么纹理格式更适合渲染
决定素材能否被 GPU 直接使用的关键一点,就是能否快速访问像素颜色和坐标。
我们先看看 PNG 的编码格式:
PNG 本质上是一种存储格式,是为了尽可能较少文件体积,便于存储和网络传输,所以内部采用了 zlib 压缩和 CRC 循环冗余校验等算法;
但 GPU 渲染的时候并不 care 这些,它没法快速访问像素颜色和坐标。
他们虽然有区别,但是都有一个共同特点:
- 以 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()
。
参考: