けんごのお屋敷

2015-01-27

テクスチャマッピング

OpenGL 基礎シリーズ の第 5 回です。

テクスチャマッピング

実は OpenGL では jpg や png といった画像の類をそのままフレームバッファに描画することはできないし、そもそも画像読み込み系の関数すら持ち合わせていない困ったチャンなのだ。この時点で画像描画に関しては詰んでそうだけど、そんなことはなくって (そんなことがあったら今頃 OpenGL なんて誰も使っていない) ちゃんと方法がある。それがテクスチャだ。

テクスチャという単語は聞いたことがあるかもしれないし「テクスチャを貼り付ける」だとか「画像を貼り付ける」だとかいうのは良く言われる。2D でゲームを作る場合はテクスチャを画像と同じ大きさの四角形のポリゴンに対して貼り付けることで画像を、例えばキャラクターの画像など画面上に表示させることができる。でも一体テクスチャとはどういうものなのか、どういう原理で画像をディスプレイに表示しているんだろうか。それがわかれば OpenGL で自由に画像を表示できるようになる。エフェクトをつけるのだって自由自在。

あなたは既にテクスチャのある世界への一歩を踏み出している。

テクスチャの準備

画像を表示するためにテクスチャを使うならば兎にも角にも、表示したい画像の用意が必要になる。ここでは Android Studio で新規プロジェクトを作った時に最初から同梱されているあの有名なドロイド君を使って説明を進めてみようと思う。いつもありがとう、ドロイド君。画像に困ったときは君だね☆

ドロイド君登場

さて、ドロイド君に登場してもらったところで実際にこのドロイド君をテクスチャとして利用するためには、いろいろと前準備をやっとかないといけない。もっとも単純には GPU に対して以下のような要求を送ってやることになる。

  • テクスチャが欲しいので新しく作ってちょうだい!
  • ありがとう。じゃぁこれからこのテクスチャに対していろいろ操作するからね
  • フィルタリングの設定はこれでお願い
  • 画像データをそっちに送るのでよろしく〜

それぞれの要求には専用の API が用意されているのでそれを呼び出すだけでいいので簡単なのだ。猫でもわかる OpenGL!

※マルチテクスチャを利用する場合はもう少し手順が増えることになるけど、それにもちゃんと専用の API が用意されている。とりあえず最低限これだけ把握できていれば問題はない。

テクスチャの生成

画像を読み込んでそれをテクスチャに割り当てるためには GPU に対して新しいテクスチャを生成するよう要求しないといけない。Android には glGenTextures というメソッドがあり、それを使って新しいテクスチャを生成する。テクスチャが生成されると整数型の数字 (1 とか 2 とか) を受け取り、それがテクスチャの ID となる。ちなみに Android では一気に複数のテクスチャの生成を要求することができる。

テクスチャ生成のイメージ図

生成したテクスチャのバインド

テクスチャを生成したらそのテクスチャをバインドする必要がある。テクスチャをバインドすることで、そのバインドされたテクスチャに対して、テクスチャパラメータの設定をしたり画像データを送信したりすることができるようになる。OpenGL のテクスチャ関連のメソッドのリファレンスを確認すればわかるけど、それらのメソッドの引数にはテクスチャ ID がない。つまりテクスチャに対する操作は、メソッドの引数でテクスチャ ID を指定するのではなく、OpenGL 側でテクスチャの状態を持っていて現在バインドされているテクスチャに対して行われるということになる。

テクスチャのバインドは glBindTexture というメソッドがあり、引数としてテクスチャ生成時に受け取ったテクスチャの ID を渡す。

テクスチャバインドのイメージ図

フィルタリングの設定

テクスチャをバインドしたら、まずそのテクスチャに対してフィルタリングの設定をしてあげる。ここでフィルタリングの話をするよりかは、順番的にはまず画像を表示できるまでの仕組みを説明して、その後でフィルタリングの話をした方がわかりやすいので、ここでは省略。

画像データを GPU へ転送

そしてバインドされたテクスチャに対して画像データを送信する。画像データを送信するためにはメモリにピクセルデータを読み込む必要があるけど、最初に書いたとおり OpenGL には画像読み込みの関数はない。ただし、Android には Bitmap という画像を取り扱えるクラスがあってそれをそのまま OpenGL に渡せるように Android がラッパーを準備してくれている。とっても親切!こんな風にしてバインドしているテクスチャを変更しながら画像データを送信してテクスチャを準備していく。

画像データ送信のイメージ図

※Android ではない場合でも、たとえば WebGL の場合だと HTML の img 要素を DOM から取得してそれをそのまま渡せるような仕組みになっている。C 言語とかだと、画像読み込み部分は自作せずとも既存のライブラリがたくさんあるのでそれを使って画像を読み込んで、OpenGL にはそのポインタを渡せばいい。世の中には偉大な先人たちが作った便利なものがたくさんあるもので、OpenGL にその機能がないのであれば他から拝借すればよい。

頂点座標と UV 座標の準備

画像データの送信まで完了すればテクスチャを使う準備は完了していて、バインドされているテクスチャはフラグメントシェーダによって参照される。フラグメントシェーダでは、バインドされているテクスチャのどこの座標の色を使うのかを指定することでその座標の色を抽出してきてくれる。この テクスチャのどこの座標を使う という情報が UV 座標と言って、左上を原点とした 0 〜 1 の範囲の座標系になっている。左上が (0, 0) 、右上が (1, 0) 、左下が (0, 1) 、そして図には書いてないけど右下はもちろん (1, 1) になる。真ん中は (0.5, 0.5) ってことだね。

UV 座標

