[翻译]Using Texture Compression in OpenGL(S3TC)
概述:
OpenGL扩展, 包括ARB_texture_compression和EXT_texture_compression_s3tc, 从不同层次为渲染管线提供了强大的支持:
1. 渲染速度更快
2. 内存需求更小
3. 纹理传输到内存中的速度更快
4. 在硬盘中的占用空间更小,访问速度更快
此外, 压缩纹理可以让程序共用一块更高分辨率的纹理.
我们将按照开发和运行时两部分来向开发者们介绍如何使用纹理压缩.
在开发中压缩纹理
A. 纹理制作
a. 一般方法
1. 使用GL压缩纹理
2. 将压缩后的图片保存到硬盘
b. S3TC DDS文件格式
1. 使用ISV的S3TC压缩工具压缩图片
2. 在GL中载入DDS图片
运行时中使用
B. 运行时
1. 从硬盘中载入压缩文件
2. 上传压缩后的文件到GL
3. 不要压缩动态纹理
纹理制作
我们可以在使用之前, 先将所有的纹理加载好. 这可以降低图片在硬盘中的占用空间, 并且提升运行时的加载速度. 压缩纹理有两种方式:
1. 使用OpenGL压缩
2. 在ISV中选择一种S3TC的文件格式, 提供给OpenGL直接使用
一般方法
ARB_texture_compression扩展可以通过调用glTexImage2D, 并设置internalFormat参数来压缩纹理. 压缩可以通过以下两种方式中的任意一种来完成:
1. 使用表1中一般的压缩格式(internalFormat)进行压缩
表1. 一般的压缩格式和对应未压缩的输入格式
| 通用的纹理压缩格式 | 基础格式 |
| GL_COMPRESSED_RGB_ARB |
RGB |
| GL_COMPRESSED_RGBA_ARB |
RGBA |
| GL_COMPRESSED_ALPHA_ARB |
ALPHA |
| GL_COMPRESSED_LUMINANCE_ARB |
LUMINANCE |
| GL_COMPRESSED_LUMINANCE_ALPHA_ARB |
LUMINANCE_ALPHA |
| GL_COMPRESSED_INTENSITY_ARB |
INTENSITY |
2. 使用表2中的S3TC压缩格式进行压缩
表2. S3TC压缩格式和对应未压缩的输入格式
| S3TC纹理压缩格式 | 基础格式 |
| GL_COMPRESSED_RGB_S3TC_DXT1_EXT |
RGB |
| GL_COMPRESSED_RGBA_S3TC_DXT1_EXT |
RGBA |
| GL_COMPRESSED_RGBA_S3TC_DXT3_EXT |
RGBA |
| GL_COMPRESSED_RGBA_S3TC_DXT5_EXT |
RGBA |
注: S3TC只能对未压缩的RGB或RGBA进行压缩
压缩纹理与非压缩纹理对比, 除了内容是否压缩这点不同之外, 其他工作机制都是相同的. 此外, 使用代理纹理可以在纹理压缩之前检测纹理是否需要被压缩.
接下来, 确保纹理已经被正确的压缩, 可以通过调用glGetTexLevelParameteriv, 并将pname参数设置为GL_TEXTURE_COMPRESSED_ARB, 如果返回值不为0, 则该纹理已经被成功压缩. 倘若驱动压缩失败, 则纹理将被视为支持的压缩格式.
如果使用通用的压缩格式, OpenGL会自动选取对应的压缩格式. 而我们需要获取这个格式, 可以再次调用glGetTexLevelParameteriv, 并将pname参数设为GL_TEXTURE_INTERNAL_FORMAT.
再下一步, 向OpenGL请求压缩后的纹理的宽高. 再次调用glGetTexLevelParameteriv, 并将pname参数设为GL_TEXTURE_COMPRESSED_IMAGE_SIZE_ARB. 根据返回的大小, 申请一块新的内存.
然后, 调用glGetCompressedTexImageARB, 并将刚才申请的内存的地址作为img的参数, 获取压缩后的纹理数据.
最后, 需要将压缩后纹理的元数据保存起来. 对于压缩数据, 通常需要保存以下数据, 以在运行时中使用:
1. 内存大小
2. 压缩格式
3. 宽度
4. 高度
5. 边缘---非S3TC格式(请看下面S3TC的注解)
6. 深度---非S3TC格式且是3D纹理(请看下面S3TC的注解)
注: 当使用S3TC格式时, border必须为0. 当border不为0时, glCompressedTexImage2DARB会产生INVALID_OPERATION的错误.
注: S3TC为2D图片格式, 因此不需要depth字段. 当压缩格式是S3TC时, glCompressedTexImage1DARB和glCompressedTexImage3DARB会产生INVALID_ENUM的错误.
当压缩格式不是标准格式时, 会产生跨平台的问题, 因此必须确保平台所支持的压缩格式. 通过调用glGetIntegerv, 并设置参数为GL_NUM_COMPRESSED_TEXTURE_FORMATS_ARB和GL_COMPRESSED_TEXTURE_FORMATS_ARB, 可以获取到对应平台支持的压缩格式. 见代码例子1.
|
GLint * compressed_format;
GLint num_compressed_format;
compressed_format = (GLint*)malloc(num_compressed_format * sizeof(GLint)); glGetIntegerv(GL_COMPRESSED_TEXTURE_FORMATS_ARB, compressed_format); |
代码例子1. 如何列举支持的纹理压缩格式
值得注意的是, 列举出来的格式不一定和支持的格式完全相同. 列举格式的主要目的是找出那些"普通"的格式, 然后就可以让程序测试这些格式, 而不需要其他扩展包的知识. 这解释了为什么S3TC格式的列举中找不到GL_COMPRESSED_RGBA_S3TC_DXT1_EXT---这不是一个"普通"的RGBA格式.
代码例子2总结了这个过程.
|
glBindTexture(GL_TEXTURE_2D, compressed_decal_map);
glTexImage2D(GL_TEXTURE_2D, 0, GL_COMPRESSED_RGB_ARB, width, height, 0, GL_BGR_EXT, GL_UNSIGNED_BYTE, pixels);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_ARB, &compressed);
/* if the compression has been successful */
if (compressed == GL_TRUE)
{
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_INTERNAL_FORMAT, &internalformat);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_COMPRESSED_IMAGE_SIZE_ARB, &compressed_size);
img = (unsigned char *)malloc(compressed_size * sizeof(unsigned char));
glGetCompressedTexImageARB(GL_TEXTURE_2D, 0, img);
} |
代码例子2. 压缩一个纹理, 并保存到硬盘中.
有多种方式可以评估压缩后的图片质量. 可以评估纹理的质量参数(RED_BITS, GREEN_BITS, …), 或者实际压缩一下纹理. 对于后者, 可以通过调用glGetTexImage来获取未压缩的纹理数据.
S3TC DDS文件格式
DDS文件格式提供可选的方式来处理压缩纹理. ISV工具, 比如S3的Adobe PhotoShop插件, 微软DirectX的Dxtex, 都可以将普通的纹理文件压缩为DDS文件. 即Direct Draw Surface. 这意味着文件会被转存为DirectX Direct Draw Surface. 因此, DDS的读取必须添加DirectX的ddraw.h文件以获取到DDSURFACEDESC2的定义(见代码例子3). 处理DirectX的结构体需要注意的是DirectX的坐标原点为屏幕的左上角, 而OpenGL的坐标原点是屏幕的左下角. 因此需要在转换之前, 对纹理进行竖向翻转, 或者将t纹理坐标取反.
|
#include <ddraw.h>
gliGenericImage *ReadDDSFile(const char *filename, int * bufsize, int * numMipmaps)
{
}
} |
代码例子3. DDS文件读取
最后需要额外注意的是, DDS文件会被视为mipmap. 每个DDS文件中都包含着全部压缩后的mipmap, 而GL对此每次只取一个mipmap. DDS文件无法直接被GL的缓存读取. 我们需要计算每个mipmap对应的缓冲区的偏移量, 并将正确的内存地址和属性传给glCompressedTexImage2DARB. 代码列表4实现了这个过程., 即如何使用glCompressedTexImage2DARB函数, 请参考运行时章节.
|
/* load the .dds file */
ddsimage = ReadDDSFile("flowers.dds",&ddsbufsize,&numMipmaps);
height = ddsimage->height;
width = ddsimage->width;
offset =0;
blockSize = (ddsimage->format == GL_COMPRESSED_RGBA_S3TC_DXT1_EXT) ? 8 : 16;
glBindTexture(GL_TEXTURE_2D, dds_compressed_decal_map);
/* load the mipmaps */
for (i = 0; i < numMipmaps && (width || height); ++i) {
|
代码列表4. 如何获取DDS的mipmap数据到GL中.
最后, 微软的DirectX SDK DXTex工具中提供了将mipmap的整图压缩到一张DDS中的方式. 这个功能非常方便, 因为我们可以把所有的纹理数据压缩到一张整图中. 为了从整图中读出每个纹理, DDS的读取也需要做对应的修改. OpenGL的读取方式和DirectX相同, 可以参考DirectX的文档.
S3TC数据信息
同一种压缩格式的S3TC的压缩比是固定的, 见下表.
| S3TC压缩格式 | 压缩(bits/textel) | 压缩比(8bits/channel) |
| GL_COMPRESSED_RGB_S3TC_DXT1_EXT |
4 | 6:1/8:1 |
| GL_COMPRESSED_RGBA_S3TC_DXT3_EXT |
8 | 4:1 |
| GL_COMPRESSED_RGBA_S3TC_DXT5_EXT |
8 | 4:1 |
注: 24位的纹理, 在内存中实际是被存储为32位, 因此, GL_COMPRESSED_RGB_S3TC_DXT1_EXT可以被视为8:1的压缩比. S3TC的压缩公式为:
ImageSize = blockSize * ceil(width / 4) * ceil(height / 4)
其中, 当格式为DXT1时, blockSize为8字节, 当格式为DXT3/5时, blockSize为16字节.
以下图片的对比可以看出不同的放缩比率(抖动)和不同的图像分辨率下, 压缩和未压缩图像的质量:


例子1. 左, 128*128未压缩的纹理. 右, S3TC RGB DXT1压缩的纹理, 抖动算法为GL_NEAREST.


例子2. 左, 128*128未压缩的纹理. 右, S3TC RGB DXT1压缩的纹理, 抖动算法为GL_LINEAR.


例子3. 左, 64*64未压缩的纹理. 右, S3TC RGB DXT1压缩的纹理, 抖动算法为GL_NEAREST.


例子4. 左, 64*64未压缩的纹理. 右, S3TC RGB DXT1压缩的纹理, 抖动算法为GL_LINEAR.
有趣的是, 将64*64未压缩的纹理和128*128压缩后的纹理放在一起, 它们所占用的内存是相同的, 然而, 压缩后的纹理效果却更好一些(见例子5).


例子5. 左, 64*64未压缩的纹理. 右, 128*128 S3TC RGB DXT1压缩的纹理, 抖动算法为GL_LINEAR. 可以看到, 它们消耗的内存相同, 但是压缩后的纹理效果看起来更好一些.
运行时
纹理被压缩之后, 在运行时就需要将其解压. 程序在从硬盘中读取压缩后的纹理, 并载入到OpenGL的过程中时, 就可以得到很大的优化了. 首先, 它降低了在硬盘中的占用空间, 并增快了整体的加载速度. 其次, 压缩后的纹理在内存中的占用比未压缩时的占用小的多. 更重要的是, 单个纹理的内存占用减少后, 可以保证更多的纹理持续缓存在内存中, 而不需要频繁地删除纹理以腾出空间供新的纹理载入, 这很大程度上减少了从AGP和主存中申请和访问内存的次数. 最后, 压缩纹理降低了渲染时占用的带宽. 以上几点都可以增快整体的渲染速度. 例如, 在32位色运行雷神之锤3, 压缩后会有20%左右的速度提升.
开始之前, 首先得从硬盘中载入对应的文件. 处理好纹理的宽度, 高度, 边缘, 深度, 压缩格式, 内存大小和纹理本身之后, 可以调用glCompressedTexImage2D并设置对应的压缩格式到internalFormat参数. 处理完之后, 运行时的压缩不会出现, 因为纹理会直接被存储到纹理内存中.
|
glBindTexture(GL_TEXTURE_2D, already_compressed_decal_map);
glCompressedTexImage2D(GL_TEXTURE_2D, 0, internalFormat, width, height, border, image_size, data);
|
代码例子5. 如何将压缩纹理直接存储到纹理内存中.
特别注意的是, 不要将动态纹理存到纹理内存中, 因为那非常非常的慢.
总结
按照以上步骤完成, 纹理的压缩处理可以很大程度的提高程序的效率, 尽管这牺牲了小部分的图片质量. 以上的扩展包描述对应所有Release 5.xx驱动的NVIDIA GPU.
参考文献:
NVIDIA Developer web site (OpenGL Code example – OpenGL S3TC):
OpenGL Extension Registry:
• ARB_texture_compression specification
• EXT_texture_compression_s3tc
DirectX 7.0 documentation (MSDN search for: Texture Compression)
浙公网安备 33010602011771号