Fish Thinking

Think of nothing things, think of wind.


GPU Compressed Textures


Preface

GPU 的压缩纹理格式是个庞杂而艰深的话题,而且鲜有人关心 —— 毕竟引擎(纹理压缩)和显卡硬件(纹理解压)已经把所有事情都做完了。从 API 的角度看来,不过是各式各样的 GL_XXVK_FORMAT_XX 枚举值。
但在诸如 Android 模拟器、Switch 模拟器之类的项目上,事情变得复杂了起来:由于历史、专利等等原因,移动端 GPU 和桌面级 GPU 往往采用不同的纹理压缩标准,而且互不兼容。例如在高通芯片上常用的 ASTC 纹理,在大多数桌面级 GPU 上并不支持。 幸好诸如 gfxstream、ANGLE 等社区开发者的聪明才智解决了这个问题 —— 利用 compute shader,可以实时地将移动端 GPU 纹理格式解压成 RGBA 格式供桌面级 GPU 识别;甚至还能进一步重新压缩成桌面级 GPU 可识别的压缩格式,从而进一步减轻显存占用和带宽压力。
我在相关的工作中,虽然基本只是照搬了下开源社区的方案,但本着知其然也得知其所以然的宗旨,也稍微熟悉了下 S3TC、ETC、BPTC、ASTC 等等压缩纹理格式,在这里简单做个记录。

Why compressed textures?

常见的 CPU 压缩格式图片往往包含着整个图片的所有信息。举例来说,对于一个 1024 * 1024 的 JPEG 图片,如果我们想要访问坐标 (10, 20) 的像素,我们必须把整个压缩图片解压,然后才能知道 (10, 20) 位置的像素值。
然而与 CPU 读取压缩格式图片不同,GPU 对纹理的访问频率高、随机性大,显然像这样每次都“解压整个图片再访问”的做法开销太大了 —— CPU 端的图片压缩方式在 GPU 的角度上并不适用。
所以为了适应 GPU 随机访问纹理的特点,GPU 压缩纹理都是局部可解压缩的。
具体而言,压缩纹理往往把整个图片分割成一个个大小固定的 block (例如 BC1 的 block 长宽为 4 * 4),而且每个 block 的信息用固定大小存储(例如 BC1 用 64 bits 表示一个 block)。这样,一个 1024 * 1024 的原始纹理,在经过 BC1 格式的压缩后,会被划分为 256 * 256 个每个长宽为 4 * 4 的 block,如果我们想访问 (10, 20) 坐标的数据,我们只需要找到该坐标所对应的 block,单独解压这个 block 即可。而且大小固定的 block 往往由 GPU 硬件处理解压,速度很快。

Then how?

那么纹理具体是怎么被压缩的呢?虽然各种纹理格式的具体压缩处理方式各不相同,但基本都来源于一个简单朴素的压缩原理,即所谓的“调色盘”压缩算法。
具体而言,就是由于 block 内颜色往往变化较小且线性变化,我们可以选取 block 内两个极值存储为 endpoint color,而 block 内其他的颜色则以这两个 endpoint color 配合权重(weight)通过差值计算得到。

S3TC

路漫漫兮,我们先从最简单的压缩纹理格式开始。
S3TC 由 S3 公司提出,即 S3 Texture Compression,是桌面级 GPU 上常见的纹理格式,也被称为 DXTC (DirectX Texture Compression)。在 Vulkan 下,也称为 BC1 ~ BC3 (Block Compression)。

BC1 – 4×4 – 64 bits

BC1 中一个 block 的存储布局如图所示。

BC1 格式布局

每 64 bits 压缩数据代表 4×4 的 texels。
有两个 R5G6B5 (16-bits) base color,即 color_0 和 color_1。
每个 texel 用 2 bits 代表,即 00、01、10、11 四种可能。

color_0 = 00
color_1 = 01
color_2 = 10
color_3 = 11

color_0 和 color_1 就是 base color 本身。而 color_2 和 color_3 由 base color 插值得到。

color_2 = 2/3*color_0 + 1/3*color_1
color_3 = 1/3*color_0 + 2/3*color_1

这样通过两个 16 bits 的 endpoint color 以及每个 texel 2 bits 的 weight,我们就可以从这 64 bits 的数据还原出原始的图像数据。

对称性

