月別アーカイブ: 2012年8月

OpenGL ES 2.0 BGRA Texture を直接読む

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 を読み込む場合は色の並び順を反転させる
必要があります。

並びの違いについて詳しくは下記の記事を参照してください。

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

ですが結局 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 ETC1 の互換性と KTX の落とし穴

昨日の記事に書いたように 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 Texture の読み込み方

OpenGL / OpenGL ES KTX Texture の読み込み方

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

関連エントリ
OpenGL / OpenGL ES テクスチャファイルフォーマット KTX と DDS

OpenGL / OpenGL ES テクスチャファイルフォーマット KTX と DDS

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 種類。

Texture File Format

関連エントリ
Android OpenGL ES 2.0 の圧縮テクスチャ
Direct3D10 と DDS テクスチャフォーマット
DirectX SDK November 2007 Gather と 新DDS フォーマット