OpenGLの話 第4回
テクスチャの使い方 14/01/19 up
年明け一発目はOpenGLでのテクスチャの使い方です。
テクスチャフォーマットとしては.ddsを利用しています。
テクスチャ読み込み部分はDirectXTexを参考にしています。
すべてのフォーマットで正常に動作するかは確認していませんが、たぶん大丈夫じゃないかな…
以前、libpngでも使ってテクスチャ読み込みをやろうかな、とか言ってたのですが、結局.ddsにしたのにはいくつか理由があります。
比較的簡単に実装できるし、外部ライブラリなしでも実装が難しくないというのが1つの理由です。
また、.ddsはゲームのテクスチャとしてよく使われるミップマップやキューブマップにも対応しています。
一般的な画像フォーマットではこれらのフォーマットに対応していません(拡張機能を利用することでなんとかなるやつもあるかもしれませんが)。
オリジナルフォーマットを採用するのも1つの手ではあるのですが、オーサリングツールに対応させるのが難しいという問題があります。
DirectXで使用できるフォーマットに限られますが、圧縮テクスチャにも対応しているという強みがあります。
弱点を上げるとするなら、DirectXで使用できない圧縮テクスチャに対応していないという点です。
OpenGLで主に使われるETCや、ハードウェア固有の圧縮フォーマット(PowerVRのPVRTCなど)はモバイルプラットフォームではよく使われています。
モバイルプラットフォームはDirectXで使用できるBC1~7のフォーマットはたいてい使えません。
逆にWindows用のハードウェアではBC1~7以外が使えないということも多いです。
とはいえ、モバイルプラットフォームも一括りにできるわけではなく、結局のところ、各プラットフォームごとにリソースを用意してやる必要があります。
なので読み込み命令もプラットフォームごとに分けた方がいいのではないかと個人的には思います。
今回はWindowsPCメインということで.ddsのみ対応となっています。
テクスチャの読み込みとオブジェクト生成命令はgl_texture.hで宣言されているLoadDDSFromMemory()、もしくはLoadDDSFromFile()で行います。
これらの関数の戻り値がテクスチャオブジェクトのIDです。0 なら読み込みに失敗しています。
ファイルから読み込む場合はFileReaderを継承したファイル読み込み用のクラスを用意する必要があります。
プラットフォームやアプリに合わせて実装を変更することができます。
現在対応しているのは2Dテクスチャのみです。ミップマップは対応しています。
テクスチャパラメータとして設定しているのはミップマップの最大レベルだけです。
フィルタやラップモードはサンプラステートで設定するようにしています。
さて、OpenGLのテクスチャを利用する場合、DirectXとの細かい違いで戸惑うことがあるかと思います。
実際、自分も最初にテクスチャを描画しようとして失敗して、エラーも出てないから何が悪いのかわからないという状況に陥りました。
細かな違いが結構大きな問題を引き起こすこともあるので注意が必要です。
例えば、これはテクスチャに限った話ではないですが、OpenGLの場合はオブジェクトの生成と初期化は別々に分かれています。
テクスチャを例にとると、DirectXではId3D11Device::CreateTexture2D()にデスクリプションを与えて生成します。
デスクリプションにはテクスチャのサイズ、ミップマップレベル、フォーマットといった諸情報を書き込んでおき、それに合わせてオブジェクトが生成されます。
OpenGLの場合、glGenTextures()関数で1つ以上のテクスチャオブジェクトを生成し、そのIDを返してもらいます。
返ってきたIDはそのままではテクスチャとして使用できませんので、glTexImage2D()、もしくはglCompressedTexImage2D()で初期化します。
こうしてテクスチャが使用できるようになるわけですが、OpenGLではシェーダやステートオブジェクトなんかもこのような形で生成、および初期化をする必要があります。
問題点としては、初期化の際に設定する項目を忘れてしまうことです。
例えばテクスチャの場合、ミップマップレベルの設定はglTexParameteri()関数を利用して設定します。
ミップマップが存在しないテクスチャを作成する場合、glTexImage2D()関数はレベル0の1回だけで済みます。
しかし、DirectXと違って、この段階ではミップマップレベルが最大まであるものと認識されてしまいます。
この状態でミップマップフィルタありでテクスチャフェッチを行うと、存在していないミップマップレベルのイメージ、つまり初期化されていないイメージをフェッチしてくることになります。
こうなると真っ黒のカラーしか取得できず、テクスチャの生成に失敗しているように見えてしまいます。
私はこの問題で半日を費やしました。
ミップマップの最大レベルは以下のように設定します。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, miplevel - 1);
ミップマップレベルから1を減算しているのは、このパラメータがミップマップの最大レベルを設定する命令だからです。
ミップマップレベルが1なら、ミップマップイメージは1枚だけ、つまりレベル0のみが存在していることになります。
最大レベルは実際に存在するミップマップレベルのインデックスを指定するので、ミップマップレベルが1なら0を設定する必要があるというわけです。
もしもこのパラメータを設定していないのであれば、ミップマップフィルタを利用しないフィルタパラメータを設定してやればOKだったりしますが、それはそれで面倒なので必ず設定するようにした方がいいでしょう。
次にわかりにくいのはglBindTexture()関数でしょうか。
このglBindTexture()には2つの使用用途があるのですが、関数自体は1つしかないので両方の用途を同時に満たしてしまいます。
1つ目の使用用途はテクスチャパラメータの設定や初期化への利用です。
先に紹介したglTexImage2D()やglTexParameteri()は第1引数で指定されるテクスチャの種類に対して設定が行われます。
この種類というのがglBindTexture()の第1引数になり、ひいては第2引数で指定したテクスチャオブジェクトに対して行われることになります。
わかりにくいと思いますので、もう少し詳しく説明します。
OpenGLにはテクスチャの種類がいくつか存在します。
よく使われるGL_TEXTURE_2D以外にも、GL_TEXTURE_1D、GL_TEXTURE_3D、GL_TEXTURE_CUBE_MAP、GL_TEXTURE_2D_ARRAYなどです。
これらのテクスチャの種類に応じてOpenGL内部には枠が用意されていて、glBindTexture()でその枠にテクスチャオブジェクトを設定することができます。
表現としては以下のようになっていると思ってください。
default
→
glBindTexture(GL_TEXTURE_2D, 1);
→
glBindTexute(GL_TEXTURE_3D, 2);
→
glBindTexture(GL_TEXTURE_2D, 10);
GL_TEXTURE_1D
GL_TEXTURE_2D
GL_TEXTURE_3D
0
0
0
GL_TEXTURE_1D
GL_TEXTURE_2D
GL_TEXTURE_3D
0
1
0
GL_TEXTURE_1D
GL_TEXTURE_2D
GL_TEXTURE_3D
0
1
2
GL_TEXTURE_1D
GL_TEXTURE_2D
GL_TEXTURE_3D
0
10
2
各種類のテクスチャに対して、最後にバインドしたテクスチャにパラメータやイメージ設定が行えるという感じです。
表の順番で設定し、最後にGL_TEXTURE_2Dに対してパラメータを設定すると、2DテクスチャのID 10番のテクスチャに対してパラメータが設定されるというわけです。
たいてい、テクスチャへのパラメータ設定が終わった後は0番ID(つまり、テクスチャなし)をバインドしてやりますが、これは間違ってパラメータを設定し直したりしないようにするための配慮です。
もう1つの使用用途はシェーダのテクスチャユニットに対する設定です。
この場合はglActiveTexture()命令も併用します。
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 2);
glActiveTexture()命令で特定番号のテクスチャユニットを有効にします。
そのあとにglBindTexture()を行うと、このユニットに対してテクスチャをバインドすることができます。
ここでまたぞろ出てくるのはテクスチャの種類です。
DirectXの場合、シェーダリソースとして設定できるのは1つだけです。0番ユニットに対して2Dテクスチャと3Dテクスチャを設定することはできません。
しかし、OpenGLはそうではないようです。
各テクスチャユニットに対して前述のような種類に応じたコンテナを持っていて、それぞれの種類、それぞれのユニットにテクスチャを設定することができます。
つまり、2Dテクスチャと3Dテクスチャを両方ともテクスチャユニット0番に設定することが可能です。
ただ、シェーダから参照できる各ユニットのテクスチャは1種類だけらしいです。
ユニット0番に2Dテクスチャ、3Dテクスチャの両方が設定されていてもシェーダはその両方を同時に使うことはできないというわけです。
当然、第2の用途でglBindTexture()を使用しても第1の用途もかねて設定が行われてしまいますので、パラメータ変更を行うと痛い目を見ます。
以下のコードはたぶん、使用者が考えている通りの動作はしません。
// テクスチャユニット0番に1番IDの2Dテクスチャを設定
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, 1);
// テクスチャユニット1番に2番IDの2Dテクスチャを設定
glActiveTexture(GL_TEXTURE1);
glBindTexture(GL_TEXTURE_2D, 2);
// 1番IDの最大ミップマップレベルを変更したい!
glActiveTexture(GL_TEXTURE0);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAX_LEVEL, 1); // Error: 2番IDの2Dテクスチャが変更されてしまう
実際には、テクスチャ自体の設定とシェーダへのバインドは別々に行われるものと思いますが、内部動作には十分注意して処理を行ってください。
個人的に、OpenGLのバインド関係のわかりにくさは嫌いです…
テクスチャのフィルタリングやラップモードの設定は2種類の方法があります。
OpenGL2.0以前ではこれらのパラメータはテクスチャが持っているものでした。
そのため、例えば1つのテクスチャで2種類のフィルタリングを使用したい場合などにテクスチャオブジェクトが2つ必要になったりしました。
このような用途はあまりないのですが、テクスチャに特殊なパラメータを埋め込んでおく場合などに使うことがありますね。
OpenGL3.0以降であればサンプラステートが使用できます。
サンプラステートの生成、設定、使用方法は以下のようになります。
// 生成
glGenSamplers(1, &g_SamplerBilinear);
// 設定
glSamplerParameteri(g_SamplerBilinear, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glSamplerParameteri(g_SamplerBilinear, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glSamplerParameteri(g_SamplerBilinear, GL_TEXTURE_WRAP_S, GL_REPEAT);
glSamplerParameteri(g_SamplerBilinear, GL_TEXTURE_WRAP_T, GL_REPEAT);
// バインド
glBindSampler(0, g_SamplerBilinear);
生成は他のOpenGL命令と同様ですが、設定はバインド→設定という形ではなく、IDをそのまま渡して設定します。
使用する場合もインデックス番号を指定してバインドしてやるだけでいいのですが、直感的でわかりやすいです。
テクスチャの設定、使用に比べると雲泥の差ですね。
古いバージョンとの互換性のためなのかもしれませんが、テクスチャ設定関連の流れはもっと単純化してほしいな、と個人的には思いますね。
もう1つの注意点としては、テクスチャ座標の軸方向です。
画像の座標系は基本的に左上が原点となり、左から右方向のベクトルが画像座標系のX軸、上から下方向のベクトルが画像座標系のY軸となっています。
DirectXのUV座標は画像座標系と同様で、左から右方向がU軸、上から下方向がV軸です。
OpenGLではU軸は同様なのですが、V軸は下から上方向となり、原点も左下となります。
3Dオブジェクトのオーサリングツール(MayaとかSoftImageとか)は大半がOpenGL準拠だったりします。
3DモデルのDirectX用コンバータを書いたことがある人ならわかるのではないかと思いますが、たいていはV座標を1.0-Vで求め直したんじゃないでしょうか。
全画面にテクスチャを張り付けるなどの処理を行う場合はUV座標の方向に注意してください。
ではサンプルです。
3種類の.ddsファイルを読み込んでポリゴンに張り付けているだけです。
テンキーの0~2でテクスチャを切り替えられます。
0番はA8R8G8B8フォーマットのミップマップなし、1番はBC1圧縮のミップマップ最大、2番はBC3圧縮のミップマップ最大です。
GPUによってはBC6HやBC7のテクスチャも使用できるはずです。DX11対応のGPUなら問題ないんじゃないでしょうか?
次回は未定ですが、マルチスレッド対応をやるかもしれません。