OpenGLES2.0 DDS テクスチャを読み込む

DXT/BC 等の圧縮や Mipmap/Cubemap, HDR 対応などを考えると DDS は便利な
テクスチャフォーマットです。OpenGLES でも読めるようにしておきます。
まずは非圧縮の 2D 形式のみ考えます。

とりあえず適当に…
これ を書いた当人と思えないくらい いい加減 な判定ですが、DDS (Direct3D) と OpenGL
のフォーマットの対応はこんな感じになります。
後で説明しますがこの段階ではまだ不完全です。

GLuint _flFASTCALL
TextureCache::DDSLoader( void* mem )
{
    const T_DDSHEADER*	dp= reinterpret_cast( mem );
    int     width= dp->dwWidth;
    int     height= dp->dwHeight;
    void*   data= reinterpret_cast( reinterpret_cast( mem ) + sizeof(T_DDSHEADER) );

    GLenum  format= GL_NONE;
    GLenum  ftype= GL_UNSIGNED_BYTE;

    switch( dp->dwRGBBitCount ){
    case 32: // 8888
        switch( dp->dwRBitMask ){
        case 0x000000ff: // R G B A = GL
            format= GL_RGBA;
            ftype= GL_UNSIGNED_BYTE;
            break;
        case 0x00ff0000: // B G R A = DX
            format= GL_RGBA;
            ftype= GL_UNSIGNED_BYTE;
            break;
        }
        break;
    case 24: // 888
        switch( dp->dwRBitMask ){
        case 0x0000ff: // R G B = GL
            format= GL_RGB;
            ftype= GL_UNSIGNED_BYTE;
            break;
        case 0xff0000: // B G R = DX
            format= GL_RGB;
            ftype= GL_UNSIGNED_BYTE;
            break;
        }
        break;
    case 16:
        switch( dp->dwGBitMask ){
        case 0x00f0: // 4444
            format= GL_RGBA;
            ftype= GL_UNSIGNED_SHORT_4_4_4_4;
            break;
        case 0x07e0: // 565
            format= GL_RGB;
            ftype= GL_UNSIGNED_SHORT_5_6_5;
            break;
        case 0x03e0: // 1555
            format= GL_RGBA;
            ftype= GL_UNSIGNED_SHORT_5_5_5_1;
            break;
        case 0x0000: // L A 88
            format= GL_LUMINANCE_ALPHA;
            ftype= GL_UNSIGNED_BYTE;
            break;
        }
        break;
    case 8:
        if( dp->dwRGBAlphaBitMask ){
            format= GL_ALPHA;
            ftype= GL_UNSIGNED_BYTE;
        }else{
            format= GL_LUMINANCE;
            ftype= GL_UNSIGNED_BYTE;
        }
        break;
    }

    GLuint  texid= 0;
    glGenTextures( 1, &texid );
    glBindTexture( GL_TEXTURE_2D, texid );

    glTexImage2D(
            GL_TEXTURE_2D,
            0,      // level
            format, // internal format
            width,
            height,
            0,      // border
            format,
            ftype,
            data
            );

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

    return  texid;
}

OpenGLES で対応できそうなのは下記の各フォーマットです。
A8R8G8B8, A8B8G8R8, R8G8B8, A4R4G4B4, R5G6B5, A1R5G5B5, A8L8, A8, L8
でも実際に表示してみると正しく出るのは一部だけ。
A8R8G8B8, R8G8B8, A4R4G4B4, A1R5G5B5 は色ずれがあり失敗します。

Direct3D と OpenGL ではピクセル内の配置が異なっているようです。
調べてみました。

● OpenGL の pixel 配列

原則として R G B A の順番となります。

各コンポーネント(RGB)が独立している場合は、メモリ上に R G B A の順で配置
されます。たとえば 32bit 8888 がそうです。Byte 単位で書き込まれています。
64bit, 128bit といった Float 形式も同様です。
この順番は x y z w の並びに一致するため、シェーダーから見ても自然なものです。

ただし 8888 を DWORD (32bit) で読み込んだ場合、リトルエンディアンでは下記の
bit 並びになります。最下位が R で最上位が Alpha です。

31 H<---------------->L 0
|  A  |  B  |  G  |  R  |    GL_RGBA + GL_UNSIGNED_BYTE  (R8 G8 B8 A8)

