本来以为,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:

<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> ETC_PKM_HEADER_SIZE 16</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

将ETC1纹理存成pkm文件时,加上这个文件头,便于读取时获知大小、格式,上传压缩纹理时把这个头去掉。

文件头内容为:特征符——编码宽——编码高——实际宽——实际高

<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">const</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">char</span> kMagic[] = { <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'P'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'K'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'M'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">' '</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'1'</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'0'</span> };

<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">const</span> etc1_uint32 ETC1_PKM_FORMAT_OFFSET = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>;
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">const</span> etc1_uint32 ETC1_PKM_ENCODED_WIDTH_OFFSET = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8</span>;
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">const</span> etc1_uint32 ETC1_PKM_ENCODED_HEIGHT_OFFSET = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>;
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">const</span> etc1_uint32 ETC1_PKM_WIDTH_OFFSET = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">12</span>;
<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">static</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">const</span> etc1_uint32 ETC1_PKM_HEIGHT_OFFSET = <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">14</span>;</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>

尽管ETC1是固定的压缩比,但考虑到像素不对齐的情况,实际宽和实际高还是有必要存储的。

编码构成

Jpeg压缩标准是把图像划分为一系列8X8的像素块,然后每个像素块压缩成变长编码的。ETC1则是4X4的像素块压缩成固定的64位编码(8字节),由于固定,才有利于GPU内部实现并行解压缩。

<code class="hljs cs has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> ETC1_ENCODED_BLOCK_SIZE 8</span>
<span class="hljs-preprocessor" style="color: rgb(68, 68, 68); box-sizing: border-box;">#<span class="hljs-keyword" style="box-sizing: border-box;">define</span> ETC1_DECODED_BLOCK_SIZE 48 // RGB 三分量 * 4 * 4</span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>

因此,不考虑像素非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 
详细内容直接上文档上的图: 
total

横分/竖分示例图,图中每个字母代码一个像素: 
flip

编码过程

ETC1的编码流程如下: 
1、将图像划分为一系列 4X4 的子块。 
2、对每个子块,尝试所有的编码可能性,取解码后和原block像素差值和最小的一种编码。 
(1)是否flip,这个决定subblock如何划分 
(2)每个subblock用哪一行差值表 
(3)每个像素取哪一列的差值 
注:决定好flip之后,颜色均值和是否能用diff方式已经确定,这个不用遍历。 
3、合并所有子块编码。

不难看出,编码过程需要遍历所有可能性,其复杂度远大于解码,每一个 RGB 像素变成了一个精度较低的RGB均值和一个2位差值号,因此产生压缩。这种预测式表述自然本身就存在偏差,压缩损失亦来自于此。

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