在大多数情况下,纹理是用二维图像表示的。然而,标准的压缩算法(RLE, LZW, Deflate)和流行的压缩图像格式(JPEG, PNG, TIFF)并不适合纹理。传统压缩算法的主要问题是无法实现对内存中整个纹理的其中一块Texel随机访问——不解压整个纹理,就不可能解压一个特定的texel。纹理访问模式是高度随机的:渲染时只使用需要的部分纹理,并且访问的顺序无法提前预知。因此,图形子系统的整体性能在很大程度上取决于纹理访问的效率。“随机存取”的需求决定了各种纹理压缩格式的主要特征。如果没有各种类型的纹理,3D计算机图形的世界是不可想象的。这些各种各样的纹理元素极大地提高了3D对象的视觉质量和细节水平,而不会增加几何复杂性。简单纹理是把二维图像直接映射到三维表面。图像的单个像素称为Texels(纹理元素)。纹理不仅可以存储颜色,还可以存储高度,法线方向,反射因子和其他每个Texel信息。现代游戏和3D应用消耗大量内存,其中超过一半的内存被纹理占用。这导致了对内存大小和带宽的严苛要求。各种纹理压缩技术被广泛采用,以减少对内存和带宽的压力。在这种情况下,纹理存储在内存中并以压缩形式传送到GPU。解压缩只发生在GPU内部,通常在L1和L2缓存之间。这种方法减少了纹理内存占用。最重要的是,这种压缩还可以节省带宽,这是一种非常宝贵的资源。纹理压缩还可以降低功耗,因为GPU到VRAM的数据传输就意味着为功耗。这对于笔记本电脑、平板电脑和智能手机等移动设备尤为重要。

基于压缩算法的特征,提供了现代PC、平板电脑和智能手机中使用的纹理压缩技术及其硬件实现的详细分析和比较,例如压缩率和图像质量。首先最早被大量使用的压缩方案系列,S3TC(BC1-BC3)。然后转到了BC4,BC5,BC6H和BC7格式,它们通过提供更大的灵活性以及引入块分区改善了图像质量。然后由爱立信针对的移动端设备开发的ETC系列算法(Packman,ETC1(iPackman)和ETC2 / EAC),以及由Imagination Technologies公司发明的PVRTC系列格式。最后由AMD和ARM的协作产生的最新的纹理压缩技术ASTC。

ASTC

Adaptive Scalable Texture Compression (ASTC)是Arm和AMD共同研发的一种纹理压缩格式,不同于ETC和ETC2的固定块尺寸(4x4),ASTC支持可变块大小的压缩,从而获得灵活的更大压缩率的纹理数据,降低GPU的带宽和能耗。 ASTC虽然尚未成为OpenGL的标准格式,只是以扩展的形式存在,但目前已经广泛地被主流GPU支持,可谓不是标准的的标准扩展。但在Vulkan中,ASTC已经是标准的特性了。

特性

  • 格式灵活。ASTC可以压缩1到4个通道之间的数据,包括一个非相关通道,如RGB+A(相关RGB,非相关alpha)。并且块大小可变,如4x4、5x4、6x5、10X5等。
Adreno A5X及以上的GPU芯片支持ASTC以下不同块大小的格式(包含二维和三维):
ASTC_4X4
ASTC_5X4
ASTC_5X5
ASTC_6X5
ASTC_6X6
ASTC_8X5
ASTC_8X6
ASTC_8X8
ASTC_10X5
ASTC_10X6
ASTC_10X8
ASTC_10X10
ASTC_12X10
ASTC_12X12
ASTC_3X3X3
ASTC_4X3X3
ASTC_4X4X3
ASTC_4X4X4
ASTC_5X4X4
ASTC_5X5X4
ASTC_5X5X5
ASTC_6X5X5
ASTC_6X6X5
ASTC_6X6X6
  • 灵活的比特率。ASTC在压缩图像时提供了广泛的比特率选择,在0.89位和8位每texel (bpt)之间。比特率的选择与颜色格式的选择无关。而传统的ETC等格式只能是整数的比特率。
  • 高级格式支持。ASTC可以压缩图像在低动态范围(LDR)、LDR sRGB、高动态范围(HDR)颜色空间,还可以压缩3D体积纹理。
  • 改善图像质量。尽管具有高度的格式灵活性,但在同等比特率下,ASTC在图像质量上的表现优于几乎所有传统的纹理压缩格式(ETC2、PVRCT和BC等)。
  • 格式矩阵全覆盖。在ASTC尚未出现之前,传统的纹理压缩格式支持的颜色格式和比特率的组合相对较少,如下图所示:

