一、为啥要了解压缩纹理
1.1 为啥要使用压缩纹理
为什么图片导入Unity之后,默认会设置为压缩格式?
在glTexImage2D
这个接口里面,设置图片数据保存为压缩格式后,会有什么好处?
/**
* @brief 将图片数据上传到GPU;
* @param target 目标纹理,GL_TEXTURE_2D(2D纹理)
* @param level 当图片数据是包含多个mipmap层级时,指定使用mipmap层级。
* @param internalformat 图片数据上传到GPU后,在显存中保存为哪种格式?
* @param width
* @param height
* @param border
* @param format 上传的图片数据格式,RGB、RGBA、Alpha等
* @param type 图片数据变量格式,一般都是GL_UNSIGNED_BYTE(0-255范围)
* @param pixels 图片数据
* @return
*/
void glTexImage2D(GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const void * pixels);
答案就是:节省显存。
如今的3A大作,各种4K贴图、超高精度模型,一个场景数据量几G,这些数据都是要上传到显存的,为了让游戏适配更多的硬件,开发者们也是各显神通,压缩纹理就是OpenGL官方提供的一种手段。
1.2 如何自定义压缩纹理以及使用压缩纹理的效果
1.2.1 使用压缩纹理节省显存
比如:之前glTexImage2D这个接口的 internalformat 参数值是GL_RGB
,
//3. 将图片rgb数据上传到GPU;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture2d->width_, texture2d->height_, 0, texture2d->gl_texture_format_, GL_UNSIGNED_BYTE, data);
现在只要设置为对应的压缩纹理格式GL_COMPRESSED_RGB
即可。
//3. 将图片rgb数据上传到GPU;
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, texture2d->width_, texture2d->height_, 0, texture2d->gl_texture_format_, GL_UNSIGNED_BYTE, data);
代码编译运行后,正常绘制了立方体,但是怎么样确认压缩纹理的效果呢,就是说怎么样确认显存占用降低?
这里借助GPU状态工具 - GPU-Z 来查看实时显存。
使用未压缩纹理,glTexImage2D接口调用前后显存对比如下:
从52m 变动到 116m,使用了约 64m
显存。
使用压缩纹理,接口调用前后显存对比如下:
从52m 变动到 60m,使用了约 8m
显存。
效果很明显,压缩纹理后,占用的显存是原来的 1/6
左右。
具体查看5.6 压缩纹理
1.2.2 自定义压缩纹理:将压缩好的纹理数据保存在本地
将压缩好的纹理数据,保存到硬盘作为图片文件。
带来的好处是:
- 无需解析,直接上传GPU。
- 数据已经压缩,无需再次压缩。
- 数据已经压缩,上传数据量小
压缩纹理数据已经从GPU下载到内存,并保存为.cpt
文件,cpt
是我取自compressed texture
的缩写。
具体查看5.7 图片压缩工具
1.2.3 使用自定义的压缩纹理
带来的性能提升,如下所示:
解析耗时(ms) | 上传耗时(ms) | |
---|---|---|
jpg | 1571 | 960 |
cpt | 4 | 16 |
有很大的性能提升。
回顾一下,我们是先将.jpg
图片,解析得到RGB
数据,调用OpenGL API
进行压缩上传至GPU
,然后再从GPU
下载压缩纹理数据,保存为.cpt
文件。
这其实就是Unity
导入图片的时候干的活,现在你知道为什么Unity
导入图片那么慢了!
具体查看 5.8 使用压缩纹理
1.2.3 示例原理
截图的效果可以参考下面几篇博客:
二、纹理压缩相关知识
2.0 什么是压缩纹理
纹理压缩(Texture compression)是一种专为在三维计算机图形渲染系统中存储纹理而使用的图像压缩技术。与普通图像压缩算法的不同之处在于,纹理压缩算法为纹素的随机存取做了优化。
在实际应用特别是游戏中纹理占用了相当大的包体积,而且GPU无法直接解码目前流行的图片格式,图片必须转换为RGB等类型的格式才能上传到GPU内存,这显然增加了GPU内存的占用。为了处理这些问题于是出现了GPU支持的压缩纹理格式,在GPU中进行解码。压缩纹理属于有损压缩,更在意解码速度,而编码在程序运行之前,因此速度较慢。
2.1 通用压缩纹理格式
2.2 压缩纹理相关API的使用
- 获得GPU的型号
glGetString(GL_RENDERER)
- 获得GPU的生产厂商
glGetString(GL_VENDOR);
- 获取GPU支持哪些压缩纹理
string extensions = (const char*)glGetString(GL_EXTENSIONS);
- 判断是否支持ETC1格式的压缩纹理
return (extensions.find("GL_OES_compressed_ETC1_RGB8_texture")!= string::npos);
- 判断是否支持DXT格式的压缩纹理
return (extensions.find("GL_EXT_texture_compression_dxt1")!= string::npos ||
extensions.find("GL_EXT_texture_compression_s3tc")!= string::npos);
- 判断是否支持PVRTC格式的压缩纹理
return (extensions.find("GL_IMG_texture_compression_pvrtc")!= string::npos);
- 判断是否支持ATITC格式的压缩纹理
return (extensions.find("GL_AMD_compressed_ATC_texture")!= string::npos ||
extensions.find("GL_ATI_texture_compression_atitc")!= string::npos);
- 上传压缩纹理数据
void glCompressedTexImage2D(
GLenum target,
GLint level,
GLenum internalformat,
GLsizei width,
GLsizei height,
GLint border,
GLsizei imageSize,
const GLvoid * data);
internalformat即是压缩纹理格式的类型。
- 查看设备支持的texture压缩格式
int num_formats;
glGetIntegerv(GL_NUM_COMPRESSED_TEXTURE_FORMATS, &num_formats);
std::cout<<"Texture extensions: "<<num_formats<<std::endl;
int *formats = (int*)alloca(num_formats * sizeof(int));
glGetIntegerv(GL_COMPRESSED_TEXTURE_FORMATS, formats);
for(int i=0; i<num_formats; i++)
{
std::cout<<i<<" 0x"<<hex<<formats[i]<<dec<<std::endl;
}
//注意使用PVRTC格式纹理时,纹理的filter mode不能设置为 GL_LINEAR_MIPMAP_LINEAR,
//否则的话加载出来的画线显示黑色, 这里有提到。
- glTexImage中指定压缩格式可以对上传的纹理进行压缩以改善内存使用,通过设置intenalFormat为表中一个值实现。通过这种方式进行图像压缩增加了纹理加载的开销,但却能够通过更有效地使用纹理存储空间来增加纹理性能,如果由于某些原因无法对纹理进行压缩,OpenGL就会使用下表中所列出的基本内部格式,并加载未经压缩的纹理。
GL_COMPRESSED_RGB : GL_RGB
GL_COMPRESSED_RGBA : GL_RGBA
GL_COMPRESSED_SRGB : GL_SRGB
GL_COMPRESSED_SRGB_ALPHA : GL_RGBA
GL_COMPRESSED_RED : GL_RED
GL_COMPRESSED_RG : GL_RG
除了这些压缩格式外,OpenGL中还加入了一些特定的压缩格式,即
GL_COMPRESSED_SIGNED_RED_RGTC1
GL_COMPRESSED_SIGNED_RED_RGTC2
GL_COMPRESSED_SIGNED_RG_RGTC2
它们用于各种单颜色通道和双颜色通道压缩纹理,他们代替了兼容版本中GL_LUMINANCE
和GL_LUMINANCE_ALPHA
的功能
- 判断纹理是否被成功压缩 和 指定选择压缩格式的方式
//判断纹理是否被成功压缩
GLint comFlag;
glGetTexLevelParameteriv(GL_TEXTURE_2D,0,GL_TEXTURE_COMPRESSED,&comFlag);
//根据选择的压缩纹理格式,
//选择最快、最优、⾃行选择的算法⽅式选择压缩格式。
glHint(GL_TEXTURE_COMPRESSION_HINT,GL_FASTEST); //最快
glHint(GL_TEXTURE_COMPRESSION_HINT,GL_NICEST); //质量最好
glHint(GL_TEXTURE_COMPRESSION_HINT,GL_DONT_CARE); //自行选择
参数说明:
- target:
GL_TEXTURE_1D
、GL_TEXTURE_2D
、GL_TEXTURE_3D
。 - Level: 指定所加载的mip贴图层次。⼀一般我们都把这个参数设置为0。 internalformat:每个纹理理单元中存储多少颜⾊色成分。
- width、height、depth参数: 指加载纹理理的宽度、⾼高度、深度。==注意!==这些值必须是2的整数次⽅方。(这是因为O
旧版本上的遗留留下的⼀一个要求。当然现在已经可以⽀支持不不是2的整数次⽅方。但是开发者们还是习惯使⽤用以2的整数次⽅方去
参数。) - border参数: 允许为纹理理贴图指定⼀一个边界宽度。
format、type、data
参数:与我们在讲glDrawPixels
函数对应的参数相同
glGetTexLevelParameter
函数提取的压缩纹理格式如下:
GL_TEXTURE_COMPRESSED:如果纹理被压缩返回1,否则返回0
GL_TEXTURE_COMPRESSED_IMAGE_SIZE:获取压缩后的纹理大小(以字节为单位)
GL_TEXTURE_INTERNAL_FORMAT:所使用的压缩格式
GL_NUM_COMPRESSED_TEXTURE_FORMATS:支持的压缩纹理格式数量
GL_COMPRESSED_TEXTURE_FORMATS:支持的压缩纹理格式数组
GL_TEXTURE_COMPRESSION_HINT: 选择压缩格式的方式
GL_EXT_texture_compression_s3tc
压缩格式如下:
2.3 压缩纹理的常见格式
基于OpenGL ES的压缩纹理有常见的如下几种实现:
- ETC1(Ericsson texture compression)
- ETC2(Ericsson texture compression)
- PVRTC (PowerVR texture compression)
- ATITC (ATI texture compression)
- S3TC (S3 texture compression)
ETC
ETC是Android平台通用的压缩格式,质量较低
- ETC1:不支持透明通道,图片宽高必须是2的整数次幂
- ETC2:是ETC的扩展,支持透明通道,且图片宽高只要是4的倍数即可
ETC1
ETC1
格式是OpenGL ES图形标准的一部分,并且被所有的Android设备所支持。
扩展名为: GL_OES_compressed_ETC1_RGB8_texture
,不支持透明通道,所以仅能用于不透明纹理。 且要求大小是2次幂。
当加载压缩纹理时,参数支持如下格式: GL_ETC1_RGB8_OES
(RGB,每个像素0.5个字节)
ETC2
ETC2
是 ETC1
的扩展,压缩比率一样,但压缩质量更高,而且支持透明通道,能完整存储 RGBA 信息。
ETC2 需要 OpenGL ES 3.0(对应 WebGL 2.0)
环境,目前还有不少低端 Android 手机不兼容,iOS 方面从 iPhone5S 开始都支持 OpenGL ES 3.0。
ETC2 和 ETC1 一样,长宽可以不相等,但要求是 2 的幂次方。
ETC压缩算法介绍
-
也是将图片分成4*4的小块进行压缩,其中每一块会分成两半,用1位表示是横着还是竖着拆分。
-
取2种颜色,从2半中各取一个,分为individual模式还是 differential模式,用1位表示取色模式。
-
individual模式:取两个RGB444的颜色,适用于两边颜色差异大的情况
-
differential模式:取RGB555+RGB333的颜色,其中,第二个块的颜色是偏移值,适用于两边颜色差异不大的情况,精度更高。
压缩时会生成一个全局的映射表,两个子块各需要3位来确定使用哪一行的数据。
对于每一个像素点,使用2位数据表示使用这一行的哪一个modifier,去除一个偏移值。例如表中(0,0)格子的-8,偏移值就是(-8,-8,-8),在子块颜色的基础上加上偏移值得到当前像素的颜色。
-
压缩前:
16*24=384
位 -
压缩后:
1+1+24+3*2+2*16=64
位,压缩比例1/6
PVRTC
支持的GPU为Imagination Technologies
的PowerVR SGX
系列。
OpenGL ES的扩展名为: GL_IMG_texture_compression_pvrtc
。
当加载压缩纹理时,参数支持如下几种格式:
GL_COMPRESSED_RGB_PVRTC_4BPPV1_IMG
(RGB,每个像素0.5个字节)GL_COMPRESSED_RGB_PVRTC_2BPPV1_IMG
(RGB,每个像素0.25个字节)GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG
(RGBA,每个像素0.5个字节)GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG
(RGBA,每个像素0.25个字节)
ATITC
支持的GPU为Qualcomm
的Adreno系列。
支持的OpenGL ES扩展名为: GL_ATI_texture_compression_atitc
。
当加载压缩纹理时,参数支持如下类型的纹理:
GL_ATC_RGB_AMD
(RGB,每个像素0.5个字节)GL_ATC_RGBA_EXPLICIT_ALPHA_AMD
(RGBA,每个像素1个字节)GL_ATC_RGBA_INTERPOLATED_ALPHA_AMD
(RGBA,每个像素1个字节)
S3TC/DXTC
也被称为DXTC
,在PC上广泛被使用,但是在移动设备上还是属于新鲜事物。支持的GPU为NVIDIA Tegra系列。
OpenGL ES扩展名为: GL_EXT_texture_compression_dxt1
和GL_EXT_texture_compression_s3tc
。
当加载压缩纹理时,参数有如下几种格式:
GL_COMPRESSED_RGB_S3TC_DXT1
(RGB,每个像素0.5个字节)GL_COMPRESSED_RGBA_S3TC_DXT1
(RGBA,每个像素0.5个字节)GL_COMPRESSED_RGBA_S3TC_DXT3
(RGBA,每个像素1个字节)GL_COMPRESSED_RGBA_S3TC_DXT5
(RGBA,每个像素1个字节)
DXT压缩算法
DXT
压缩将图片拆分成4*4
的小块,每一块取极值的2
种颜色,剩下的颜色取插值、共有00,01,10,11
四种颜色值。- 压缩后单个颜色是
16
位,是RGB565
、 - 不支持透明通道,原图
RGB24
,16
个格子,需要24*16=384
位 - 压缩后,两个16位颜色+16个插值,
16*2+2*16=64
位,压缩比例1/6
DXT3和DXT5压缩
在DXT1的基础上,支持透明图片,额外使用64位数据来保存透明通道值,压缩比例1/3。
- DXT3压缩:每一个像素点使用4位保存alpha值,透明值较为粗糙
- DXT5压缩:透明通道也单独取插值,有2个8位极值,每一个像素点使用3位插值,82+316=64位
DXT缺点
细节上会有丢失,例如:
ASTC
ASTC是Android和IOS平台下的一种高质量压缩方式,支持Android5.0和iPhone6以上机型
ASTC压缩算法介绍
也是分块压缩,一块128位,块的大小很灵活,有4*4,6*6,8*8,12*12
等多种大小。支持LDR、HDR、2D和3D
纹理。每个块也有端点对endpoints
,端点对不一定是RGBA
的,也可以只用其中部分通道,比如RG
通道,这样可以对法线贴图进行更好的压缩。
BISE
算法:例如有5
个数字,值为0,1,2
,正常存储需要2*5=10
位。但是实际上只有3^5=243<256
种情况,可以使用8位表示这些情况,即使用8
位就可以表示5个值为0,1,2的整数。
ASTC压缩时,各个部分长度不固定。
-
BlockMode
:平面数、权重范围、权重网格的大小 -
Part
:分区数量 -
ConfigData、MoreConfigData
:每个端点对的端点模式
对于块内颜色分布较为复杂的情况,分析块内颜色的分布,并做分区,分别存储其对应的endpoints
;对于块内的像素取值时先定位分区,再计算在其在分区内的颜色。
对块内每个像素存储一个插值weight
,但是weight
的数量可以比像素少。对于规格较大的块,例如12x12
,只能存储4x6
的权重网格。解码时,权重网格会被双线性放大到块的大小。
块大小选择(块越大,压缩质量越差,但是图片越小)
-
法线贴图:尽量选择
4*4
,避免丢失过多数据 -
细节处的贴图:选择
4*4或者6*6
,否则会丢失细节 -
一般的贴图:选择
6*6或8*8
-
无关紧要,但是尺寸特别大的图:可以考虑
8*8,10*10,12*12
,不然打包出来太大
2.4 压缩纹理工具
每种压缩纹理以及相应的厂商都提供了压缩纹理的工具,包括可视化工具和命令行工具,可自行下载
-
Imagination Technologies PowerVR
PVETextTool -
Qualcomm Adreno
Adreno Texture Tool -
ARM Mali
Mail Texture Compression Tool -
nVIDIA Tegra
DirectX Texture Tool
2.5 Filament中支持的压缩纹理
在Ktx1Reader::isSrgbTextureFormat
中有声明,如下所示:
bool isSrgbTextureFormat(TextureFormat format) {
switch(format) {
// Non-compressed
case Texture::InternalFormat::RGB8:
case Texture::InternalFormat::RGBA8:
return false;
// ASTC
case Texture::InternalFormat::RGBA_ASTC_4x4:
case Texture::InternalFormat::RGBA_ASTC_5x4:
case Texture::InternalFormat::RGBA_ASTC_5x5:
case Texture::InternalFormat::RGBA_ASTC_6x5:
case Texture::InternalFormat::RGBA_ASTC_6x6:
case Texture::InternalFormat::RGBA_ASTC_8x5:
case Texture::InternalFormat::RGBA_ASTC_8x6:
case Texture::InternalFormat::RGBA_ASTC_8x8:
case Texture::InternalFormat::RGBA_ASTC_10x5:
case Texture::InternalFormat::RGBA_ASTC_10x6:
case Texture::InternalFormat::RGBA_ASTC_10x8:
case Texture::InternalFormat::RGBA_ASTC_10x10:
case Texture::InternalFormat::RGBA_ASTC_12x10:
case Texture::InternalFormat::RGBA_ASTC_12x12:
return false;
// ETC2
case Texture::InternalFormat::ETC2_RGB8:
case Texture::InternalFormat::ETC2_RGB8_A1:
case Texture::InternalFormat::ETC2_EAC_RGBA8:
return false;
// DXT
case Texture::InternalFormat::DXT1_RGB:
case Texture::InternalFormat::DXT1_RGBA:
case Texture::InternalFormat::DXT3_RGBA:
case Texture::InternalFormat::DXT5_RGBA:
return false;
default:
return true;
}
}
其中TextureFormat类源代码如下:
/** Supported texel formats
* These formats are typically used to specify a texture's internal storage format.
*
* Enumerants syntax format
* ========================
*
* `[components][size][type]`
*
* `components` : List of stored components by this format.\n
* `size` : Size in bit of each component.\n
* `type` : Type this format is stored as.\n
*
*
* Name | Component
* :--------|:-------------------------------
* R | Linear Red
* RG | Linear Red, Green
* RGB | Linear Red, Green, Blue
* RGBA | Linear Red, Green Blue, Alpha
* SRGB | sRGB encoded Red, Green, Blue
* DEPTH | Depth
* STENCIL | Stencil
*
* \n
* Name | Type
* :--------|:---------------------------------------------------
* (none) | Unsigned Normalized Integer [0, 1]
* _SNORM | Signed Normalized Integer [-1, 1]
* UI | Unsigned Integer @f$ [0, 2^{size}] @f$
* I | Signed Integer @f$ [-2^{size-1}, 2^{size-1}-1] @f$
* F | Floating-point
*
*
* Special color formats
* ---------------------
*
* There are a few special color formats that don't follow the convention above:
*
* Name | Format
* :----------------|:--------------------------------------------------------------------------
* RGB565 | 5-bits for R and B, 6-bits for G.
* RGB5_A1 | 5-bits for R, G and B, 1-bit for A.
* RGB10_A2 | 10-bits for R, G and B, 2-bits for A.
* RGB9_E5 | **Unsigned** floating point. 9-bits mantissa for RGB, 5-bits shared exponent
* R11F_G11F_B10F | **Unsigned** floating point. 6-bits mantissa, for R and G, 5-bits for B. 5-bits exponent.
* SRGB8_A8 | sRGB 8-bits with linear 8-bits alpha.
* DEPTH24_STENCIL8 | 24-bits unsigned normalized integer depth, 8-bits stencil.
* DEPTH32F_STENCIL8| 32-bits floating-point depth, 8-bits stencil.
*
*
* Compressed texture formats
* --------------------------
*
* Many compressed texture formats are supported as well, which include (but are not limited to)
* the following list:
*
* Name | Format
* :----------------|:--------------------------------------------------------------------------
* EAC_R11 | Compresses R11UI
* EAC_R11_SIGNED | Compresses R11I
* EAC_RG11 | Compresses RG11UI
* EAC_RG11_SIGNED | Compresses RG11I
* ETC2_RGB8 | Compresses RGB8
* ETC2_SRGB8 | compresses SRGB8
* ETC2_EAC_RGBA8 | Compresses RGBA8
* ETC2_EAC_SRGBA8 | Compresses SRGB8_A8
* ETC2_RGB8_A1 | Compresses RGB8 with 1-bit alpha
* ETC2_SRGB8_A1 | Compresses sRGB8 with 1-bit alpha
*
*
* @see Texture
*/
enum class TextureFormat : uint16_t {
// 8-bits per element
R8, R8_SNORM, R8UI, R8I, STENCIL8,
// 16-bits per element
R16F, R16UI, R16I,
RG8, RG8_SNORM, RG8UI, RG8I,
RGB565,
RGB9_E5, // 9995 is actually 32 bpp but it's here for historical reasons.
RGB5_A1,
RGBA4,
DEPTH16,
// 24-bits per element
RGB8, SRGB8, RGB8_SNORM, RGB8UI, RGB8I,
DEPTH24,
// 32-bits per element
R32F, R32UI, R32I,
RG16F, RG16UI, RG16I,
R11F_G11F_B10F,
RGBA8, SRGB8_A8,RGBA8_SNORM,
UNUSED, // used to be rgbm
RGB10_A2, RGBA8UI, RGBA8I,
DEPTH32F, DEPTH24_STENCIL8, DEPTH32F_STENCIL8,
// 48-bits per element
RGB16F, RGB16UI, RGB16I,
// 64-bits per element
RG32F, RG32UI, RG32I,
RGBA16F, RGBA16UI, RGBA16I,
// 96-bits per element
RGB32F, RGB32UI, RGB32I,
// 128-bits per element
RGBA32F, RGBA32UI, RGBA32I,
// compressed formats
// Mandatory in GLES 3.0 and GL 4.3
EAC_R11, EAC_R11_SIGNED, EAC_RG11, EAC_RG11_SIGNED,
ETC2_RGB8, ETC2_SRGB8,
ETC2_RGB8_A1, ETC2_SRGB8_A1,
ETC2_EAC_RGBA8, ETC2_EAC_SRGBA8,
// Available everywhere except Android/iOS
DXT1_RGB, DXT1_RGBA, DXT3_RGBA, DXT5_RGBA,
DXT1_SRGB, DXT1_SRGBA, DXT3_SRGBA, DXT5_SRGBA,
// ASTC formats are available with a GLES extension
RGBA_ASTC_4x4,
RGBA_ASTC_5x4,
RGBA_ASTC_5x5,
RGBA_ASTC_6x5,
RGBA_ASTC_6x6,
RGBA_ASTC_8x5,
RGBA_ASTC_8x6,
RGBA_ASTC_8x8,
RGBA_ASTC_10x5,
RGBA_ASTC_10x6,
RGBA_ASTC_10x8,
RGBA_ASTC_10x10,
RGBA_ASTC_12x10,
RGBA_ASTC_12x12,
SRGB8_ALPHA8_ASTC_4x4,
SRGB8_ALPHA8_ASTC_5x4,
SRGB8_ALPHA8_ASTC_5x5,
SRGB8_ALPHA8_ASTC_6x5,
SRGB8_ALPHA8_ASTC_6x6,
SRGB8_ALPHA8_ASTC_8x5,
SRGB8_ALPHA8_ASTC_8x6,
SRGB8_ALPHA8_ASTC_8x8,
SRGB8_ALPHA8_ASTC_10x5,
SRGB8_ALPHA8_ASTC_10x6,
SRGB8_ALPHA8_ASTC_10x8,
SRGB8_ALPHA8_ASTC_10x10,
SRGB8_ALPHA8_ASTC_12x10,
SRGB8_ALPHA8_ASTC_12x12,
};