2013年6月25日火曜日

テクスチャ画像の圧縮

1メモリに確保するテクスチャ画像を圧縮する方法としてPVRTCアルゴリズムをiPhoneのGPUであるPowerVR MBX/SGXで利用可能です。

1. ファイルの変換方法

[準備] 
パスを追加
$ vi ~/.bash_profile 
export PATH=$PATH:/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/usr/bin

[利用方法]
$ texturetool  --bits-per-pixels-2 --channel-weighting-linear  ¥
    -e PVRTC -fPVR -o output.pvr  input.png

   --bits-per-pixels-2     2bit(サイズ1/16)    
   --bits-per-pixels-4    4bit(サイズ1/8)

   --channel-weighting-linear   誤差をRGBに均等に分散  
   --channel-weighting-perceptual  緑を優先に
  -m  ミップマップをつける(ファイルサイズが 1.3倍になる)

[変換サイズ例]
input.png     328kB(パレットカラー) 非圧縮だと4MB
output-2.pvr    256kB
output-4.pvr    512kB

2. テクスチャとして読み込む方法

const char PVRIdentifier[4] = { 'P', 'V', 'R', '!' };
enum Constants {
    PVR_FLAG_TYPE_PVRTC_2 = 24,
    PVR_FLAG_TYPE_PVRTC_4 = 25,
    PVR_FLAG_TYPE_MASK    = 0xff,
    PVR_MAX_SURFACES      = 16
};
struct Header {
    uint32_t headerSize;
    uint32_t height;
    uint32_t width;
    uint32_t numMipmaps;
    uint32_t flags;
    uint32_t dataSize;
    uint32_t bpp;
    uint32_t bitmaskRed;
    uint32_t bitmaskGreen;
    uint32_t bitmaskBlue;
    uint32_t bitmaskAlpha;
    uint32_t tag;
    uint32_t numSurfaces;
};
struct Surface {
    GLuint      size;
    const void* bits;
};

static void _loadCompressedTexture( const char* fname, 
    uint *texture, uint *_width, uint *_height) {
    
    char *buf;
    GLuint  width, height;
    GLenum format;
    //bool    hasAlpha;
    GLuint  numSurfaces;
    Surface surfaces[PVR_MAX_SURFACES];
    
    NSString* fileLocation = [[NSBundle mainBundle] 
           pathForResource:[NSString stringWithUTF8String:fname] ofType:@""];
    std::ifstream f;
    f.open([fileLocation UTF8String]);
    if(!f.good())
        return;
    
    f.seekg(0, std::ios::end);
    int size = f.tellg();
    f.seekg(0, std::ios::beg);
    buf = new char[size];
    f.read(buf, size);
    const Header& header = *reinterpret_cast(buf);
    uint32_t      tag    = OSSwapLittleToHostInt32(header.tag);
    if(PVRIdentifier[0] != ((tag >>  0) & 0xff) ||
       PVRIdentifier[1] != ((tag >>  8) & 0xff) ||
       PVRIdentifier[2] != ((tag >> 16) & 0xff) ||
       PVRIdentifier[3] != ((tag >> 24) & 0xff)) {
        return;
    }
    uint32_t flags       = OSSwapLittleToHostInt32(header.flags);
    uint32_t formatFlags = flags & PVR_FLAG_TYPE_MASK;
    if(formatFlags == PVR_FLAG_TYPE_PVRTC_4 
        || formatFlags == PVR_FLAG_TYPE_PVRTC_2) {
        if(formatFlags == PVR_FLAG_TYPE_PVRTC_4)
            format = GL_COMPRESSED_RGBA_PVRTC_4BPPV1_IMG;
        else if(formatFlags == PVR_FLAG_TYPE_PVRTC_2)
            format = GL_COMPRESSED_RGBA_PVRTC_2BPPV1_IMG;
        else
            return; // false;
        
        width       = OSSwapLittleToHostInt32(header.width);
        height      = OSSwapLittleToHostInt32(header.height);
       // hasAlpha    = OSSwapLittleToHostInt32(header.bitmaskAlpha) ? true : false;
        numSurfaces = 0;
        
        GLuint         w      = width;
        GLuint         h      = height;
        GLuint         offset = 0;
        GLuint         size   = OSSwapLittleToHostInt32(header.dataSize);
        const uint8_t* pBytes = reinterpret_cast(buf) + sizeof(header);
        
        while(offset < size && numSurfaces < PVR_MAX_SURFACES) {
            GLuint   blockSize, widthBlocks, heightBlocks, bpp;
            Surface& surface = surfaces[numSurfaces++];
            
            if (formatFlags == PVR_FLAG_TYPE_PVRTC_4) {
                blockSize    = 4 * 4;
                widthBlocks  = w / 4;
                heightBlocks = h / 4;
                bpp = 4;
            } else {
                blockSize    = 8 * 4;
                widthBlocks  = w / 8;
                heightBlocks = h / 4;
                bpp = 2;
            }
            
            if (widthBlocks < 2)
                widthBlocks = 2;
            if (heightBlocks < 2)
                heightBlocks = 2;
            
            surface.size = widthBlocks * heightBlocks * ((blockSize  * bpp) / 8);
            surface.bits = &pBytes[offset];
            
            (w >>= 1) || (w = 1);
            (h >>= 1) || (h = 1);
            offset += surface.size;
        }
        
        if(numSurfaces <= 0) return;
        
        // テクスチャに登録
        w = width;
        h = height;
        (*_width) = width;
        (*_height) = height;
        
        for(GLuint i = 0 ; i < numSurfaces ; ++i)
        {
            const Surface& surface = surfaces[i];

            glGenTextures(1, texture);
            glBindTexture(GL_TEXTURE_2D, (*texture) );
            // テクスチャの設定を行う
            // glEnable(GL_TEXTURE_2D);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
            glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
            glEnable(GL_BLEND);
            glDisable(GL_DEPTH_TEST);

            glCompressedTexImage2D(GL_TEXTURE_2D,
                                   i,
                                   format,
                                   w,
                                   h,
                                   0,
                                   surface.size,
                                   surface.bits);
            (w >>= 1) || (w = 1);
            (h >>= 1) || (h = 1);
        }
        glBindTexture(GL_TEXTURE_2D, 0 );
        free(buf);
    }
}

3. 結果

圧縮画像を使用するとメモリ使用量が50%以下に低減した(1/8, 1/16まで小さくはならなかった)。2bit/4bitの圧縮はイラストだと線の描画でジャギーが目立つ。

自然画像を多用する場合は、iPhoneに限りPVTRCを利用することを検討するのが良いと思う。

参考