ステンシルバッファでアウトライン
今回のサンプルの実行結果
ステンシルバッファの活用
前回はステンシルバッファの基本的な使い方を解説しました。
ステンシルバッファは整数値でデータを管理し、場合によりレンダリングを行なわないように設定することができるなど、特殊な処理を行うことができる概念でしたね。
今回はステンシルバッファを用いた代表的な処理の一つであるアウトラインのレンダリングを行なってみたいと思います。アウトラインをレンダリングするテクニックには、必然的にシルエットのレンダリングを行なう技術を伴います。少しだけ複雑な手順を踏むことになりますが、それでもまだ、3D プログラミングの類としては簡単な部類の処理です。焦らずじっくり取り組みましょう。
さて、それではまずシルエットをレンダリングすることから考えてみます。
シルエットはまるで影絵のように、オブジェクトの輪郭だけをレンダリングすることで表現できます。今回の場合、まずシルエットをレンダリングしたいモデルを描画する際、ステンシルバッファのビットをインクリメントします。その後、ステンシルバッファの値を参照し、インクリメントされていない部分だけにレンダリングが行なわれるようにすれば、シルエットが浮かび上がるはずですね。
要は、マスキングテープを貼るかのように、ステンシルバッファにモデルの輪郭を焼き付けておき、それから背景をレンダリングすればいいわけですね。
アウトラインレンダリング
シルエットをレンダリングする方法はなんとなくイメージできましたか。
シルエットを描画することができたのなら、アウトラインを実現することもそう難しくありません。アウトラインとは、要はシルエットの上に、もう一度重ね塗りをするような感じでレンダリングすれば実現できるからです。
まず最初に、ステンシルテストを有効にして、ほんの少しだけ法線方向にスケールを掛けたモデルをレンダリングします。要は、ちょっとだけ膨らませたモデルを、ステンシルバッファにだけ描き込むわけです。
その後、ステンシルテストは有効のまま背景をレンダリングします。このときステンシルバッファの基準値に応じて型抜きの要領でレンダリングを行なえば、最初にステンシルバッファに描き込まれたモデルの場所だけが穴が空いたようにきれいに抜けるはずですね。
さらに、ステンシルテストを無効にしてから、本来レンダリングされるはずだったモデルをレンダリングします。背景にはすっぽりと穴が空いているわけですから、その上にモデルがレンダリングされると、まるでアウトラインが描かれているかのように見えるわけです。
シルエットレンダリングを利用することで、結果的にはアウトラインレンダリングをも行なうことができるわけですね。図を見ながら考えてみると概念が理解しやすいのではないでしょうか。
カラーバッファと深度バッファのマスク
先ほどアウトラインレンダリングの方法を解説したとき、まず最初にステンシルバッファにだけ描き込むと書きました。これは言い換えると、色を扱うカラーバッファと、深度を扱う深度バッファの両者に対して、レンダリング結果が反映されないようにすることだと言えます。
この処理を実現するためには、カラーバッファと深度バッファに対してマスクを適用します。マスクが適用されると、マスクされた部分へのレンダリングが無効化されます。結果的に、レンダリングはステンシルバッファに対してのみ有効となります。
カラーバッファへのマスクの適用には colorMask
メソッドを使います。このメソッドは四つの引数を取り、それぞれ RGBA の各要素へマスクをかけるかどうか設定できます。設定は true
か false
の二択、つまり真偽値を使って指定します。
colorMask メソッドを使ってカラーバッファをマスクする
gl.colorMask(false, false, false, false);
上記のように記述すると、RGBA の全ての要素への描き込みがマスクされます。要は、カラーバッファを更新するかどうか、それを真偽値で指定すればいいわけですね。
同様に、深度バッファに対してマスクを適用するには depthMask
メソッドを利用します。こちらは引数は一つ、指定の仕方は colorMask
メソッドと同じ真偽値です。
depthMask メソッドを使って深度バッファをマスクする
gl.depthMask(false);
これでカラーバッファと深度バッファへのデータの描き込みがマスクされますので、純粋にステンシルバッファに対してのみレンダリング結果が反映されるようになりましたね。
今回のアウトラインレンダリングは、三つのパスでレンダリングします。一つ目はステンシルバッファへのスケールしたモデルのレンダリング。二つ目は背景のレンダリング。三つ目が本来のモデルのレンダリングです。
一つ目のパスでは、ステンシルテスト有効、カラーマスク有り、深度マスク有りですね。
この設定を部分的に抜粋すると以下のようにコードを記述すればいいでしょう。
ステンシル・マスクの設定部分を抜粋
// ステンシルテストを有効にする
gl.enable(gl.STENCIL_TEST);
// カラーと深度をマスク
gl.colorMask(false, false, false, false);
gl.depthMask(false);
// シルエット用ステンシル設定
gl.stencilFunc(gl.ALWAYS, 1, ~0);
gl.stencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE);
あらかじめ clear
メソッドでステンシルバッファが 0 でクリアされていれば、上記の設定でレンダリングするとステンシルバッファ上には、モデルのレンダリングされた部分にだけ基準値 1 が書きこまれることになりますね。
続いては二パス目。
今度はステンシルテスト有効のまま、カラーマスク無し、深度マスク無しです。
コードの一部を抜粋
// カラーと深度のマスクを解除
gl.colorMask(true, true, true, true);
gl.depthMask(true);
// 背景用ステンシル設定
gl.stencilFunc(gl.EQUAL, 0, ~0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
ここではマスクを解除し、ステンシルテストでは基準値が 0 のところにだけレンダリングが行なわれるように設定しています。
そして最後の三パス目では、ステンシルテストを無効にしてしまいます。ステンシルテストが無効化されたことで、従来どおり普通にレンダリングが行なわれます。その状態で本来描かれるはずだったモデルをレンダリングすれば OK ですね。
サンプルの解説
今回のサンプルは、今までのサンプルに比べると少々複雑な構造になっています。
まず、第一のパスで[ 少し法線方向に膨らませたモデル ]をレンダリングしなければなりませんが、これは頂点シェーダにやってもらいます。
なにはともあれまずは、今回の頂点シェーダのソースを見てみましょう。
頂点シェーダのソース
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
attribute vec2 textureCoord;
uniform mat4 mvpMatrix;
uniform mat4 invMatrix;
uniform vec3 lightDirection;
uniform bool useLight;
uniform bool outline;
varying vec4 vColor;
varying vec2 vTextureCoord;
void main(void){
if(useLight){
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
float diffuse = clamp(dot(normal, invLight), 0.1, 1.0);
vColor = color * vec4(vec3(diffuse), 1.0);
}else{
vColor = color;
}
vTextureCoord = textureCoord;
vec3 oPosition = position;
if(outline){
oPosition += normal * 0.1;
}
gl_Position = mvpMatrix * vec4(oPosition, 1.0);
}
頂点バッファには、モデルの頂点データとして[ 位置 ]・[ 法線 ]・[ 色 ]・[ テクスチャ座標 ]の四つの頂点属性を attribute 変数としてプッシュします。さらに、今回はグーローシェーディングによるライティングも行ないますので、uniform 変数としてライト関連のデータも受け取るようにしています。
注目してほしいのは二つの bool
型 uniform 変数です。まず useLight
という uniform 変数はライティングを行なうかどうかを判別するために使われます。メインの javascript プログラムから任意にライティングをオン・オフできるようにするための機構です。
そして outline
という uniform 変数を使っている部分、実はここが法線方向に頂点を膨らませている処理を行なっている部分です。純粋に法線を加算してしまうとモデルが膨らみすぎてしまうので適宜数値は調節しないといけませんね。今回は 10 %だけ膨張するようにしています。こちらもライトと同じように javascript プログラムのほうで任意に切り替えが行なえるようにしています。
続いてはフラグメントシェーダです。
こちらではテクスチャを使うかどうか切り替えるようにしてありますが、それ以外は特別なことはやっていません。
フラグメントシェーダのソース
precision mediump float;
uniform sampler2D texture;
uniform bool useTexture;
varying vec4 vColor;
varying vec2 vTextureCoord;
void main(void){
vec4 smpColor = vec4(1.0);
if(useTexture){
smpColor = texture2D(texture, vTextureCoord);
}
gl_FragColor = vColor * smpColor;
}
シェーダが何をやっているのかがわかったところで、メインプログラムである javascript のソースについても見ていきます。
そもそも今回のサンプルでは、トーラスをアウトライン付きでレンダリングします。そして、背景のレンダリングには球体モデルを使います。要は、大きな球体モデルでカメラとトーラスをすっぽりと囲んでしまうようなイメージです。
第一のパスでは、トーラスをアウトライン用の設定でレンダリングします。
アウトライン用トーラスのレンダリング関連
// ステンシルテストを有効にする
gl.enable(gl.STENCIL_TEST);
// カラーと深度をマスク
gl.colorMask(false, false, false, false);
gl.depthMask(false);
// トーラス(シルエット)用ステンシル設定
gl.stencilFunc(gl.ALWAYS, 1, ~0);
gl.stencilOp(gl.KEEP, gl.REPLACE, gl.REPLACE);
// トーラスの頂点データ
set_attribute(tVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
// トーラスモデル座標変換行列の生成
m.identity(mMatrix);
m.rotate(mMatrix, rad, [0.0, 1.0, 1.0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniform1i(uniLocation[3], false); // *ライティング OFF
gl.uniform1i(uniLocation[5], false); // *テクスチャ OFF
gl.uniform1i(uniLocation[6], true); // *アウトライン ON
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);
第一のパスではカラーバッファと深度バッファをマスクし、ステンシルバッファに対してのみレンダリングするのでしたね。シェーダに対してライトやテクスチャ、アウトラインに関するフラグをプッシュしているのも * 印付きのコメント部分を見るとわかると思います。
第二のパスでは背景用に大きくスケーリングを適用した球体モデルをレンダリングします。スケーリングを行なうためのモデル座標変換行列の処理などに注目ですね。
背景用の球体のレンダリング関連
// カラーと深度のマスクを解除
gl.colorMask(true, true, true, true);
gl.depthMask(true);
// 球体モデル用ステンシル設定
gl.stencilFunc(gl.EQUAL, 0, ~0);
gl.stencilOp(gl.KEEP, gl.KEEP, gl.KEEP);
// 球体モデルの頂点データ
set_attribute(sVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, sIndex);
// 球体モデル座標変換行列の生成
m.identity(mMatrix);
m.scale(mMatrix, [50.0, 50.0, 50.0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniform1i(uniLocation[3], false); // *ライティング OFF
gl.uniform1i(uniLocation[4], 0);
gl.uniform1i(uniLocation[5], true); // *テクスチャ ON
gl.uniform1i(uniLocation[6], false); // *アウトライン OFF
gl.drawElements(gl.TRIANGLES, sphereData.i.length, gl.UNSIGNED_SHORT, 0);
背景用の球体モデルはもともと単位球(半径が 1.0)なので、50 倍にスケーリングをかけています。またあくまでも背景用なので、ライティングは行なわず、当然アウトラインに関しても無効化してあるのがわかると思います。
第三のパスでは普通にトーラスをレンダリングします。ステンシルバッファの影響を受けないようにするために、ステンシルテストを無効化することに注意します。
トーラスのレンダリング
// ステンシルテストを無効にする
gl.disable(gl.STENCIL_TEST);
// トーラスの頂点データ
set_attribute(tVBOList, attLocation, attStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, tIndex);
// トーラスモデル座標変換行列の生成
m.identity(mMatrix);
m.rotate(mMatrix, rad, [0.0, 1.0, 1.0], mMatrix);
m.multiply(tmpMatrix, mMatrix, mvpMatrix);
// uniform変数の登録と描画
gl.uniformMatrix4fv(uniLocation[0], false, mvpMatrix);
gl.uniformMatrix4fv(uniLocation[1], false, invMatrix);
gl.uniform3fv(uniLocation[2], lightDirection);
gl.uniform1i(uniLocation[3], true); // *ライティング ON
gl.uniform1i(uniLocation[5], false); // *テクスチャ OFF
gl.uniform1i(uniLocation[6], false); // *アウトライン OFF
gl.drawElements(gl.TRIANGLES, torusData.i.length, gl.UNSIGNED_SHORT, 0);
これで型抜きされた背景の内側に、トーラスがレンダリングされます。トーラスには今回テクスチャを貼りませんので、ライティングのみオンになっています。
今回のサンプルでは、背景用の球体でカメラを包み込んでしまうわけですから、球体モデルを内側から見ることになります。球体を内側から見るということは、ポリゴンの裏表が逆になります。ですからカリングをオフにしている点も地味に重要だったりします。
また、ステンシルバッファによって型抜きされるということは、その抜かれた部分に見えるのは初期化された直後のコンテキストの色そのものということになりますね。ですから clear
メソッドでクリアする色を変えると、アウトラインとして見える色が変化します。
まとめ
さて、アウトラインのレンダリング、いかがでしたでしょうか。なかなか複雑な手順を踏むことになりましたが、考え方自体はそれほど難しくないと思います。
ステンシルバッファは、使い方が理解できても、どう活用したらいいのかがなかなか思い浮かばない概念のような気がします。今回のサンプルを通して、少しでもステンシルバッファの扱いに慣れていただけたらと思います。
サンプルへのリンクはいつものように下記に用意してあります。クォータニオンによるカメラ制御を行なっていますので、いろんな角度からトーラスを眺めることが可能です。どんなにカメラが動いても、しっかりとアウトラインが描画されていることを確認してみてください。
次回はフレームバッファをやる予定です。