コンポーネントが UNSIGNED_SHORT にパックされている場合は下記の内容になります。
リトルエンディアンで読み込んだ 32bit 8888 と逆順です。

15 H<-------->L 0
| R | G | B | A |          GL_RGBA + GL_UNSIGNED_SHORT_4_4_4_4  (R4 G4 B4 A4)

15 H<-------->L 0
| R  | G  | B |A|          GL_RGBA + GL_UNSIGNED_SHORT_5_5_5_1  (R5 G5 B5 A1)

15 H<-------->L 0
| R  |  G  | B  |          GL_RGB + GL_UNSIGNED_SHORT_5_6_5  (R5 G6 B5)

OpenGL の 32bit 8888 形式は、GL_UNSIGNED_BYTE というシンボル通り Byte
アクセスを想定しています。

● Direct3D の pixel 配列 (32bit 8888 の場合)

・ Direct3D9 以前

Direct3D9 まではフォーマット名称の表記が OpenGL と逆順でした。Direct3D 上は
ARGB と表記されますが、OpenGL の呼び方にあわせると BGRA 相当です。
これは下記の bit 並びを見るとよくわかります。OpenGL と比べてみてください。

31 H<---------------->L 0
|  A  |  R  |  G  |  B  |       D3DFMT_A8R8G8B8

リトルエンディアンなので D3DFMT_A8R8G8B8 をメモリに格納すると B G R A の順番
になります。

なお Direct3D は A8B8G8R8 という逆順フォーマットも持っており、両方使うことが
出来ます。こちらは OpenGL の 8888 と全く同じ並びです。

31 H<---------------->L 0
|  A  |  B  |  G  |  R  |       D3DFMT_A8B8G8R8

ピクセルだけでなく、頂点形式でも D3DDECLTYPE_D3DCOLOR, D3DDECLTYPE_UBYTE4
と 2種類のフォーマットを選択できました。これも上記 2種類の並び順に対応します。

2種類の並び順を持っているのは基本的に 32bit 8888 だけです。
A32B32G32R32F 等の HDR/浮動小数系のフォーマットはすべて A B G R で
OpenGL と同じ順番で配置されています。

◎ Direct3D10 以降

Direct3D は上記のように 32bit 8888 の場合だけ配列が 2種類あります。
Direct3D10 以降は、HDR/浮動小数や OpenGL 同様の順番でほぼ統一されました。

まずフォーマットの表記が逆順になります。

 D3D9     D3D10/11/OpenGL
 ------------------------
 ARGB  →   BGRA
 ABGR  →   RGBA

そして 32bit 8888 のデフォルトの配列が RGBA (R8G8B8A8) になりました。
もちろんこれまで通り B8G8R8A8 も使えます。

31 H<---------------->L 0
|  A  |  B  |  G  |  R  |     DXGI_FORMAT_R8G8B8A8_UNORM  (== D3DFMT_A8B8G8R8)

31 H<---------------->L 0
|  A  |  R  |  G  |  B  |     DXGI_FORMAT_B8G8R8A8_UNORM   == D3DFMT_A8R8G8B8)

D3D9 以前と表記が逆になっているのでかなり ややこしい ことになります。
DDS テクスチャを作る場合、ほとんどのツールは Direct3D9 以前のフォーマット
表記になっているからです。

さらに Direct3D11 (DXGI1.1) では一見廃れるかと思われた B8G8R8A8 系が復活し
バリエーションが大幅に追加されています。

● Direct3D の pixel 配列 (16bit)

標準形式だった D3DFMT_A8R8G8B8 が ARGB の並びであることを思い出してください。
32bit で Alpha は最上位になります。
16bit にパックされた 565, 1555, 4444 も同様です。

31 H<---------------->L 0
|  A  |  R  |  G  |  B  |     D3DFMT_A8R8G8B8

15 H<-------->L 0
| A | R | G | B |             D3DFMT_A4R4G4B4

15 H<-------->L 0
|A| R  | G  | B |             D3DFMT_A1R5G5B5

15 H<-------->L 0
| R  |  G  | B  |             D3DFMT_R5G6B5

OpenGLES の形式と Alpha の位置が異なっています。
これで正しく表示できなかった原因が判明しました。
Alpha が存在しない 565 だけ正しく表示できたのも納得です。

独立したコンポーネントを Byte アクセスする OpenGL と違い、Direct3D は
リトルエンディアンの 32bit (DWORD) で読み込んだときに、互換のある形式に
なっているわけです。

