你可以把 GPU 纹理压缩想象成一件非常“抠门但聪明”的事手机显存就那么点带宽也紧张GPU 还得每秒采样几十亿次纹理。于是工程师们想了个办法——“别把每个像素老老实实存 RGBA 四个通道了太费。咱们一小块一小块地存每块先存个大概颜色再存一堆‘怎么调味’的小指令。用的时候再把这一块‘现煮’出来。”这类方法就叫块压缩Block Compression。ETC1/ETC2 就是移动端最典型的一套块压缩格式。这篇文章的目标以 ETC1 为主先把它讲透顺带讲块压缩共性思路你看完能举一反三理解 BC1/BC3/ASTC重点讲 ETC1/ETC2 的“块解码怎么做”给出步骤、公式、常见坑风格大白话、生动、有点段子但不胡扯为了可读性我会分很多小节信息密度高但尽量不让你昏。1. 先把“块压缩”这个套路吃透大家都在干同一件事1.1 为什么块压缩几乎都选 4×4因为 4×4 是个神奇平衡点块太小每块要存“头信息”端点/模式/表头占比太大压不下去块太大块内像素差异大靠少量参数很难拟合画质糊成马赛克4×4头信息占比合理画质还能接受还方便硬件并行所以你会看到ETC14×4每块 8 字节BC1/DXT14×4每块 8 字节BC3/DXT54×4每块 16 字节ASTC虽然块尺寸可变但核心也还是“块”大白话压缩像打包快递箱子太小浪费胶带箱子太大容易把货压坏。4×4 就是最合适的箱子。1.2 块压缩的“祖传配方”长啥样几乎所有块压缩都遵循这套逻辑端点/基准色存一两个“代表色”endpoints / base color像素索引每个像素只存 2~4 bit 的索引插值/偏移根据索引从端点插值或加减偏移得到最终颜色输出 16 个像素一次解码一个块你把这个套路背下来学 ETC1/ETC2 就很顺。2. ETC1 是啥一块 8 字节负责 4×4 的 RGB没有 AlphaETC1 是 OpenGL ES 2.0 时代的“全民纹理压缩格式”优点是兼容性爆表老安卓几乎都支持。缺点也很致命没有 Alpha。2.1 ETC1 块里到底存了什么ETC1 一个块 64 bit核心信息可以分成两堆颜色信息两块子区域subblock的基准色索引信息16 个像素各自的“调味指令”2 bit/像素还有几个关键“开关”diff用独立颜色还是差分颜色flip子块是左右分还是上下分table0/table1两块子区域各自用哪张“调味表”大白话ETC1 就像一块 4×4 的饼干先决定是“左右夹心”还是“上下夹心”然后每半块给一个底味再告诉每个小格子要加点甜还是加点咸。3. ETC1 解码从 8 字节煮出 16 个像素步骤版你要写 ETC1 解码器最靠谱的思路是把流程固定成“七步走”每步不犯错就稳Step 1读入块数据64 bit把 8 字节拼成一个uint64 block注意端序。之后所有字段都靠位运算取。Step 2取控制位diff、flip、table0、table1这些告诉你颜色字段怎么解释子块怎么划分modifier 表用哪套Step 3算出两个子块的基准色 baseColor0/baseColor1这一步有两种模式3.1 Individual 模式diff0左右/上下两块各自存 4bit RGBR0,G0,B0各 4bit → 扩展到 8bitR1,G1,B1各 4bit → 扩展到 8bit4bit 扩展到 8bit的经典方法x8 (x4 4) | x4大白话4bit 只有 0~15扩到 0~255就相当于把它复制一遍填满A 变成 AA。3.2 Differential 模式diff1存一份 5bit 基准色 一份 3bit 有符号差值base0R0(5),G0(5),B0(5)deltadR(3),dG(3),dB(3)范围 -4…3base1R1R0dRG1G0dGB1B0dB5bit 扩展到 8bit常用x8 (x5 3) | (x5 2)**差值的符号扩展3bit → int**要做对3bit 最高位是符号位比如0b00000b01130b100-40b111-1大白话3bit 差值像“我比你多一点/少一点”只有 -4 到 3 这么点幅度够用来表达局部变化。Step 4准备 modifier 表调味表ETC1 有固定的 8 张表每张 4 个 modifier常用规范表0: -8 -2 2 8 1: -17 -5 5 17 2: -29 -9 9 29 3: -42 -13 13 42 4: -60 -18 18 60 5: -80 -24 24 80 6:-106 -33 33 106 7:-183 -47 47 183每个子块分别有table0和table1也就是两半饼干可以“调味风格”不同。Step 5取出 16 个像素的索引2bit/像素这一块是 ETC1 最容易把人整懵的地方。它不是把 2bit/像素按顺序存而是用两个 16bit 位平面lsb16 个像素的低位msb16 个像素的高位像素 i 的 indexindex ((msbi)1)1 | ((lsbi)1)但“像素 i 对应哪个 (x,y)”还有一个固定映射顺序规范规定。大多数实现会写一个mapXYToBitIndex[4][4]表来取 bit。大白话它像把 16 个人的身份证后两位拆开记先记所有人的“十位”再记所有人的“个位”。你用的时候要再合回去。Step 6判断像素属于哪个子块flip 决定对于像素坐标 (x,y)flip0左右分块x2 → subblock0x2 → subblock1flip1上下分块y2 → subblock0y2 → subblock1Step 7合成最终颜色并 clamp对每个像素选子块 sbase baseColor[s]table tableCode[s]index 0…3modifier modifierTable[table][index]final clamp(base modifier, 0…255) 对 RGB 三个通道都加同一个 modifier最后得到 16 个 RGB 像素。到这一步你就解码出 ETC1 的结果了。4. ETC1 的画质特点为什么它看起来“像统一调亮调暗”你可能注意到了modifier 是加到 RGB 三通道同一个值。这意味着 ETC1 更擅长表达一块区域的“同色调 明暗变化”不太擅长表达同一块里有很强烈的色相变化比如红绿蓝交错所以 ETC1 对 UI 大色块/渐变/低频纹理很友好对高频彩色细节容易糊或脏。大白话ETC1 像给照片做“亮度微调”不是给每个像素独立调色。5. 讲讲 ETC2它跟 ETC1 是亲兄弟但更会玩ETC2 可以理解为ETC1 的升级版主要补了两件事更强的 RGB 编码模式能应付更复杂的颜色变化加上 Alpha 支持通过 EAC从此透明也能压缩5.1 ETC2 的 RGB 部分多了几种“模式”ETC2 RGB(A) 块对 RGB 的编码在某些模式下会从 ETC1 的 individual/diff 扩展为更多分支常见说法有ETC1-compatible类似 ETC1 的 basemodifierT 模式H 模式Planar 模式平面拟合它们本质上是在说“如果这块颜色变化复杂我不用老一套 basemodifier我换一套更能拟合的表达方式。”大白话ETC1 是“一个汤底两种调味”ETC2 说“不够我还可以烤、可以煎、可以蒸”。5.2 ETC2 的 AlphaEAC单独一套压法ETC2 的 RGBA 常见组合是RGBETC2 RGB8字节AEAC Alpha8字节合起来 16 字节/块4×4也就是 8bpp跟 BC3/DXT5 类似。Alpha 的 EAC 思路也很“块压缩”存一个 base alpha存一个 multiplier存一个 table index选一张偏移表每像素存 3bit 索引比 ETC1 的 2bit 更细然后alpha base modifierTable[table][index] * multiplier最后 clamp 到 0…255。大白话Alpha 不再是“随便凑合”而是单独做了一套更细腻的透明度调味方案。6. ETC2 解码怎么写一个“能跑”的块解码器思路分解6.1 解码 ETC2 RGB 块先判模式再走对应公式ETC2 RGB 块解码的第一步不是直接算 baseColor而是读控制位/字段判定当前块是哪种模式ETC1-like / T / H / Planar每种模式有各自的端点/插值/偏移规则仍然输出 4×4 个 RGB这里最关键的工程建议是不要靠猜直接按官方规范的模式判定条件写每个模式单独写函数别把逻辑搅成一锅粥先写 ETC1-like 模式跑通再加 T/H/Planar6.2 解码 ETC2 RGBARGB Alpha 分开解这是最清晰的结构DecodeRGB_ETC2(block.rgbPart) - 16 RGBDecodeAlpha_EAC(block.alphaPart) - 16 A合并成 RGBA如果你只是想“看懂流程”记住就行ETC2 的 Alpha 不是 ETC1 那种“没有”也不是“外面再贴一张灰度图”而是格式里自带一套 alpha block。7. 把 ETC1/ETC2 放到“常见块压缩家族”里对比一下帮助你举一反三格式块大小字节/块bppAlpha思路ETC14×484无两子块 modifierETC2 RGB4×484无多模式提升拟合ETC2 RGBA4×4168有RGB EAC AlphaBC1/DXT14×4841bit或无两端点插值BC3/DXT54×4168有BC1Alpha块ASTC可变可变可变有更通用、更复杂你会发现ETC2 RGBA 和 BC3 的结构非常像颜色一块 alpha 一块。这就是块压缩的“家族相似性”。8. 写解码器最容易翻车的坑ETC1/ETC2 通用8.1 位序/端序搞错直接花屏很多人把blockBytes[0]当低位、当高位反了结果整个图像像被猫抓过。建议明确你的 bit 编号体系MSB63还是LSB0写一个单元测试用已知块解码成已知像素8.2 像素索引映射错出现“棋盘错位”典型现象是颜色大体对但像素像交错换位。这通常是mapXYToBitIndex表错了。8.3 差分符号扩展错颜色离谱dR/dG/dB 是 3bit signed错一次就变“我比你多 7”这种不存在的差值。8.4 clamp 忘了溢出导致诡异颜色modifier 有可能把 base 推出 0…255必须 clamp。8.5 ETC2 模式判定写错某些块全错ETC2 的多模式是靠特定字段组合来判断的不按规范写很容易“把 Planar 当 ETC1-like”。9. 如果你要在 Unity/手游里用这些知识它到底有什么用你可能会说“我又不打算自己写 ETC 解码器Unity 都帮我做了我看这干嘛”用途其实挺实在你能解释清楚为什么 ETC1 没 alpha→ 进而理解 ETC1 Alpha Split、ETC2、ASTC 的选择你能理解压缩伪影从哪来→ UI 黑边、色带、脏块为什么出现你能做出正确的资源规范哪些贴图适合 ETC1哪些必须 ASTC/ETC2你能写离线工具链比如在导出时做预处理扩边、降噪、分通道大白话懂解码就像懂“菜怎么做的”。你不一定天天自己炒但你能一眼看出这菜为啥咸、为啥糊、该怎么改配方。10. 结尾把 ETC1/ETC2 解码记成一个“厨师口诀”ETC1 解码口诀4×4 一块先看 diff/flip/两张表读两份 base color独立或差分取 16 个 index两层 bitplane 合成按 flip 分子块按表取 modifierbase modifierclamp 输出 RGBETC2 解码口诀更高级RGB 部分先判模式ETC1-like/T/H/Planar再解Alpha 用 EACbase tableOffset*multiplier每像素 3bit index最后合 RGBA