目录
- 一、压缩纹理概念
- 二、OpenGL 接口
- 1.glCompressedTexImage2D
- 2.判断压缩纹理是否支持
- 三、压缩纹理加载
- 1.ETC1
- 2.ETC2
- 3.ASTC
- 四、总结
本文介绍了什么是压缩纹理,以及加载压缩纹理的核心步骤。并在 Android OpenGLES 平台上实现了压缩纹理的显示。
一、压缩纹理概念
传统的图片文件格式有 PNG 、 JPEG 等,这种类型的图片格式无法直接被 GPU 读取,需要先经过 CPU 解码后再上传到 GPU 使用,解码后的数据以 RGB(A) 形式存储,无压缩。
纹理压缩顾名思义是一种压缩的纹理格式,它通常会将纹理划分为固定大小的块(block)或者瓦片(tile),每个块单独进行压缩,整体显存占用更低,并且能直接被 GPU 读取和渲染(无需 CPU 解码)。
纹理压缩支持随机访问,随机访问是很重要的特性,因为纹理访问的模式高度随机,只有在渲染时被用到的部分才需要访问到,且无法提前预知其顺序。而且在场景中相邻的像素在纹理中不一定是相邻的 ,因此图形渲染性能高度依赖于纹理访问的效率。综上,相比普通格式图片,纹理压缩可以节省大量显存和 CPU 解码时间,且对 GPU 友好。
二、OpenGL 接口
想要使用 OpenGL 加载压缩纹理,只需要了解一个接口:glCompressedTexImage2D
。
1.glCompressedTexImage2D
接口声明如下,注释里说明了各参数的含义:
void glCompressedTexImage2D (GLenum target, | |
GLint level, | |
GLenum internalformat, // 格式 | |
GLsizei width, // 纹理宽度 | |
GLsizei height, // 纹理高度 | |
GLint border, | |
GLsizei imageSize, // 纹理数据大小 | |
const void *data) // 纹理数据 | |
所以加载一个压缩纹理,主要有以下几个要点:
- 获取到压缩纹理存储格式
- 获取压缩纹理的大小
- 获取压缩纹理的图像大小
2.判断压缩纹理是否支持
有的设备可能不支持压缩纹理,使用前需要进行判断。
std::string extensions = (const char*)glGetString(GL_EXTENSIONS); | |
if (extensions.find("GL_OES_compressed_ETC1_RGB8_texture")!= string::npos) { | |
// 支持 ETC1 纹理 | |
} | |
if (extensions.find("GL_OES_texture_compression_astc") != std::string::npos) { | |
// 支持 ASTC 纹理 | |
} |
三、压缩纹理加载
为了方便描述,定义了一个压缩纹理的结构体:
// 压缩纹理相关信息 | |
struct CompressedTextureInfo { | |
bool is_valid; // 是否为一个有效的压缩纹理信息 | |
GLsizei width; | |
GLsizei height; | |
GLsizei size; | |
GLenum internal_format; | |
GLvoid *data; | |
}; |
下面介绍ETC1、ETC2和ASTC格式的压缩纹理如何解析成CompressedTextureInfo
对象。
1.ETC1
ETC1格式是OpenGL ES图形标准的一部分,并且被所有的Android设备所支持。
扩展名为: GL_OES_compressed_ETC1_RGB8_texture,不支持透明通道,所以仅能用于不透明纹理。且要求大小是2次幂。
当加载压缩纹理时,参数支持如下格式: GL_ETC1_RGB8_OES(RGB,每个像素0.5个字节)
ETC1 压缩纹理的加载,主要参考了Android源码:etc1.cpp
解析 ETC1 纹理:
// 解析 ETC1 纹理 | |
static const CompressedTextureInfo ParseETC1Texture(unsigned char* data) { | |
CompressedTextureInfo textureInfo; | |
textureInfo.is_valid = false; | |
const etc1::etc1_byte *header = data; | |
if (!etc1::etc1_pkm_is_valid(header)) { | |
LogE("LoadTexture: etc1_pkm is not valid"); | |
return textureInfo; | |
} | |
unsigned int width = etc1::etc1_pkm_get_width(header); | |
unsigned int height = etc1::etc1_pkm_get_height(header); | |
GLuint size = 8 * ((width + 3) >> 2) * ((height + 3) >> 2); | |
GLvoid *texture_data = data + ETC1_PKM_HEADER_SIZE; | |
textureInfo.is_valid = true; | |
textureInfo.width = width; | |
textureInfo.height = height; | |
textureInfo.size = size; | |
textureInfo.internal_format = GL_ETC1_RGB8_OES; | |
textureInfo.data = texture_data; | |
return textureInfo; | |
} |
2.ETC2
ETC2 是 ETC1 的扩展,压缩比率一样,但压缩质量更高,而且支持透明通道,能完整存储 RGBA 信息。ETC2 需要 OpenGL ES 3.0(对应 WebGL 2.0)环境,目前还有不少低端 Android 手机不兼容,iOS 方面从 iPhone5S 开始都支持 OpenGL ES 3.0。ETC2 和 ETC1 一样,长宽可以不相等,但要求是 2 的幂次方。
首先定义好 ETC2 的 Header:
// etc2_texture.h | |
class Etc2Header { | |
public: | |
Etc2Header(const unsigned char *data); | |
unsigned short getWidth(void) const; | |
unsigned short getHeight(void) const; | |
unsigned short getPaddedWidth(void) const; | |
unsigned short getPaddedHeight(void) const; | |
GLsizei getSize(GLenum internalFormat) const; | |
private: | |
unsigned char paddedWidthMSB; | |
unsigned char paddedWidthLSB; | |
unsigned char paddedHeightMSB; | |
unsigned char paddedHeightLSB; | |
unsigned char widthMSB; | |
unsigned char widthLSB; | |
unsigned char heightMSB; | |
unsigned char heightLSB; | |
}; | |
// etc2_texture.cpp | |
Etc2Header::Etc2Header(const unsigned char *data) { | |
paddedWidthMSB = data[8]; | |
paddedWidthLSB = data[9]; | |
paddedHeightMSB = data[10]; | |
paddedHeightLSB = data[11]; | |
widthMSB = data[12]; | |
widthLSB = data[13]; | |
heightMSB = data[14]; | |
heightLSB = data[15]; | |
} | |
unsigned short Etc2Header::getWidth() const { | |
return (widthMSB << 8) | widthLSB; | |
} | |
unsigned short Etc2Header::getHeight() const { | |
return (heightMSB << 8) | heightLSB; | |
} | |
unsigned short Etc2Header::getPaddedWidth() const { | |
return (paddedWidthMSB << 8) | paddedWidthLSB; | |
} | |
unsigned short Etc2Header::getPaddedHeight() const { | |
return (paddedHeightMSB << 8) | paddedHeightLSB; | |
} | |
GLsizei Etc2Header::getSize(GLenum internalFormat) const { | |
if (internalFormat != GL_COMPRESSED_RG11_EAC | |
&& internalFormat != GL_COMPRESSED_SIGNED_RG11_EAC | |
&& internalFormat != GL_COMPRESSED_RGBA8_ETC2_EAC | |
&& internalFormat != GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC) { | |
return (getPaddedWidth() * getPaddedHeight()) >> 1; | |
} | |
return (getPaddedWidth() * getPaddedHeight()); | |
} |
解析 ETC2 数据:
// ETC2 魔数 | |
static const char kMagic[] = { 'P', 'K', 'M', ' ', '2', '0' }; | |
static const bool IsEtc2Texture(unsigned char *data) { | |
return memcmp(data, kMagic, sizeof(kMagic)) == 0; | |
} | |
static const CompressedTextureInfo ParseETC2Texture(unsigned char *data, GLenum internal_format) { | |
CompressedTextureInfo textureInfo; | |
textureInfo.is_valid = false; | |
if (!IsEtc2Texture(data)) { | |
LogE("ParseETC2Texture: not a etc2 texture"); | |
return textureInfo; | |
} | |
Etc2Header etc2Header(data); | |
textureInfo.is_valid = true; | |
textureInfo.width = etc2Header.getWidth(); | |
textureInfo.height = etc2Header.getHeight(); | |
textureInfo.size = etc2Header.getSize(internal_format); | |
textureInfo.internal_format = internal_format; | |
textureInfo.data = data + ETC2_PKM_HEADER_SIZE; | |
return textureInfo; | |
} |
3.ASTC
由ARM & AMD研发。ASTC同样是基于block的压缩方式,但块的大小却较支持多种尺寸,比如从基本的4x4到12x12;每个块内的内容用128bits来进行存储,因而不同的块就对应着不同的压缩率;相比ETC,ASTC不要求长宽是2的幂次方。
// ASTC 魔数 | |
const unsigned char ASTC_MAGIC_NUMBER[] = {0x13, 0xAB, 0xA1, 0x5C}; | |
// ASTC header declaration | |
typedef struct | |
{ | |
unsigned char magic[4]; | |
unsigned char blockdim_x; | |
unsigned char blockdim_y; | |
unsigned char blockdim_z; | |
unsigned char xsize[3]; /* x-size = xsize[0] + xsize[1] + xsize[2] */ | |
unsigned char ysize[3]; /* x-size, y-size and z-size are given in texels */ | |
unsigned char zsize[3]; /* block count is inferred */ | |
} AstcHeader; | |
static const bool IsAstcTexture(unsigned char* buffer) { | |
return memcmp(buffer, ASTC_MAGIC_NUMBER, sizeof(ASTC_MAGIC_NUMBER)) == 0; | |
} | |
static const CompressedTextureInfo ParseAstcTexture(unsigned char *data, GLenum internal_format) { | |
CompressedTextureInfo textureInfo; | |
textureInfo.is_valid = false; | |
if (internal_format < GL_COMPRESSED_RGBA_ASTC_4x4_KHR | |
|| internal_format > GL_COMPRESSED_SRGB8_ALPHA8_ASTC_12x12_KHR) { | |
LogE("parseAstcTexture: invalid internal_format=%d", internal_format); | |
return textureInfo; | |
} | |
if (!IsAstcTexture(data)) { | |
LogE("parseAstcTexture: not a astc file."); | |
return textureInfo; | |
} | |
// 映射为 ASTC 头 | |
AstcHeader* astc_data_ptr = (AstcHeader*) data; | |
int x_size = astc_data_ptr->xsize[0] + (astc_data_ptr->xsize[1] << 8) + (astc_data_ptr->xsize[2] << 16); | |
int y_size = astc_data_ptr->ysize[0] + (astc_data_ptr->ysize[1] << 8) + (astc_data_ptr->ysize[2] << 16); | |
int z_size = astc_data_ptr->zsize[0] + (astc_data_ptr->zsize[1] << 8) + (astc_data_ptr->zsize[2] << 16); | |
int x_blocks = (x_size + astc_data_ptr->blockdim_x - 1) / astc_data_ptr->blockdim_x; | |
int y_blocks = (y_size + astc_data_ptr->blockdim_y - 1) / astc_data_ptr->blockdim_y; | |
int z_blocks = (z_size + astc_data_ptr->blockdim_z - 1) / astc_data_ptr->blockdim_z; | |
unsigned int n_bytes_to_read = x_blocks * y_blocks * z_blocks << 4; | |
textureInfo.is_valid = true; | |
textureInfo.internal_format = internal_format; | |
textureInfo.width = x_size; | |
textureInfo.height = y_size; | |
textureInfo.size = n_bytes_to_read; | |
textureInfo.data = data; | |
return textureInfo; | |
} |
得到CompressedTextureInfo
对象后,即可进行压缩纹理的显示了:
CompressedTextureInfo textureInfo = etc1::ParseETC1Texture(input_data); | |
if (!textureInfo.is_valid) { | |
LogE("LoadTexture: etc1 textureInfo parsed invalid."); | |
} | |
GLuint texture_id = 0; | |
glGenTextures(1, &texture_id); | |
glBindTexture(GL_TEXTURE_2D, texture_id); | |
glCompressedTexImage2D(GL_TEXTURE_2D, | |
0, | |
textureInfo.internal_format, | |
textureInfo.width, | |
textureInfo.height, | |
0, | |
textureInfo.size, | |
textureInfo.data); |
四、总结
压缩纹理的加载,主要是搞清楚如何解析压缩纹理数据。一般而言,压缩纹理加载到内存后,都有一个 Header,通过 Header 可以解析出其宽高等信息,计算出纹理图像大小。最后调用glCompressedTexImage2D
方法即可渲染。
可见压缩纹理完全没有图像的解码工作,大大提升加载速度。
最后,介绍几款纹理压缩工具:
- etc2comp:支持生成etc2纹理
- etc1tool:支持生成etc1纹理,在Android SDK目录下,Android/sdk/platform-tools/etc1tool
- ISPCTextureCompressor:支持etc1、astc等
- astc-encoder:ASTC官方编码器