ステンシルバッファ
今回のサンプルの実行結果
描かない技術
前回は点のレンダリングを発展させたポイントスプライトについて解説しました。
ポイントスプライトを用いることで、負荷を抑えつつビルボード処理を行うことが可能になるのでしたね。また、ポイントスプライトを実装するにあたりフラグメントシェーダ内で利用できる discard
というステートメントについても触れました。
さて、今回は WebGL におけるステンシルバッファの扱いについて解説します。ステンシルバッファを用いると、今まで以上に多彩な表現や特殊なテクニックを扱うことができるようになります。若干理解しにくい部分もあるかもしれませんが、これまで同様、落ち着いて考えればきっと理解できるはずです。がんばって取り組んでください。
それでは早速ステンシルバッファについて考えてみます。
ステンシル( stencil )は直訳すると[ 型紙 ]や[ 原紙 ]といった意味になる言葉で、3D プログラミングにおいては[ 型抜き ]という意味としてステンシルバッファは利用されています。
美術の授業などで、マスキングテープを使ったことがある人はそれを思い浮かべてみてください。特定の部位にテープを貼ってから一面にスプレーなどで色を塗り、最後に貼ってあったテープを剥がすとその部分だけが白く型を抜かれたような状態で模様として残ります。ステンシルバッファを用いると、これと似たような効果を得ることが可能です。
今までは、工夫を凝らしてコンテキスト上に様々なものを描画してきました。しかしステンシルバッファを用いると、あえて描画しないという選択肢を効果的に利用することが可能になります。これによって実現できる代表的な処理には以下のようなものがあります。
- シルエットやアウトラインの描画
- デカール
- フェードインやフェードアウト演出
上記はごく一部ですが、ステンシルバッファを用いて実現できる処理の一例です。
この言葉だけを見てもすぐにはイメージが湧かないかもしれません。しかし、ステンシルバッファはその使い方次第で、思いもよらない新しい演出効果を生み出すこともできる面白い概念です。今回はまず、ステンシルバッファを利用するための手順の解説と、それに倣ったサンプルとして簡単な型抜き処理を行なってみましょう。
今回のテキストはあくまでもステンシルバッファを理解するということに重点を置きます。一部、説明を省いているように感じる部分もあるかもしれませんがそのつもりでご覧いただければと思います。
ステンシルバッファの概念
3D プログラミングの世界では、頂点の座標などで浮動小数点数を多用します。これまでも、それこそ当たり前のように小数点以下の数値を扱う場面がたくさんありましたよね。しかし、ステンシルバッファは違います。ステンシルバッファ上のデータは全て整数値によって管理されます。これはステンシルバッファを理解する上で地味に重要です。
ステンシルバッファは先ほども書いたように[ 型抜き ]などに使われます。要は、この部分は型を抜く、別の部分は型を抜かない、といったように特定のフラグメントをレンダリングするべきか否かを決定する権限を持っているのです。
ステンシルバッファに対して行なえるのは、そのピクセルにモデルなどがレンダリングされることを許可するかどうか、それを判断するための基準となる値を設定することです。基準となる値を設定できたら、その値を参照しながら様々に処理を分岐させることができます。
ステンシルバッファに対して設定された基準値は、ステンシルテストに使われます。ステンシルテストは[ テスト ]の名が示すように対象のピクセルにレンダリングを行なうか否かを決定するための評価を行ないます。このステンシルテストの評価方法を決めるのが stencilFunc
メソッドです。このメソッドは三つの引数を取り、記述例を示すと以下のようになります。
stencilFunc メソッドの記述例
gl.stencilFunc(gl.ALWAYS, ref, mask);
なにやら変な感じがするかもしれませんが、まずパッと見て、第一引数には何かしらの定数値を指定するらしいということはすぐにわかりますね。この第一引数には指定できる定数がいくつかあり、ここでどんな定数を指定したかによって、ステンシルテストの評価方法が変化します。
stencilFunc メソッドに指定できる引数の一覧
定数名 | 意味 |
---|---|
gl.ALWAYS | 常にステンシルテストを通過する |
gl.NEVER | 常にステンシルテストを通過しない |
gl.LESS | [ ref & mask ] < [ pixel & mask ] のとき通過する |
gl.LEQUAL | [ ref & mask ] <= [ pixel & mask ] のとき通過する |
gl.EQUAL | [ ref & mask ] == [ pixel & mask ] のとき通過する |
gl.NOTEQUAL | [ ref & mask ] != [ pixel & mask ] のとき通過する |
gl.GREATER | [ ref & mask ] > [ pixel & mask ] のとき通過する |
gl.GEQUAL | [ ref & mask ] >= [ pixel & mask ] のとき通過する |
理解を簡単にするために、まずは mask についてはいったん忘れてしまっても構いません。重要なのは、上記の表にある ref と pixel の意味です。
ここで登場する ref は stencilFunc
メソッドの第二引数に指定した値です。これは、先ほども書いたように整数値です。そして、pixel というのはステンシルテストを行なったその瞬間に、ステンシルバッファに書き込まれている基準値です。
ちょっとややこしくなってきましたね。
ここは落ち着いて考えましょう。
まず、何かしらの方法を用いてステンシルバッファのあるピクセルに 3 という基準値が既に書き込まれた状態だと仮定します。このとき stencilFunc
メソッドに次のように指定されていたとしたら、ステンシルテストの評価は合格でしょうか、それとも不合格でしょうか。
問題
gl.stencilFunc(gl.EQUAL, 3, mask);
ステンシルバッファの基準値は先述の通り 3 が書き込まれた状態です。そして stencilFunc
メソッドの第一引数に gl.EQUAL
が指定されており、さらに ref には 3 が指定されています。
[ ref( 3 ) == ステンシルバッファの pixel の基準値( 3 ) ]という式が成り立っている状態ですので、この場合はステンシルテストは合格だということがわかりますね。つまりこの場合、対象のピクセルにはレンダリングされた結果が反映されることになるわけです。
このように、ステンシルバッファはあくまでも基準値を保存するためのバッファとして機能します。その基準値をどのように扱うのかを決めるのが stencilFunc
メソッドです。この関係性をまずはしっかり把握しておいてください。
基準値の書き込み
さて、基準値とその評価方法については理解できましたでしょうか。
続いては、ステンシルバッファに基準値を書き込む方法を説明します。
ステンシルバッファに対して何かしらの値を書き込むには、どのように値を書き込むのかルールを決めてやり、その上で実際にレンダリングを行ないます。たとえば[ A の場合には 1 を書き込みなさい ]とルールをあらかじめ決めた上でなにかのモデルをレンダリングするとします。そのモデルがレンダリングされる対象ピクセルが A の条件を満たしていると、ステンシルバッファ上の対象ピクセルに 1 という基準値が書き込まれます。
この基準値をどう扱うのかというルールは stencilOp
メソッドによって自由に指定することができます。
このメソッドは引数を三つ取ります。記述例は以下のような感じです。
stencilOp メソッドの記述例
gl.stencilOp(fail, zfail, zpass);
はい、またまた意味がわかりませんね。
第一引数の fail は、ステンシルテストが不合格だった場合に基準値をどう扱うのかを指定します。
同様に、第二引数はステンシルテストには合格したけれども、深度テストが不合格だった場合に基準値をどう扱うのか指定します。第三引数はステンシルテストも深度テストも双方共に合格だった場合の指定です。
要は、ステンシルテストの結果や深度テストの結果に応じて、非常に細かく設定が行なえるということですね。
この stencilOp
メソッドの各引数には、以下のいずれかの定数を指定します。
定数名 | 意味 |
---|---|
gl.ZERO | 基準値を 0 にする |
gl.KEEP | 現状の基準値を維持する |
gl.REPLACE | 直前の stencilFunc メソッドで指定された ref の値を基準値として設定する |
gl.INCR | 現在の基準値をインクリメント( +1 )する |
gl.DECR | 現在の基準値をデクリメント( -1 )する |
gl.INVERT | 現在の基準値をビット反転する |
gl.INCR_WRAP | 現在の基準値をインクリメント( +1 )するが最大値を超えた場合には 0 に戻す |
gl.DECR_WRAP | 現在の基準値をデクリメント( -1 )するが 0 を下回る場合には最大値に設定する |
これを見ると、ステンシルバッファの基準値をどのように扱うことができるのかわかると思います。先ほどステンシルバッファは整数値によって管理されており、そのことが地味に重要ですと書きましたが、その理由がなんとなくわかったのではないでしょうか。
ステンシルバッファに設定できる最大値
先ほどの stencilOp
メソッドに指定できる引数の一覧で[ 最大値 ]という単語が出てきます。この最大値は、実行環境によって変わります。これは要するにステンシルバッファのピクセルあたりのビット長であり、基本的には最低でも 8 ビット分は保証されると考えても大丈夫だと思います。ビットってなんだ! ビットとバイトの違いがわからんぞ! という人は、それくらいは各自で調べましょう。
8 ビットでは 2 進数で 8 桁分までのデータを表すことができますので、ステンシルバッファに設定できる基準値の範囲は 0 ~ 255 までの 256 段階になります。しかし stencilOp
で指定できる引数の gl.INCR_WRAP
などを用いる場合には、この最大値の範囲が明確になっていないとまずいことになりますね。
ステンシルバッファの最大ビット長を調べるには getParameter
メソッドに gl.STENCIL_BITS
を指定して呼び出します。ここで返される整数値が、そのまま最大ビット長になりますので必要に応じて事前に調査するようにしたほうがいいでしょう。
ステンシルバッファを有効にする
さてさて、だいぶ前置きが長くなりましたが、実際にステンシルバッファを利用するための手順についても解説していきます。ただし、ステンシルバッファを利用する具体的な方法を解説する前に、まずは考えてみてください。そもそもステンシルバッファとは、いったいどこに存在しているものなのでしょうか。
WebGL には、これまでにもいくつかのバッファと名の付く概念が登場しましたね。たとえば、頂点属性を扱うための[ 頂点バッファ ]は、WebGL に実装されているメソッドを実行することで新たに生成することが可能でした。一方で[ 深度バッファ ]、もしくは[ デプスバッファ ]の場合は、WebGL に始めから実装されており、それを使うことを WebGL に通知して初めて利用が可能になりました。
ステンシルバッファは、深度バッファなどと同様に WebGL コンテキストが初期化されたときに同時に生成されます。ただし、このコンテキストの初期化段階で、ステンシルバッファを生成するように明示してやる必要があります。これは WebGL の仕様であり、デフォルトではステンシルバッファは生成されないように設定されています。
ステンシルバッファをコンテキストの初期化と同時に生成するようにするためには、以下で示すように getContext
メソッドの実行において引数を追加します。
コンテキストの初期化処理をステンシルバッファ有効で行なう
var gl = canvas.getContext('webgl', {stencil: true}) || canvas.getContext('experimental-webgl', {stencil: true});
このように stencil
メンバに true
を設定して引数に指定すると、コンテキストの初期化時にステンシルバッファを生成してくれます。しかし、ここまでの処理ではあくまでもステンシルバッファが内部的に生成されただけにしか過ぎません。深度バッファがそうであったように、ステンシルバッファを使いたい場合にはそれを WebGL に対して通知して有効化してやります。これには、もはやお馴染みとなりつつある enable
メソッドを使います。
ステンシルバッファを有効にする
gl.enable(gl.STENCIL_TEST);
有効化には enable
メソッドを、無効化するには disable
メソッドを使います。引数には、組み込み定数 gl.STENCIL_TEST
を与えてやれば OK です。
この辺は深度バッファのときと同じですね。
また、深度バッファと同じ部分は他にもあり、ステンシルバッファは深度バッファ同様、レンダリングされるたびに毎回クリアされるように設定するのが普通です。そこで、カラーや深度をクリアするために使っていた clear
メソッドの引数に gl.STENCIL_BUFFER_BIT
という組み込み定数を加えます。
clear メソッドを修正
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT | gl.STENCIL_BUFFER_BIT);
これで、クリア処理が掛かるたびにステンシルバッファも初期化されるようになりました。ステンシルバッファを初期化する際には、必ずしも 0 に初期化されるわけではありません。初期化される際にどのような基準値が設定されるようにするのかは clearStencil
メソッドを使って自由に設定することが可能です。
clearStencil メソッドの記述例
gl.clearStencil(0);
引数には整数値で、初期化する際に設定する基準値を指定します。まぁ、特別な理由がない限りは 0 で初期化するのが普通ですね。
ステンシルバッファの利用(ステンシルテストの実行)を有効化することができたら、あとはステンシルバッファをしっかりと初期化し、その上で適宜 stencilFunc
メソッドや stencilOp
メソッドを実行してからレンダリングを行なっていきます。
サンプルの補足
今回のサンプルでは、平行光源によるライティング、クォータニオンを使ったマウスによるカメラ制御を行ないながら、テクスチャを貼った板ポリゴンをレンダリングします。
板ポリゴンは全部で三枚レンダリングしますが、それぞれのレンダリングを行なう際に、以下のようにステンシルテストの設定をしてレンダリングしてやります。
板ポリゴンのステンシル関連設定
一枚目のポリゴン:Z 軸を奥側にずらしてレンダリング
gl.stencilFunc(gl.ALWAYS, 1, ~0);
gl.stencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE);
二枚目のポリゴン:Z 値は 0.0
gl.stencilFunc(gl.ALWAYS, 0, ~0);
gl.stencilOp(gl.KEEP, gl.INCR, gl.INCR);
三枚目のポリゴン:Z 軸は手前にずらしてレンダリング
gl.stencilFunc(gl.EQUAL, 2, ~0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
一枚目のポリゴンは stencilFunc
メソッドの第一引数が gl.ALWAYS
であることから、常にステンシルテストをパスしますね。そして stencilOp
メソッドによってステンシルテストにパスした場合の処理として gl.REPLACE
を指定していますので、ステンシルバッファの対象ピクセルは基準値が 1 に書き換えられます。
二枚目のポリゴンは一枚目同様、必ずステンシルテストにはパスしますね。しかし今度は stencilOp
メソッドでの指定が gl.INCR
となっていますので、ステンシルバッファの対象ピクセルの基準値はインクリメントされます。
つまり、二枚目のポリゴンがレンダリングされた時点で、この双方のポリゴンが重なり合っている領域は基準値が 2 になっているはずです。逆に双方のポリゴンが重なっていない部分では、基準値は最大でも 1 にしか成りえないことになります。
そして三枚目のポリゴンです。ここでは stencilFunc
メソッドの第一引数に gl.EQUAL
が指定されているので、第二引数( ref )に設定されている 2 という値と、ステンシルバッファの基準値とがイコール、つまり等しかった場合にしかステンシルテストにパスできません。
一枚目のポリゴンと、二枚目のポリゴンが重なり合っている領域、そこが基準値 2 の領域でしたよね。ですから、三枚目のポリゴンは二つのポリゴンが重なり合っている領域にしかレンダリングされません。それ以外の領域はスッパリと型抜きされてしまいます。
クォータニオンによるカメラ制御を行なっていますので、実際に視点をぐりぐりと動かしてみると面白いと思います。
stencilFunc メソッドの第三引数
最初のほうで書いたとおり stencilFunc
メソッドの第三引数に指定する mask については、ステンシルバッファの基本を知る上ではあまり気にしなくても大丈夫です。理由としては、あまり特殊なことをしない限りはこの第三引数には[ ~0 ]を指定すれば問題ないからです。
この第三引数は本来、第二引数に指定する ref の値や、ステンシルバッファ上の基準値の値( pixel )に対してマスクを掛けたい場合に使われます。マスクはビット演算によって処理され、ステンシルテストの最初の段階でマスク値と ref や pixel の間で AND 演算による比較が行なわれます。
ここで登場した[ ~0 ]という表記法はビット演算を行なう際に使われるもので、意味としては NOT 演算を表します。これはつまりビット演算における 0 の NOT を表すわけですから、全てのビットが立っている状態を表します。
全てのビットが立っているということは、いかなる値とマスク値を AND 演算しても結果は変わりません。つまり、第三引数に[ ~0 ]を渡すことによって、プログラマは第二引数に指定する整数値と、その他の処理にだけ集中すればいいことになるわけです。ビット関連の処理はプログラマとしての基本的なスキルがないと意味不明に感じるかもしれませんが、このあたりに強くなっていると様々な場面で役に立ちます。気になる人はビット演算について調べてみるといいかもしれません。
まとめ
ステンシルバッファの基本的な使い方を解説しただけでしたが、だいぶ長いテキストになってしまいました。若干難しい部分もあったかと思いますが、ゆっくりじっくり考えてみてください。
いつものように、サンプルへは下のほうにあるリンクから飛べます。
次回はステンシルバッファを使ってモデルのアウトラインレンダリングをやってみたいと思います。