圧縮テクスチャフォーマットの wiki を更新しました。
関連ページ
・Texture File Format
OpenGL と Direct3D は Texture の RGB の並びが違っています。
Vista の D3D10 で一度 OpenGL の並び順に統一されましたが
Windows 7 以降、D3D11 で元の Windows 配列が復活しました。
DDS 形式のデータはデフォルトで Windows 順の配列になっています。
Windows : B8 G8 R8 A8 OpenGL : R8 G8 B8 A8
OpenGL ES 2.0 では BGRA は定義されておらず、Texture Swizzle も
ないので DDS の Texture を読み込む場合は色の並び順を反転させる
必要があります。
並びの違いについて詳しくは下記の記事を参照してください。
ですが結局 GPU としては OpenGL/D3D 両方に対応するものがほとんど
なので、この両者にそれほど違いはありません。
どちらかに決めてしまえばシェーダーでも入れ替えできます。
下記のデータを見ても、PowerVR, Adreno, Tegra は
GL_EXT_texture_format_BGRA8888 に対応していることがわかります。
・OpenGL ES Extension (Mobile GPU)
同じ PowerVR でも iOS の場合は GL_EXT_texture_format_BGRA8888
ではなく GL_APPLE_texture_format_BGRA8888 になっています。
どちらも DirectX の Windows 配列のピクセルをそのまま glTexImage2D() に
渡せる点は同じです。でも使い方が少し違います。
Extension InternalFormat Format Type --------------------------------------------------------------- EXT BGRA8888 GL_BGRA_EXT GL_BGRA_EXT GL_UNSIGNED_BYTE APPLE BGRA8888 GL_RGBA GL_BGRA_EXT GL_UNSIGNED_BYTE
おそらく内部で RGBA 並びに統一した方が効率的にアクセスできるのでしょう。
PowerVR は lowp の swizzle が苦手ということもありますし、
結局ロード時に自分で byte 並びを入れ替えても変わらないかもしれません。
Mali-400MP は GL_EXT_texture_format_BGRA8888 に対応していません。
Extension として GL_ARM_rgba8 がありますが、こちらは Texture ではなく
フレームバッファの並び順です。
関連エントリ
・OpenGLES2.0 DDS テクスチャを読み込む
昨日の記事に書いたように OpenGL / OpenGL ES の場合 KTX 形式の
テクスチャ読み込みが非常に簡単に出来ます。
ところが OpenGL ES 3.0 / OpenGL 4.3 の場合、ETC1 の読み込みに
問題がありました。
KTX のロード時に、ヘッダに GL_ETC1_RGB8_OES があれば
GL_COMPRESSED_RGB8_ETC2 に置き換える必要があります。
これは GPU が ETC2 に対応している場合だけです。
OpenGL ES 3.0 / OpenGL 4.3 で採用された ETC2 形式は ETC1 と
完全に上位互換性があります。
フォーマットも同一で、ETC1 は ETC2 の一部になりました。
そのため OpenGL ES 3.0 では事実上 ETC1 が無くなっています。
実際に Emulator (ES3 compatibility) で
glGetIntegerv( GL_COMPRESSED_TEXTURE_FORMATS, lists )
を列挙すると↓ ETC1 が含まれていないことがわかります。
tc[00]=83f0 GL_COMPRESSED_RGB_S3TC_DXT1_EXT tc[01]=83f2 GL_COMPRESSED_RGBA_S3TC_DXT3_EXT tc[02]=83f3 GL_COMPRESSED_RGBA_S3TC_DXT5_EXT tc[03]=8b90 GL_PALETTE4_RGB8 tc[04]=8b91 GL_PALETTE4_RGBA8 tc[05]=8b92 GL_PALETTE4_R5_G6_B5 tc[06]=8b93 GL_PALETTE4_RGBA4 tc[07]=8b94 GL_PALETTE4_RGB5_A1 tc[08]=8b95 GL_PALETTE8_RGB8 tc[09]=8b96 GL_PALETTE8_RGBA8 tc[10]=8b97 GL_PALETTE8_R5_G6_B5 tc[11]=8b98 GL_PALETTE8_RGBA4 tc[12]=8b99 GL_PALETTE8_RGB5_A1 tc[13]=9274 GL_COMPRESSED_RGB8_ETC2 tc[14]=9275 GL_COMPRESSED_SRGB8_ETC2 tc[15]=9276 GL_COMPRESSED_RGB8_PUNCHTHROUGH_ALPHA1_ETC2 tc[16]=9277 GL_COMPRESSED_SRGB8_PUNCHTHROUGH_ALPHA1_ETC2 tc[17]=9278 GL_COMPRESSED_RGBA8_ETC2_EAC tc[18]=9279 GL_COMPRESSED_SRGB8_ALPHA8_ETC2_EAC tc[19]=9270 GL_COMPRESSED_R11_EAC tc[20]=9271 GL_COMPRESSED_SIGNED_R11_EAC tc[21]=9272 GL_COMPRESSED_RG11_EAC tc[22]=9273 GL_COMPRESSED_SIGNED_RG11_EAC
ドキュメントによれば ETC1 画像を読み込む場合 API に
GL_COMPRESSED_RGB8_ETC2 を渡すことになっています。
そのため KTX に ETC1 が含まれている場合、OpenGL 4.3 / OpenGL ES 3.0
ではヘッダの値をそのまま使うとエラーになります。
bool HasFormat( GLenum format ) { GLint numformat= 0; glGetIntegerv( GL_NUM_COMPRESSED_TEXTURE_FORMATS, &numformat ); const int MAX_TLISTBUFFER= 128; GLint lists[MAX_TLISTBUFFER]; glGetIntegerv( GL_COMPRESSED_TEXTURE_FORMATS, lists ); for( int fi= 0 ; fi< numformat ; fi++ ){ if( lists[fi] == format ){ return true; } } return false; } GLuint KTX_Loader_Compressed( const void* memory, size_t memory_size ) { const KTX_Header* hp= reinterpret_cast( memory ); const void* image_data= address_add( memory, sizeof(KTX_Header) + hp->bytesOfKeyValueData ); unsigned int image_size= *reinterpret_cast ( image_data ); image_data= address_add( image_data, sizeof(int32_t) ); GLuint texid= 0; glGenTextures( 1, &texid ); glBindTexture( GL_TEXTURE_2D, texid ); GLenum cformat= dp->glInternalFormat; if( cformat == GL_ETC1_RGB8_OES && HasFormat( GL_COMPRESSED_RGB8_ETC2 ) ){ // **** ここで ETC2 に置き換える **** cformat= GL_COMPRESSED_RGB8_ETC2; } glCompressedTexImage2D( GL_TEXTURE_2D, 0, cformat, hp->pixelWidth, hp->pixelHeight, 0, image_size, image_data ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); return texid; }
OpenGL / OpenGL ES の場合 KTX 形式のテクスチャの読み込みは簡単です。
ファイルを全部メモリに読み込んだと仮定して
const void* address_add( const void* a, uintptr_t b ) { return reinterpret_cast( reinterpret_cast ( a ) + b ); } GLuint KTX_Loader( const void* memory, size_t memory_size ) { const KTX_Header* hp= reinterpret_cast ( memory ); const void* image_data= address_add( memory, sizeof(KTX_Header) + hp->bytesOfKeyValueData ); image_data= address_add( image_data, sizeof(int32_t) ); GLuint texid= 0; glGenTextures( 1, &texid ); glBindTexture( GL_TEXTURE_2D, texid ); glTexImage2D( GL_TEXTURE_2D, 0, hp->glInternalFormat, hp->pixelWidth, hp->pixelHeight, 0, hp->glBaseInternalFormat, hp->glType, image_data ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); return texid; }
このように、API に渡す必要な情報が全部ヘッダに入っています。
ヘッダの構造は下記を参照してください。
・KTX File Format Specification
圧縮テクスチャの場合も簡単です。glType が 0 なら圧縮テクスチャです。
GLuint KTX_Loader_Compressed( const void* memory, size_t memory_size ) { const KTX_Header* hp= reinterpret_cast( memory ); const void* image_data= address_add( memory, sizeof(KTX_Header) + hp->bytesOfKeyValueData ); unsigned int image_size= *reinterpret_cast ( image_data ); image_data= address_add( image_data, sizeof(int32_t) ); GLuint texid= 0; glGenTextures( 1, &texid ); glBindTexture( GL_TEXTURE_2D, texid ); glCompressedTexImage2D( GL_TEXTURE_2D, 0, hp->glInternalFormat, hp->pixelWidth, hp->pixelHeight, 0, image_size, image_data ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); return texid; }
上の例はそのまま API に渡しているので、GPU が対応していない圧縮形式の場合
エラーが発生する可能性があります。
厳密には CPU とファイルの endian が一致していないケースを考慮しなければなりません。
ヘッダの endiannness を見て必要に応じて byte swap します。
データ作成環境と実行環境が一致していればあまり問題はないと思います。
KTX の注意点は DDS とデータの並びが異なっていることです。
例えば cubemap の場合、DDS は face 毎に mipmap が並びますが、
KTX は同じ mip level の face が先に来ます。
DDS KTX -------------------------- [X+ 32x32] [X+ 32x32] [X+ 16x16] [X- 32x32] [X+ 8x8] [Y+ 32x32] [X+ 4x4] [Y- 32x32] [X+ 2x2] [Z+ 32x32] [X+ 1x1] [Z- 32x32] [X- 32x32] [X+ 16x16] [X- 16x16] [X- 16x16] [X- 8x8] [Y+ 16x16] [X- 4x4] [Y- 16x16] [X- 2x2] [Z+ 16x16] [X- 1x1] [Z- 16x16] [Y+ 32x32] [X+ 8x8] [Y+ 16x16] [X- 8x8] [Y+ 8x8] [Y+ 8x8] [Y+ 4x4] [Y- 8x8] [Y+ 2x2] [Z+ 8x8] [Y+ 1x1] [Z- 8x8] : :
↑このようにデータの順番が異なっています。
wiki にも追記しましたが、データの入れ子構造の詳細は下記の通りです。
DDS : ( ( ( ( width * height ) * volume_depth ) * mip ) * cube_face ) * array KTX : ( ( ( ( width * height ) * volume_depth ) * cube_face ) * array ) * mip
DDS の利点は、2D のテクスチャを 6 個並べるだけで cubemap ができること。
KTX の利点は同じサイズのサーフェースを出来るだけ一度に並べようとしていることです。
KTX の画像は下記のように 32bit のデータサイズが挿入されます。
int32 imageSize; // 32x32 [X+ 32x32] [X- 32x32] [Y+ 32x32] [Y- 32x32] [Z+ 32x32] [Z- 32x32] int32 imageSize; // 16x16 [X+ 16x16] [X- 16x16] [Y+ 16x16] [Y- 16x16] [Z+ 16x16] [Z- 16x16] int32 imageSize; // 8x8 [X+ 8x8] [X- 8x8] [Y+ 8x8] [Y- 8x8] [Z+ 8x8] [Z- 8x8] int32 imageSize; // 4x4 ~
imageSize は 1画像分の byte 数で、同じサイズのサーフェースが
並んでいるためアドレス計算が簡単になります。
glCompressedTexImage2D() にそのまま渡すことができます。
mipmap に対応するとこんな感じです。
GLuint KTX_Loader_Mipmap( const void* memory, size_t memory_size ) { const KTX_Header* hp= reinterpret_cast( memory ); const void* image_data= address_add( memory, sizeof(KTX_Header) + hp->bytesOfKeyValueData ); GLuint texid= 0; glGenTextures( 1, &texid ); glBindTexture( GL_TEXTURE_2D, texid ); unsigned int mip_level= hp->numberOfMipmapLevels > 0 ? dp->numberOfMipmapLevels : 1; unsigned int width= hp->pixelWidth; unsigned int height= hp->pixelHeight; for( unsigned int mip= 0 ; mip < mip_level ; mip++ ){ unsigned int image_size= *reinterpret_cast ( image_data ); image_data= address_add( image_data, sizeof(int32_t) ); glTexImage2D( GL_TEXTURE_2D, mip, hp->glInternalFormat, width, height, 0, hp->glBaseInternalFormat, hp->glType, image_data ); width= int_max( width>>1, 1 ); height= int_max( height>>1, 1 ); image_data= address_add( image_data, (image_size+ 3) & ~3 ); } if( mip_level > 1 ){ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR ); }else{ glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); } glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); return texid; }
これで mip + 圧縮テクスチャ や cubemap への対応も容易にできます。
KTX の一番の問題はデータの作成方法にあるようです。
ツールが絶望的に揃っていない状況で、まだあまり安定もしていません。
ARM の Texture Compression Tool は 64bit 環境でうまく動かず、
PVR の TexTool で ktx に出力すると PVRTC 以外は正しいヘッダ値が入りませんでした。
関連ページ
・Texture File Format
DXT テクスチャの格納や cubemap, 3D Texture, HDR 形式など、
DDS は非常に便利なファイルフォーマットでした。
ところが DirectX は仕様自体に圧縮テクスチャが含まれているため
DXT,BC 以外のフォーマットが定義されていません。
ATITC や ETC1 など、独自の FourCC で対応しているツールもあります。
同時に PVRTC/PVRTC2、ETC2/EAC や ASTC など、DDS に格納できない
データも増えています。
KTX 形式は OpenGL の値をそのまま格納するので、Extension など
GLenum 値が決まればどんな形式も格納することができます。
今後 PVRTC2, ETC2/EAC, ASTC が出てくることを考えると ktx への
対応をきちんと考えておいた方が良さそうです。
テクスチャのファイルフォーマットについて下記のページにまとめました。
取り敢えず dds, ktx, pvr, pkm の 4 種類。
関連エントリ
・Android OpenGL ES 2.0 の圧縮テクスチャ
・Direct3D10 と DDS テクスチャフォーマット
・DirectX SDK November 2007 Gather と 新DDS フォーマット