ステップアップ OpenGL ES 2.0
0. OpenGL スケルトン
Android では、GLSurfaceView の枠組みの中で OpenGL を処理する形になる。
1. タイルを表示するサンプル
4 個の頂点座標データに基いて、たった一つの矩形のタイルを描くだけだが、この最初の一歩の段階で一挙にハードルが上がる。主には、シェーダーで定義した変数を通じて頂点データを入力するための手続。あとは Kotlin / Java 固有の話として、ダイレクトバッファの形でデータを OpenGL 側に入力するという点。
- シェーダー
- Kotlin / Java 側では、シェーダーは String データに過ぎないが、OpenGL 内部での処理を GLSL で定義するもの。C 言語的なスクリプトであり、公式のクイックリファレンスカードによくまとまっている(詳細なリファレンスも公開されている)。gl_* が予約された変数で、この変数に意図した値を出力することで OpenGL 内の次の処理へと頂点データが渡ることになる。
このサンプルでは、a_Position を通じて Kotlin / Java 側から入力した値をそのまま次へリレーしている。またフラグメントシェーダーにおいては、u_Color を通じて Kotlin / Java 側からセットした値をそのまま各フラグメント(ピクセル)の色として渡している。ちなみに、u_Color は uniform なので、いわゆるグローバル変数的なものとして定義されており、4 つの全ての頂点間で共有されている。一方の a_Position の方は attribute であり、各頂点毎に属する値である(cf. クイックリファレンス p3 Qualifiers)。 - シェーダーの使い方
- 各シェーダーをコンパイルし、バーテックスとフラグメントを組み合わせて一つのシェダープログラムとし、さらに今有効なプログラムとしてそれをセットする(シェーダープログラムは複数用意して使い分けることも可能なため)。
- シェーダー変数の確保
- シェーダープログラムから、OpenGL への入力に使用するシェーダー変数(のポインターインデックス)を得る。
- 頂点データの配列
- Kotlin / Java 固有の話として、JavaVM 内部のメモリーヒープを使うわけにはいかないため、ダイレクトバッファ(ここでは FloatBuffer)を通じてデータを入出力する。
- 頂点データの配列の順次処理
- glEnableVertexAttribArray によって OpenGL が頂点データの配列を次々に処理させるようにする。また、配列をどのように読み取っていけばいいかという設定がその前の glVertexAttribPointer である。
- その他
- OpenGL 特有の座標系により、(-1, -1) - (1, 1) の範囲で座標が表現されるため、タイルの座標は -0.5 ~ +0.5 で定義した。さらに、この (-1, -1) - (1, 1) の範囲は、画面全体の相対座標なので、このサンプルで表示される矩形は正方形にはならない。画面の縦横それぞれ半分の幅を持つ矩形となる。
2. プロジェクション行列とビュー行列の導入
OpenGL のシェーダープログラムを通じた処理の枠組みは「1. タイルを表示するサンプル」で示した。次は、まさしく OpenGL ぽい話題だが、変換行列を使った、画面への空間的な投影処理である。プログラム的なコーディング上の枠組み自体はもう 1. で紹介し終ったと言えるのだが、ここから先は、グラフィックス的な論理モデルの部分の話であり、コーディング以前のアルゴリズムの問題である。1. に引き続いて、さらにもう一段一挙にハードルが上がる部分だが、これが最後の山とも言える。
- バーテックスシェーダー
- u_MVPMatrix という変数を追加し、gl_Position の出力にあたって、素の座標に u_MVPMatrix を左から掛けたものを使うようにする。
- MVP 行列
- これはモデル行列、ビュー行列、プロジェクション行列を掛け合わせたものである。このサンプルでは、特にモデルを移動したり変形するわけではないので、モデル用の変換行列は考えずに済み、ビュー行列とプロジェクション行列についてのみ用意する必要がある。
- プロジェクション行列
- このサンプルでは frustumM を使った。なぜかネットで見つかるものは perspectiveM を使う用例ばかりだったので(出所が同じものが使い回されているのだろうか?)、frustumM の実際の使い方は、自力で試行錯誤して見出す他なかった。いずれもプロジェクション行列の作成に使えるのは同じなのに、なぜ perspectiveM ではなくて frustumM を使いたかったのかというと、perspectiveM の場合は角度で視野角を定義するので、きっちり狙ったサイズになるのか不安だった点と、それ以上に、視野角が水平(x 方向)ではなく垂直(y 方向)で指定するという点である(パソコンでの利用が前提となっているからなのだろうか?)。ともかく、どう考えても、frustumM の方が厳密で良い感じがするのだが、不思議とネットで見つかる用例は perspectiveM ばかり(やはりほとんどが見よう見真似で書かれたブログ記事の類だということか)。調べてみるとやはり、Khronos 公式の掲示板では、frustumM が正統で、perspectiveM は、frustumM を間接的に使うためのユーティリティ関数とのこと。
- ビュー行列
- カメラの上下を引っくり返してみた。こうすることで、右手系のまま、Y 軸が下向きに、Z 軸は奥に向って正方向となる、デバイス画面の座標系に寄せることができる。さらに、視軸を画面サイズの半分の位置にすることにより、原点の位置を画面の中央ではなく左上に合わせることができ、より完璧にデバイス座標系に一致させられる。
- MVP 行列の適用の仕方
- multiplyMM で互いを乗算して一つの行列にした後、glUniformMatrix4fv でシェーダープログラムの変数にセットする。
サンプルでは、(0, 0)-(256, 256) のタイルが、画面上でも 256x256 の正方形として左上座標 (0, 0) で表示されている。
3. テクスチャー
テクスチャー自体はそれほど難解なものではないと思う。最終的に得られたサンプルは大層なものに見えるが、2. でやったことと比較すれば、テクスチャーを処理するために、主にフラグメントシェーダーに手を加え(ただし、テクスチャー自身の矩形に関する頂点処理も行う必要があるため、バーテックスシェーダーにも手が加わっている)、それに伴うシェーダープログラムへのテクスチャーデータの入力処理などが加わっている。基本的にはバーテックスシェーダー単独で必要だったことを、テクスチャーについても似たような形で行っている感じである。最終的には、フラグメントシェーダーでの処理の仕方が変わり、色を直接割り付けるのではなく、テクスチャーを使って各ピクセルに割り当てる色を決定するようになっている点が、2. と異なっているのである。
また、タイルサイズを定義する配列において、テクスチャーを読み取る方向を決定するための (s, t) ベクトルが付け加えられている点にも留意。
それ以外の、リソースから Bitmap オブジェクトを読み込んだりするような部分は Android 一般のノウハウに過ぎず、OpenGL として何ら特記に値するような事項ではないだろう。
以上で Android/Kotlin で OpenGL ES 2.0 を使い、ドットバイドット表示の 2D グラフィックスを実現するサンプルを組み上げることができた。orthoM でも 2D グラフィックスを安直に実現できるのだが、frustumM (perspectiveM) を使っているので、オブジェクトの z 位置を動かすことで、簡単に拡大・縮小を実現できるのがポイントである。
4. クラス分けの例
これは OpenGL とは直接関係がなく、何が正解ということもない、Kotlin / Java の言語的な話として様々なやり方が考えられる話題だが、プログラムの複雑化に備えて、クラス分けを行ってみた。
まず、アプリの実行を通じて、(一種類とは限らないものの)一貫したオブジェクトとして扱えるであろうシェーダープログラムを Kotlin / Java 的なクラスとしてまとめて Renderer から分離してみた。シェーダーの定義や変数を内包しており、一旦コンパイルなどの一定の手続が終われば、基本的にシェーダー変数にアクセスすることだけが必要なわけで、それ以外の諸々の処理は隠蔽してしまうとスッキリすることになる。
シェーダープログラムを Kotlin / Java クラス化してしまえば、あとは、描画されるオブジェクト側を種類別にクラス化して鋳型化する感じである。描画オブジェクトの頂点やテクスチャーに関するデータ定義を内包し、初期化し、onDrawFrame で繰り返し描画時に処理されるべきコードをまとめておいた。
ここでは描画オブジェクトは 1 種類のみだが、2 個のタイルを実体化して並べて表示してみている。
5. アルファブレンディング
サンプルをさらに改良して、テクスチャーの重ね合わせを行なっている。ポイントは 2 つあり、glEnable(GL_BLEND) と glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA) を使っている点と、もう一つは読み込んだテクチャーに後づけでアルファを設定するためにフラグメントシェーダーを工夫している点である。
コメント
コメントを投稿