以上格式还受图形API或操作系统限制,因此任何单一平台的压缩选择都非常有限。ASTC的出现解决了上述问题,几乎实现了所需格式矩阵的完整覆盖,为内容创建者提供了广泛的比特率选择。下图显示了可用的格式和比特率:

ASCT是如何达成上述目标的呢?答案就在于ASTC用了一种特殊的压缩算法和数据结构。ASTC的算法技术要点和阐述如下:

  • 块压缩

    实时图形的压缩格式需要能够快速有效地将随机样本转换为纹理,因此对压缩技术必须做到以下几点:

    • 仅给定一个采样坐标,计算内存中数据的地址。
    • 能够在不解压太多周围数据的前提下解压随机采样。

    所有当代实时压缩格式(包括ASTC)使用的标准解决方案,是将图像分割成固定大小的像素块,然后每个块被压缩成固定数量的输出位。这保证Shader以任意顺序快速访问texels,并具有良好的解压成本。

    ASTC中的2D Block footprints范围从4x4 texels到12x12 texels,它们都被压缩成128位输出块。通过将128位除以占用空间中的像素数,便能得到格式比特率,这些比特率范围从8 bpt $ (128 / (4\cdot4)) $ 到0.89 bpt ($128 / (12\cdot12))$ 。下面是不同比特率的画质对比图:

  • 颜色端点(Color endpoint)

    块的颜色数据被编码为两个颜色端点之间的梯度。每个texel沿着梯度选择一个位置,然后在解压期间插值。ASTC支持16色端点编码方案,称为端点模式( endpoint mode)。端点模式的选项允许改变以下内容:

    • 颜色通道的数量。 例如:亮度、亮度+alpha、rgb或rgba。
    • 编码方法。 例如:直接、基数+偏移、基数+比例或量化级别。
    • 数据范围。 例如:低动态范围或高动态范围。

    允许逐块选择不同的端点模式和端点颜色BISE量化级别。

  • 颜色分区(Color partition)

    块内的颜色通常是复杂的,单色渐变通常不能准确地捕捉块内的所有颜色。例如,躺在绿色草地上的红球,需要进行两种颜色的划分,如下图所示:

    ASTC允许单个块最多引用四个颜色梯度,称为分区。为了解压,每个texel被分配到一个单独的分区。直接存储每个texel的分区分配将需要大量的解压缩硬件来存储所有块大小。 相反,ASTC使用分区索引作为seed值,以算法生成一系列模式。压缩过程为每个块选择最佳匹配的模式,然后块只需要存储最佳匹配模式的索引。下图显示了8 × 8块大小的2个(图像顶部)、3个(图像中间)和4个(图像底部)分区生成的模式:

    可以在每个块的基础上选择分区的数量和分区索引,并且可以在每个分区上选择不同的颜色端点模式。

  • 颜色编码

    ASTC使用渐变来指定每个texel的颜色值。每个压缩块存储渐变的端点颜色,以及每个像素的插值权重。在解压过程中,每个像素的颜色值是根据每个像素的权重在两个端点颜色之间插值生成的。下图显示了各种texel权重的插值:

    方块通常包含复杂的颜色分布,例如一个红色的球放在绿色的草地上。在这些情况下,单一的颜色梯度不能准确地代表所有不同的texel颜色值。 ASTC允许一个块定义多达四个不同的颜色梯度,称为分区(partition),并可以将每个texel分配到一个单独的分区。下图显示了分区索引是如何为每个texel指定颜色渐变的(两个分区,一个用于红球像素,一个用于绿草像素):

  • 存储字符表(Storing alphabet)

    尽管每个像素的颜色和权重值理论上是浮点值,但可以直接存储实际值的位太少了。为了减小存储大小,必须在压缩期间对这些值进行量化。例如,如果对0.0到1.0范围内的每个texel有一个浮点权重,可以选择量化到5个值:0.0、0.25、0.5、0.75和1.0,再使用整数0-4来表示存储中的这五个量化值。一般情况下,如果选择量化N层,需要能够有效地存储包含N个符号的字符表中的字符。一个N个符号表包含每个字符的log2(N)位信息。如果有一个由5个可能的符号组成的字符表,那么每个字符包含大约2.32位的信息,但是简单的二进制存储需要四舍五入到3位,这浪费了22.3%的存储容量。下图表显示了使用简单的二进制编码存储任意N个符号字符表所浪费的位空间百分比:

    上述图表显示,对于大多数字符大小,使用整数位每个字符浪费大量的存储容量。对于压缩格式来说,效率是至关重要的,因此这是ASTC需要解决的问题。一种解决方案是将量化级别四舍五入到2的下一次方,这样就不用浪费额外的比特了。然而,这种解决方案迫使编码器消耗了本可以在其它地方使用获得更大收益的比特位,因此此方案降低了图像质量,并非最优解决方案。

  • 五元和三元数(Quint and trit)

    一个更有效的解决方案是将三个五元字符组合在一起,而不是将一个五元字符组合成三个位。五个字母中的三个字符有 $5^3=125$个组合,包含6.97位信息。我们可以以7位的形式存储这三个quint字符,而存储浪费仅为0.5%。

    我们也可以用类似的方法构造一个三符号的字母表,称为三个一组,并将五个一组的三个一组字符组合起来。每个字符组有 $ 3^5=243$ 个组合,包含7.92位信息。我们可以以8位的形式存储这5个trit字符,而存储浪费仅为1%。

  • 有界整数序列编码(Bounded Integer Sequence Encoding)

    ASTC使用的有界整数序列编码(Bounded Integer Sequence Encoding,BISE)允许使用最多256个符号的任意字符存储字符序列。每一个字符大小都是用最节省空间的位、元和五元进行编码的。

    • 包含最多 $2^n-1$ 个符号的字母表可以使用每个字符n位进行编码。
    • 包含最多 $3\cdot(2^n - 1)$ 个符号的字母表可以使用每个字符用n位(m)和一个trit (t)进行编码,并使用方程 $(t \cdot 2^n) + m$ 重建。
    • 包含最多 $5\cdot(2^n - 1)$ 个符号的字母表可以使用每个字符用n位(m)和一个quint (q)进行编码,并使用方程 $(q \cdot 2^n) + m $重建。

    当序列中的字符数不是3或5的倍数时,必须避免在序列末尾浪费存储空间,因此在编码上添加了另一个约束。如果序列中要编码的最后几个值为零,则已编码位串的最后几个位也必须为零。理想情况下,非零位的数目很容易计算,并且不依赖于先前编码值的大小。这在压缩期间很难妥当处理,但也是可能解决的。意味着不需要在位序列结束后存储任何填充,因为我们可以安全地假设它们是零位。

    有了这个约束,通过对bit、trit和quint的智能打包,BISE使用固定位数对N个符号字母表中的S个字符串进行编码:

    • S最大值为 $ 2^N - 1 $ ,使用 $N \cdot S $ 位。
    • S最大值为 $ 3\cdot2^N - 1$ ,使用 $ N\cdot S + \text{ceil}(8S / 5)$ 位。
    • S最大值为 $ 5\cdot2^N - 1$ ,使用 $ N\cdot S + \text{ceil}(7S / 3) $ 位。

    压缩器选择为所存储的字母大小产生最小存储空间的选项。一些使用二进制,一些使用bit和trit,还有一些使用bit和quint。下图显示了BISE存储相对于二进制存储的效率增益:

    此外,在压缩过程中,会为每个块选择最佳编码,在计算texel权重值时,除了上述的BISE,还有双平面权重(Dual-plane weights)算法。

    ASTC免费自由使用,容易集成,被众多主流系统和硬件支持。支持ASTC需要以下OpenGL扩展:

