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

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