在定义完某个纹理压缩算法后,我们往往会发现一些冗余,亦或没有实际意义的数据格式。这些格式可以被加以开发利用,扩展出更多的用途。
而调色盘算法中,对称性就是最常见的一种冗余。BC1 格式中对其也有利用。
插值的对称性用代码来表示即:

mix(a, b, l) == mix(b, a, 1.0 - l)

或者如图所示:
对称性

可见,由于 endpoint color 的两个值可交换,我们可以用对称的两种方式存储同一个 block。
所以 color_0 和 color_1 哪一个大,是额外冗余的 1 bit 的信息 —— 故我们可以利用这个 1 bit,存储更多的信息,定义另一种模式。
当 color_0 > color_1 时,采用上述的计算方式,即

color_2 = 2/3*color_0 + 1/3*color_1
color_3 = 1/3*color_0 + 2/3*color_1

而当 color_0 < color_1 时,我们采用另一种差值方式,这往往称为 punch-through 模式:

color_2 = 1/2*color_0 + 1/2*color_1;
color_3 = 0;

color_3 的 0 代表透明,这样我们就可以有了 1 bit 的 alpha 通道。

BC2 – 4×4 – 128 bits

BC2 中一个 block 的存储布局如图所示。

BC2 中对 RGB 的处理和 BC1 完全一样。
只是额外用了 64 bits,给每个 texel 4 bits 直接存放 alpha 值的信息。
另外,BC2 以及接下来的 BC3 中的 RGB 不再有 BC1 中的 punch-through 模式。

BC3 – 4×4 – 128 bits

BC3 中一个 block 的存储布局如图所示。

BC3 和 BC2 的唯一差别在于对 alpha 通道的处理。
BC2 的 alpha 通道,是用 4 bits 直接存放 alpha 值;而 BC3 中的 alpha 通道采用了与 BC1 的 RGB 类似的插值方式存储,即采用了两个 8 bits alpha base,alpha_0 和 alpha_1,以及每个 texel 用 3 个 bit 表示 alpha 插值权重。
BC3 中也利用对称性定义了两种模式 —— alpha_0 > alpha_1 时,以 6 插值;alpha_0 < alpha_1,以 4 插值,并添加全透明(0)和不透明(255):

if( alpha_0 > alpha_1 )
{
  // 6 interpolated alpha values.
  alpha_2 = 6/7*alpha_0 + 1/7*alpha_1; // bit code 010
  alpha_3 = 5/7*alpha_0 + 2/7*alpha_1; // bit code 011
  alpha_4 = 4/7*alpha_0 + 3/7*alpha_1; // bit code 100
  alpha_5 = 3/7*alpha_0 + 4/7*alpha_1; // bit code 101
  alpha_6 = 2/7*alpha_0 + 5/7*alpha_1; // bit code 110
  alpha_7 = 1/7*alpha_0 + 6/7*alpha_1; // bit code 111
}
else
{
  // 4 interpolated alpha values.
  alpha_2 = 4/5*alpha_0 + 1/5*alpha_1; // bit code 010
  alpha_3 = 3/5*alpha_0 + 2/5*alpha_1; // bit code 011
  alpha_4 = 2/5*alpha_0 + 3/5*alpha_1; // bit code 100
  alpha_5 = 1/5*alpha_0 + 4/5*alpha_1; // bit code 101
  alpha_6 = 0;                         // bit code 110
  alpha_7 = 255;                       // bit code 111
}

从 alpha 的处理上可以看出 BC2 和 BC3 的差别:alpha 通道与 RGB 相关度低时,BC2 更合适;alpha 通道与 RGB 相关度高时,BC3 更合适。

RGTC

RGTC 即 Red Green Texture Compression,顾名思义,常常用在单通道和双通道的数据上。在 Vulkan 下,也称为 BC4 ~ BC5。
RGTC 的压缩算法与 S3TC 一致,只在使用场景上有所区别。

BC4 – 4×4 – 64 bits

BC4 中一个 block 的存储布局如图所示。

BC4 就是把 BC3 中的 alpha 通道单拎出来,用来处理单一通道的压缩。
有两个 (8-bits) base color,即 red_0 和 red_1。
每个 texel 用 3 bits 代表,进行插值计算。
BC4 适合存储浮点数据,并且有 BC4_UNORM 和 BC4_SNORM 两个变种,分别存储 [0.0, 1.0] 和 [-1.0, 1.0] 的数据。
red_0 > red_1 时,以 6 插值;red_0 < red_1,以 4 插值,并添加 0.0f(UNORM)/ -1.0f(SNORM) 和 1.0f。
BC4_UNORM:

