纹理压缩格式
需要预先学习:
1. 为什么不用 PNG 和 JPG
纹理 里加载纹理用的是 png、jpg 这类常见图片格式,解码后上传给 glTexImage2D。这在学习阶段没问题,但看一下 GPU 视角就会发现两个问题:
解码成本。png 与 jpg 是为"存储与传输"设计的压缩:压缩率高,但必须整体解码成未压缩像素才能使用。GPU 无法直接采样一张 png——每帧渲染前都得把整张图解压,显存里实际存放的还是未压缩数据,压缩只在磁盘上省了空间。
随机访问。GPU 采样纹理是随机的:一次双线性插值(见 纹理)要读 4 个相邻纹素,三线性过滤还要跨 mip 层。jpg 的压缩依赖整张图的频域信息(DCT 变换块之间有关联),想取"坐标 的一个像素",必须把周围一大片都解出来——随机访问的代价是灾难性的。
显存带宽。未压缩的 RGBA8 纹理每像素 4 字节。一张 4096×4096 的纹理 64 MB,加上 mipmap 约 85 MB;采样时的带宽、显存的占用,都会成为瓶颈(移动端尤其致命)。
GPU 纹理压缩(GPU texture compression,也称块压缩,block compression)为这三个问题而生,它的设计目标与 png/jj 完全不同:
| 目标 | PNG / JPG 的取舍 | GPU 纹理压缩的取舍 |
|---|---|---|
| 压缩率 | 高(1/10 ~ 1/20) | 固定且温和(每像素 4~8 bit,即 1/4 ~ 1/2) |
| 解码 | CPU 整图解码,代价高 | GPU 硬件实时解码,几乎免费 |
| 访问 | 顺序访问 | 随机访问:任意纹素只依赖自己所在的块 |
| 压缩时机 | 保存时 | 离线工具预压缩,运行时直接加载 |
固定压缩率是关键设计:每个块占的字节数固定,GPU 由纹理坐标直接算出块的位置与块内偏移,只解码需要的那个块。解码单元就做在纹理采样器旁边。
2. 块压缩的原理
主流 GPU 压缩格式共享同一套骨架,以 BC1(DXT1)为例:
- 把纹理切成 像素的块(block / tile);
- 每块存两个 16 位 RGB 颜色(端点色,endpoint),通常一个偏亮、一个偏暗;
- 沿端点连线生成 4 个插值色,每像素用 2 bit 存一个索引(index),指出自己取 4 个插值色中的哪一个。
4x4 像素块(16 像素):
┌──┬──┬──┬──┐ 块内存储:
│0 │1 │2 │1 │ 端点色A(16bit RGB565)
├──┼──┼──┼──┤ 端点色B(16bit RGB565)
│2 │2 │1 │0 │ + 16 像素 × 2bit 索引
├──┼──┼──┼──┤
│1 │0 │2 │2 │ 总计 4 + 4 + 32 bit = 40 bit
├──┼──┼──┼──┤ 每像素 2.5 bit,约为 RGBA8 的 1/5.3
│0 │1 │2 │0 │ 解码:索引 -> 端点连线上的插值色
└──┴──┴──┴──┘
整块只有 8 字节(对比未压缩 4×4×4 = 64 字节)。压缩的思路本质是调色板的退化版:每块自带一个 4 色迷你调色板,颜色渐变区域几乎无损;色块边界比 4×4 细的细节会有损失(块与块之间各管各的调色板)。
各种格式的差异,基本都在"端点怎么存、索引几档、支持几个通道"上做文章。
3. 桌面端:BCn 家族
BC(Block Compression,Direct3D 侧称 DXT 或 DXTC / BCn)系列是桌面 GPU 的原生格式:
| 格式 | 旧名 | 每像素位深 | 内容 | 典型用途 |
|---|---|---|---|---|
| BC1 | DXT1 | 4 bit | RGB(可 1 bit alpha) | 无透明度的颜色贴图,最省 |
| BC2 | DXT3 | 8 bit | RGB + 4 bit 显式 alpha | 已被 BC3 取代,少见 |
| BC3 | DXT5 | 8 bit | RGB + 索引式 alpha(第二个 BC1 块管 alpha) | 带透明通道的颜色贴图 |
| BC4 | ATI1 | 4 bit | 单通道 | 高度图、遮罩、灰度数据 |
| BC5 | ATI2 / 3Dc | 8 bit | 双通道(两个 BC4) | 法线贴图(存 、) |
| BC6H | — | 8 bit | RGB 半浮点(HDR) | HDR 环境贴图(见 IBL) |
| BC7 | — | 8 bit | 高质量 RGB(A),更智能的端点与索引模式 | 高质量颜色贴图,主流选择 |
两个值得展开的点:
BC4/BC5 是数据通道的压缩。它把端点从"颜色"变成"标量区间"——每块存最小值与最大值,16 个像素各自索引区间上的插值。BC5 两个独立通道恰好装下法线的 、, 用 重建(法线贴图见 法线贴图与TBN矩阵):法线贴图大部分区域是平滑渐变,BC5 几乎无损,而用 BC1/BC3 压法线会产生方向性噪声(RGB 各通道共享调色板,解出来的向量严重偏离单位长度)。
BC6H/BC7 是 DX11 时代的格式。BC6H 端点是半浮点 HDR 颜色,服务于 色调映射 的 HDR 流程;BC7 每块可在多种分区与索引精度模式中选择,质量接近未压缩,代价是编码器复杂(离线压缩慢,无所谓)。
4. 移动端:ETC 与 ASTC
移动 GPU(高通、Arm Mali、Apple)不支持 BCn,有自己的一套。
ETC(Ericsson Texture Compression):
- ETC1:Android 早期的通用格式,只有 RGB、没有 alpha——透明贴图得拆成两张(颜色 + alpha 各一张)合成;
- ETC2:ETC1 的扩展(OpenGL ES 3.0 起 GPU 必须支持),补全 RGBA,含 EAC(alpha 单通道)变体,对应关系类似 BC1~BC5。
ASTC(Adaptive Scalable Texture Compression,自适应可伸缩纹理压缩)是现在的移动端主流(OpenGL ES 3.2 / 大多数 2015 后的设备,Apple 全系 Metal 原生支持):
- 块大小可变:从 到 共十几种块尺寸可选——小块质量高(每像素 8 bit),大块压缩狠(每像素可低到 0.89 bit),质量与压缩率按需权衡,这是对 BCn 固定 的最大改进;
- 每块内部也有多种编码模式(双平面、权重范围、端点量化),细节丰富的地方自动用好模式;
- 原生支持 HDR、法线贴图专用模式(类似 BC5 的通道分离思想);
- 压缩工具按内容选择块尺寸,质量普遍优于 ETC2。
| 格式 | 平台支持 | 每像素位深 | 质量 | 现状 |
|---|---|---|---|---|
| BCn | 桌面(N/A/Intel)、主机 | 4 ~ 8 bit 固定 | BC7 很好 | 桌面标配 |
| ETC2 | OpenGL ES 3.0+ 的 Android | 4 ~ 8 bit 固定 | 一般 | 兼容兜底 |
| ASTC | ES 3.2 / Metal / Vulkan 现代设备 | 0.89 ~ 8 bit 可选 | 同码率下最好 | 移动端首选 |
注:跨平台项目(手机 + PC)通常为同一张纹理离线压缩多套格式(PC 用 BC7/BC5,移动用 ASTC),运行时按设备选择加载。引擎的资源系统按后缀或清单区分,如
brick_color.bc7/brick_color.astc。
5. 在 OpenGL 中使用
压缩纹理的用法与普通纹理几乎一样,区别只在 internalFormat 指定压缩格式、数据是已经压缩好的块数据(不能把 png 塞进去让 OpenGL 压缩):
// BC7 压缩的 RGBA 纹理
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_BPTC_UNORM,
width, height, 0, dataSize, compressedData);
// BC5 压缩的法线贴图
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RG_RGTC2,
width, height, 0, dataSize, compressedData);
// ASTC 4x4
glCompressedTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGBA_ASTC_4x4,
width, height, 0, dataSize, compressedData);
格式支持用扩展查询确认(GL_texture_compression_bptc、GL_KHR_texture_compression_astc_ldr 等)。压缩数据通常装在 KTX(Khronos Texture,跨平台容器,含 mip 链与格式描述)或 DDS(DirectDraw Surface,Windows 生态传统容器)文件里,用 ktx 库或 SOIL2 等加载,比自己解析省事。
5.1 压缩工具链
离线压缩是内容管线的固定环节:
| 工具 | 说明 |
|---|---|
| Compressonator | AMD 出品,GUI + CLI,BCn 全家 |
| astcenc | Arm 的 ASTC 官方编码器,可调块尺寸与质量档 |
| nvtt / texturec | NVIDIA Texture Tools / bgfx 的 texturec,跨格式批量 |
| 引擎内置 | Unity / UE 的纹理导入设置(勾选压缩格式即可) |
6. 工程注意事项
mipmap 逐级压缩。压缩发生在每一个 mip 级上,glGenerateMipmap 无法为压缩纹理生成 mip—— mip 链必须离线生成再逐级压缩,这也是 KTX 容器存在的意义之一。
sRGB 与压缩的叠加。颜色贴图是 sRGB 编码(见 伽马校正),压缩格式有对应的 sRGB 变体(如 GL_COMPRESSED_SRGB_ALPHA_BPTC_UNORM):硬件在解码块之后再做 sRGB → 线性转换。用错变体(线性格式装 sRGB 数据)会让颜色与 alpha 加权失真,这是移动端常见的"画面偏灰"来源之一。
法线、数据贴图不要用颜色格式压。整理一张选择表:
| 贴图类型 | 桌面推荐 | 移动推荐 | 备注 |
|---|---|---|---|
| 颜色 / albedo | BC7(或 BC1/BC3 省空间) | ASTC 6x6 / 8x8 | 用 sRGB 变体 |
| 法线贴图 | BC5 | ASTC(normal 模式或 4x4/5x5) | 见 法线贴图与TBN矩阵 |
| 粗糙度 / 金属度 / 遮罩 | BC4(单通道打包) | ASTC | 多张可打进通道复用一张 BC7 |
| HDR 环境贴图 | BC6H | ASTC HDR | 服务 IBL 预计算 |
压缩纹理不可写。压缩格式没有 CPU 侧的 glTexSubImage2D 局部修改的合理语义(块的端点色依赖全块内容),需要运行时写入的纹理(渲染目标、动态数据)只能用未压缩格式——压缩与 帧缓冲 的渲染到纹理互不相干,只作用于"采样用的静态纹理"。
尺寸必须对齐。块压缩要求纹理边长是块尺寸(BCn 为 4)的整数倍,或依赖硬件对边缘块的处理(ASTC 支持任意尺寸,边缘块自动裁剪)。离线工具一般会自动处理,手写加载器要注意补齐。
注:一张 4096×4096 的 RGBA8 纹理 64 MB,BC7 压到 8 bit/px 后是 16 MB,ASTC 8x8(2 bit/px)只要 4 MB——对带宽敏感的移动端,纹理压缩同时省下显存、加载时间与采样带宽三样,是所有引擎的必做项。它与 mipmap(纹理)、各向异性过滤一样,属于"上线前必须配置"的纹理基础项。