そしてこの UV 座標、どこで指定するのかというと、頂点座標と一緒にバーテックスシェーダへ渡してあげる。フラグメントシェーダに直接渡すのではなくバーテックスシェーダに渡して、そこからフラグメントシェーダへ渡されることになる。UV 座標は主にフラグメントシェーダで使われるのに、まずバーテックスシェーダへ渡さなきゃいけないのはなんだか回りくどいけど、例えば四角形のポリゴンにテクスチャを描画することを考えてみる。

四角形ポリゴンとテクスチャ画像

まずポリゴンの頂点情報はこんな風になる。

float[] vertices = new float[] {
    // 1つめの三角形
    0f, 1f, 0f, // 左上
    0f, 0f, 0f, // 左下
    1f, 0f, 0f, // 右下

    // 2つめの三角形
    1f, 0f, 0f, // 右下
    1f, 1f, 0f  // 右上
    0f, 1f, 0f  // 左上
};

次に、この各頂点と対応した UV 座標はこんな風になる。

float[] texCoords = new float[] {
    0f, 0f, // 左上の頂点に対するUV座標
    0f, 1f, // 左下の頂点に対するUV座標
    1f, 1f, // 右下の頂点に対するUV座標

    1f, 1f, // 右下の頂点に対するUV座標
    1f, 0f  // 右上の頂点に対するUV座標
    0f, 0f  // 左上の頂点に対するUV座標
};

各頂点と UV 座標が対応しているのがわかる。

頂点座標と UV 座標の対応

これらの頂点座標 (vertices) と UV 座標 (texCoords) をバーテックスシェーダに渡した上で OpenGL の API を介して GPU に対して描画命令を発行する。もちろん、バーテックスシェーダ内では UV 座標をフラグメントシェーダへ渡すようにコーディングしている前提で。すると、前の記事で書いたように座標変換の後、ラスタライザによってフラグメントが生成される。そしてフラグメントシェーダでは、その生成されたフラグメント及び UV 座標を受け取る。UV 座標はラスタライザによって補完されているので、頂点毎に対応する座標を指定するだけで良くなっている、というわけ。

フラグメントシェーダでは受け取った UV 座標を元に、バインドされているテクスチャから指定された UV 座標の位置の色情報を参照し、それをそのまま出力させることで画面には画像が表示されるという仕組みになっている。実際には描画は三角形ポリゴン毎に行われるので、四角形ポリゴンでまとまった処理はされずに 2 つの三角形が順次処理されていく。

ポリゴンにドロイド君が描画される様子

※ちなみに UV 座標の UV という名前の由来は xyz と同じで単なる記号。私達が 3D 空間を表す時は、座標として (x, y, z) という文字を使うけど、UV 座標もそれと同じで xyz の前にある文字から 2 文字取ってきただけ。おっと、アルファベットの並び順からみて u-v-w-x-y-z となってるので、前の 2 文字なんだったら vw じゃないか!いやいや実は w は既に使われている (座標系変換の話のところで同次座標系という話がちらっと出てきたけど、そこで 1 を指定していた部分。あの要素に w が使われている)。なので uv になる。ただのトリビアだけどこういうどうでもいい話も好きだな。

テクスチャのフィルタリング

これまでで画像をテクスチャに読み込み、画面に表示させるまでの仕組みをみてきた。最後にテクスチャに対するフィルタリングの説明をしてこの記事を終わろうと思う。テクスチャの UV 座標は 0 〜 1 の範囲になっていた。ただ当然ながらテクスチャに読み込ませた元画像はそうなってなくって、例えばだけど幅と高さがそれぞれ 256 ピクセルなのであれば元々の座標は 0 〜 255 まであるはずなんだよね。それを 0 〜 1 の範囲として扱うわけなので、当然 UV 座標と元の座標がぴったり一致せずにずれることがある。その時にホントはどの色を使えばいいの?という色の決め方を OpenGL に教えてあげることができる。

UV 座標と元の座標のずれ

フィルタリングには代表的には最近傍フィルタリングと線型フィルタリングの 2 種類がある。

最近傍フィルタリング

UV 座標から見て画像のピクセルの中心点に一番近いピクセルの色が採用される。図では赤いプラスマークで表された UV 座標から見てピクセルの中心点に一番近いのは右上のピクセルになるので、右上のピクセルの色が採用される。

最近傍フィルタリング

線型フィルタリング

UV 座標の周りにある画像のピクセルの色を参照して、各ピクセルの中心までの距離に応じて色を混ぜ、最終的にその混ぜた色が採用される。

線型フィルタリング


最近傍フィルタリングより線型フィルタリングの方が見た目は綺麗になるけど、計算量は線型フィルタリングの方が多いので負荷が高いのは線型フィルタリングということになる。これはケースバイケースで使い分けていくのが良い。フィルタリングの設定は Android の場合は glTexParameteri というメソッドを使って行う。このメソッドの引数にはテクスチャ ID はないので、例のごとく現在バインドされているテクスチャに対しての設定をする、ということになる。また、これらのフィルタリングは テクスチャが拡大される時 及び テクスチャが縮小される時 でそれぞれ設定することができる。

// テクスチャが縮小される時は線型フィルタリング
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MIN_FILTER, GLES20.GL_LINEAR);

// テクスチャが拡大される時は線型フィルタリング
GLES20.glTexParameteri(GLES20.GL_TEXTURE_2D, GLES20.GL_TEXTURE_MAG_FILTER, GLES20.GL_LINEAR);

まとめ

OpenGL でゲームを作る時、ほとんどの場合はなにかしらの画像を表示することになる。その時にこのテクスチャの知識は必須になってくるけど、UV 座標の指定の仕方さえ理解していればあとはどうにでも応用することができる。この辺まできてようやく OpenGL で Hello World するための下地が整ってきたような感じがするかな?

次は最後の記事。次回 Hello World in OpenGL! に続く。

  • このエントリーをはてなブックマークに追加