本来以为,ETC1作为Android 设备的OpenGL标准,开源且最常用的的一种压缩纹理格式,总会有人去翻译一下khronos的文档,读一下代码,给大家作个普及的,不料就是搜不到。没办法,尽管英文不好,还是硬啃了下文档,把 ETC1压缩纹理的实现原理弄清楚了。
https://www.khronos.org/registry/gles/extensions/OES/OES_compressed_ETC1_RGB8_texture.txt
至于什么是压缩纹理,如何使用,可以参考:
http://blog.csdn.net/wanglang3081/article/details/8869589
ETC1的文件头
文件头大小为16:
#define ETC_PKM_HEADER_SIZE 16
将ETC1纹理存成pkm文件时,加上这个文件头,便于读取时获知大小、格式,上传压缩纹理时把这个头去掉。
文件头内容为:特征符——编码宽——编码高——实际宽——实际高
static const char kMagic[] = { 'P', 'K', 'M', ' ', '1', '0' };
static const etc1_uint32 ETC1_PKM_FORMAT_OFFSET = 6;
static const etc1_uint32 ETC1_PKM_ENCODED_WIDTH_OFFSET = 8;
static const etc1_uint32 ETC1_PKM_ENCODED_HEIGHT_OFFSET = 10;
static const etc1_uint32 ETC1_PKM_WIDTH_OFFSET = 12;
static const etc1_uint32 ETC1_PKM_HEIGHT_OFFSET = 14;
尽管ETC1是固定的压缩比,但考虑到像素不对齐的情况,实际宽和实际高还是有必要存储的。
编码构成
Jpeg压缩标准是把图像划分为一系列8X8的像素块,然后每个像素块压缩成变长编码的。ETC1则是4X4的像素块压缩成固定的64位编码(8字节),由于固定,才有利于GPU内部实现并行解压缩。
#define ETC1_ENCODED_BLOCK_SIZE 8
#define ETC1_DECODED_BLOCK_SIZE 48 // RGB 三分量 * 4 * 4
因此,不考虑像素非4对齐的情况,它的压缩比是固定的48/8=6,至于常用的把ARGB分别存储为两张ETC1纹理的做法,压缩比是64/16=4。
像素不对齐的情况如何处理,就自己想想吧,反正不是大问题。
4X4的像素点与64位编码对应关系如下:
1、将 4X4 的像素点划分为两个subblock,横分或者竖分【1个位表示:flipbit】。
2、两部分的像素RGB求均值,总共用24个位表示两个RGB均值,分两种方案【1个位表示:diffbit】
(1)两个subblock的RGB均值都用4位表示。刚好4*3*2=24位。
(2)两个subblock的RGB值相差在 [-4,3]区间(5位表示的情况下,对应常规的8位表示是[-32,24]),第一个subblock的RGB值用5位表示,第二个用3位差值表示。
3、解码时,像素值由RGB均值加上差值而得,差值表为8X4大小。每一个subblock使用的是其中一行【行号需要3个位,两个subblock,加起来6个位】,每个像素点对应所取差值行中一个数【编号需要2个位,这样就16X2=32个位】。PS:RGB三个分量是用同一个差值。
64位的编码分成两部分,高位和低位。
低位存储每个像素点的差值行序号,按大端存储方式,分两部分,前一部分存储高位,后一部分存储低位。
高位存储RGB均值,subblock所用的差值表行号,再加上 flipbit 和 diffbit
详细内容直接上文档上的图:
横分/竖分示例图,图中每个字母代码一个像素:
编码过程
ETC1的编码流程如下:
1、将图像划分为一系列 4X4 的子块。
2、对每个子块,尝试所有的编码可能性,取解码后和原block像素差值和最小的一种编码。
(1)是否flip,这个决定subblock如何划分
(2)每个subblock用哪一行差值表
(3)每个像素取哪一列的差值
注:决定好flip之后,颜色均值和是否能用diff方式已经确定,这个不用遍历。
3、合并所有子块编码。
不难看出,编码过程需要遍历所有可能性,其复杂度远大于解码,每一个 RGB 像素变成了一个精度较低的RGB均值和一个2位差值号,因此产生压缩。这种预测式表述自然本身就存在偏差,压缩损失亦来自于此。