Android 使用壓縮紋理

本文介紹了什麼是壓縮紋理,以及加載壓縮紋理的核心步驟。並在 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的壓縮方式,但塊的大小卻較支持多種尺寸,比如從基本的4×4到12×12;每個塊內的內容用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官方編碼器