2009/08/21
OpenGLES2.0 DDS テクスチャを読み込む
DXT/BC 等の圧縮や Mipmap/Cubemap, HDR 対応などを考えると DDS は便利な
テクスチャフォーマットです。OpenGLES でも読めるようにしておきます。
まずは非圧縮の 2D 形式のみ考えます。
とりあえず適当に…
これ を書いた当人と思えないくらい いい加減 な判定ですが、DDS (Direct3D) と OpenGL
のフォーマットの対応はこんな感じになります。
後で説明しますがこの段階ではまだ不完全です。
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 です。
コンポーネントが UNSIGNED_SHORT にパックされている場合は下記の内容になります。
リトルエンディアンで読み込んだ 32bit 8888 と逆順です。
OpenGL の 32bit 8888 形式は、GL_UNSIGNED_BYTE というシンボル通り Byte
アクセスを想定しています。
● Direct3D の pixel 配列 (32bit 8888 の場合)
・ Direct3D9 以前
Direct3D9 まではフォーマット名称の表記が OpenGL と逆順でした。Direct3D 上は
ARGB と表記されますが、OpenGL の呼び方にあわせると BGRA 相当です。
これは下記の bit 並びを見るとよくわかります。OpenGL と比べてみてください。
リトルエンディアンなので D3DFMT_A8R8G8B8 をメモリに格納すると B G R A の順番
になります。
なお Direct3D は A8B8G8R8 という逆順フォーマットも持っており、両方使うことが
出来ます。こちらは OpenGL の 8888 と全く同じ並びです。
ピクセルだけでなく、頂点形式でも D3DDECLTYPE_D3DCOLOR, D3DDECLTYPE_UBYTE4
と 2種類のフォーマットを選択できました。これも上記 2種類の並び順に対応します。
2種類の並び順を持っているのは基本的に 32bit 8888 だけです。
A32B32G32R32F 等の HDR/浮動小数系のフォーマットはすべて A B G R で
OpenGL と同じ順番で配置されています。
◎ Direct3D10 以降
Direct3D は上記のように 32bit 8888 の場合だけ配列が 2種類あります。
Direct3D10 以降は、HDR/浮動小数や OpenGL 同様の順番でほぼ統一されました。
まずフォーマットの表記が逆順になります。
そして 32bit 8888 のデフォルトの配列が RGBA (R8G8B8A8) になりました。
もちろんこれまで通り B8G8R8A8 も使えます。
D3D9 以前と表記が逆になっているのでかなり ややこしい ことになります。
DDS テクスチャを作る場合、ほとんどのツールは Direct3D9 以前のフォーマット
表記になっているからです。
さらに Direct3D11 (DXGI1.1) では一見廃れるかと思われた B8G8R8A8 系が復活し
バリエーションが大幅に追加されています。
● Direct3D の pixel 配列 (16bit)
標準形式だった D3DFMT_A8R8G8B8 が ARGB の並びであることを思い出してください。
32bit で Alpha は最上位になります。
16bit にパックされた 565, 1555, 4444 も同様です。
OpenGLES の形式と Alpha の位置が異なっています。
これで正しく表示できなかった原因が判明しました。
Alpha が存在しない 565 だけ正しく表示できたのも納得です。
独立したコンポーネントを Byte アクセスする OpenGL と違い、Direct3D は
リトルエンディアンの 32bit (DWORD) で読み込んだときに、互換のある形式に
なっているわけです。
● DDS の読み込み続き
原因が判明したのでローダーの続きです。
読み込みと同時に互換性のないピクセル並びを変換します。
まずは上で省略してしまった DDS ヘッダの定義。(詳しくはこちらをどうぞ)
ピクセルの入れ替えです。
変換が必要な場所で呼び出します。
このコードは入力したメモリを直接書き換えている点に注意です。
DDSLoader() は、読み込んだ DDS ファイルのメモリイメージをそのまま受け取ります。
メモリイメージはテンポラリ相当なので、この仕様で問題になることはあまりないかもしれません。
もし DDSLoader() 終了後もすぐにメモリを解放せずに、同じデータで何度も
DDSLoader() を呼び出すような再利用があれば問題が生じます。
安全のためには バッファを const で受け取り、変換が必要な場合のみバッファを複製するよう
書き換えが必要かもしれません。
今回のローダーは Mipmap に対応していないので DDSLoader() 最後の 2行は重要です。
デフォルトで 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 もうひとつのユニファイド
テクスチャフォーマットです。OpenGLES でも読めるようにしておきます。
まずは非圧縮の 2D 形式のみ考えます。
とりあえず適当に…
これ を書いた当人と思えないくらい いい加減 な判定ですが、DDS (Direct3D) と OpenGL
のフォーマットの対応はこんな感じになります。
後で説明しますがこの段階ではまだ不完全です。
GLuint _flFASTCALL TextureCache::DDSLoader( void* mem ) { const T_DDSHEADER* dp= reinterpret_cast<T_DDSHEADER*>( mem ); int width= dp->dwWidth; int height= dp->dwHeight; void* data= reinterpret_cast<void*>( reinterpret_cast<intptr_t>( 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<unsigned int AShift, unsigned int AMask> static void _flFASTCALL DDStoGL16( unsigned int count, void* data ) { unsigned short* ptr= reinterpret_cast<unsigned short*>( 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<unsigned int*>( 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 もうひとつのユニファイド