if( red_0 > red_1 )
{
  // 6 interpolated color values
  red_2 = (6*red_0 + 1*red_1)/7.0f; // bit code 010
  red_3 = (5*red_0 + 2*red_1)/7.0f; // bit code 011
  red_4 = (4*red_0 + 3*red_1)/7.0f; // bit code 100
  red_5 = (3*red_0 + 4*red_1)/7.0f; // bit code 101
  red_6 = (2*red_0 + 5*red_1)/7.0f; // bit code 110
  red_7 = (1*red_0 + 6*red_1)/7.0f; // bit code 111
}
else
{
  // 4 interpolated color values
  red_2 = (4*red_0 + 1*red_1)/5.0f; // bit code 010
  red_3 = (3*red_0 + 2*red_1)/5.0f; // bit code 011
  red_4 = (2*red_0 + 3*red_1)/5.0f; // bit code 100
  red_5 = (1*red_0 + 4*red_1)/5.0f; // bit code 101
  red_6 = 0.0f;                     // bit code 110
  red_7 = 1.0f;                     // bit code 111
}

BC4_SNORM:

if( red_0 > red_1 )
{
  // 6 interpolated color values
  red_2 = (6*red_0 + 1*red_1)/7.0f; // bit code 010
  red_3 = (5*red_0 + 2*red_1)/7.0f; // bit code 011
  red_4 = (4*red_0 + 3*red_1)/7.0f; // bit code 100
  red_5 = (3*red_0 + 4*red_1)/7.0f; // bit code 101
  red_6 = (2*red_0 + 5*red_1)/7.0f; // bit code 110
  red_7 = (1*red_0 + 6*red_1)/7.0f; // bit code 111
}
else
{
  // 4 interpolated color values
  red_2 = (4*red_0 + 1*red_1)/5.0f; // bit code 010
  red_3 = (3*red_0 + 2*red_1)/5.0f; // bit code 011
  red_4 = (2*red_0 + 3*red_1)/5.0f; // bit code 100
  red_5 = (1*red_0 + 4*red_1)/5.0f; // bit code 101
  red_6 = -1.0f;                     // bit code 110
  red_7 =  1.0f;                     // bit code 111
}

BC5 – 4×4 – 64 bits

BC5 中一个 block 的存储布局如图所示。

BC5 就是把两个 BC4 合并在一起,分别压缩两个通道。BC5 也有 BC5_UNORM 和 BC5_SNORM 两个变种。

ETC

ETC 由 Ericsson 公司提出,即 Ericsson Texture Compression,是 OpenGL ES 的标准纹理格式,在安卓设备上被广泛支持。
ETC1 最早提出时称为 PACMAN,只支持 2×4 或 4×2 的纹理。后对其进行改进,支持了 4×4 纹理,称为 iPACMAN。
而 ETC2 针对 ETC1 未被定义的数据做了进一步的拓展,在原本的基础上新增了更多的模式。ETC2 向前兼容 ETC1。

ETC1

ETC 格式的思路与 S3TC 类似,也有 base color 的概念,但在处理上有很大区别。
ETC 压缩算法的核心是,发现与色彩信息相比,人眼对明度的信息更敏感,所以可以使用单一的 base color,然后使用明度的差值(modifier)进行调制。

PACMAN – 2×4/4×2 – 32 bits

先来看一下最早提出的 PACMAN 格式。

  • 12 bits – 存储 RGB444 base color
  • 4 bits – 存储 codeword table 索引,table 中共有 16 个 index
  • 16 bits – 分配给 8 个 texel,每个 texel 有 2 bits,指代特定 table index 中,4 个 entry 中的某一个,通过将 base color 与表格中 entry 相加,我们就得到了对应 texel 的值

PACMAN 的 codeword table 是预设的,所以这个表格并不占用存储。

在接下来的其他格式中,我们也会看到这种“预设表格来减少空间占用”的思路。

iPACMAN – 4×4 – 64 bits

