OpenGL ESでETC1圧縮テクスチャを使う

PKMフォーマットに格納したETC1圧縮テクスチャを、AndroidからC言語でOpenGL ES 2.0に読み込ませてみました。

ETC1圧縮テクスチャは、OpenGL ESではオプション(GL_OES_compressed_ETC1_RGB8_texture)ですが、Android搭載のほぼ全てのモバイルGPUでサポートしています。ETC1テクスチャを格納するためによく使われるフォーマットとして、PKMとKTXがあります。KTXはDDSフォーマットのOpenGL版のようなもので、高機能ですが複雑です。今回は、シンプルなPKMフォーマットを使い、OpenGLへテクスチャをロードさせました。

■ETC1圧縮テクスチャとは

そもそも圧縮テクスチャ(Compressed Texture)は、GPUにおけるグラフィックスメモリの容量と帯域の削減を目的として使われています(どちらもモバイルSoCでは非常にボトルネックになりやすいポイントです)。圧縮テクスチャは一般に不可逆圧縮で、ハフマン符号化やランレングス符号化のようにビット長が可変長のフォーマットは採用されません。テクスチャのごく一部を読み込むだけでいいシーンで、わざわざ先頭からデコードしないといけないようなアルゴリズムでは困ります。そこで、多くの圧縮テクスチャは、テクスチャを小さなブロックに分割し、各ブロックに対して個別に圧縮します。Direct3DでサポートされているDXT1圧縮テクスチャ(OpenGLではGL_EXT_texture_compression_dxt1などで提供)では、4×4のブロックを単位とし、2つの16bit基準色と、4×4個の2bit補間値を持たせる構造になっています。つまり、1ブロックあたり2×16+4×4×2=64bitで、RGB16bitの非圧縮テクスチャに比べてたったの8分の1の容量で済みます。しかも、アルゴリズムが単純でハードウェアへの実装も簡単です。

しかし、DXT系のアルゴリズムはS3 Graphicsが特許を持っており、OpenGLとは(思想的な意味で)相性が良くありません。また、アルゴリズムが単純ゆえに品質が低く、ブロックノイズが見えたり、エッジにノイズが大量に乗ったりします。

ETC1はスウェーデンのエリクソン(正式にはテレフォナクティーボラーゲLMエリクソンらしい)が開発した特許フリーの圧縮テクスチャです。現在、Android搭載のSoCでETC1に対応しないものはありません(私が知る限りでは)。ARM Mali-400のように、ETC1以外の圧縮テクスチャをサポートしないGPUもあります。ETC1は、RGB24bitの非圧縮テクスチャを6分の1に圧縮します。アルファ値は格納できません。

ちなみに、iOSではPVRTC、Windows RTではBCxテクスチャのみが使え、ETC1は使えないようです。また、OpenGL ES 3.0やOpenGL 4.3からETC1の進化版にあたるETC2をサポートします。

■PKMフォーマット

ETC1/ETC2(以下、併せてETCnと略す)のためだけに作られたフォーマットで、非常にシンプルです。ETC1の旧名称がiPACKMANで、縮めてPKMになったのだと思われます。Android(2.2以降)ではandroid.opengl.ETC1クラスで読み書きすることができます。


struct etc1 {
  char magicNumber[4];
  char version[2];
  u16 type;
  u16 width;
  u16 height;
  u16 activeWidth;
  u16 activeHeight;
  char *data;
};

magicNumerは”PKM “(0x504B4D20)です。versionは”10″(0x3130)か”20″(0x3230)です。typeは普通0でETC1 RGBを表します。widthとheightは展開後の幅と高さです。activeWidthとactiveHeightはオリジナルの幅と高さです。ETC1は4×4ピクセルを1ブロックとして圧縮するので、activeWidthとactiveHeightを4を単位として切り上げるとwidthとheightになります。ここまでの16バイトがヘッダで、dataに実際の圧縮テクスチャが入ります。

圧縮テクスチャのデータのバイト数は、width×height×0.5で求められます。

バイトオーダはビッグエンディアンです。しかし、AndroidのARM版とx86版はリトルエンディアンなので、エンディアンの変換が必要です。しかもMIPS版はビッグエンディアンだそうで、プログラム側で条件分けしないと移植で問題が起きます。C言語なら”machine/_types.h”にある_BYTE_ORDERマクロで条件分けできますし、JavaならByteBufferのorderをBIG_ENDIANにすれば勝手に変換されます。が、個人的には独自に管理用XMLファイルなどを用意する方が楽な気がします。

KPMはミップマップをサポートしません。厳密にはバージョン”10″のみ対応しているそうですが、ツール側が対応していないことが多く、OpenGLのAPI呼び出しも複数回行わないといけないので、1ミップマップで1ファイルが一般的のようです。

■ETC1圧縮のためのツール

Mali GPU Texture Compression Toolがおすすめです。Windows版では、JPEGなどの画像ファイルを左側にドラッグ&ドロップし、File -> Compress selected images…を押すとオプション画面が現れ(このときOutput file formatをPKMにする)、OKを押すとデフォルトでユーザのホームフォルダ(C:/Users/[アカウント名]/)に*.pkmファイルができあがります。Compression speedをSlowにすると高品質になりますが、圧縮時間がかなり長くなります。また、Enable mipmapsを選択すると、1×1サイズになるまでの全レベルのミップマップが生成されます。

■OpenGL ESによるETC1圧縮テクスチャの読み込み

通常のテクスチャはglTexImage2D()を使いますが、圧縮テクスチャではglCompressedTexImage2D()を使います。PKMファイルを直接読み込ませる場合、fseek()などでヘッダを読み飛ばしてください。


glCompressedTexImage2D( GL_TEXTURE_2D, level, GL_ETC1_RGB8_OES, width, height, 0, numBytes, memAddr );

なお、OpenGL ES 2.0でミップマップを使用するときは、テクスチャサイズは2の冪乗でなければなりません。ただし、GL_NV_texture_npot_2D_mipmapなどのextensionがあれば制限が緩和されます。

JavaならばデータのInputStreamをandroid.openglETC1Util#createTextureに渡し、返されたETC1Util.ETC1TextureのgetData()メソッドをGLES20.glCompressedTexImage2Dの最後の引数に渡せばよいと思いますが、試していません。

■まとめ

AndroidならETC1はほとんど対応している

PKMファイルはビッグエンディアンでヘッダが16バイトある

glCompressedTexImage2D()とGL_ETC1_RGB8_OESを使う

■参考文献

Android OpenGL ES 2.0 の圧縮テクスチャ

opegl:texturefileformat

ETC1

ETC1Util.java

おしり

広告

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト / 変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト / 変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト / 変更 )

Google+ フォト

Google+ アカウントを使ってコメントしています。 ログアウト / 変更 )

%s と連携中