● DDS の読み込み続き

原因が判明したのでローダーの続きです。
読み込みと同時に互換性のないピクセル並びを変換します。
まずは上で省略してしまった DDS ヘッダの定義。(詳しくはこちらをどうぞ)

#define	DDSCAPS_MIPMAP	0x00400000
struct T_DDSHEADER {
    unsigned int    dwMagic;
    unsigned int    dwSize;
    unsigned int    dwFlags;
    unsigned int    dwHeight;
    unsigned int    dwWidth;
    unsigned int    dwPitchOrLinearSize;
    unsigned int    dwDepth;
    unsigned int    dwMipMapCount;
    unsigned int    dwReserved1[11];
    unsigned int    dwPfSize;
    unsigned int    dwPfFlags;
    unsigned int    dwFourCC;
    unsigned int    dwRGBBitCount;
    unsigned int    dwRBitMask;
    unsigned int    dwGBitMask;
    unsigned int    dwBBitMask;
    unsigned int    dwRGBAlphaBitMask;
    unsigned int    dwCaps;
    unsigned int    dwCaps2;
    unsigned int    dwReservedCaps[2];
    unsigned int    dwReserved2;
};
#define	__DDS__MAGIC	0x20534444	// ' SDD'

ピクセルの入れ替えです。

template
static void _flFASTCALL
DDStoGL16( unsigned int count, void* data )
{
    unsigned short*	ptr= reinterpret_cast( data );
    for(; count-- ;){
        unsigned int	color= *ptr;
        unsigned int	alpha= color & AMask;
        *ptr++= (color << (16-AShift)) | (alpha >> AShift);
    }
}

static void _flFASTCALL
DDStoGL32( unsigned int count, void* data )
{
    unsigned int*	ptr= reinterpret_cast( data );
    for(; count-- ;){
        unsigned int	color= *ptr;
        *ptr++= (color & 0xff00ff00)
                |((color >> 16) & 0xff)
                |((color << 16) & 0xff0000);
    }
}

変換が必要な場所で呼び出します。

  case 32: // 8888
        switch( dp->dwRBitMask ){
        ~
        case 0x00ff0000: // B G R A = DX
            format= GL_RGBA;
            ftype= GL_UNSIGNED_BYTE;
            DDStoGL32( width*height, data );      // ← ここ
            break;
        }
        break;
    case 16:
        switch( dp->dwGBitMask ){
        case 0x00f0: // 4444
            format= GL_RGBA;
            ftype= GL_UNSIGNED_SHORT_4_4_4_4;
            DDStoGL16<12,0xf000>( width*height, data );     // ← ここ
            break;
        case 0x07e0: // 565
            ~
        case 0x03e0: // 1555
            format= GL_RGBA;
            ftype= GL_UNSIGNED_SHORT_5_5_5_1;
            DDStoGL16<15,0x8000>( width*height, data );     // ← ここ
            break;
        case 0x0000: // L A 88
            ~

このコードは入力したメモリを直接書き換えている点に注意です。

DDSLoader() は、読み込んだ DDS ファイルのメモリイメージをそのまま受け取ります。
メモリイメージはテンポラリ相当なので、この仕様で問題になることはあまりないかもしれません。
もし DDSLoader() 終了後もすぐにメモリを解放せずに、同じデータで何度も
DDSLoader() を呼び出すような再利用があれば問題が生じます。
安全のためには バッファを const で受け取り、変換が必要な場合のみバッファを複製するよう
書き換えが必要かもしれません。

今回のローダーは Mipmap に対応していないので DDSLoader() 最後の 2行は重要です。

    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );

デフォルトで MIPMAP 有効になっていることがあったためです。
明示的に MIPMAP を切っておかないと真っ黒なテクスチャが表示されることがあります。

長くなったので Mipmap 対応は次回にします。
書き込んでから気がつきました。24bit 888 の対応も忘れていました。

関連エントリ
OpenGLES2.0 Direct3D とのフォーマット変換
OpenGLES 2.0 頂点フォーマットの管理
OpenGLES2.0 の頂点
OpenGLES2.0 D3D座標系
OpenGLES2.0 シェーダー管理
Direct3D11/DirectX11 (4) FeatureLevel と旧 GPU の互換性、テクスチャ形式など
D3D関連 DDSテクスチャの取り扱い
Direct3D もうひとつのユニファイド