« 断点单步跟踪是一种低效的调试方法 | 返回首页 | 跟踪 skynet 服务间的消息请求及性能分析 »

Ericsson Texture 压缩贴图 EAC 的编码器

最近在做新引擎 UI 模块的工作。汉字字体纹理需要占比较大的一张贴图,考虑到这张贴图只需要用一个通道就够了,所以我决定使用压缩贴图。在手机设备上,GL_COMPRESSED_R11_EAC 是一个不错的选择。

EAC 是 Ericsson 提出的对单通道贴图的压缩方案,现已进入 OpenGL 的官方标准。它通常会结合 ETC2 一起使用。ETC2 负责 RGB 部分,EAC 负责 Alpha 通道。偶尔也可以单独使用。它会将每个像素解码为 [0,2047] 的整数,有 11bits 的精度,故而被称为 R11_EAC 。不过,我阅读文档后发现,其实有效精度是 8 bits ,一般也是从 8bits 的原始数据中编码得到的。压缩后,每 4 * 4 = 16 个像素会被编码为 64 bits ,压缩比 2 : 1 。用于字体纹理的话,可以节省一半的显存空间。

bgfx 暂时不支持 EAC ,我已经向作者提了需求,希望将来可以官方支持。当然自己改改也很容易。

bgfx 的贴图编码工具 texturec 自然也暂未支持 EAC ,所以不能压缩带 alpha 通道的贴图。现在网络上能找到的可压缩 EAC 的开源代码似乎只有 Ericsson 官方开源的 etcpack 。我原本想直接利用它,但阅读后,发现代码质量很低,所以最后还是选择自己直接根据格式文档实现一个 codec 。

在 OpenGL 官方文档中,可以找到 EAC 的编码格式 。文档为了定义清晰,洋洋洒洒写了一大篇,其实几句话就能讲明白。

和以往所有的压缩贴图算法一样,首先需要将贴图切割成 4*4 的单元,每个单元独立编码。对于 EAC 来说,就是要将 16 个 8 bits 像素编码到 64bits ,即 8 个字节中,平均每个像素 4 bits 。

最粗暴的压缩方法是直接截断每个像素的低 4 bits , 但这样图像损失太大。比较好的方法是用 3 bits 来记录几个索引值,这样 48 bits 就能描述 16 像素。再用额外的 16bits 记录数值基准。在 PC 上常用的 S3 压缩算法 (DXT5)做的比较简单,直接 2 个字节记录当前区块的上限和下限,然后均匀分割为 8 个色阶;或是对 0 和 255 做特殊处理,其余分为 6 个色阶。

但 S3 这种压缩方式对区块内灰度过度不均匀的值,精度损失比较大。EAC 对这种算法做了改良,但同时也增加编码的复杂性。

EAC 的方法是只保存一个字节的基准值,然后用另一个字节定义固化在 GPU 中的索引表。这样就可以通过不同的索引表来表达后续 48 bits 的 16 个索引量基于基准值的偏移。为了硬件设计方便,只需要给出 16 张索引表,然后再给一个 [1,15] 的缩放倍率,就能给出 16*15 种不同的索引表来。

这张索引表是这样的:

    {-3,-6,-9 ,-15,2,5,8,14}, //0  
    {-3,-7,-10,-13,2,6,9,12}, //1  
    {-2,-5,-8 ,-13,1,4,7,12}, //2  
    {-2,-4,-6 ,-13,1,3,5,12}, //3  
    {-3,-6,-8 ,-12,2,5,7,11}, //4  
    {-3,-7,-9 ,-11,2,6,8,10}, //5  
    {-4,-7,-8 ,-11,3,6,7,10}, //6  
    {-3,-5,-8 ,-11,2,4,7,10}, //7  
    {-2,-6,-8 ,-10,1,5,7,9 }, //8  
    {-2,-5,-8 ,-10,1,4,7,9 }, //9  
    {-2,-4,-8 ,-10,1,3,7,9 }, //10 
    {-2,-5,-7 ,-10,1,4,6,9 }, //11 
    {-3,-4,-7 ,-10,2,3,6,9 }, //12 
    {-1,-2,-3 ,-10,0,1,2,9 }, //13 
    {-4,-6,-8 ,-9 ,3,5,7,8 }, //14 
    {-3,-5,-7 ,-9 ,2,4,6,8 }, //15 

假设我们的基准值为 128 ,倍率为 2 ,索引表为 10 。像素值为 130 时,就可以被编码为 4 ,即 128 + 2 * table[10][4] 。注意,这里经过查表运算,把偏移量加到基准值上后,结果可能超出 [0,255] 的范围,需要做一次截断处理。截断对处理 alpha 为 0 或 255 (最常见的值)时非常有用。只要选择合适的倍率,总能准确编码出 0 和 255 。

此处倍率可以为 0 。这种情况下,16 个像素都等于基准值。对于整个区块的 alpha 值完全相同的时候,就可以方便的无损编码。

我在实现编码器的时候,对单一 alpha 值和只有两个 alpha 值,且其中一个是 0 或 255 的特殊情况做了特殊处理。因为这类特殊情况采用 EAC 压缩是可以完全无损,用 O(1) 时间就可以编码。压缩点阵字库的时候非常有用。

其它情况,用了暴力穷举的方式猜测使用哪张表、什么基准值,什么倍率比较好,压缩后的数据和原始数据方差最小。因为只涉及 16 个像素,所以速度可以接受。

etcpack 也是用的暴力穷举的方法来做的,但是它实现的不太好,剪枝和匹配的算法选的不好。我做了一些改进。首先,16 个像素的值是可以先做一次排序,合并相同的量。排序后做匹配的阶段可以把时间复杂度从 O(n*n) 降低到 O(n) 。

基准值我没有采用 etcpack 那样计算整个区块的平均值作为猜测基准,而是使用排序后像素最大和最小值的中值。基准值测试区间我认为在正负 4 倍倍率就够了。这是因为索引表都是对称的,不同的表的区别在于靠近中值时的分布疏密。基准值也只需要在在附近波动。

倍率是可以估算出来的,所选择的索引表最大可以表示区间乘上这个倍率后不应该小于该区块距离最远的两个色阶。倍率也不大会超过最小倍率的两倍,即利用一半的索引表。


我的代码放在 github 上了 ,它提供了对 16 像素编码和解码的基础功能,并不像 etcpack 那样能转换图形文件。我在 bgfx 上提了个 issue ,希望将来可以集成到 texturec 工具里。

Comments

云风一直是我的精神明灯
@CC etc2是gles3的标准,所以无论iOS还是Android,只要支持es3的就支持etc2。iOS是A7的cpu开始支持的,Android是从4.4系统开始就支持了,相关兼容性列表可以在http://opengles.gpuinfo.org/gles_compressedformats.php查到。 另外@cloudwu,字体以外的其他贴图也可以考虑ASTC格式,如果引擎就从es3支持起的话。
IOS上对应的有类似的解决方案吗?
感觉这个编码算法很有潜力用SIMD优化, 性能提高能提高几倍.
做引擎看起来真好玩啊,支持云风,加油!

Post a comment

非这个主题相关的留言请到:留言本