GL_AMD_compressed_ATC_texture
GL_ANDROID_extension_pack_es31a

相比传统的纹理压缩格式(ETC、BC、PVRTC等),使用ASTC的压缩效果非常明显,画质更贴近原图,压缩率更高


左:原始法线贴图;中:压缩成ETC的效果;右:压缩成ASTC的效果。

由此带来的直观收益就是占用更少的内存、带宽,每帧大约能减少24.4%的带宽:

astc文件头

astc 图片可以直接在mac finder里预览, 尽管 .astc 只能在 arm处理器进行运行时解压, mac finder 里应该是在 cpu 上解压成 rgba 格式图片

struct astc_header
{
    uint8_t magic[4];
    uint8_t block_x;
    uint8_t block_y;
    uint8_t block_z;
    uint8_t dim_x[3];
    uint8_t dim_y[3];
    uint8_t dim_z[3];
};

其中前四个字节是 ASTC 图片的格式标志。

magic[0] = 0x13;
magic[1] = 0xAB;
magic[2] = 0xA1;
magic[3] = 0x5C;

block_* 是压缩快的大小, 对于一个2D图片来说, block_z 这个值始终是1。

图片分辨率存储在dim_*字段, 对于 2D图片来说, z方向肯定是1。 astc采用24位无符号数值来储存。它的计算公式如下:

decoded_dim = dim[0] + (dim[1] << 8) + (dim[2] << 16);

和 Unity 兼容性

可以确定的是 Unity 里的 astc的图片头信息, 是记录在.meta 文件里的, 即通过 Asset Importer导入之后, 打包或者生成 Assetbundle 是 strip 掉16 位头信息的。 因此, 对于 Assets文件夹里的.astc资源 你可以通过Resources.Load("path") 或者 AssetDatabase.LoadAssetAtPath("path")能够正确的加载 .astc 图片。 但是如果你的.astc图片是有云侧/服务器(比如 docker)生成的, 如果你通过 Texture2D.LoadRowTextureData 得到的图片就会显示左侧有像素偏差。如下图所示:

因此要正确显示astc的图片, 需要在LoadRowTextureData的时候裁剪调16位头信息, 这也是unity 处理astc时候与原生图片的差异的地方。 如果不考虑效率(), 可以参考如下代码:

private byte[] ReadBuf(string path)
{
    var bs = File.ReadAllBytes(path);
    int len = bs.Length - 16;
    var b2 = new byte[len];
    Array.Copy(bs, 16, b2, 0, len);
    return b2;
}

if (GUILayout.Button("load", GUILayout.MinWidth(120)))
{
    var tex = new Texture2D(479, 320, TextureFormat.ASTC_8x8, false); 
    tex.LoadRawTextureData(ReadBuf("Assets/Scenes/hdr.astc"));
    tex.Apply();
    raw.texture = tex;
}

通过 asset impoter 导入的 astc 图片, 默认是不解析 hdr的图片, 即 hdr图片也是按照 ldr的图片来解析的, 因此hdr-astc图片导入之后显示肯定是不正确的, 比如上图的美女就变成这样的球样:

因此, 通过AssetDatabase.LoadAssetAtPath、Resources.Load、AssetBundle.Load 等相关的接口都是拿不到正确的结果的, 这些都是以 ldr的方式读取的。

不过倒是可以 自己IO 并指定 format 为ASTC_HDR_8x8 的方式来正确加载:

var tex = new Texture2D(479, 320, TextureFormat.ASTC_HDR_8x8, false); 
tex.LoadRawTextureData(ReadBuf("Assets/Scenes/hdr.astc"));
tex.Apply();

这里注意一下LoadRawTextureData 的方式内存开销, 因为传过去的bytes并不是io读取所有的buf, 这里需要裁减掉16位头信息剩余的bytes。 由于 unity engine 没有提供 类似于下面的接口:

tex.LoadRawTextureData(byte[] buf, int offset, int size);

因此事先需要使用 类似 Array.Copy 这类的函数复制一个很大的buf, 此时不仅会有一个很大内存的开销, 而且还很有可能触发GC, 最好的方式在c++处理掉offset, 然后直接将计算好偏移的地址传递给c#, 具体的实现可以参考我的一个测试 demo工程

转换ASTC图片

使用 astc-encoder 不仅可以将 jpg、png、tga 这些 LDR 的图片转换成 atsc,还支持了 HDR, 比如说: .exr .hdr等格式的图片

下载 astc-encoder好之后, 按照说明文档编译好之后, 就可以以命令行的形式来生成不通压缩块和质量的图片了。

astcenc -tl example.png example.tga 5x5 -thorough

默认的话, astcenc 生成的图片y 方向是反着的, 因此需要在生成的时候需要进行额外的翻转, 可以再加一个参数来做翻转

astcenc -tl example.png example.tga 5x5 -thorough -yflip

除此之外, 还有界面话的工具 Mali_Texture_Compression_Tool 也支持进行转换, 比如说 ARM 官方开发了一款 gui 的工具, 不仅支持图片转换, 还支持动态的和原图进行 逐像素 对比。