iPACMAN 对 PACMAN 进行了扩展,。

  • 24 bits – 存储 base color,可以是 2 个 RGB444,也可以是 1 个 RGB555,剩余的作为 dRGB333 diff 用来计算第二个 base color
  • 1 bit – 用来区分是上述的用两个独立 RGB444 (individual mode),还用 RGB555 + dRGB333 (differential mode)
  • 1 bit – 用来区分两个 sub-block 是横向排列还是纵向排列
  • 6 bits – 存储两个 sub-block 的 codeword table 索引,即 2 个 3 bits (table 大小由 16 缩减为 8 个)
  • 32 bits – 分配给 16 个 texel,每个 texel 有 2 bits,指代特定 table index 中,4 个 entry 中的某一个

iPACMAN 的 codeword table:

ETC2 – 4×4 – 64 bits

ETC2 兼容 ETC1,在原本的基础上新增了更多的模式。
在 ETC1 中,有一些数据格式是未定义的,没有被 encoder 采用(即 ETC1 decoder 也不会遇到这种数据)。而 ETC2 正是挖掘了这些没有被利用的数据格式,创建了更多的压缩模式。
ETC2 一共有这几种模式:

  • individual mode
  • differential mode
  • T mode
  • H mode
  • Planar mode

more bits!

ETC2 是在 ETC1 的 diff 模式下,color base 的计算中,发掘了潜在可用的 bit 位。
ETC1 diff 模式下,color base 是以 RGB555 + dRGB333 的方式计算的。 RGB555 是无符号的 5×3 bits,dRGB333 是有符号的 3×3 bits,且 RGB 三通道分别计算。

以下以 Red 通道举例。
R5 的范围是 [0, 31],dR3 的范围是 [-4, 3]。
那么一共有多少种数据格式溢出(即无效)的呢?
一共有 16 种:

  • R5 为 0,dR3 在 [-4, -1] —— 4 种溢出的可能
  • R5 为 1,dR3 在 [-4, -2] —— 3 种溢出的可能
  • R5 为 2,dR3 在 [-4, -3] —— 2 种溢出的可能
  • R5 为 3,dR3 为 -4 —— 1 种溢出的可能
  • R5 为 29,dR3 为 3 —— 1 种溢出的可能
  • R5 为 30,dR3 在 [2, 3] —— 2 种溢出的可能
  • R5 为 31,dR3 在 [1, 3] —— 3 种溢出的可能

那这 16 种情况,实际就可以多出 4 bits 可用。

然后进行下分类讨论。

Red 通道的溢出情况:

  • 1 bit – diff mode
  • 8 bits – R5, dR3
    64 – 1 – 8 = 55 bits,但如上所述,Red 通道的溢出情况有 16 种,多出了 4 bits 可用,所以一共有 55 + 4 = 59 bits 。

Red 通道溢出时,有 59 bits 可用。

Green 通道的溢出情况:

  • 1 bit – diff mode
  • 8 bits – R5, dR3
  • 8 bits – G5, dG3
    64 – 1 – 8 – 8 = 47 bits 。但注意,这时 R 通道没有溢出,未溢出的情况有 2^8 – 16 = 240 种,可以多出 7 bits 可用。而 Green 通道溢出的情况也是 16 种,即可以多出 4 bits。所以47 + 7 + 4 = 58 bits。

Green 通道溢出时,有 58 bits 可用。

Blue 通道的溢出情况:

  • 1 bit – diff mode
  • 8 bits – R5, dR3
  • 8 bits – G5, dG3
  • 8 bits – B5, dB3

同理,不难算出,64 – 1 – 8 – 8 – 8 + 7 + 7 + 4 = 57 bits。
Blue 通道溢出时,有 57 bits 可用。

综上,对于 R、G、B 三个通道的溢出情况,我们分别有了额外的 59、58、57 bits,可以定义三种新的模式。

T-Mode – 59 bits

59 bits 的 T-Mode 适用于以下的色彩分布:

如图所示,如果纹理一部分数据的颜色基本一样(左上角黑色),另一部份颜色有变化,但变化是线性的。
显然单独的一条直线没有办法很好地拟合这种情况,这时我们就可以把这两类数据分开处理。
颜色相近的(左上角黑色),用一个 base color 表示(左上角蓝色)。
颜色线性变化的,定义另一个 base color(右下角中间蓝色),然后选取一个 diff 值(modifier),来加减中间的 base color,得到临近的另外两个 base color。这个 diff 怎么拿到呢?
我们定义一个有 8 个元素的 distance table,然后用 3 bits 来选取。

