最近在工作中遇到一个PNG图片最终渲染出来丢失了Alpha(透明度)的问题,在经过不断地查阅资料和思考后终于解决了这个问题,在这里想和大家分享一下过程中学习到的一些知识点。
问题背景
目前所做的项目是一个跨平台渲染引擎,使用的是stb_image库读取图片像素数据(以RGBA排列),之后通过OpenGL的glTexImage2D生成纹理并渲染,iOS平台使用EAGLLayer来显示最后的渲染结果。
然而发现了放在Xcode工程下面的PNG图片能够正常的显示,而通过下载的PNG图片却出现了Alpha信息丢失的情况,如下所示分别是加载Xcode工程中的PNG图片和加载通过下载的PNG图片。
那么为什么会从工程中读取的PNG像素数据能正常显示,而下载的PNG图片却丢失了Alpha信息呢?我们发现在Xcode的Build Settings中有个和PNG相关的选项叫做Compress PNG Files,默认是开启的,我们尝试着关闭这个选项,发现再加载工程的PNG图片也出现了Alpha信息丢失的情况了。那么问题就定位到了这个Compress PNG Files了,到底这个Compress PNG Files到底做了什么事情呢?
关于PNG
首先我们来简单看下PNG是啥,维基百科上面的定义是便携式网络图形(英语:Portable Network Graphics,缩写:PNG)是一种无损压缩的位图图形格式,支持索引、灰度、RGB三种颜色方案以及Alpha通道等特性。
如果想更具体的了解PNG,可以看一下PNG文件结构。
关于Xcode 中的Compress PNG Files
Compress PNG Files从字面上简单翻译一下就是"压缩PNG文件"的意思,但是当你把这个选项开启之后会发现编译之后的ipa包不但没有减小,反而增大了,这是怎么回事呢?说好的压缩呢?这个Compress PNG Files到底做了啥事呢?
当 Xcode 优化一个 PNG 文件的时候,它将 PNG 文件变成一个从技术上讲不再是有效的PNG文件。但是 iOS 可以读取这种文件,并且这比解压缩正常的 PNG 文件更快。
它做了以下几件事情:
- 额外关键块(CgBI)
- byteswapped(RGBA - > BGRA)像素数据,大概是用于帧缓冲的高速直接blitting
- 从IDAT块中删除了zlib页眉,页脚和CRC
- 预乘alpha(
颜色'=颜色* alpha / 255
)
明显的改动就是在IHDR块之前插入了CgBI块来表示这种格式,CgBI文件格式因其额外标题而得名,是Apple对PNG图像格式的专有扩展,同时修改了IDAT块中的数据,原因就是在iPhone中,图像是以BGRA格式在内存中处理的,到这里就可以发现,其实这个所谓的Compress PNG Files,最主要的目的并不是压缩图片的大小,而是将图片转换成iPhone能更方便处理的格式,加快处理速度。
这里我们重点看下有一个和Alpha相关的操作预乘Alpha,那么什么是预乘Alpha呢,这个操作的作用是什么呢?
关于预乘透明度(Premultiplied Alpha)
简单地说,比如常规的半透明半纯红色图像RGBA归一化值为(0.5, 0, 0, 0.5),由预乘透明度图像方式存储则RGBA值为(0.25, 0, 0, 0.5)。由此可知,即每个颜色分量都乘以alpha通道值作为结果值:
`color.rgb *= color.alpha`
为什么要引入预乘呢?
这种做法可以加速图片渲染的速度,如果你的颜色分量是已经经过预乘处理过的,则在渲染时可以直接使用而不需要再进行3次的乘法运算,从而提高渲染效率。此外,实际上它还解决了以下几个问题。
- 解决纹理比例缩放映射产生的颜色错误问题
- 可以和其他纹理一起正常混合而不打破批次渲染
关于iOS平台下的渲染
iOS平台中如果使用CAEAGLLayer
对象作为顶部图层并且与它下面的图层进行颜色混合,那么渲染缓存的颜色数据必须用一种预先乘好的(premultiplied)alpha格式。而读取出来的像素数据没有经过预乘Alpha操作,所以造成了渲染结果丢失了Alpha的现象。
综上,所在我们读取出来的RGB值分别乘上Alpha后就能够正常渲染了:
uint8_t alpha;
for (int y = 0; y < p->s->img_x; y++) {
for (int x = 0; x < p->s->img_y; x++) {
alpha = *(pixels + 3); // 取出Alpha值
*pixels = round(*pixels * alpha / 255.f); // R * Alpha
*(pixels + 1) = round(*(pixels + 1) * alpha / 255.f); // G * Alpha
*(pixels + 2) = round(*(pixels + 2) * alpha / 255.f); // B * Alpha
pixels += 4;
}
}
经过手动乘上Alpha处理后的图片: