小ネタです。
OpenGL では描くのは三角形ベース(モノによっては四角形も使用できる)です。
もしある平面が全体に凸型なのであれば、以下のような組み合わせで頂点を結び、三角形を描いていけば面を埋めることができます。
{0, 1, 2}
{0, 2, 3}
{0, 3, 4}
{0, 1, 5}
・・・
※平面の頂点の番号が0, 1, 2, 3,,,,, と並んでいて、平面に穴が無いことが条件。
しかし凹型の場合は一筋縄ではいきません。で、調べてみると、
http://www.gamedev.net/topic/483457-dividing-concave-polygon-into-convex-ones/
の Dave さんの返答がドンピシャ。
この情報を元にたどり着いたのが以下の2つのページ。
http://www.cgal.org/Manual/3.2/doc_html/cgal_manual/Partition_2/Chapter_main.html
http://www.bringyou.to/compgeom/
1つめのリンクはソースを読めばわかると思います。が、自分は2つめでアルゴリズムに納得できたので1つめはよくは見ていません。
さて2つめのリンクの「Hertel-Mehlhorn Algorithm」が肝です。ほぼ直訳ですが、和訳してみましょう。
========================================
(1) 三角形を作っていく。
(2) inessential である辺を除去する。
(3) 以上を繰り返す。
対角線の削除が凹型の面を作るかどうかは
「in constant timeで対角線を囲む辺と弦(?chords)によって」
locallyに行わうことができる。
もしある頂点において内側の角度が180度以上なら、そのポリゴンにおいてその頂点はreflexである。
reflexな頂点を作らない対角線は除去して良い。
対角線dによってつくられる凸型ポリゴンの領域について、
もし対角線dの除去によって「頂点vにおいて凹む領域になる」のなら
対角線dを頂点vに対して "essential" であるという。
dがessentialであるのなら、明らかにそれは頂点vにつながっているはず。
でもって、vはreflex(つまり凹む箇所)あるに違いない。
その対角線がどちらの頂点についても essential でないのなら、"inessential" であるとする。
========================================
書きながら結構書き直しました。図が欲しいヨ!(といいつつ時間が無いので自分も割愛。)
こっからはなんとなくの理解を書いてみます。もしかしたら間違っているかもしれません。
(1) 頂点を結んで三角形を1つ作ってみる。これは端っこからでいいんじゃないかな?原文では任意の箇所となっていたように思います。
(2) その三角形の一辺について
「その辺を除去したら今作った三角形が凹型になってしまう」
のならそこに線をひく。(元の図形の縁については考える必要なし。)
(3) 除去しても凹型にならないならそこには線は引かない。
(4) 以上を繰り返す。
こうするといくつか線を引いた結果、凸型の部屋だけになります。なるはず。
ちなみに穴のあるタイプについても解決できるかについては考えていません。四角形の中に四角形の穴ならできそうですね。蛇腹の中に蛇腹なんかはどうでしょうね。ここらは宿題ということで。
凸型の部屋だけになったら、あとはそれぞれの部屋を三角形に分割していけばいいんですね。なるほど〜。実装についてはリンクを見て下さいね(下の方にソースがあります)。意外と長い。。。
さらに細かい情報が欲しい方は
Approximate convex decomposition of polygons Lien
で検索すると幸せになれます。英語が大丈夫な人限定ですが。
2012年4月28日土曜日
2012年4月25日水曜日
OpenGL ES テクスチャ設定 @iOS
OpenGL ES をちまちまといじっているわけですが、少し(かなり)はまった箇所があったので書いておきます。
参考:http://www.cocos2d-iphone.org/forum/topic/2319
現象:テクスチャを貼ったのに黒くしかならない。
対策1:
テクスチャの設定で以下のようにする。
これでも解決しましたが、第3引数に GL_REPEAT を指定したいときもあるよね、ということで米国の掲示板を探してみました。
対策2:
テクスチャに使用する元画像(自分の場合はPNG)のサイズを power of two にする。
power of two は日本語にすると「2のべき乗」。つまり、1, 2, 4, 8, 16, 32, 64,…といった数値のことです。
実際に128x128に調整したところ、GL_REPEAT でも正しく表示されました。縦と横を異なる数値に(128x64など)してもいけるのかどうかは試していないので不明です。
今回の場合は iOS の ES ですが、他のバージョンやプラットフォームでは違うかもしれませんせんせーん。
参考:http://www.cocos2d-iphone.org/forum/topic/2319
現象:テクスチャを貼ったのに黒くしかならない。
対策1:
テクスチャの設定で以下のようにする。
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
これでも解決しましたが、第3引数に GL_REPEAT を指定したいときもあるよね、ということで米国の掲示板を探してみました。
対策2:
テクスチャに使用する元画像(自分の場合はPNG)のサイズを power of two にする。
power of two は日本語にすると「2のべき乗」。つまり、1, 2, 4, 8, 16, 32, 64,…といった数値のことです。
実際に128x128に調整したところ、GL_REPEAT でも正しく表示されました。縦と横を異なる数値に(128x64など)してもいけるのかどうかは試していないので不明です。
今回の場合は iOS の ES ですが、他のバージョンやプラットフォームでは違うかもしれませんせんせーん。
2012年4月13日金曜日
OpenGl ステンシルバッファ
ちょっと OpenGL(ES) のステンシル関連が頭の中でまとまらないので、書きながら考えてみます。最終的に目指すのは複数のマスクを置くこと。ただし1つの絵に対して複数のマスクを設定することについては考えていません。
何かのお役にたったらとっても幸いかもということで。正確性については話半分とし、公式のリファレンスなどでご確認ください。
> OpenGL ES API リファレンス
まずはメソッドのリファレンスを和訳しながら、あいだに補足を書いていきます。
ステンシルテストの検査方法と比較値を設定する。
func
ステンシルテストの方法を指定する。
ref
ステンシルテストのための比較値を指定する。初期値はゼロ。
mask
ステンシルテストが行われた時の、ref の値とステンシル値に対してビット単位のAND演算する値を指定する。初期値は2進数で表した時に11111…となる数値。
ステンシルテストではデプスバッファと同じように「そのピクセル描いてよいかどうか」を検査する(つまりフラグメントシェーダー段階の処理)。ステンシル面は OpenGL のプリミティブとして描かれ、これを型紙として画像や物体が描かれる。ステンシルはマルチパスレンダリングアルゴリズムでよく使われ、デカール・アウトラインといった特殊効果に利用される。
例えば(x, y)=(25, 322)の位置のピクセルについて黄色がかった赤い色を付けたいんだけどどうよ?というのがステンシルテストというものです。テストで不合格ならばそれ(25, 322への黄赤)は最終的な絵には反映されないことになります。ステンシルはマスク機能にも使われます。
ステンシルテストは「比較値」と「ステンシルバッファの値」を比較した結果を判断し、そのピクセルへの描画を許可するかを決める。ステンシルテストを有効化するためには
引数として func, ref, mask があるが、実際は表と裏があってそれぞれに設定が必要。両面とも同じ設定ならば glStencliFunc() を使用し、別々に設定するならば glStencilFuncSeparate() を使用する。
引数の func は下に示す8つの定数を設定し、これでステンシルテストの検査方法を決定する。ref はステンシルの比較で使用される整数の値。これは0から2^n-1までの値を取り、nはステンシルバッファの bitplanes の数である(後述)。mask は比較値(ref の値)およびステンシル値と比較(ビット単位のAND演算)される。この結果の2つの値がさらに比較される。
(この段は sonson@Picture&software の記述を参考にしています。)
つまりは3回の比較が行われるみたい。
ちなみに mask の値については、1番と2番の比較検査をすることがない場合、111111…と1で埋まった値を使います。実際には「~0」というような値を使います(ビット演算の知識)。
bitplanes というワードが出ましたが、これについては以下の記事に書きました。
> ビット平面の解説
ステンシルバッファにも1つ1つのピクセルについて10011110100…という値(ステンシル値)が記録されています。例えばこの値の第3桁をステンシル値としてステンシルテストはどうよ?というような検査を行います(この辺は推測を含みます。検証はしてません。)
で、bitplanes の数がどうのという話でした。これは言い換えるとステンシル値を2進数で表現した時の桁数となります。10110なら5桁なので5、111011011なら9桁でbitplanes は9枚(平面だから数え方は枚?)となります。さらに言うと bitplanes の数と同じだけのステンシルの型紙を作ることができる、という理解で良いと思います。
(わざわざ bitplanes を持ち出す必要があったのかしら。複数のビットとAND演算するとか?)
リファレンスに戻ります。
以下のリストは引数 func によって指定できる比較検査関数のそれぞれの処理である。この表で "stencil" は対応する位置のステンシルバッファ内の値(ステンシル値)を表すものである。この値は0から2^n-1の値をとる。また、ステンシルテストの合格が意味するのはラスタライズの次のステップへ進むということであり、これに合格したからといって描画されると決まるわけでない。
GL_NEVER:すべて不合格
GL_LESS:( ref & mask ) < ( stencil & mask ) で合格
GL_LEQUAL:( ref & mask ) <= ( stencil & mask ) で合格
GL_GREATER:( ref & mask ) > ( stencil & mask ) で合格
GL_GEQUAL:( ref & mask ) >= ( stencil & mask ) で合格
GL_EQUAL:( ref & mask ) = ( stencil & mask ) で合格
GL_NOTEQUAL:( ref & mask ) != ( stencil & mask ) で合格
GL_ALWAYS:すべて合格
※[&]はビット単位のAND演算
あまり理解は進んでいませんが、とりあえずもう1つの関数のリファレンスも訳してみましょう。
ステンシルテストによる処理を設定する。
sfail
ステンシルテストが失敗した時の処理を指定する。
dpfail
ステンシルテストは通ったけどデプステストが通らなかった時の処理を指定する。
dppass
これらの3つの引数は(ステンシルテストが有効であって)記録されているステンシル値がどうなるか、ということを指定するものである。もしステンシルテストが不合格ならば、カラーバッファやデプスバッファーは変更されず、そのときは sfailの指定によりステンシルバッファの値がどのようになるかが決定される。
それぞれに指定する値は以下。デフォルト値は3引数ともに GL_KEEP。
GL_KEEP : 現在の値を保持する。
GL_ZERO : 値をゼロとする。
GL_REPLACE : 値を glStencilFunc() の引数の ref の値にする。
GL_INCR : 現在の値を+1する。
GL_INCR_WRAP : 現在値+1するが、最大値を超える場合はゼロにする。
GL_DECR : 現在の値を-1する。ゼロ以上に補正される。
GL_DECR_WRAP : 現在値-1するが、ゼロ以下になるなら最大値にする。
GL_INVERT : 現在の値をビット演算で反転させた値にする。
ステンシルバッファの「値」というものは最大値がある。これは 2^n-1 と表され、nの実際の値は GL_STENCIL_BITS で取得できる。
実はビット数値をそのまま表示して何だこの中途半端な3175という数字は、ということをやっていたのですが、glGet*() 系の関数で取得するんですね。iPhone プロジェクトで8でした(on シュミレータ)。つまり値としては0〜255、ビット単位では8桁、ということになります。
dpfail と dppass の引数についてはデプステストの結果が出たあとの処理を書くことになる。引数 sfail と同様に↑の8つの定数を指定する。
初期状態ではステンシルテストは無効。もしステンシルバッファーが無いのならが、ステンシルはテストされず、すべてのピクセルに対して描画は許可される。
さて。ここからは OpenGL de プログラミングさんのコード を参考に考えていきます。一部修正しています。/* */ のコメントの箇所は実際には何らかのコードを書く場所で、// はただのコメント。
読む前に簡単に振り返っておくと、 glStencilFunc() は「ステンシルテストの検査方法を指定」し、glStencilOp は「ステンシルテストの結果によってステンシルバッファの内容をどう変化させるか」ということを設定する。
※下記のコードは仮想的なものです。
ここからはいろいろテストしながら実際の処理を確認して行きましょう。はっきり言ってステンシル値のあたりがよくわかってません。
とりあえずテスト用のポリゴンを用意。
左が実際に描画するポリゴン4枚(ナンバリングは下から順に0, 1, 2, 3)。右が型紙として使用するポリゴン2枚です(左から0, 1)。
まずステンシルの書き込みですが、左はステンシル値1で、右は2で書き込んでみます。※コードは Objective-C, iPhone5.1 ベース。(ちなみに GLKView に対してステンシルバッファを使用することを明示しなければならない。←はまった。)
ここで、上のコードの※の関数の第2引数xを0, 1, 2にしてみます。1つずつ見ていきましょう。
※の第2引数x=1→ステンシル値が1ならば描画
ステンシル値が1に設定されているのは左側の型紙の箇所だけ。というわけで左側の型紙と重複される箇所だけが描画されます。
※の第2引数x=2→ステンシル値が2ならば描画
同様に、ステンシル値が2に設定されているのは右の型紙のとこだけ。右の型紙と重複される箇所だけが描画されます。
※の第2引数x=0→ステンシル値が0ならば描画
ステンシル値の初期値はゼロに設定されています。 2つの型紙がある部分に関してはステンシル値が1と2に書き換えられてしまっていますが、残りの部分はステンシル値は初期値のゼロのままです。よって型紙以外の部分の絵が描画されます。
もう少し遊んでみましょう。コードの※の箇所をすぐ下にある forループの中に入れてしまい、ステンシルの比較値はループの i を使います。
下から順に比較値は0, 1, 2, 3
どうでしょう、予想したとおりになりましたか?
一番下のステンシル比較値はゼロ。よってステンシル値がゼロに該当する部分、つまり型紙の外だけ描画。
下から2番目のポリゴンの比較値は1。よってステンシル値が1である部分、つまり左側の型紙の中だけ描画されます。
1つ飛ばして一番上のポリゴンの比較値は3。ステンシル値=3となる箇所はないので、このポリゴンは描画されません。
うんうん、だいたい分かって来ましたね。glStaincliFunc() の機能が2重なのがまぎらわしいですが。
じゃあ型紙が重なってしまったときはどうなるの?というわけでやってみましょう。
型紙の形を変更します。
ちょっとわかりずらいかな?四角形などの方がわかりやすいかもしれませんが。
これを型紙として同じように描画してみると。
下から3つめのポリゴンは良いとして、下から2つめのポリゴンは右側の型紙でオーバーラップされた箇所が見えなくなってしまいました。本当は左側の型紙だけで切り抜きたいのですが。
さてどうしますかね。。。というわけで。シンプルな解決策が見つかったら続きを書こうと思います。誰か教えてエライ人っ。
と、思いましたが意外と単純なことでした。1時間ほどで解決。手順として
左の型紙→右の型紙→ポリゴン4枚
という順番で行なっていましたが、これを
左の型紙→下2つのポリゴン→右の型紙→上2つのポリゴン
の順番で行うと、思ったようになりました。GPU に渡した時点で型紙の位置は固定だと思ってましたが、描画の順番によるようですね。
これで複数マスクはできそうです。ステンシル自体についてもおおよそ理解できたかなと思います。
以上、ステンシルバッファのもろもろでした。
と思いきや、 毎回ステンシルバッファにポリゴンなどを描画するのって非効率ですよね。型紙が変わらないならはじめにステンシルバッファを作ってしまって、あとは絵を描画するときに型紙だけ指定すればオッケイにしたいんですが、やり方が思いつきません。どうしたもんですかね。。。
(追記:多分引数 mask で制限すればよさそう。)
それから iOS の環境ではステンシルは8枚(8ビットだから)なので、個人的には違う方法を模索することになるかも、という状況です。8つマスク書いて→クリア→8マスク→クリア→…でも行けそうですがあまり現実的ではありません。ちょびショックー。
何かのお役にたったらとっても幸いかもということで。正確性については話半分とし、公式のリファレンスなどでご確認ください。
> OpenGL ES API リファレンス
まずはメソッドのリファレンスを和訳しながら、あいだに補足を書いていきます。
void glStencilFunc(GLenum func, GLint ref, GLuint mask);
ステンシルテストの検査方法と比較値を設定する。
func
ステンシルテストの方法を指定する。
ref
ステンシルテストのための比較値を指定する。初期値はゼロ。
mask
ステンシルテストが行われた時の、ref の値とステンシル値に対してビット単位のAND演算する値を指定する。初期値は2進数で表した時に11111…となる数値。
ステンシルテストではデプスバッファと同じように「そのピクセル描いてよいかどうか」を検査する(つまりフラグメントシェーダー段階の処理)。ステンシル面は OpenGL のプリミティブとして描かれ、これを型紙として画像や物体が描かれる。ステンシルはマルチパスレンダリングアルゴリズムでよく使われ、デカール・アウトラインといった特殊効果に利用される。
例えば(x, y)=(25, 322)の位置のピクセルについて黄色がかった赤い色を付けたいんだけどどうよ?というのがステンシルテストというものです。テストで不合格ならばそれ(25, 322への黄赤)は最終的な絵には反映されないことになります。ステンシルはマスク機能にも使われます。
ステンシルテストは「比較値」と「ステンシルバッファの値」を比較した結果を判断し、そのピクセルへの描画を許可するかを決める。ステンシルテストを有効化するためには
glEnable(GL_STENCIL_TEST);とする。ステンシルテストの結果の扱いは glStencliOp() で指定する。
引数として func, ref, mask があるが、実際は表と裏があってそれぞれに設定が必要。両面とも同じ設定ならば glStencliFunc() を使用し、別々に設定するならば glStencilFuncSeparate() を使用する。
引数の func は下に示す8つの定数を設定し、これでステンシルテストの検査方法を決定する。ref はステンシルの比較で使用される整数の値。これは0から2^n-1までの値を取り、nはステンシルバッファの bitplanes の数である(後述)。mask は比較値(ref の値)およびステンシル値と比較(ビット単位のAND演算)される。この結果の2つの値がさらに比較される。
(この段は sonson@Picture&software の記述を参考にしています。)
つまりは3回の比較が行われるみたい。
- ref vs mask の比較
- ステンシル値 vs mask の比較
- 1.の結果 vs 2.の比較検査(引数 func で指定された方法で)
ちなみに mask の値については、1番と2番の比較検査をすることがない場合、111111…と1で埋まった値を使います。実際には「~0」というような値を使います(ビット演算の知識)。
bitplanes というワードが出ましたが、これについては以下の記事に書きました。
> ビット平面の解説
ステンシルバッファにも1つ1つのピクセルについて10011110100…という値(ステンシル値)が記録されています。例えばこの値の第3桁をステンシル値としてステンシルテストはどうよ?というような検査を行います(この辺は推測を含みます。検証はしてません。)
で、bitplanes の数がどうのという話でした。これは言い換えるとステンシル値を2進数で表現した時の桁数となります。10110なら5桁なので5、111011011なら9桁でbitplanes は9枚(平面だから数え方は枚?)となります。さらに言うと bitplanes の数と同じだけのステンシルの型紙を作ることができる、という理解で良いと思います。
(わざわざ bitplanes を持ち出す必要があったのかしら。複数のビットとAND演算するとか?)
リファレンスに戻ります。
以下のリストは引数 func によって指定できる比較検査関数のそれぞれの処理である。この表で "stencil" は対応する位置のステンシルバッファ内の値(ステンシル値)を表すものである。この値は0から2^n-1の値をとる。また、ステンシルテストの合格が意味するのはラスタライズの次のステップへ進むということであり、これに合格したからといって描画されると決まるわけでない。
GL_NEVER:すべて不合格
GL_LESS:( ref & mask ) < ( stencil & mask ) で合格
GL_LEQUAL:( ref & mask ) <= ( stencil & mask ) で合格
GL_GREATER:( ref & mask ) > ( stencil & mask ) で合格
GL_GEQUAL:( ref & mask ) >= ( stencil & mask ) で合格
GL_EQUAL:( ref & mask ) = ( stencil & mask ) で合格
GL_NOTEQUAL:( ref & mask ) != ( stencil & mask ) で合格
GL_ALWAYS:すべて合格
※[&]はビット単位のAND演算
あまり理解は進んでいませんが、とりあえずもう1つの関数のリファレンスも訳してみましょう。
void glStencilOp(GLenum sfail, GLenum dpfail, GLenum dppass);
ステンシルテストによる処理を設定する。
sfail
ステンシルテストが失敗した時の処理を指定する。
dpfail
ステンシルテストは通ったけどデプステストが通らなかった時の処理を指定する。
dppass
- ステンシルテスト・デプステスト両方が通った
- ステンシルテストが成功したけどデプスバッファが無いかデプスバッファが無効(glDisable)である
これらの3つの引数は(ステンシルテストが有効であって)記録されているステンシル値がどうなるか、ということを指定するものである。もしステンシルテストが不合格ならば、カラーバッファやデプスバッファーは変更されず、そのときは sfailの指定によりステンシルバッファの値がどのようになるかが決定される。
それぞれに指定する値は以下。デフォルト値は3引数ともに GL_KEEP。
GL_KEEP : 現在の値を保持する。
GL_ZERO : 値をゼロとする。
GL_REPLACE : 値を glStencilFunc() の引数の ref の値にする。
GL_INCR : 現在の値を+1する。
GL_INCR_WRAP : 現在値+1するが、最大値を超える場合はゼロにする。
GL_DECR : 現在の値を-1する。ゼロ以上に補正される。
GL_DECR_WRAP : 現在値-1するが、ゼロ以下になるなら最大値にする。
GL_INVERT : 現在の値をビット演算で反転させた値にする。
ステンシルバッファの「値」というものは最大値がある。これは 2^n-1 と表され、nの実際の値は GL_STENCIL_BITS で取得できる。
実はビット数値をそのまま表示して何だこの中途半端な3175という数字は、ということをやっていたのですが、glGet*() 系の関数で取得するんですね。iPhone プロジェクトで8でした(on シュミレータ)。つまり値としては0〜255、ビット単位では8桁、ということになります。
dpfail と dppass の引数についてはデプステストの結果が出たあとの処理を書くことになる。引数 sfail と同様に↑の8つの定数を指定する。
初期状態ではステンシルテストは無効。もしステンシルバッファーが無いのならが、ステンシルはテストされず、すべてのピクセルに対して描画は許可される。
さて。ここからは OpenGL de プログラミングさんのコード を参考に考えていきます。一部修正しています。/* */ のコメントの箇所は実際には何らかのコードを書く場所で、// はただのコメント。
読む前に簡単に振り返っておくと、 glStencilFunc() は「ステンシルテストの検査方法を指定」し、glStencilOp は「ステンシルテストの結果によってステンシルバッファの内容をどう変化させるか」ということを設定する。
※下記のコードは仮想的なものです。
void setUp() { //======================================== /* 初期設定の処理 */ //======================================== glEnable(GL_DEPTH_TEST); glEnable(GL_STENCIL_TEST);//ステンシルテストを有効化 } void renderAtEachFrame() { // 前回の描画情報を掃除 glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT| GL_STENCIL_BUFFER_BIT); //======================================== /* その他準備の処理 */ //======================================== // 以降で描くポリゴンなどは絵として描かない glColorMask(0,0,0,0); glDepthMask(0); // 値はステンシルテストで検査せず(GL_ALWAYS)、そのまま書き込みをする。 glStencilFunc(GL_ALWAYS, 1, ~0); // テスト合格でステンシル値を glStencilFunc() の第2引数の値に書き換える(GL_REPLACE) // ステンシルバッファがない場合はステンシル値はそのまま(第1引数=GL_KEEP) glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE); //======================================== /* ポリゴンなどでステンシルの型紙部分を描く処理 */ // 「描かない」宣言をしてるので、ここで描いたオブジェクトは絵には反映されない。 // ステンシルバッファにのみ書き込まれる。 //======================================== // 以降で描くポリゴンなどは絵として描く glColorMask(1,1,1,1); glDepthMask(1); // 以降でステンシル値は書き換えない glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP); // ステンシル値が1の所だけを書き込む glStencilFunc( GL_EQUAL, 1, ~0); //======================================== /* ポリゴンなどで実際の絵を描く処理その1 */ // 個々のピクセルについてステンシル値が1ならば描画される。 //======================================== // ステンシル値が0の所だけを書き込む glStencilFunc(GL_EQUAL, 0, ~0); //======================================== /* ポリゴンなどで実際の絵を描く処理その2 */ // 個々のピクセルについてステンシル値が0ならば描画される。 //======================================== }
ここからはいろいろテストしながら実際の処理を確認して行きましょう。はっきり言ってステンシル値のあたりがよくわかってません。
とりあえずテスト用のポリゴンを用意。
左が実際に描画するポリゴン4枚(ナンバリングは下から順に0, 1, 2, 3)。右が型紙として使用するポリゴン2枚です(左から0, 1)。
まずステンシルの書き込みですが、左はステンシル値1で、右は2で書き込んでみます。※コードは Objective-C, iPhone5.1 ベース。(ちなみに GLKView に対してステンシルバッファを使用することを明示しなければならない。←はまった。)
- (void)glkView:(GLKView *)view drawInRect:(CGRect)rect { glEnable(GL_DEPTH_TEST); glEnable(GL_STENCIL_TEST); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT); glClearColor(0.0f, 0.0f, 0.0f, 1.0);// 背景色 /* いろいろ必要な処理 */ // 描画開始 [self.effect prepareToDraw]; // ステンシルの設定を開始しまーす。 glStencilOp(GL_KEEP,GL_REPLACE,GL_REPLACE);// 以降はステンシル値を書き込みます。 glColorMask(0,0,0,0); glDepthMask(0); // ここからはステンシル値を1で書き込みます。 glStencilFunc(GL_ALWAYS, 1, ~0); // 型紙1枚目描画 glBindVertexArrayOES(stencilVertexArrayIDs_[0]); glDrawArrays(GL_TRIANGLES, 0, 3); // ここからはステンシル値を2で書き込みます。 glStencilFunc(GL_ALWAYS, 2, ~0); // 型紙2枚目を描画 glBindVertexArrayOES(stencilVertexArrayIDs_[1]); glDrawArrays(GL_TRIANGLES, 0, 3); // ここからは絵(ポリゴン)を描きまーす。 // 設定を絵の描画用に戻す。 glColorMask(1,1,1,1); glDepthMask(1); glStencilOp(GL_KEEP,GL_KEEP ,GL_KEEP);// 以降はステンシル値は変更しない。 // ここからはステンシル値がxの部分だけ描画します。 glStencilFunc(GL_EQUAL, x, ~0);// ※ for (int i=0; i<4; i++) { glBindVertexArrayOES(vtxArrayIDs_[i]); glDrawArrays(GL_TRIANGLES, 0, 3); } glBindVertexArrayOES(0); }
ここで、上のコードの※の関数の第2引数xを0, 1, 2にしてみます。1つずつ見ていきましょう。
※の第2引数x=1→ステンシル値が1ならば描画
ステンシル値が1に設定されているのは左側の型紙の箇所だけ。というわけで左側の型紙と重複される箇所だけが描画されます。
※の第2引数x=2→ステンシル値が2ならば描画
同様に、ステンシル値が2に設定されているのは右の型紙のとこだけ。右の型紙と重複される箇所だけが描画されます。
※の第2引数x=0→ステンシル値が0ならば描画
ステンシル値の初期値はゼロに設定されています。 2つの型紙がある部分に関してはステンシル値が1と2に書き換えられてしまっていますが、残りの部分はステンシル値は初期値のゼロのままです。よって型紙以外の部分の絵が描画されます。
もう少し遊んでみましょう。コードの※の箇所をすぐ下にある forループの中に入れてしまい、ステンシルの比較値はループの i を使います。
for (int i=0; i<4; i++) { glStencilFunc(GL_EQUAL, i, ~0); glBindVertexArrayOES(vtxArrayIDs_[i]); glDrawArrays(GL_TRIANGLES, 0, 3); }
下から順に比較値は0, 1, 2, 3
どうでしょう、予想したとおりになりましたか?
一番下のステンシル比較値はゼロ。よってステンシル値がゼロに該当する部分、つまり型紙の外だけ描画。
下から2番目のポリゴンの比較値は1。よってステンシル値が1である部分、つまり左側の型紙の中だけ描画されます。
1つ飛ばして一番上のポリゴンの比較値は3。ステンシル値=3となる箇所はないので、このポリゴンは描画されません。
うんうん、だいたい分かって来ましたね。glStaincliFunc() の機能が2重なのがまぎらわしいですが。
じゃあ型紙が重なってしまったときはどうなるの?というわけでやってみましょう。
型紙の形を変更します。
ちょっとわかりずらいかな?四角形などの方がわかりやすいかもしれませんが。
これを型紙として同じように描画してみると。
下から3つめのポリゴンは良いとして、下から2つめのポリゴンは右側の型紙でオーバーラップされた箇所が見えなくなってしまいました。本当は左側の型紙だけで切り抜きたいのですが。
さてどうしますかね。。。というわけで。シンプルな解決策が見つかったら続きを書こうと思います。誰か教えてエライ人っ。
と、思いましたが意外と単純なことでした。1時間ほどで解決。手順として
左の型紙→右の型紙→ポリゴン4枚
という順番で行なっていましたが、これを
左の型紙→下2つのポリゴン→右の型紙→上2つのポリゴン
の順番で行うと、思ったようになりました。GPU に渡した時点で型紙の位置は固定だと思ってましたが、描画の順番によるようですね。
これで複数マスクはできそうです。ステンシル自体についてもおおよそ理解できたかなと思います。
以上、ステンシルバッファのもろもろでした。
と思いきや、 毎回ステンシルバッファにポリゴンなどを描画するのって非効率ですよね。型紙が変わらないならはじめにステンシルバッファを作ってしまって、あとは絵を描画するときに型紙だけ指定すればオッケイにしたいんですが、やり方が思いつきません。どうしたもんですかね。。。
(追記:多分引数 mask で制限すればよさそう。)
それから iOS の環境ではステンシルは8枚(8ビットだから)なので、個人的には違う方法を模索することになるかも、という状況です。8つマスク書いて→クリア→8マスク→クリア→…でも行けそうですがあまり現実的ではありません。ちょびショックー。
ビット平面
ちょっと解説。専門家ではありませんので正確性は話半分ということで。
bitplane、ビットプレーンともいう。
OpenGL にかかわらず情報処理技術全般で使われる言葉。"bitaplane" で検索するよりも「ビットプレーン」で検索したほうがわかりやすい例が出ました。
検索結果1(wikipedia)、 検索結果2、検索結果3
ある画像ファイルを考えてみます。画像ファイルというのはピクセル(画素)と呼ばれる点が集まったものです。300個のピクセルが200列に並べば300x200のサイズの画像になります。
ここではある画像ファイルがあり、ピクセルのそれぞれについて
- そこが輝いているかどうか
- そこが赤いかどうか
- マウスクリックに反応するかどうか
それぞれの情報が true か false というのは1か0という値で表現できます。例えば輝きがあって(=1)、赤くて(=1)、クリックに反応しない(=0)ピクセルが持つ情報は[110]となります。他にも例示してみると以下の様になります。
輝かない白でクリック反応:001
輝かない赤でクリック反応:011
輝く青でクリックは無反応:100
今、幅3px高さ2pxの小さな絵があったとします。その情報は以下のようになっているとしましょう。
[111][001][110]
[011][001][010]
この時「赤かどうか」ということだけに注目し2つめの情報だけをピックアップして並べてみると
[1][0][1]
[1][0][1]
となります。1つ1つのピクセルの情報が1とゼロだけで表現されました。
ある1つの要素(ビット)に注目して取り出した時に、1とゼロの2次元配列(幅x高さ)で表すことができます。この2次元配列がビット平面、ということになります。
ピクセルごとの情報は必ずしも2進数で表現されている必要はありません。例として3x2の画像で2つの情報(例えば明度と彩度とか)が以下のように記録されていたとします。
[2, 10][5, 11][6, 12]
[3, 10][2, 12][2, 12]
10進数は2進数に変換することができます。ただし桁数が決まっていないといろいろ不都合なので、ここでは4桁として変換すると
[0010, 1010][0101, 1011][0110, 1110]
[0011, 1010][0010, 1110][0010, 1110]
(であってるかな?)
それぞれのピクセルのデータはカンマで2分割されていますが、これはくっつけてしまいましょう。
[00101010][01011011][01101110]
[00111010][00101110][00101110]
こうすると(x, y)=(2,1)の情報は "00101110" だ、というふうに1つの数値で表現することができます。
ここまで来ればビット平面の抽出は同じ。それぞれのピクセルの情報について右から3桁目だけを抽出して並べると、
[0][0][1]
[0][1][1]
となります。他の桁でも同じ事ですね。
2012年4月11日水曜日
All about OpenGL ES 2.x 2/3 -5
> 2/3 -4 の続き
2/3 の最後です。超特急風味。
バッファーオブジェクトの使用はとっても簡単!GL_ARRAY_BUFFER と GL_ELEMENT_ARRAY_BUFFERの2つのバッファーを同時にバインドし、 glDraw* のメソッドを呼んで開始インデックスを指定する。
では、(ついに最後だ!)次はどうやって EGL API を使ってレンダリングを完成するかを見てみよう。
ここまでは基本的なデバイススクリーンへの描画について見てきた。しかしフレームバッファのようなオフスクリーンやテクスチャに描画することもできるし、それをファイルに保存したり、デバイススクリーンの中で画像を作ることもできる。
Pre-Render
レンダリングを2つのステップとして捉えよう。1段階目は pre-render。このステップにおいては僕らは古いゴミを掃除する必要がある。覚えているかな、フレームバッファーとはレンダーバッファの画像の集合体。描画処理を完了したら、スクリーンへの描画が終わってもレンダーバッファにはデータが残っている。というわけでこの pre-render 段階ではレンダーバッファを綺麗にしてあげる。もし前段階のイメージを使いたいのなら話は別だが。
レンダーバッファの掃除のためには以下のメソッドを使う。
ここまででわかっていると思うが、港のクレーンのフックの一つへの指令というのは最後にバインドしたオブジェクトに対して行われる。よって、このメソッドを使うときには使いたい frame buffer をバインドしておく必要がある。引数には OR "|" ででもって複数の掃除をすることができる。
掃除が出来ればあとはレンダリング glDraw* を呼び出す段階だ。
Drawing
実はここまでよく出てきているが、以下の2つの関数でもって描画が行われる。
はじめに、どのようにこれらの関数が動くのかを説明しよう。まず programable pipeline において重要なのは VSH が実行される回数が定義されるということ。この回数は引数の count で決まる。もちろん GPU は最適化を行うが、一般には VSH は僕らが定義した attributes と uniforms にその回数だけ処理されることになる。なぜこんなことを言うかというと、例えば200の頂点があって、でもあるときそのうちの3つだけを使いたいということがあるかもしれない。そういうときに役立つのはインデックス配列を使うことだ。立方体なら頂点は8つであるが、描画に必要な数を数えると24になる。
glDrawArrays を使うとき、"first" の引数は頂点ごとの attributes の開始点オフセットを決める。2を指定すれば、glVertexAttribPointer で指定したデータのうち、第2番の要素から開始することになる。
glDrawElements を使うとき、"first" はインデックスの開始点オフセット指定となり、頂点ごとのオフセットとはならない。"type" はデータ型。でもってここにちょっとヒント。もし要素が255以下ならば GL_UNSIGNED_BYTE を使おう。
(補足:glDrawElements には first という引数は無い。書き間違い?)
次は "mode" 。メッシュといってもいろいろ書き方がある。次の画像を見てくれ。
(>元ネタサイトへ)
この画像で、すべての頂点データは
(>元ネタサイトへ)
この画像で、すべての頂点データは
繰り返すが、 GL_LINES と GL_TRIANGLEを使うべし。他の STRIP, LOOP などに関しては OpenGL の他の部分んの最適化で達成できる部分であり、メッシュのポリゴンを減らしたり、シェーダーのプロセスを最適化することによって出来る。
Render
最後は(例え鋭角にフレームバッファを使っていなかったとしても)ただフレームバッファの結果をスクリーンに映すだけである。これは EGL の説明の時に書いたこと。
ここではそれを繰り返しはしないが、EAGL API を使っているなら presentRenderbuffer:GL_RENDERBUFFER を呼び出す前にカラーレンダーバッファとフレームバッファを必ずバインドしなければならない。なぜなら レンダーバッファはフレームバッファの「内部に」置かれているから。
EGL API を使っているなら、これで内部のバッファーが反転する処理が行われる
以上!これがレンダリングの基本。OpenGL はマルチサンプルと呼ばれるなんとやらも提供しているが、これはアンチアライアスなイメージを作るための特別な処理。ここでは取り上げない。次の項で取り上げよう。
全く長くなってしまった。
※Couclusion 省略
以上。
結構省略してしまいましたが、愛嬌ということでお許し下さい。(特に後半にかけて)直訳だけで終わってしまった感があって少し残念ですが、もう自分の頭の中で少し噛み砕いてから他の形でまとめれればいいなと考えています。
3/3 は書きません。発展的なテクニックであり、OpenGL の基礎的な部分から外れるので、個人的にはまだいいやという感じなので。です。
2/3 の最後です。超特急風味。
Using the Buffer Objects
バッファーオブジェクトの使用はとっても簡単!GL_ARRAY_BUFFER と GL_ELEMENT_ARRAY_BUFFERの2つのバッファーを同時にバインドし、 glDraw* のメソッドを呼んで開始インデックスを指定する。
では、(ついに最後だ!)次はどうやって EGL API を使ってレンダリングを完成するかを見てみよう。
Rendering
ここまでは基本的なデバイススクリーンへの描画について見てきた。しかしフレームバッファのようなオフスクリーンやテクスチャに描画することもできるし、それをファイルに保存したり、デバイススクリーンの中で画像を作ることもできる。
Pre-Render
レンダリングを2つのステップとして捉えよう。1段階目は pre-render。このステップにおいては僕らは古いゴミを掃除する必要がある。覚えているかな、フレームバッファーとはレンダーバッファの画像の集合体。描画処理を完了したら、スクリーンへの描画が終わってもレンダーバッファにはデータが残っている。というわけでこの pre-render 段階ではレンダーバッファを綺麗にしてあげる。もし前段階のイメージを使いたいのなら話は別だが。
レンダーバッファの掃除のためには以下のメソッドを使う。
GLvoid glClear(GLbitfield mask)
ここまででわかっていると思うが、港のクレーンのフックの一つへの指令というのは最後にバインドしたオブジェクトに対して行われる。よって、このメソッドを使うときには使いたい frame buffer をバインドしておく必要がある。引数には OR "|" ででもって複数の掃除をすることができる。
掃除が出来ればあとはレンダリング glDraw* を呼び出す段階だ。
Drawing
実はここまでよく出てきているが、以下の2つの関数でもって描画が行われる。
GLvoid glDrawArrays(GLenum mode, GLint first, GLsizei count) GLvoid glDrawElements(GLenum mode, GLsizei count, GLenum type, const GLvoid* indices)
はじめに、どのようにこれらの関数が動くのかを説明しよう。まず programable pipeline において重要なのは VSH が実行される回数が定義されるということ。この回数は引数の count で決まる。もちろん GPU は最適化を行うが、一般には VSH は僕らが定義した attributes と uniforms にその回数だけ処理されることになる。なぜこんなことを言うかというと、例えば200の頂点があって、でもあるときそのうちの3つだけを使いたいということがあるかもしれない。そういうときに役立つのはインデックス配列を使うことだ。立方体なら頂点は8つであるが、描画に必要な数を数えると24になる。
glDrawArrays を使うとき、"first" の引数は頂点ごとの attributes の開始点オフセットを決める。2を指定すれば、glVertexAttribPointer で指定したデータのうち、第2番の要素から開始することになる。
glDrawElements を使うとき、"first" はインデックスの開始点オフセット指定となり、頂点ごとのオフセットとはならない。"type" はデータ型。でもってここにちょっとヒント。もし要素が255以下ならば GL_UNSIGNED_BYTE を使おう。
(補足:glDrawElements には first という引数は無い。書き間違い?)
次は "mode" 。メッシュといってもいろいろ書き方がある。次の画像を見てくれ。
(>元ネタサイトへ)
この画像で、すべての頂点データは
{v0,v1,v2,v3,v4,v5}
のようになっている(それぞれはx, y, z の要素をもった頂点)。一番融通が利くのは GL_LINES 。これを使っておこう。ただし特定の条件では他の2つは最適化されたものとして利用価値はある。お次は三角形の図。(>元ネタサイトへ)
この画像で、すべての頂点データは
{v0,v1,v2,v3,v4,v5}
のようになっている(それぞれはx, y, z の要素をもった頂点)。線の時と同じように、GL_TRIANGLE が最も使える。他は特定の使用においては最適な形。繰り返すが、 GL_LINES と GL_TRIANGLEを使うべし。他の STRIP, LOOP などに関しては OpenGL の他の部分んの最適化で達成できる部分であり、メッシュのポリゴンを減らしたり、シェーダーのプロセスを最適化することによって出来る。
Render
最後は(例え鋭角にフレームバッファを使っていなかったとしても)ただフレームバッファの結果をスクリーンに映すだけである。これは EGL の説明の時に書いたこと。
ここではそれを繰り返しはしないが、EAGL API を使っているなら presentRenderbuffer:GL_RENDERBUFFER を呼び出す前にカラーレンダーバッファとフレームバッファを必ずバインドしなければならない。なぜなら レンダーバッファはフレームバッファの「内部に」置かれているから。
- (void) makeRender { glBindFramebuffer(_framebuffer); glBindRenderbuffer(_colorRenderbuffer); [_context presentRenderbuffer:GL_RENDERBUFFER]; }
EGL API を使っているなら、これで内部のバッファーが反転する処理が行われる
以上!これがレンダリングの基本。OpenGL はマルチサンプルと呼ばれるなんとやらも提供しているが、これはアンチアライアスなイメージを作るための特別な処理。ここでは取り上げない。次の項で取り上げよう。
全く長くなってしまった。
※Couclusion 省略
以上。
結構省略してしまいましたが、愛嬌ということでお許し下さい。(特に後半にかけて)直訳だけで終わってしまった感があって少し残念ですが、もう自分の頭の中で少し噛み砕いてから他の形でまとめれればいいなと考えています。
3/3 は書きません。発展的なテクニックであり、OpenGL の基礎的な部分から外れるので、個人的にはまだいいやという感じなので。です。
2012年4月4日水曜日
All about OpenGL ES 2.x 2/3 -4
> 2/3 -3 の続き
ここからはメソッドの説明が多いのようなので概念的なところだけにします。長いし。
まずはこの図を見てみよう。(元ネタサイトへ)ここでは Attributes, Uniforms, 変数(定義済み変数)、ビルトイン関数を紹介します。
頂点シェーダー(VSH) はいつも1つ以上の Attributes を持つ。Attributes は3Dオブジェクトの頂点を生成するために使われ、頂点ごとにのみ定義される。頂点の最終的な位置を決めるためには OpenGL の変数 gl_Position や gl_PointSize を、フラグメントシェーダーなら gj_FlagColor なんかを使うことになる。
Attributes, Uniforms, 変数は GPU と CPU の間の橋渡しをするものだ。僕らはレンダリングをする( = glDraw* メソッドを使用する)まえに、VSH などに Attributes の設定(?)を渡したいこともあるはずだ。その設定は頂点によって違うかもしれないし、同じかもしれない。デフォルトでは OpenGL の programmable pipeline は最低でも8つの Attributes の設定をすることができる。
一方 FSH には直接には設定をすることはできないが、VSH に Verying を設定して、これを FSH に設定を流しこむことはできる。しかしあまり一般的ではない。こちらも最低8つの
Veryings の設定をすることができる。
シェーダーにアクセスするためのもう1つの手段は Uniforms を使うことだが、名前が示すとおりこれらはすべてのシェーダーの処理において一定の値が用いられる。最もよく使われるのは sampler だ。覚えてるかな?テクスチャを保持するために使われるユニットだ。(補足:この和訳では省略した Shader Language の項のリストにデータ型として載っている。)中身としては int 型だが、つくりは特別。サポートされる最低サイズはシェーダーによって異なり、VSH:128、FSH:16。
次は定義済み変数 (=Built-in Variables)。僕らはシェーダーに対してそれらを設定しなければならない。例えば VSH には頂点の最終的な位置を決めるが、これには gl_Position やら gl_PointSize なんかを使う。gl_PointSize は FSH に対して「それぞれの頂点がいくつの頂点に影響するか」を設定する。簡単に言うと3Dのテンのスクリーンでのサイズ。これはパーティクル(炎など)を作るとき非常に役に立つ。VSH の gl_FrontFacing は面がどちらを向いているかが書いてある読み込み専用の変数。
FSH で使われるのは gl_FragColor という変数。(gl_FragData という場合もあるが省略。) FSH の読込み専用変数は gl_FrontFacing, gl_FragCoord, gl_PointCoord の3つがある。gl_FragCoord はvec4 型のデータでウィンドウに対する相対座標を表す。gl_PointCoord はテクスチャ座標を取得するのに使う。
最も重要なことは「ビルトイン変数への書き込みは最終的な値になる」ということ。よって僕らは何度も gl_Position を書き換えたりすることはできない。gl_FragColor もまたしかり。
(ビルトイン変数のリスト:省略)
(VSH の実際のコード:省略)
(FSH の実際のコード:省略)
では OpenGL の API に戻って Attributes や Uniforms などの設定に戻ってみてみよう。先程書いたように、Varyings に関しては直接コントロールすることはできないので主に VSH の処理の中で Attributes を通してこれにアクセスすることになる。
シェーダーの中の変数を特定するために、Program Object はその変数の位置(=indexと同義)を決めている。Attributes と Uniforms が設定された位置がわかれば僕らはそれを使って値を設定できる。
Uniforms を設定するためには、Program Object がリンクしてからシェーダー内の名前に応じた欲しい Uniform の位置をゲットする。
Attributes を設定するためには2通りの方法があるが「Program Object がリンクしたあとに設定位置を取得する」のが一般的。
「glBind何たら = 欲しいコンテナ」という書き方をするが、ここでは港のクレーンは実際的なコンテナを持ってくるわけではない。Program Object 内部の処理過程に関連する箇所に bind (つなげる)という意味になり、Attributes の名前によって Program Object の内部位置につなげる。1つめのメソッドは以下。
GLvoid glBindAttribLocation(GLuint program, GLuint index, const GLchar* name)
このメソッドは Program Object が生成してから、かつリンクの前に呼ばれなければいけない。というのは実は Program Object の生成段階であるので、おすすめしない。次の関数を使うほうが良い。
次は Attributes と Uniforms の位置をリンク処理の後に取得する方法。どっちの方法をやるにしろ、それぞれのシェーダーへの位置は僕らは自前でキープしておかなければいけない(後で使うから)。
GLint glGetAttribLocation(GLuint program, const GLchar* name)
GLint glGetUniformLocation(GLuint program, const GLchar* name)
(補足:name にはシェーダープログラムで使用する uniform 変数名を指定する。これは末尾が null である文字列でなければ行けない。変数名はスペースを含んではならない。)
一度位置を取得してしまえば、あとはいつでも値を設定することができる。28の関数を使って定数や動的変数の設定を行うことができる。動的な変数を設定するときにはそれを有効にしておく必要がある。Uniforms と動的な変数の違いが何かだって?良い質問だ!例えば GL_FRONT_AND_BACK という役に立たない定数を OpenGL が使い続けているのが僕には理解出来ない。それにメモリの使い方も同じだし、パフォーマンスも同じ。意味ねーっす。結論としては、「動的な値には動的変数だけを使って、もし一定の値なら Uniforms を使おう」ってとこかな。
加えて、Uniform に定数を設定するには2つの理由がある。Uniforms は 頂点シェーダーで128回使えるが、Attributes は8回しか使えない* 。そして Attributes は配列データでないということ。これについては後で説明する。デフォルトでは OpenGL は Attributes を定数として使っているが、本来は動的なものとして利用される。
(* 原文:Uniforms can be used 128 times in the vertex shader but the attributes just 8 times)
とにかく、その動的な Attributes と Uniforms と役に立たない 定数 Attributes を見てみよう。Uniforms はどんなデータ型でも、構造体でも、どんな形の配列でも扱うことができる。では関数をば。
わあってる、わかってる。1ずつ説明させてくれ。
{1234}や{if}という表記はそのなかのいずれかを書くことを表す。 vとfvの箇所はそのまま書く。[N]の箇所については{1234}で指定した数だけ引数を書くことを表す。まあつまりは以下の19ってこと。
どっひゃーって感じだけど、実はそんなに難しくない。
で、重要なのは2点。両方のシェーダーでおなじ Uniform が使用される。2つめは重要で、Uniforms が現在使われている Program Object に対して設定されるということ。よって僕らは Uniforms やら Attributes やらを設定する前にプログラムをスタートしなければならない。glUseProgram に欲しいIDを渡して呼べば良い。
では次、Attributes への値設定。Attributes が読めるのは float, vec2, vec3, vec4, mat2, mat3, mat4 のデータ型のみ。配列や構造体としては宣言できない。
名前付けのルールは Uniforms の時と同じような感じだね。上の2つは定数を設定し、3つめは動的な値を設定する関数。
ここで面倒なのは定数はシェーダーによってのデフォルトの処理であるということ。動的な値を設定するのならば一時的にこれらの変更を有効にしなければならない。動的な値は頂点ごとに設定される。動的値の有効/無効を切り替えるためには以下のメソッドを使用する。
glVertexAttribPointer() を使って設定する前に、glEnableVertexAttribArray() でもって有効にしないとならない。
前に示した VSH と FSH のコードを使うときに、次のようなプログラムで値を設定することができる。
(Attributes と Uniforms の設定をするコード:省略)
例示するために enabled したり disabled したりしています。しかしながら先に行ったように、enabled や disabled したりするのは OpenGL にとって非常にコストの高い処理です。よってそれらは1回だけその位置を取得したときに行うのがよさそうです。
> 2/3 -5 へ
コードは元ネタサイトを見てくださいね。今回は直訳ざっくりな箇所が多いかもしれません。が、書いている本人は仕組みの裏側がわかってきたきがするのですこし楽しいです。
ここからはメソッドの説明が多いのようなので概念的なところだけにします。長いし。
Vertex and Fragment Structures
まずはこの図を見てみよう。(元ネタサイトへ)ここでは Attributes, Uniforms, 変数(定義済み変数)、ビルトイン関数を紹介します。
頂点シェーダー(VSH) はいつも1つ以上の Attributes を持つ。Attributes は3Dオブジェクトの頂点を生成するために使われ、頂点ごとにのみ定義される。頂点の最終的な位置を決めるためには OpenGL の変数 gl_Position や gl_PointSize を、フラグメントシェーダーなら gj_FlagColor なんかを使うことになる。
Attributes, Uniforms, 変数は GPU と CPU の間の橋渡しをするものだ。僕らはレンダリングをする( = glDraw* メソッドを使用する)まえに、VSH などに Attributes の設定(?)を渡したいこともあるはずだ。その設定は頂点によって違うかもしれないし、同じかもしれない。デフォルトでは OpenGL の programmable pipeline は最低でも8つの Attributes の設定をすることができる。
一方 FSH には直接には設定をすることはできないが、VSH に Verying を設定して、これを FSH に設定を流しこむことはできる。しかしあまり一般的ではない。こちらも最低8つの
Veryings の設定をすることができる。
シェーダーにアクセスするためのもう1つの手段は Uniforms を使うことだが、名前が示すとおりこれらはすべてのシェーダーの処理において一定の値が用いられる。最もよく使われるのは sampler だ。覚えてるかな?テクスチャを保持するために使われるユニットだ。(補足:この和訳では省略した Shader Language の項のリストにデータ型として載っている。)中身としては int 型だが、つくりは特別。サポートされる最低サイズはシェーダーによって異なり、VSH:128、FSH:16。
次は定義済み変数 (=Built-in Variables)。僕らはシェーダーに対してそれらを設定しなければならない。例えば VSH には頂点の最終的な位置を決めるが、これには gl_Position やら gl_PointSize なんかを使う。gl_PointSize は FSH に対して「それぞれの頂点がいくつの頂点に影響するか」を設定する。簡単に言うと3Dのテンのスクリーンでのサイズ。これはパーティクル(炎など)を作るとき非常に役に立つ。VSH の gl_FrontFacing は面がどちらを向いているかが書いてある読み込み専用の変数。
FSH で使われるのは gl_FragColor という変数。(gl_FragData という場合もあるが省略。) FSH の読込み専用変数は gl_FrontFacing, gl_FragCoord, gl_PointCoord の3つがある。gl_FragCoord はvec4 型のデータでウィンドウに対する相対座標を表す。gl_PointCoord はテクスチャ座標を取得するのに使う。
最も重要なことは「ビルトイン変数への書き込みは最終的な値になる」ということ。よって僕らは何度も gl_Position を書き換えたりすることはできない。gl_FragColor もまたしかり。
(ビルトイン変数のリスト:省略)
(VSH の実際のコード:省略)
(FSH の実際のコード:省略)
では OpenGL の API に戻って Attributes や Uniforms などの設定に戻ってみてみよう。先程書いたように、Varyings に関しては直接コントロールすることはできないので主に VSH の処理の中で Attributes を通してこれにアクセスすることになる。
Setting the Attributes and Uniforms
シェーダーの中の変数を特定するために、Program Object はその変数の位置(=indexと同義)を決めている。Attributes と Uniforms が設定された位置がわかれば僕らはそれを使って値を設定できる。
Uniforms を設定するためには、Program Object がリンクしてからシェーダー内の名前に応じた欲しい Uniform の位置をゲットする。
Attributes を設定するためには2通りの方法があるが「Program Object がリンクしたあとに設定位置を取得する」のが一般的。
「glBind何たら = 欲しいコンテナ」という書き方をするが、ここでは港のクレーンは実際的なコンテナを持ってくるわけではない。Program Object 内部の処理過程に関連する箇所に bind (つなげる)という意味になり、Attributes の名前によって Program Object の内部位置につなげる。1つめのメソッドは以下。
GLvoid glBindAttribLocation(GLuint program, GLuint index, const GLchar* name)
このメソッドは Program Object が生成してから、かつリンクの前に呼ばれなければいけない。というのは実は Program Object の生成段階であるので、おすすめしない。次の関数を使うほうが良い。
次は Attributes と Uniforms の位置をリンク処理の後に取得する方法。どっちの方法をやるにしろ、それぞれのシェーダーへの位置は僕らは自前でキープしておかなければいけない(後で使うから)。
GLint glGetAttribLocation(GLuint program, const GLchar* name)
GLint glGetUniformLocation(GLuint program, const GLchar* name)
(補足:name にはシェーダープログラムで使用する uniform 変数名を指定する。これは末尾が null である文字列でなければ行けない。変数名はスペースを含んではならない。)
一度位置を取得してしまえば、あとはいつでも値を設定することができる。28の関数を使って定数や動的変数の設定を行うことができる。動的な変数を設定するときにはそれを有効にしておく必要がある。Uniforms と動的な変数の違いが何かだって?良い質問だ!例えば GL_FRONT_AND_BACK という役に立たない定数を OpenGL が使い続けているのが僕には理解出来ない。それにメモリの使い方も同じだし、パフォーマンスも同じ。意味ねーっす。結論としては、「動的な値には動的変数だけを使って、もし一定の値なら Uniforms を使おう」ってとこかな。
加えて、Uniform に定数を設定するには2つの理由がある。Uniforms は 頂点シェーダーで128回使えるが、Attributes は8回しか使えない* 。そして Attributes は配列データでないということ。これについては後で説明する。デフォルトでは OpenGL は Attributes を定数として使っているが、本来は動的なものとして利用される。
(* 原文:Uniforms can be used 128 times in the vertex shader but the attributes just 8 times)
とにかく、その動的な Attributes と Uniforms と役に立たない 定数 Attributes を見てみよう。Uniforms はどんなデータ型でも、構造体でも、どんな形の配列でも扱うことができる。では関数をば。
GLvoid glUniform{1234}{if}(GLint location, T value[N]) GLvoid glUniform{1234}{if}v(GLint location, GLsizei count, const T* value) GLvoid glUniformMatrix{234}fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)
わあってる、わかってる。1ずつ説明させてくれ。
{1234}や{if}という表記はそのなかのいずれかを書くことを表す。 vとfvの箇所はそのまま書く。[N]の箇所については{1234}で指定した数だけ引数を書くことを表す。まあつまりは以下の19ってこと。
glUniform1i(GLint location, GLint x) glUniform1f(GLint location, GLfloat x) glUniform2i(GLint location, GLint x, GLint y) glUniform2f(GLint location, GLfloat x, GLfloat y) glUniform3i(GLint location, GLint x, GLint y, GLint z) glUniform3f(GLint location, GLfloat x, GLfloat y, GLfloat z) glUniform4i(GLint location, GLint x, GLint y, GLint z, GLint w) glUniform4f(GLint location, GLfloat x, GLfloat y, GLfloat z, GLfloat w) glUniform1iv(GLint location, GLsizei count, const GLint* v) glUniform1fv(GLint location, GLsizei count, const GLfloat* v) glUniform2iv(GLint location, GLsizei count, const GLint* v) glUniform2fv(GLint location, GLsizei count, const GLfloat* v) glUniform3iv(GLint location, GLsizei count, const GLint* v) glUniform3fv(GLint location, GLsizei count, const GLfloat* v) glUniform4iv(GLint location, GLsizei count, const GLint* v) glUniform4fv(GLint location, GLsizei count, const GLfloat* v) glUniformMatrix2fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) glUniformMatrix3fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value) glUniformMatrix4fv(GLint location, GLsizei count, GLboolean transpose, const GLfloat* value)
どっひゃーって感じだけど、実はそんなに難しくない。
- 設定したい値の数を{1234}から選ぶ
- 配列を設定したかったらvを選ぶ
- マトリクス型のデータを設定するならfvを選ぶ
- int データを設定するならi, floatならfを選ぶ
で、重要なのは2点。両方のシェーダーでおなじ Uniform が使用される。2つめは重要で、Uniforms が現在使われている Program Object に対して設定されるということ。よって僕らは Uniforms やら Attributes やらを設定する前にプログラムをスタートしなければならない。glUseProgram に欲しいIDを渡して呼べば良い。
では次、Attributes への値設定。Attributes が読めるのは float, vec2, vec3, vec4, mat2, mat3, mat4 のデータ型のみ。配列や構造体としては宣言できない。
GLvoid glVertexAttrib{1234}f(GLuint index, GLfloat value[N]) GLvoid glVertexAttrib{1234}fv(GLuint index, const GLfloat* values) GLvoid glVertexAttribPointer(GLuint index, GLint size, GLenum type, GLboolean normalized, GLsizei stride, const GLvoid* ptr)
名前付けのルールは Uniforms の時と同じような感じだね。上の2つは定数を設定し、3つめは動的な値を設定する関数。
ここで面倒なのは定数はシェーダーによってのデフォルトの処理であるということ。動的な値を設定するのならば一時的にこれらの変更を有効にしなければならない。動的な値は頂点ごとに設定される。動的値の有効/無効を切り替えるためには以下のメソッドを使用する。
GLvoid glEnableVertexAttribArray(GLuint index) GLvoid glDisableVertexAttribArray(GLuint index)
glVertexAttribPointer() を使って設定する前に、glEnableVertexAttribArray() でもって有効にしないとならない。
前に示した VSH と FSH のコードを使うときに、次のようなプログラムで値を設定することができる。
(Attributes と Uniforms の設定をするコード:省略)
例示するために enabled したり disabled したりしています。しかしながら先に行ったように、enabled や disabled したりするのは OpenGL にとって非常にコストの高い処理です。よってそれらは1回だけその位置を取得したときに行うのがよさそうです。
> 2/3 -5 へ
コードは元ネタサイトを見てくださいね。今回は直訳ざっくりな箇所が多いかもしれません。が、書いている本人は仕組みの裏側がわかってきたきがするのですこし楽しいです。
2012年3月31日土曜日
All about OpenGL ES 2.x 2/3 -3
> 2/3 -2 の続き
ようこそ、グレートな3Dワールドへ!
ここまで読んできていればシェーダーがどんなものかということはイメージできているでしょう。今一度まとめてみましょう。
Shader and Program Creation
では シェーダーオブジェクトについて見てみよう。他の OpenGL のオブジェクトと同様にまずは名前/IDsを付け、設定を行う。他のと違うのは「コンパイルする」という手順があるということ。シェーダーは GPU によって処理され、OpenGL に最適なバイナリフォーマットにコンパイルされる。
シェーダーの作成に関連する関数は以下のとおり。
名前/IDsをつけて、ソースコードを渡して、それをコンパイルする。他のコードをすでにもっているシェーダーにソースコードを渡すと元のコードは破棄される。一度コンパイルしたら処理を変えることはできない。
それぞれのシェーダーはコンパイルされたかどうかのフラグを持つ。適正にコンパイルされラバ TRUE となるので、デバッグの時にはチェックすべき箇所です。加えてログを取得するのもよいでしょう。glGetShaderiv() で状態を、glGetShaderInfoLog()で状態メッセージを取得できます。
シェーダーの名前/IDを1つのリストで管理することはようにしましょう。例えば[1]という名前/IDをもつ VSH を生成して、次に作るシェーダーの名前はおそらく[2]になるでしょうし、そのあとは[3], [4], [5],,, となっていき、名前/IDが重複することはなくなります。
シェーダーペアを作ったら、次はそれらを格納する Program Object を作ります。手順としては Program Object を生成し、何かしら(ここではシェーダーのペア)をアップロードし、"link" する。このリンクによってシェーダーペア同士をつなげ、それ自体を OpenGL のコア部分にリンクさせる。はい、ここ重要です。なぜならこの過程でシェーダーに多くの検証がなされるから。シェーダーと同じように Program Object でもリンク状態とそのログを見ることができ、そこからチェックを行うべきです。リンクが成功すればそれらは正確に動くでしょう。Probram Object の関数は以下のとおり。
glCreateProgram() は引数を持たない。なぜなら Program Object は一種類しか存在しないから。
シェーダーの名前/IDを単一のリストで管理する、というのは覚えているかな?OpenGL はそれぞれに特有な名前/IDによってシェーダーのタイプを得しています。よって、重要なのは glAttachShader() を2回(VSH + FSH)呼ぶということ。VSH を2つアタッチしたり、3つ以上のシェーダーをアタッチしたりするとちゃんとリンクされない。
(補足:以下、Program Object によってシェーダーペアがリンクされたオブジェクトを「プログラム」と呼んでいるみたい。)
僕らはたくさんのプログラムを作ることができるが、OpenGL はどのプログラムを使うのか?OpenGL はプログラムオブジェクトのためのアームとフックは持っていない。じゃあどうやってそれを知るのか?実はここだけ例外的な処理方法であり、バインドの関数ではないが以下のメソッドを使うことによってフックと同じように使うことができる。
この関数以降に書かれた glDraw*() メソッドではこのプログラムが使われることになる。glUseProgram(0) で現在のプログラムを解除する。
では実際のコードを見てみよう。
OpenGL オブジェクトを作る関数から独立させるような形でテンプレートを作るとだいたい上のようになる。使うシェーダーとそのプログラムだけ変更すれば良い。
ここまではシェーダーとそのプログラムの作り方ここからは Shader Language (SL)、とくに GLSL ES について見ていこう。
Shader Language
SL はC言語によく似ている。変数宣言と関数定義の書き方は同じだし、if-else やらループ文の書き方も同じでマクロも書ける。SL は可能な限り最速になるように作られている。よってループや条件分岐の使用はコストが高いので注意が必要である。シェーダーは GPU によって処理され、浮動小数点での計算が最適化されているということを覚えておこう。この処理のため、SL は3D世界で使うための独特なデータ型をもつ。
(リスト省略/> 元ネタサイトへ)
vec 系のベクターデータはドット演算子で、もしくは[ ]演算子でそれぞれの要素にアクセスすることができる(アクセサ)。{x, y, z, w} などの表記が要素を表している。例えば、aaa.xyz はve4 型の aaa の中のはじめの3つの状態変数を表すことができる。この要素表記の順序を変えることもできる。例えば aaa.yzx というのもOK。また、{x, y, z} と {r, g, b} と同時に持ったようなものもある。重要なのはこれらをミックスもできる、ということ。例えば aaa.xrt という表記も可能。
次は変換そ見てみよう。これについてはいくつか注意が必要。SL は値の最小値と最大値を定義して値の幅を定めており、これを Precision Qualifiers という。
変数宣言で使うことのできるキーワードは多くない。以下がそのリストになるが、これはメーカーによって桁数が増える場合がある。よって下のリストは SL に必要な最小レンジである。
(表省略/> 元ネタサイトへ)
これらの修飾子を型宣言で使う代わりに、"precision" というキーワードを使って修飾子のデフォルトを設定することもできる。また、あまりよくないことではあるが、これらの修飾子を使って異なるデータを変換できる。例えば小数点型から整数型に変換するために、 mediump float と lowp int を使うべきであり、lowp float から変換すると -2.0 〜 2.0 にまるめられてしまうので注意しよう。埋め込み関数も使える。
GPU で直接に処理することによるもう1つの利点は浮動小数点の計算である。掛け算やその他の計算をごく簡単に実行することができる。行列、ベクター、小数点型は完全に互換性があり、いろいろできる。以下は例。
もちろんCのように配列を使って初期化することもできる。SL はどのシェーダーも
では 頂点シェーダー(VSH) とフラグメントシェーダー(FSH) の作るものについてもっと見てみよう。
> 2/3-4 へ
Shaders
ようこそ、グレートな3Dワールドへ!
ここまで読んできていればシェーダーがどんなものかということはイメージできているでしょう。今一度まとめてみましょう。
- シェーダーは GLSL もしくは GLSL ES を使っている。
- シェーダーはいつも頂点シェーダー(VSH) とフラグメントシェーダー(FSH) のペアではたらく。
- glDrawArrays() や glDrawElements() といった描画コマンドを送るごとにシェーダーのペアは処理を実行する。
- VSH は頂点の数だけ処理を行う。
- FSH はオブジェクトの可視断片のそれぞれについて処理を実行する。FSH はグラフィックパイプラインにおいて Fragment Operations の前に処理されるので、その時点では OpenGL はどのオブジェクトが前面にあるかといった情報は知らない。つまり、隠れているようなオブジェクトも処理するということ。FSH の仕事は断片の最終的な色を定義すること。
- VSH と FSH は別々にコンパイルされ、Program Object としてリンクされなければならない。コンパイルされたシェーダーは複数の Program Object に再利用することができる。ただしそれぞれの Program Object にリンクできるのは1つのシェーダーペアである。
Shader and Program Creation
では シェーダーオブジェクトについて見てみよう。他の OpenGL のオブジェクトと同様にまずは名前/IDsを付け、設定を行う。他のと違うのは「コンパイルする」という手順があるということ。シェーダーは GPU によって処理され、OpenGL に最適なバイナリフォーマットにコンパイルされる。
シェーダーの作成に関連する関数は以下のとおり。
GLuint glCreateShader(GLenum type) GLvoid glShaderSource(GLuint shader, GLsizei count, const GLchar** string, const GLint* length) GLvoid glCompileShader(GLuint shader) // 引数の説明は省略
名前/IDsをつけて、ソースコードを渡して、それをコンパイルする。他のコードをすでにもっているシェーダーにソースコードを渡すと元のコードは破棄される。一度コンパイルしたら処理を変えることはできない。
それぞれのシェーダーはコンパイルされたかどうかのフラグを持つ。適正にコンパイルされラバ TRUE となるので、デバッグの時にはチェックすべき箇所です。加えてログを取得するのもよいでしょう。glGetShaderiv() で状態を、glGetShaderInfoLog()で状態メッセージを取得できます。
シェーダーの名前/IDを1つのリストで管理することはようにしましょう。例えば[1]という名前/IDをもつ VSH を生成して、次に作るシェーダーの名前はおそらく[2]になるでしょうし、そのあとは[3], [4], [5],,, となっていき、名前/IDが重複することはなくなります。
シェーダーペアを作ったら、次はそれらを格納する Program Object を作ります。手順としては Program Object を生成し、何かしら(ここではシェーダーのペア)をアップロードし、"link" する。このリンクによってシェーダーペア同士をつなげ、それ自体を OpenGL のコア部分にリンクさせる。はい、ここ重要です。なぜならこの過程でシェーダーに多くの検証がなされるから。シェーダーと同じように Program Object でもリンク状態とそのログを見ることができ、そこからチェックを行うべきです。リンクが成功すればそれらは正確に動くでしょう。Probram Object の関数は以下のとおり。
GLuint glCreateProgram(void) GLvoid glAttachShader(GLuint program, GLuint shader) GLvoid glLinkProgram(GLuint program)
glCreateProgram() は引数を持たない。なぜなら Program Object は一種類しか存在しないから。
シェーダーの名前/IDを単一のリストで管理する、というのは覚えているかな?OpenGL はそれぞれに特有な名前/IDによってシェーダーのタイプを得しています。よって、重要なのは glAttachShader() を2回(VSH + FSH)呼ぶということ。VSH を2つアタッチしたり、3つ以上のシェーダーをアタッチしたりするとちゃんとリンクされない。
(補足:以下、Program Object によってシェーダーペアがリンクされたオブジェクトを「プログラム」と呼んでいるみたい。)
僕らはたくさんのプログラムを作ることができるが、OpenGL はどのプログラムを使うのか?OpenGL はプログラムオブジェクトのためのアームとフックは持っていない。じゃあどうやってそれを知るのか?実はここだけ例外的な処理方法であり、バインドの関数ではないが以下のメソッドを使うことによってフックと同じように使うことができる。
GLvoid glUseProgram(GLuint program)
この関数以降に書かれた glDraw*() メソッドではこのプログラムが使われることになる。glUseProgram(0) で現在のプログラムを解除する。
では実際のコードを見てみよう。
GLuint _program; GLuint createShader(GLenum type, const char **source) { GLuint = glCreateShader(type); glShaderSource(name, 1, &source, NULL); glCompileShader(name); // "DEBUG"はデバッグのためのマクロ #if defined(DEBUG) GLint logLength; // GL_INFO_LOG_LENGTH を使う代わりに、COMPILE_STATUS も使える。 // 僕は前者が好き。もしコンパイル成功なら長さはゼロになり、 // もしエラーがでたらどのみち GL_INFO_LOG_LENGTH は必要なので。 glGetShaderiv(name, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetShaderInfoLog(name, logLength, &logLength, log); // Shows the message in console. printf("%s",log); free(log); } #endif return name; } GLuint createProgram(GLuint vertexShader, GLuint fragmentShader) { GLuint name; // プログラムの名前/IDを作成 name = glCreateProgram(); // VSH と FSH を接続 glAttachShader(name, vertexShader); glAttachShader(name, fragmentShader); // それらを OpenGL のコアへリンクさせる glLinkProgram(_name); #if defined(DEBUG) GLint logLength; // この関数はシェーダーのとはちょっと違う。 glGetProgramiv(name, GL_INFO_LOG_LENGTH, &logLength); if (logLength > 0) { GLchar *log = (GLchar *)malloc(logLength); glGetProgramInfoLog(name, logLength, &logLength, log); printf("%s",log); free(log); } #endif return name; } void initProgramAndShaders() { const char *vshSource = "... Vertex Shader source using SL ..."; const char *fshSource = "... Fragment Shader source using SL ..."; GLuint vsh, fsh; vsh = createShader(GL_VERTEX_SHADER, &vshSource); fsh = createShader(GL_FRAGMENT_SHADER, &fshSource); _program = createProgram(vsh, fsh); // シェーダーの廃棄 // シェーダーはコンパイルされてしまええば OpenGL 内に保持される。 glDeleteShader(vsh); glDeleteShader(fsh); // ここでこのプログラムを使うために _program 変数をつかえるようになった。 // もしオブジェクト指向なプログラムとしてつくっているのなら // ここでクラスのインスタンス変数などに格納すること。 // glUseProgram(_program); }
OpenGL オブジェクトを作る関数から独立させるような形でテンプレートを作るとだいたい上のようになる。使うシェーダーとそのプログラムだけ変更すれば良い。
ここまではシェーダーとそのプログラムの作り方ここからは Shader Language (SL)、とくに GLSL ES について見ていこう。
Shader Language
SL はC言語によく似ている。変数宣言と関数定義の書き方は同じだし、if-else やらループ文の書き方も同じでマクロも書ける。SL は可能な限り最速になるように作られている。よってループや条件分岐の使用はコストが高いので注意が必要である。シェーダーは GPU によって処理され、浮動小数点での計算が最適化されているということを覚えておこう。この処理のため、SL は3D世界で使うための独特なデータ型をもつ。
(リスト省略/> 元ネタサイトへ)
vec 系のベクターデータはドット演算子で、もしくは[ ]演算子でそれぞれの要素にアクセスすることができる(アクセサ)。{x, y, z, w} などの表記が要素を表している。例えば、aaa.xyz はve4 型の aaa の中のはじめの3つの状態変数を表すことができる。この要素表記の順序を変えることもできる。例えば aaa.yzx というのもOK。また、{x, y, z} と {r, g, b} と同時に持ったようなものもある。重要なのはこれらをミックスもできる、ということ。例えば aaa.xrt という表記も可能。
vec4 myVec4 = vec4(0.0, 1.0, 2.0, 3.0); vec3 myVec3; vec2 myVec2; myVec3 = myVec4.xyz; // myVec3 = {0.0, 1.0, 2.0}; myVec3 = myVec4.zzx; // myVec3 = {2.0, 2.0, 0.0}; myVec2 = myVec4.bg; // myVec2 = {2.0, 1.0}; myVec4.xw = myVec2; // myVec4 = {2.0, 1.0, 2.0, 1.0}; myVec4[1] = 5.0; // myVec4 = {2.0, 5.0, 2.0, 1.0};
次は変換そ見てみよう。これについてはいくつか注意が必要。SL は値の最小値と最大値を定義して値の幅を定めており、これを Precision Qualifiers という。
変数宣言で使うことのできるキーワードは多くない。以下がそのリストになるが、これはメーカーによって桁数が増える場合がある。よって下のリストは SL に必要な最小レンジである。
(表省略/> 元ネタサイトへ)
これらの修飾子を型宣言で使う代わりに、"precision" というキーワードを使って修飾子のデフォルトを設定することもできる。また、あまりよくないことではあるが、これらの修飾子を使って異なるデータを変換できる。例えば小数点型から整数型に変換するために、 mediump float と lowp int を使うべきであり、lowp float から変換すると -2.0 〜 2.0 にまるめられてしまうので注意しよう。埋め込み関数も使える。
precision mediump float; precision lowp int; vec4 myVec4 = vec4(0.0, 1.0, 2.0, 3.0); ivec3 myIvec3; mediump ivec2 myIvec2; // ↓はデータ型の連携がないのでダメ //myIvec3 = myVec4.zyx; myIvec3 = ivec3(myVec4.zyx); // This is OK. myIvec2.x = myIvec3.y; // This is OK. myIvec2.y = 1024; // ↓は大丈夫ではあるが、値の型の正確性が異なっているので // myIvec3.x の値は型の範囲制限で256にまるめられる。 myIvec3.x = myIvec2.y;
GPU で直接に処理することによるもう1つの利点は浮動小数点の計算である。掛け算やその他の計算をごく簡単に実行することができる。行列、ベクター、小数点型は完全に互換性があり、いろいろできる。以下は例。
mat4 myMat4; mat3 myMat3; vec4 myVec4 = vec4(0.0, 1.0, 2.0, 3.0); vec3 myVec3 = vec3(-1.0, -2.0, -3.0); float myFloat = 2.0; // mat4 は16の要素を持ち、4つの vec4 で初期化できる。 myMat4 = mat4(myVec4,myVec4,myVec4,myVec4); // それぞれの要素に掛け算 myVec4 = myFloat * myVec4; // 4x4行列と1x4行列の計算に相当。結果は vec4 型。 myVec4 = myMat4 * myVec4; // アクセサを使って異なる配置のベクターを乗算 myVec4.xyz = myVec3 * myVec4.xyz; // mat4 のはじめの9エレメントで mat3 を作成 myMat3 = mat3(myMat4); // 3x3行列と1x3行列の乗算に相当。結果は vec3 型。 myVec3 = myMat3 * myVec3;
もちろんCのように配列を使って初期化することもできる。SL はどのシェーダーも
void main()
を持つように定めている。シェーダーの処理はここから開始する。これを持たないシェーダーはコンパイルされない。SL はインラインな言語だと覚えておこう。つまりは関数の前方宣言をするべしという話。よって void main()
は大体最後になる。では 頂点シェーダー(VSH) とフラグメントシェーダー(FSH) の作るものについてもっと見てみよう。
> 2/3-4 へ
登録:
投稿 (Atom)