比如 3 bits 的值是 5,那 diff 就是 32,另外两个颜色就是 base_color + 32 和 base_color – 32。
这样我们就有了 4 个 base color。

然后刚好剩下 32 bits,每个 texel 用 2 bits,代表选择 4 个 color 中的某一个。
即:

  • 12 bits – RGB444 base_color_0
  • 12 bits – RGB444 base_color_1
  • 3 bits – modifier index
  • 32 bits – 2 bits * 16

这 4 个 base color 形成 T 字的形状,故而得名为 T-Mode。

H-Mode – 58 bits

58 bits 的 H-Mode 适用于有两组线型变化的色彩分布。

和上述的 T-Mode 的类似,如果我们有两组线型变化的颜色,我们可以定义 4 个 color(只加减 diff,不保留 base color)。
即:

  • 12 bits – RGB444 base_color_0
  • 12 bits – RGB444 base_color_1
  • 3 bits – modifier index
  • 32 bits – 2 bits * 16

但仔细一看,这个不是和 T-Mode 一样,用了 59 个 bits 了?
别忘了对称性 —— 两个 base color,互相交换也不会影响解压后的效果,那么这个顺序刚好能为我们节省出这 1 个 bit,即:较黑的颜色在前,表示 0;较亮的颜色在前,表示 1。

Planar-Mode – 57 bits

这个模式最简单,但很有意思:直接给三个 RGB676 的值,放在 4×4 矩形的顶点,然后其他的点都插值算出。
即:

  • 57 bits – RGB676 * 3

这个模式在变化均匀缓慢的纹理中,效果特别好。

punch-through alpha – 4×4 – 64 bits

上述的 64 bits ETC2 有一个缺陷是,不支持 alpha 通道。
所以新引入了一个变种格式,gl 下叫做 GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2,vulkan 下叫做 VK_FORMAT_ETC2_R8G8B8A1_UNORM_BLOCKVK_FORMAT_ETC2_R8G8B8A1_SRGB_BLOCK
这个模式下舍弃了 individual mode,默认只有 differential mode,并用多出来的 1 位 bit 存储 alpha 信息。
即把原先的 ETC2 定义:

1 bit – 用来区分是上述的用两个独立 RGB444 (individual mode),还用 RGB555 + dRGB333 (differential mode)

修改为

1 bit – opaque bit,置为 1 时,texel 2 bits 指向的 4 个 entry 中,有两个是 0,即透明

至此我们介绍完了 ETC2 格式的所有 mode。这张对比图直观地反映了各个 mode 的适用场景,以及效果:

可以看出,ETC1 适合颜色一致,只有明暗变化的场景;ETC2 的 T-Mode / H-mode 适合两种颜色交界的场景,ETC2 的 Planar mode 适合渐变的场景。

EAC – 4×4 – 64 bits

EAC (Explicit Alpha Compression) 之于 ETC2,等同于 RGTC 之于 S3T,来存储单通道或双通道的纹理。

EAC_R11 – 4×4 – 64 bits

EAC_R11 中一个 block 的存储布局如图所示:

  • 8 bits – base_codeword
  • 48 bits – 分配给 16 个 texel,每个 texel 有 3 bits,指代特定 table index 中,8 个 entry 中的某一个
  • 4 bits – table index,存储 codeword table 索引,table 中共有 16 个 index
  • 4 bits – multiplier,存储范围为 [0, 15] 的倍率,这个倍率与查表后得到的值相乘

最后再做一个 clamp,限定在 [0, 2047],即 clamp(base_codeword + multiplier * table_entry)。
可见最终结果最大值为 2047,即 11 位,所以被称为 EAC_R11。
EAC modifier table:

EAC_RG11 – 4×4 – 128 bits

和 EAC_R11 压缩原理一致,只是多了另一个 Green 通道。不再赘述。

ETC2_EAC – 4×4 – 128 bits

顾名思义,就是 ETC2 拼接 EAC_R11,64 bits 表示 RGB,另外 64 bits 表示 alpha。不再赘述。

BPTC

BPTC (Block Partitioned Texture Compression) 在 Vulkan 中被称为 BC6H 和 BC7。在压缩原理上,BPTC 与 S3TC 一致,但相比而言复杂得多,是在桌面级 GPU 上更先进的纹理格式,支持了 HDR,以及 RGB 或 RGBA 等。BPTC 通过定义众多的模式并查表的方式,节省了大量的存储空间。

BC6H – 4×4 – 128 bits

BC6(或者叫 BC6H)是 HDR 纹理 (FP16) 的专用格式。只支持 RGB,没有 alpha 通道。
压缩的原理上,BC6H 和其他 BC 格式类似,也是用两个 color endpoints,通过权重插值计算对应的 texel。

14 modes

BC6 的一个显著特点是,开始显式地定义众多 mode。
与 ETC2 通过溢出数据定义 T-Mode / H-Mode 不同的是,BC6 在定义上就系统地考虑了颜色分区(partition)。
BC6H 一共有 14 种模式。mode 1 ~ mode 10 采用 2 个 partition;mode 11 ~ mode 14 只有 1 个 partition。
显然,2 个 partition 需要 4 个 color endpoints;1 个 partition 需要 2 个 color endpoints。

mode bits 用于确定模式,有的 bits 目前没有定义,例如 10011, 10111, 11011, 11111。
color endpoints 有两种存储方式:可以以高精度的 base + 低精度的 diff 计算,例如 mode 1;也可以直接存储,例如 mode 10,这时 base 和 diff 进度一致了,所以直接存储 4 个 RGB666 数值。
BC6 不光显式定义了模式,5 bits partition 也显示定义了 partition 的 32 种分布。

又见对称性

介绍完上述的两个表格时,可能会有两个疑问:

  1. 有 2 个 partition 时,每个 texel 用 3 bits 表示;只有 1 个 partition 时,每个 texel 用 4 bits 表示。所以 partition indices 的占用,应该是 48 bits (16*3) 和 64 bits (16*3)。那为什么它实际上是 46 bits 和 63 bits ?
  2. partiton layout 中为什么有的 1 是下划加粗的?

原因在于这里用到了 endpoint 的对称性,在每个 partition 中可以节省 1 个 bit。
上述的表格中,下划加粗的 1 ,是被区别对待的 —— 仅用 2 bits,而不是 3 bits 表示。
那用 0 标注的的 partition,为什么没有下划加粗呢?—— 因为我们约定以 0 表示的 partition 中,第 1 个 texel 就是特殊处理的,由于位置相同(左上角始终为 0),就不需要额外标注了。
在 BC6H 中,我们约定在 partition 中,有一个 texel 的 indice 的最高位(MSB)默认是 0 —— 因为如果本来不是 0,是 1 的话,我们可以通过上述的反转,让 MSB 变成 0。所以对这个 texel,我们可以省略这个 0,节省 1 bit。如果是有两个 partition,那就可以节省 2 bits。
所以,2 个 partition 时共节省了 2 个 bits,1 个 partition 时共节省了 1 个 bit。所以 48 bits 和 64 bits 变成了 46 bits 和 63 bits。
这和 BC1 中的 punch-through mode、ETC2 的 H-Mode 一样,都利用到了 endpoints 的对称性来节省数据占用。

BC7 – 4×4 – 128 bits

BC7 在原理上与 BC6 类似,适用于存储非 HDR 的纹理。
BC7 各个模式中对 alpha 通道的处理有 3 类:

  • 忽略 alpha,仅保存 RGB 数据
  • alpha 与 RGB 相关,RGBA 中的 A 与 RGB 按相同的方式处理
  • alpha 与 RGB 不相关,A 通道单独处理

BC7 中还出现了 3 个 partition (subset) 的模式。所以 BC7 一共支持 1 个、2 个或 3 个 partition。
BC7 中还利用了 shared least significant bit 的方式来表示 endpoint color,用多通道的最低位共用的方式来节省位数,以 RGBP 的方式来表示。比如 RGB 5.5.5.1 中,最后一位 P 为 1,这个 16 bits 的数据实际表示的是 18 bits 的 RGB666,RGB 三个通道的 LSB 相同。再比如 RGBAP 5.5.5.5.1,实际表示的是 RGBA6666。这个 P-bit 的共用方式也有两种 —— P-bit 只在 1 个 endpoint 中公用,还是在 partition 中多个 endpoint 公用 —— 即 unique P-bit per endpoint 还是 shared P-bit per subset。
BC7 的 index bits 也和 BC6 中一样,由于对称性,每个 partition 可以节约 1 位。
另外,在 BC7 mode 4 中,由于 index 有 31 bits 和 47 bits 两部份,RGB 和 alpha 信息哪个用哪一个,以 1-bit index selector 来区分。
BC7 中一共定义了 8 中 mode,微软的这份文档中,对每一种 mode 的布局都有简洁直观的介绍,故不再赘述。

ASTC

终于来到了最终章 —— ASTC,这也是迄今为止介绍过的最复杂的纹理格式,没有之一。OpenGL ES 的 spec 中甚至用了几十页介绍它,而且个人读完之后感觉还是没领会到所有细节。所以限于本文篇幅,我们也不会涵盖所有枯燥的 spec 细节。但所幸 ASTC 中各个特性相互正交,我们至少可以粗略地学习一下一些特性的原理,一窥其精髓。

ASTC (Adaptive Scalable Texture Compression) 在 2012 年由 AMD 和 ARM 提出。
与其他 BC 系列、ETC 系列的为特定场景提出特定压缩格式的做法相比,ASTC 一开始就制定了全面的功能,而且在比特率的选择上给了用户很高的自由度。但代价是,astc 的 encode 和 decode 非常复杂。
ASTC 有如下特点:

  • 一个 block 始终占用 128 bits
  • 支持 2D 和 3D 纹理
  • 支持 HDR / LDR
  • 支持 1、2、3、4 通道压缩(但都会解压成 RGBA)
  • block 长宽可选
  • single / dual plane 可选

How adaptive and scalable?

顾名思义,ASTC 在比特率上有众多选择,而且观察下表的 increment 栏可见,每个格式的比特率与上一级相比,增长率非常稳定——换而言之,各种格式的比特率分布均匀,用户可以自行在图形质量和大小占用中取舍,选择合适的压缩率。

当 block size 特别大(例如 12×12)时,128 bits 的大小没有办法给每一个 texel 存储 weight,所以这时会采用更小的 weight 网格,然后通过差值的方式还原到 block 大小。

一个 ASTC block 的布局如图所示:

  • Block mode 表示 Texel weight data 是如何被 encode 的
  • Part (Partition) 表示 partition 的数量 -1;如果采用 dual plane mode,partition 的数量必须小于等于 3
  • Extra configuration data 会根据 partition 数量、plane 数量而不同

BISE

ASTC 的一个核心点是使用 BISE(Bounded Integer Sequence Encoding) 方式对 texel weight 和 color endpoints 进行编解码,以节省存储空间。
BISE 编码的原理如下:
例如,我们想存储 5 个 3 进制(0、1、2)的数字,如果每个数字用 2 bits 来存储的话,一共需要 5 * 2 = 10 bits。但其实,组合一共是 3^5 = 243 种。这个 243 种可能其实可以用 8 bits (2^8 = 256 bits) 来表示。这样就可以节省 2 个 bits。
再例如,我们想存储 3 个 5 进制(0、1、2、3、4)的数字,如果每个数字用 3 bits 来存储的话,一共需要 3 * 3 = 9 bits。但其实组合一共是 5^3 = 125 种。这个 125 种可能其实可以用 7 bits (2^7 = 128 bits) 来表示。这样也可以节省 2 个 bits。
如果一个数的范围在 [0, 3 * 2^n -1],那么它就可以用一个高位的 trit (3 进制数) 加上 n bits 来表示。
如果一个数的范围在 [0, 5 * 2^n -1],那么它就可以用一个高位的 quint (5 进制数) 加上 n bits 来表示。
由上述的例子可以看出,用 trit 时 5 个数字一起压缩效率更高,而用 quint 时 3 个数字一起压缩效率更高。
所以在大小范围相同的情况下,我们可以用多个(5 个或 3 个)数字并为一组,并采用 trit / quint 的方式进行编码。

例如在 n 为 4 时,trit-based 的压缩如图所示:

8 bits 的 trits 与 5 个 4 位的 LSB bits 交替排列。

single / dual plane

ASTC 中有 single plane 和 dual plane 可选。
当纹理中某些通道没有相关性时(例如 alpha 通道用作透明遮罩时),我们就可以为每个 texel 指定两个 weight —— 即 dual plane。
在 dual plane 模式开启时,原先 single plane mode 中一个 weight 被拆成 2 个,并且 CSS (Color Component Selector) 位根据下表决定哪一个 channel 用 weight 1,其余的用 weight 0。
CSS 位的定义如下:

void extent block

如果一个 block 是单色的,在 ASTC 中被称为 void extent block,有特殊编码格式,来简化解压时间,并减少带宽。

Reference


发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注