適当にやっていると妙なところで躓きます。かたりぃなです。
今回はOpenGLES2.0を叩いてみました。
実験環境
今更GLES2.0でいいの?
最新のGLESを使ってみたいとは思うのですが、シェーダーの数が増えていて不安要因となっています。
シェーダー書いたことないので、まずは単純なシェーダー(vertex,flagmentの2つのみ)で実現するGLSL2.0をやってみることにしました。
一通り理解してからバージョンあげていったほうが最終的に近道になるだろうと思っています。
そもそも最新版で使いたい機能があるのかといわれると「?」という状態ですし。
躓いたポイント
- シェーダープログラミングのデバッグ方法が分からない
- 頂点、UVともに正しいのに、テクスチャが表示されない
躓いたポイントを整理したうえでコードを示します。
そもそもの根本で躓いてしまったのは、自分の中にある3Dグラフィックエンジンの知識が古すぎたのだと思います。
知識の古さについてですが、具体的には
- GLES1.0世代
- DirectX 5世代
になります。
上にあげた世代の古い3Dグラフィックエンジンは固定機能パイプラインのみ備えていて、基本的な処理は固定機能パイプラインが実現してくれていました。
固定機能としてライティング済み頂点とかトランスフォーム・ライティング済み頂点とか幾つかの頂点フォーマットに対するレンダリング機能がありました。
(DirectX5世代でいうとDrawPrimitiveの引数で指定する頂点形式でTLVERTEXとかLVERTEXとかあった気がします)
現代の3Dグラフィックでは固定機能パイプラインは存在せず、必要な機能はシェーダーとしてプログラミングする必要があります。
そのため「まず基本機能を実現するシェーダーを書く」ってところに時間がかかりました。
3DグラフィックAPIとしての汎用性は高くなりましたが、プログラミングのための最初の敷居が大幅に上がったと感じます。
理解してしまえばどうということはないのですが。
さて、躓いたポイントを順に整理してみます。
まずは一枚の四角形ポリゴンを出力してみて、頂点、色、UV、テクスチャを貼れるところまで。
VisualStudioでもAndroidStudioでもGLESプロジェクトのスケルトンを出力させると単純なプリミティブのレンダリングまでは行えるので、単純なプリミティブのレンダリングはできているという前提で話を進めます。
シェーダーのデバッグ方法がわからない
プログラム始めたばかりの初心者がデバッグ方法わからないというのと同じレベルですね。
まずシェーダーの概念と基本的な処理について理解して、いざ自分でコードを書き始めると詰まるポイントです。
シェーダーではC/C++でいうところのprintfデバッグはおろか、デバッガでのブレークポイントや変数ウォッチができないようです。
じゃあどうするか?
printfデバッグもデバッガの変数ウォッチも本質は「プログラムの状態を認識できる何かが出力できればいい」というところです。
シェーダーからの出力を視認できる形(とりあえず出力される色をデバッグとして使う)GLSL書いてみました。
(何かの拍子に詰まったときに振り返れるようにデバッグ用のコードも残しておきました。)
追加したデバッグ機能でシェーダーが期待通りに動いているかをアトリビュート単位で確認していきます。(頂点、UV、カラー、テクスチャetc.)
そんなこんなで一通り記述しおわったシェーダーがこちら。法線の処理はまだ入れていませんが、それはまたの機会に。(照明の機能を実装するときにいじくりまわすことでしょう。きっと。)
・頂点シェーダー
// attributeは、頂点シェーダーへの入力の定義 attribute vec4 vPosition; attribute vec4 color; attribute vec2 texcoord; // 頂点シェーダーでのverying修飾子はフラグメントシェーダーへ渡すことを意味する varying vec4 vColor; varying vec2 vTexcoord; // uniformはC/C++から固定で渡されるパラメータを意味する uniform mat4 projectionMatrix; void main() { gl_Position = projectionMatrix*vPosition; // 頂点は透視変換かけるだけ vColor = color; // 色はそのまま vTexcoord = texcoord; // テクスチャ座標もそのまま }
・フラグメントシェーダー
precision mediump float; // フラグメントシェーダーでのverying修飾子は頂点シェーダーからの入力を意味する varying vec4 vColor; varying vec2 vTexcoord; // uniformはC/C++から固定で渡されるパラメータを意味する uniform sampler2D texture; void main() { gl_FragColor = texture2D(texture, vTexcoord)*vColor; // gl_FragColor = vColor; // color情報のデバッグ用 // gl_FragColor = vec4(vTexcoord.s, vTexcoord.t, vTexcoord.s, 1.0); // uv座標のデバッグ用 // gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // 頂点情報のデバッグ用(すべて白色ポリゴンで出す) }
テクスチャが表示されないとき(プリミティブが真っ黒になる)
さて、OpenGLのテクスチャ回りでは毎度のことながらテクスチャ画像のファイルフォーマットどうするかとか、本来のやりたいことから外れた話になってしまいます。
せっかく環境にOpenCVを組み込んだのでこいつを使うことにします。
肝心のGLES2.0ではテクスチャの扱いが大幅に変わっていたので苦労しました。
最初は適当にマニュアル流し読みしながらパラメータ書いていったのですが、妙なところで詰まりました。
具体的には、ミップマップOFFでテクスチャ作っておきながら、テクスチャの補完パラメータでミップマップ使うような指定したりなど。
テクスチャの設定回りですが、他にも変更点があるようで、GLES1.0世代ではglEnableでテクスチャ有効化などをやっていましたが、これはシェーダーに取って代わられたため不要となったようです。
glActiveTextureとglBindTextureですが、このあたりはまだ完全には理解していないので、またの機会に。
(マルチテクスチャでレンダリングするときはglActiveTextureを使って複数のテクスチャユニットにglBindTextureしていくことになりそう)
テクスチャ生成のコードはこちら。
GLuint create_texture(cv::Mat& fileImage) { // opencvの機能を使ってテクスチャ画像をデコードする rawImage = cv::imdecode(fileImage, 1); // 第二引数に1を指定し、RGBフォーマットで取り出す glActiveTexture(GL_TEXTURE0); GLuint tex_id; glGenTextures(1, &tex_id); // テクスチャはまずは一個だけ実験 glBindTexture(GL_TEXTURE_2D, tex_id); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); glTexImage2D(GL_TEXTURE_2D, 0, // ミップマップレベル GL_RGB, // openGLの内部で保持するカラーフォーマット rawImage.rows, rawImage.cols, // 画像の幅、高さ 0, // 境界線の太さ GL_RGB, // 元の画像カラーフォーマット GL_UNSIGNED_BYTE, // 画素のデータ型 rawImage.data); // 画像データの先頭アドレス // テクスチャの拡大縮小方法の指定。ミップマップなしで生成したテクスチャにミップマップありパラメータ指定すると真っ黒になったりするので注意 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); // テクスチャの繰り返し方法の指定 glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT); return tex_id; }
後回しになりましたが、頂点バッファ、インデックスバッファなども作っておきます。
ほとんど同じような手順になるので頂点バッファを作る例。
パラメータは公式ドキュメントを参考に。
特にglBufferDataの最後の引数はデータがどこに置かれるかにも関わりそうなので、要注意です。
create_vertex_buffer(GLuint element_num, const GLfloat *elements) { GLuint buff_id; glGenBuffers(buffer_num, &buff_id); glBindBuffer(GL_ARRAY_BUFFER, buff_id); GLuint bufferSize_of_bytes = sizeof(GLfloat)*element_num; glBufferData(GL_ARRAY_BUFFER, bufferSize_of_bytes, elements, GL_STATIC_DRAW); return buff_id; }
あとはシェーダーに対して頂点、UV、色情報、テクスチャ、変換行列を設定してレンダリングしてあげれば完成です。
// 投影変換行列の設定 glUniformMatrix4fv(projectionMatrixLocation, 1, GL_FALSE, projectionMatrix); // テクスチャユニットの利用準備 glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, gTexture); glUniform1i(gtextureUniformLocation, 0); // 0盤目のテクスチャユニット=GL_TEXTURE0を使うよう指示 // 頂点シェーダーの利用準備 glEnableVertexAttribArray(gvertexAttributeLocation); // 頂点シェーダーを有効化 glBindBuffer(GL_ARRAY_BUFFER, gVertexBuffer); // 頂点バッファを関連付ける glVertexAttribPointer(gvertexAttributeLocation, // 頂点シェーダーのハンドル 3, // 頂点フォーマットに含まれる要素数 GL_FLOAT, // 頂点の各要素の型 GL_FALSE, // 正規化の有無(法線で使う) 0, // 頂点間のストライド 0); // 頂点データのアドレス // 頂点シェーダーでのUVバッファの利用準備 glEnableVertexAttribArray(gtexcoordAttributeLocation); // 頂点シェーダーを有効化 glBindBuffer(GL_ARRAY_BUFFER, gUvBuffer); // UVバッファを関連付ける glVertexAttribPointer(gtexcoordAttributeLocation, 2, GL_FLOAT, GL_FALSE, 0, 0); // 頂点シェーダーでの色情報の利用準備 glEnableVertexAttribArray(gcolorAttributeLocation); glBindBuffer(GL_ARRAY_BUFFER, gColorBuffer); glVertexAttribPointer(gcolorAttributeLocation, 4, GL_FLOAT, GL_FALSE, 0, 0); // インデックスバッファを指定してレンダリング glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, gIndexBuffer); glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0);
「~~AttributeLocation」は事前にglGetAttribLocation(program, "symbol")で取得しておきます。
これはシェーダーでattribute定義した変数への入力位置を示すハンドルです。
シェーダーへの入力は次の3ステップで実現できました。
1, glEnableVertexAttribArrayでシェーダーへの入力を有効化し、
2, glBindBufferでシェーダーの入力に関連付けるバッファオブジェクトを指定
3, glVertexAttribPointerでシェーダーへの入力を指定
これらを各アトリビュート(頂点、UV、色 etc.)ごとに行います。
最後にインデックスバッファを関連付けてから、glDrawElementsを呼び出します。
その結果、ここまでに設定した各アトリビュートをシェーダーが読み込んでレンダリングしてくれます。
GLES2.0の感想
OpenGLを使うたびに思うのですが、「現在の状態」を意識しながらプログラミングする環境、ちょっと辛いです。
(bindBufferで何が関連付けられているかをプログラマが把握している必要がある。)
この辛みに拍車をかけるかのようにハンドルの指定方法などが、辛いと感じました(最初は特に)。
例えば、glActiveTextureではテクスチャユニットの指定はGL_TEXTURE0などを使うのにglUniform1iでシェーダーに指定するテクスチャユニットはただの値(0とか)だったりして、低レベルAPIであることを再認識させられます。
(ここでいう低レベルは、ハードウェアに近い抽象化されていないソフトウェアレイヤーという意味です。念のため。)
GPUというハードウェア相手にプログラミングしているのだから仕方のない部分だろうとは思います。
ただ、ふつうに叩き続けるにはちょっと厳しそうなので一枚レイヤーかぶせるとか、ひと工夫したいところです。
今後の展望
今回はアトリビュート単位にバッファを分けてレンダリングする実装にしました。
これをアトリビュートをすべてまとめて1つのバッファで済ませる実装もあるかと思います。
(例えば頂点情報をこんな構造体として定義して、頂点バッファはこれの配列として定義するなど。)
struct vertex{ vec3 pos; vec3 normal; vec4 color; vec3 uv; }
どちらのほうが高速かという疑問、そのうち試してみたいと思っています。
特にボーンアニメーションなどの実装をするときに関連するのですが、
ボーンの影響を受けて頂点情報を更新する場合、実際に更新が必要となるのはposとnormalだけかもしれません。
この場合、アトリビュートを分割しておいたほうが必要なものだけ転送できる=バス帯域が節約できる=結果として高速。だったりするのかもとか。(まだ試していない)
どこまでやるかはまだ未定ですが、シェーダー周りはなかなか興味深いものでした。
あとは設計思想的にDirectXのほうはどうなのだろうと気になっています。
MMEがHLSLで書かれてるらしいというのもDirectXが気になっている理由の一つです。
とりあえず今日はここまで。