グレアフィルタ
今回のサンプルの実行結果
より美しい光の表現
前回は gaussian フィルタを解説しました。
gaussian フィルタはレンダリング結果をぼかす処理として非常にオーソドックスで、尚且つ処理結果が美しく仕上がります。昨今の美しい 3D レンダリングでは、多くの場面でぼかし処理が必要になりますので、ある意味では様々なフィルタ処理の下地となる重要なフィルタであるとも言えます。
今回は、そんな gaussian フィルタを使った拡張フィルタの一つであるグレアフィルタを実装してみます。
グレアとは、眩しい光や反射光などが溢れて見える現象のことを言い、別名ライトブルームなどとも呼ばれます。
グレアフィルタを適用してレンダリングを行なうと、反射光など、任意の光をぼんやりとぼかしつつ、光が溢れている様子を表現できます。
よく見ると、反射光がモデルの輪郭外にまで溢れているのがわかりますね。
グレアフィルタを適用すると、光を強く反射するマテリアルがぼんやりと輝き、幻想的な雰囲気を作り出すことができます。上の画像を見るとわかるとおり、全ての光が溢れるのではなく、反射光に対してだけ溢れが発生するのがポイントです。
反射光のみをブラー処理
さて、先述の通り今回のサンプルは反射光をどう扱うかが重要になります。
グレアフィルタでは、強い光がぼんやりと溢れ出す現象をシミュレートします。ライティングでは環境光や拡散光、さらには反射光をシミュレートしますが、視覚に入ってくる強烈な光を表現できるのは反射光、つまりスペキュラ成分です。
グレアフィルタを実装するためには、まずこの反射光だけを一度フレームバッファにレンダリングし、gaussian フィルタを用いてブラーを掛けます。その後、全てのシーンをレンダリングしたあと、ぼかした反射光を加算合成します。
必然的に、今回も複数のシェーダをうまく使いわけることが必要になります。少々ややこしい実装になりますが、注意深く見ていきましょう。
まず始めに、フレームバッファを二つ、オフスクリーンレンダリング用に準備します。一つ目のフレームバッファには、最終的なシーンのレンダリング結果のうち、反射光のみを抽出してレンダリングします。これには、専用のシェーダを使うことにしましょう。
反射光だけをレンダリングするためのシェーダ
// 頂点シェーダ
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform mat4 mvpMatrix;
uniform mat4 invMatrix;
uniform vec3 lightDirection;
uniform vec3 eyeDirection;
varying vec4 vColor;
void main(void){
vec3 invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
vec3 invEye = normalize(invMatrix * vec4(eyeDirection, 0.0)).xyz;
vec3 halfLE = normalize(invLight + invEye);
float specular = pow(clamp(dot(normal, halfLE), 0.0, 1.0), 50.0);
vColor = color * vec4(vec3(specular), 1.0);
gl_Position = mvpMatrix * vec4(position, 1.0);
}
// フラグメントシェーダ
precision mediump float;
varying vec4 vColor;
void main(void){
gl_FragColor = vColor;
}
このシェーダでは、まず頂点シェーダ内で反射光のみを計算し、varying 変数 vColor
に出力します。フラグメントシェーダ側では、単純に頂点シェーダから入ってきた情報を元に、反射光、つまりスペキュラ成分だけをレンダリングします。
本来は以下のようになるはずのレンダリング結果が……
最終シーンの本来のレンダリング結果
反射光だけを抽出すると次のようになります。
反射光のみを抽出してレンダリングした結果
見事に反射光だけが抜き出されていますね。この状態を一つ目のフレームバッファにまずレンダリングするわけですね。
反射光のみをフレームバッファにレンダリングできたら、今度は二つ目のフレームバッファを利用して反射光に対してブラーを掛けます。これは、前回の gaussian フィルタでやったことと全く同じで、単純にぼかし処理を掛けるだけです。
反射光をぼかしたシーンが完成したら、今度は本来のシーンをフレームバッファにレンダリングしておきます。これで反射光のぼかしレンダリングと、本来のレンダリング結果、この二つがフレームバッファに描きこまれた状態になります。
最後に、この二つのフレームバッファをテクスチャとしてシェーダに送り、反射光ぼかしレンダリングを加算合成することでグレアフィルタが完成します。この合成を行なうのにも専用のシェーダを使っています。それが以下のコード。
二つのレンダリング結果を合成するシェーダ
// 頂点シェーダ
attribute vec3 position;
attribute vec2 texCoord;
uniform mat4 mvpMatrix;
varying vec2 vTexCoord;
void main(void){
vTexCoord = texCoord;
gl_Position = mvpMatrix * vec4(position, 1.0);
}
// フラグメントシェーダ
precision mediump float;
uniform sampler2D texture1;
uniform sampler2D texture2;
uniform bool glare;
varying vec2 vTexCoord;
void main(void){
vec4 destColor = texture2D(texture1, vTexCoord);
vec4 smpColor = texture2D(texture2, vec2(vTexCoord.s, 1.0 - vTexCoord.t));
if(glare){
destColor += smpColor * 2.0;
}
gl_FragColor = destColor;
}
頂点シェーダに入ってくる行列は、正射影による座標変換行列です。このシェーダでは、単純に板ポリゴンを一枚レンダリングするだけですので、頂点シェーダでやることは基本的なことだけです。
フラグメントシェーダでは、二つのテクスチャを受け取るようになっています。一つ目のテクスチャは、フルカラーの本来のレンダリング結果を描きこんだフレームバッファのテクスチャ。二つ目が、ブラーを掛けた反射光のレンダリング結果ですね。
フラグメントシェーダ内では glare
という bool
型の uniform 変数を使って処理を分岐していますが、要は HTML に含まれる input 要素から入ってきたフラグを元にグレアフィルタを適用するかどうかで分岐しているだけです。
グレアフィルタを掛ける場合には、反射光のみブラーの色成分を単純に加算しますが、今回のサンプルではグレアフィルタの効果をわかりやすくするために、反射光のほうを 2 倍して若干強めに出るようにしてあります。
効率の良いレンダリング
今回のサンプルでは、フレームバッファ二つを使ってブラーを掛けると同時に、二つのフレームバッファに[ 最終シーン ]と[ 反射光ブラー ]の二つをレンダリングしたあと、専用のシェーダで合成しています。
ただ、実際には最終シーンを一度フレームバッファにレンダリングしなくても、反射光ブラーのシーンがフレームバッファに描きこまれた状態から、ダイレクトで合成しながらレンダリングすることも不可能ではありません。あくまでも、わかりやすさを重視したためにこのような実装となっていますが、直接合成しながらレンダリングしても全く問題ありません。
まとめ
今回のサンプルでは、実に四組ものシェーダが使われています。
- 本来のシーンをレンダリングするシェーダ
- 反射光のみをレンダリングするシェーダ
- gaussian フィルタ用のシェーダ
- 二つのシーンを合成するシェーダ
HTML ソースや、javascript のソースが結構冗長な記述になってしまっていますが、落ち着いて順番に見ていけば、それほど難しくないはずです。サンプルの javascript ソースには、ほぼ全ての処理にコメントが付記してありますので、焦らずじっくり読み解いてみてください。
今回のグレアフィルタ、単なるトーラスをレンダリングしているだけなのですが、やはり適用してみるとすごく幻想的な雰囲気が出て面白いですね。実際に利用する場面は、野外というロケーションで光沢のある素材をレンダリングする場合などだと思いますが、ポイントを絞って使えばそれなりに見映えのするものができるのではないでしょうか。
サンプルでは、グレアフィルタを掛けるかどうかや、グレア(露光)をどの程度ぼかすのかなどを調整できるようにしてあります。いろいろといじって、その効果を確かめてみてください。
いつものように、実際に動作するサンプルへのリンクは以下に。