块压缩是一种丢失的纹理压缩技术,用于减少纹理大小和内存占用,从而提高性能。 块压缩纹理的大小可以小于每个颜色通道为32位的纹理。
块压缩是用于减小纹理大小的纹理压缩技术。 与每色 32 位的纹理相比,块压缩的纹理可以小到 75%。 使用块压缩时,应用程序通常会看到性能提高,因为内存占用量较小。
虽然丢失,但块压缩效果良好,建议用于管道转换和筛选的所有纹理。 直接映射到屏幕的纹理(如图标和文本的 UI 元素)不适合进行压缩,因为失真更容易察觉。
块压缩纹理必须作为大小 4 的所有维度的倍数创建,不能用作管道的输出。
块压缩的工作原理
块压缩是减少存储颜色数据所需的内存量的技术。 通过将某些颜色按照原始大小存储,而其他颜色则使用编码方案,可以显著减少存储图像所需的内存量。 由于硬件会自动解码压缩的数据,因此使用压缩纹理不会造成性能损失。
若要查看压缩的工作原理,请查看以下两个示例。 第一个示例描述了存储未压缩数据时使用的内存量;第二个示例描述存储压缩数据时使用的内存量。
存储未压缩的数据
下图表示未压缩的 4×4 纹理。 假设每个颜色都包含一个颜色组件(例如红色),并存储在一个字节内存中。
未压缩的数据按顺序排列在内存中,需要 16 个字节,如下图所示。
存储压缩的数据
现在,你已了解未压缩映像使用的内存量,请查看压缩映像所节省的内存量。 BC4 压缩格式存储 2 种颜色(每个字节)和 16 个 3 位索引(48 位或 6 个字节),用于内插纹理中的原始颜色,如下图所示。
存储压缩数据所需的总空间为 8 个字节,比未压缩的示例节省了 50% 的内存。 使用多个颜色组件时,节省甚至更大。
块压缩提供的大量内存节省可能会导致性能增加。 这种性能提升是以图像质量为代价的(由于颜色内插),不过品质的降低往往不明显。
下一部分介绍了 Direct3D 如何在应用程序中使用块压缩。
使用块压缩
创建块压缩纹理的方法与未压缩纹理类似,只是需要指定块压缩格式。
接下来,创建一个视图以将纹理绑定到管道,因为块压缩的纹理只能用作着色器阶段的输入,因此需要创建着色器资源视图。
使用块压缩纹理的方式与使用未压缩纹理的方式相同。 为了使应用程序获取指向块压缩数据的内存指针,您需要调整 mipmap 中的内存填充,以便处理声明大小与实际大小之间的差异。
虚拟大小与物理大小
如果您的应用程序代码使用内存指针来遍历块压缩纹理的内存,那么有一个重要的考虑可能需要修改代码。 块压缩纹理在所有维度中必须是 4 的倍数,因为块压缩算法在 4x4 纹素块上运行。 对于初始维度被 4 除以 4 的 mipmap 来说,这是一个问题,但细分级别不是。 下图显示了虚拟(已声明)大小与每个 mipmap 级别的物理(实际)大小之间的区域差异。
未压缩和已压缩的 mipmap 级别
图表左侧显示了为未压缩的 60×40 纹理生成的 mipmap 级别大小。 顶级大小取自生成纹理的 API 调用;每个后续级别是上一级别大小的一半。 对于未压缩的纹理,虚拟(声明)大小与物理(实际)大小之间没有区别。
关系图的右侧显示了针对同一 60×40 纹理在压缩后生成的各 mipmap 级别的大小。 请注意,第二和第三个级别都有内存填充,以使每个级别的大小系数为 4。 这是必要的,以便算法可以在 4×4 纹素块上运行。 当你考虑小于 4×4 的 mipmap 级别时,这一点尤其明显;在分配纹理内存时,这些非常小的 mipmap 级别的大小将被四舍五入到最接近的 4 的倍数。
采样硬件使用虚拟尺寸;在采样纹理时,内存填充将被忽略。 对于小于 4×4 的 mipmap 级别,只有前四个纹素将用于 2×2 地图,并且只有第一个纹素将由 1×1 块使用。 但是,没有公开物理大小的 API 结构(包括内存填充)。
总之,在复制包含块压缩数据的区域时,请注意使用对齐的内存块。 若要在获取内存指针的应用程序中执行此操作,请确保指针使用表面步幅以考虑物理内存大小。
压缩算法
Direct3D 中的块压缩技术将未压缩的纹理数据分解为 4×4 块,压缩每个块,然后存储数据。 基于此,需要压缩的纹理必须具有以4为倍数的纹理尺寸。
前面的图显示了一个纹理被分割为纹素块。 第一个块显示标记为 a-p 的 16 个纹素的布局,但每个块都有相同的数据组织。
Direct3D 实现多个压缩方案,每个方案在存储的组件数、每个组件的位数和消耗的内存量之间实现不同的权衡。 使用此表有助于选择最适合数据类型的格式,以及最适合应用程序的数据分辨率。
源数据 | 数据压缩分辨率(以位为单位) | 选择此压缩格式 |
---|---|---|
三分量颜色和 alpha | 颜色 (5:6:5)、 Alpha (1) 或无 alpha | BC1 |
三分量颜色和 alpha | 颜色 (5:6:5), 阿尔法 (4) | BC2 |
三分量颜色和 alpha | 颜色 (5:6:5), 阿尔法 (8) | BC3 |
单一成分颜色 | 一个组件(8) | BC4 |
双组分颜色 | 两个组件(8:8) | BC5 |
BC1
使用第一个块压缩格式(BC1)(DXGI_FORMAT_BC1_TYPELESS、DXGI_FORMAT_BC1_UNORM或DXGI_FORMAT_BC1_UNORM_SRGB)来存储采用5:6:5颜色格式(5位红色,6位绿色,5位蓝色)的三分量颜色数据。 即使数据还包含 1 位 alpha,也是如此。 假设 4×4 纹理可能采用最大的数据格式,则 BC1 格式会将所需的内存从 48 字节(16 种颜色× 3 个组件/颜色× 1 字节/组件)减少到 8 字节内存。
这个算法适用于 4×4 大小的纹素块。 算法不存储 16 种颜色,而是保存 2 种参考颜色(color_0和color_1)和 16 个 2 位颜色索引(块 a-p),如下图所示。
颜色索引(a-p)用于查找颜色表中的原始颜色。 颜色表包含 4 种颜色。 前两种颜色(color_0和color_1)是最小和最大颜色。 其他两种颜色(color_2和color_3)是使用线性内插计算的中间颜色。
color_2 = 2/3*color_0 + 1/3*color_1
color_3 = 1/3*color_0 + 2/3*color_1
为四种颜色分配 2 位索引值,这些值将保存在 a–p 块中。
color_0 = 00
color_1 = 01
color_2 = 10
color_3 = 11
最后,a-p 块中的每个颜色与颜色表中的四种颜色进行比较,最接近颜色的索引存储在 2 位块中。
此算法也适合包含 1 位 alpha 的数据。 唯一的区别是,color_3设置为 0(表示透明颜色),color_2是color_0和color_1的线性混合。
color_2 = 1/2*color_0 + 1/2*color_1;
color_3 = 0;
BC2
使用 BC2 格式(DXGI_FORMAT_BC2_TYPELESS、DXGI_FORMAT_BC2_UNORM 或 DXGI_BC2_UNORM_SRGB)存储包含颜色和低连贯性 alpha 数据的数据(对于高度连贯的 alpha 数据,请使用 BC3)。 BC2 格式将 RGB 数据存储为 5:6:5 颜色(5 位红色、6 位绿色、5 位蓝色)和 alpha 作为单独的 4 位值。 假设一个 4×4 的纹理使用可能的最大数据格式,此压缩技术将所需的内存从 64 字节(16 种颜色 × 4 个组件/颜色 × 1 字节/组件)减少到 16 字节内存。
BC2 格式存储与 BC1 格式相同的位数和数据布局的颜色;但是,BC2 需要额外的 64 位内存来存储 alpha 数据,如下图所示。
BC3
使用 BC3 格式(DXGI_FORMAT_BC3_TYPELESS、DXGI_FORMAT_BC3_UNORM 或 DXGI_FORMAT_BC3_UNORM_SRGB)来存储高度一致的颜色数据(使用 BC2 处理不太一致的 alpha 数据)。 BC3 格式使用 5:6:5 颜色格式(5 位红色、6 位绿色、5 位蓝色)存储颜色数据,并使用一个字节存储 alpha 数据。 假设一个 4×4 的纹理使用可能的最大数据格式,此压缩技术将所需的内存从 64 字节(16 种颜色 × 4 个组件/颜色 × 1 字节/组件)减少到 16 字节内存。
BC3 格式存储与 BC1 格式相同的位数和数据布局的颜色;但是,BC3 需要额外的 64 位内存来存储 alpha 数据。 BC3 格式通过存储两个引用值并在它们之间内插(类似于 BC1 存储 RGB 颜色的方式)来处理 alpha。
这个算法适用于 4×4 大小的纹素块。 算法不存储 16 个 alpha 值,而是存储 2 个引用 alpha (alpha_0 和 alpha_1) 和 16 个 3 位颜色索引 (alpha a 到 p),如下图所示。
BC3 格式使用 alpha 索引(a–p)从包含 8 个值的查找表中查找原始颜色。 前两个值(alpha_0和alpha_1)是最小值和最大值;其他六个中间值是使用线性内插计算的。
该算法通过检查两个参考 α 值来确定内插 α 值的数量。 如果alpha_0大于 alpha_1,则 BC3 将内插 6 个 alpha 值;否则,它会内插 4。 当 BC3 仅内插 4 个 alpha 值时,它将设置两个额外的 alpha 值(0 表示完全透明,255 表示完全不透明)。 BC3 通过存储对应于内插 alpha 值的位代码来压缩 4×4 纹素区域中的 alpha 值,这些位代码与给定纹素的原始 alpha 最匹配。
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
}
BC4
使用 BC4 格式,以 8 位的方式存储单通道颜色数据。 由于准确性提高(与 BC1相比),BC4 非常适合使用 DXGI_FORMAT_BC4_UNORM 格式将浮点数据存储在 [0 到 1] 范围内,以及使用 DXGI_FORMAT_BC4_SNORM 格式存储在 [-1 到 +1] 范围内。 假设 4×4 纹理使用最大的数据格式,此压缩技术会将所需的内存从 16 字节(16 种颜色× 每种颜色 1 个组件× 每个组件 1 字节)减少到 8 字节。
这个算法适用于 4×4 大小的纹素块。 算法不存储 16 种颜色,而是存储 2 种参考颜色(red_0和red_1)和 16 个 3 位颜色索引(红色 a到红色 p),如下图所示。
该算法使用 3 位索引从包含 8 种颜色的颜色表中查找颜色。 前两种颜色(red_0和red_1)是最小和最大颜色。 该算法使用线性插值计算剩余颜色。
该算法通过检查两个引用值来确定内插颜色值的数量。 如果red_0大于red_1,则 BC4 内插 6 个颜色值;否则,它会内插 4。 当 BC4 仅内插 4 个颜色值时,它将设置另外两个颜色值(0.0f 表示完全透明,1.0f 表示完全不透明)。 BC4 通过存储与给定纹素的原始 alpha 值最匹配的内插 alpha 值的比特码来压缩 4×4 纹素区域中的 alpha 值。
BC4_UNORM
单组件数据的内插如下代码示例所示。
unsigned word red_0, red_1;
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
}
引用颜色被分配了 3 位索引(因为有 8 个值,所以为 000-111),这些将在压缩期间保存到从红色 a 到红色 p 的块中。
BC4_SNORM
DXGI_FORMAT_BC4_SNORM与其他格式完全相同,只是数据在SNORM范围内编码,并且在内插4个颜色值时有所不同。 单组件数据的内插如下代码示例所示。
signed word red_0, red_1;
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
}
引用颜色被分配了 3 位索引(因为有 8 个值,所以为 000-111),这些将在压缩期间保存到从红色 a 到红色 p 的块中。
BC5
使用 BC5 格式以 8 位分别存储每个颜色组件的双分量颜色数据。 由于相比 BC1准确性提高,BC5 非常适合使用 DXGI_FORMAT_BC5_UNORM 格式将浮点数据存储在 [0 到 1] 范围内,并且在 [-1 到 +1] 范围内使用 DXGI_FORMAT_BC5_SNORM 格式存储。 假设 4×4 纹理可能采用最大的数据格式,此压缩技术会将所需的内存从 32 字节(16 种颜色× 2 个组件/颜色× 1 字节/组件)减少到 16 个字节。
这个算法适用于 4×4 大小的纹素块。 算法不是为每个组件存储16种颜色,而是存储2种参考颜色(red_0, red_1, green_0和green_1),以及每个组件的16个3位颜色索引(从红色a到红色p和从绿色a到绿色p),如下图所示。
该算法使用 3 位索引从包含 8 种颜色的颜色表中查找颜色。 前两种颜色(red_0和red_1(或green_0和green_1)是最小和最大颜色。 该算法使用线性插值计算剩余颜色。
该算法通过检查两个引用值来确定内插颜色值的数量。 如果red_0大于red_1,则 BC5 内插 6 个颜色值;否则,它会内插 4。 当 BC5 仅内插 4 个颜色值时,它将其余两个颜色值设置为 0.0f 和 1.0f。
BC5_UNORM
单组件数据的内插如下代码示例所示。 绿色组件的计算方法类似。
unsigned word red_0, red_1;
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
}
引用颜色被分配了 3 位索引(因为有 8 个值,所以为 000-111),这些将在压缩期间保存到从红色 a 到红色 p 的块中。
BC5_SNORM
DXGI_FORMAT_BC5_SNORM完全相同,不同之处在于数据在 SNORM 范围内编码,当内插 4 个数据值时,这两个附加值为 -1.0f 和 1.0f。 单组件数据的内插如下代码示例所示。 绿色组件的计算方法类似。
signed word red_0, red_1;
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
}
引用颜色被分配了 3 位索引(因为有 8 个值,所以为 000-111),这些将在压缩期间保存到从红色 a 到红色 p 的块中。
格式转换
Direct3D 支持在预结构化类型纹理和相同位宽度的块压缩纹理之间复制。
可以在几个格式类型之间复制资源。 这种类型的复制操作执行一种类型的格式转换,以不同的格式类型重新解释资源数据。 假设此示例显示了重新解释数据与更典型的转换行为方式之间的差异:
FLOAT32 f = 1.0f;
UINT32 u;
若要将“f”重新解释为“u”类型,请使用 memcpy:
memcpy( &u, &f, sizeof( f ) ); // 'u' becomes equal to 0x3F800000.
在前面的重新解释中,数据的基础值不会更改;memcpy 将浮点重新解释为无符号整数。
若要执行更典型的转换类型,请使用赋值:
u = f; // 'u' becomes 1.
在前面的转换中,数据的基础值会更改。
下表列出了可在此重新解释格式转换类型中使用的允许源和目标格式。 必须为重新解释正确编码这些值,才能如预期般运作。
位宽 | 未压缩的资源 | Block-Compressed 资源 |
---|---|---|
32 | DXGI_FORMAT_R32_UINT DXGI_FORMAT_R32_SINT |
DXGI_FORMAT_R9G9B9E5_SHAREDEXP |
64 | DXGI_FORMAT_R16G16B16A16_UINT DXGI_FORMAT_R16G16B16A16_SINT DXGI_FORMAT_R32G32_UINT DXGI_FORMAT_R32G32_SINT |
DXGI_FORMAT_BC1_UNORM[_SRGB] DXGI_FORMAT_BC4_UNORM DXGI_FORMAT_BC4_SNORM |
128 | DXGI_FORMAT_R32G32B32A32_UINT DXGI_FORMAT_R32G32B32A32_SINT |
DXGI_FORMAT_BC2_UNORM[_SRGB] DXGI_FORMAT_BC3_UNORM[_SRGB] DXGI_FORMAT_BC5_UNORM DXGI_FORMAT_BC5_SNORM |