ゴッドレイフィルタ

実行結果

今回のサンプルの実行結果

ズームブラーの応用

前回は、フラグメントシェーダを利用したズームブラーを解説しました。シェーダ内で簡易な乱数ジェネレータを使い、ズーム効果にランダム性を持たせるのがポイントでした。

今回はそのズームブラーを応用することで、ゴッドレイ効果を実現してみましょう。ゴッドレイは、太陽やライトなど、強い光を発するものから放射状に伸びる光の筋のことですね。例えばそれは空の隙間から、あるいはステンドグラスや窓からさ仕込む光明としてシーンを彩ってくれます。

ゴッドレイの一例

ゴッドレイの一例

ズームブラーが理解できていてれば、その応用だけで割りと簡単に実装できるゴッドレイ。早速、その実装方法について見ていきましょう。

ゴッドレイの実装概念

さて、ゴッドレイフィルタを行う上では、ズームブラーの掛け方と合成方法がポイントになります。今回の場合、一度フレームバッファにゴッドレイ専用の背景を描き込み、その背景に対してズームブラーフィルタを適用します。この背景は白黒で表現し、最終的に加算合成で処理します。今回のサンプルでは、以下の画像を背景としてテクスチャに利用して、ゴッドレイを実現してみましょう。

ゴッドレイに利用する画像

ゴッドレイの背景画像

この当サイトのロゴを背景とし、これをベースに背景シーンをまずはレンダリングします。ただし、今回のサンプルは冒頭の画像を見ればわかると思いますが、この画像をそのまま背景として使うわけではありません。

以下の画像を見てください。手前に描かれたトーラスにより、ゴッドレイフィルタによって描かれる光の筋が遮蔽されているのがわかりますでしょうか。

モデルによる光の遮蔽

光の遮蔽

このようなゴッドレイの遮蔽を行うためには、背景画像をフレームバッファに描き込む段階で一工夫必要になります。

手順としては、まずフレームバッファの全面に板ポリゴンを使って画像を貼り付けます。この段階では、文字通り背景に画像がレンダリングされただけの状態です。ここに、手前側にくるトーラスのシルエットのみをレンダリングしてやります。すると、次のような状態になりますね。

背景とモデルのシルエット

背景とモデルのシルエット

こうしてモデルのシルエットを描き込んだ状態のフレームバッファを背景とし、この背景にズームブラーを適用します。最終的に、ズームブラーを適用した後、それをテクスチャとして加算合成すれば見事にゴッドレイフィルタが完成します。また、今回のサンプルはズームブラーしか利用していませんが、ズームブラーを掛けたあとさらにガウシアンブラーを適用すれば、以前解説したグレアフィルタのように光の溢れについても表現できるため、よりリアルなゴッドレイを実現できるでしょう。

シェーダの記述

実装方法について理解できたところで、続いてはシェーダについて考えてみます。

今回のサンプルでは、シェーダは三つ使います。一つ目は、お馴染みの基本的なライティングを行うシェーダで、これは最終的にカラフルなトーラスをレンダリングするのに使います。二つ目のシェーダは、ズームブラーを適用するシェーダで、これは前回のサンプルのソースを少々修正したものを使います。三つ目のシェーダは正射影でシーンを合成するのに使います。

さて、ズームブラーシェーダを改造した、ゴッドレイ用のシェーダを見てみましょう。

ゴッドレイシェーダ

// 頂点シェーダ
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 texture;
uniform float     strength;
uniform vec2      center;
varying vec2      vTexCoord;

const float tFrag = 1.0 / 512.0;
const float nFrag = 1.0 / 30.0;

float rnd(vec3 scale, float seed){
	return fract(sin(dot(gl_FragCoord.stp + seed, scale)) * 43758.5453 + seed);
}

void main(void){
	vec3  destColor = vec3(0.0);
	float random = rnd(vec3(12.9898, 78.233, 151.7182), 0.0);
	vec2  fc = vec2(gl_FragCoord.s, 512.0 - gl_FragCoord.t);
	vec2  fcc = fc - center;
	float totalWeight = 0.0;
	
	for(float i = 0.0; i <= 30.0; i++){
		float percent = (i + random) * nFrag;
		float weight = percent - percent * percent;
		vec2  t = fc - fcc * percent * strength * nFrag;
		destColor += texture2D(texture, t * tFrag).rgb * weight;
		totalWeight += weight;
	}
	gl_FragColor = vec4(destColor / totalWeight, 1.0);
}

今回のサンプルでも、主に仕事をするのはフラグメントシェーダです。前回のサンプル同様に rnd 関数が簡易な乱数ジェネレータとして機能し、単なるズームアップを放射状にランダムなズームブラーとして処理します。

前回と異なる点は、ズームブラーの中央の座標を uniform 変数として受け取っているというところでしょう。メインプログラムから送られてくるズームブラーの中央位置座標を使って、放射状にズームブラーを適用する点以外は前回のサンプルとほぼ同じです。もしよくわからない部分がある場合は、前回のテキストを読んでみると理解が深まるのではないでしょうか。

さて、続いてはグーローシェーディングによるライティングを行うシェーダ。今回の場合、ただ単にライティングを行う役割のほかに、トーラスのシルエットだけをレンダリングする機構を追加します。

ライティング+シルエット用のシェーダ

// 頂点シェーダ
attribute vec3 position;
attribute vec3 normal;
attribute vec4 color;
uniform   mat4 mvpMatrix;
uniform   mat4 invMatrix;
uniform   vec3 lightDirection;
uniform   vec3 eyeDirection;
uniform   vec4 ambientColor;
uniform   bool mask;
varying   vec4 vColor;

void main(void){
	if(mask){
		vColor = vec4(vec3(0.0), 1.0);
	}else{
		vec3  invLight = normalize(invMatrix * vec4(lightDirection, 0.0)).xyz;
		vec3  invEye   = normalize(invMatrix * vec4(eyeDirection, 0.0)).xyz;
		vec3  halfLE   = normalize(invLight + invEye);
		float diffuse  = clamp(dot(normal, invLight), 0.1, 1.0);
		float specular = pow(clamp(dot(normal, halfLE), 0.0, 1.0), 50.0);
		vec4  amb      = color * ambientColor;
		vColor         = amb * vec4(vec3(diffuse), 1.0) + vec4(vec3(specular), 1.0);
	}
	gl_Position = mvpMatrix * vec4(position, 1.0);
}

// フラグメントシェーダ
precision mediump float;

varying vec4 vColor;

void main(void){
	gl_FragColor = vColor;
}

ライティング関連の処理については、これまでやってきたこととまったく同じです。ただし、uniform 変数として入ってくる bool 型の変数 mask の値に応じて、処理を分岐するようにしています。もし mask true の場合には、シルエットのみを出力するようにしたいので、varying 変数 vColor には黒を渡すようにします。フラグメントシェーダ側では、頂点シェーダから受け取った色情報を出力しているだけですので簡単ですね。

正射影による合成を行うシェーダは、今までにも何度も登場したものなのでソースは載せません。シェーダ内で特別なことをしているわけでもありませんので、適宜過去のテキストやサンプルプログラムのソースを参照してみてください。

メイン javascript プログラム

さて、今回のサンプルの肝はむしろシェーダよりも javascript で記述するメインプログラムのほうです。

先程書いたように、サンプルではまず背景用の画像を画面いっぱいにレンダリングします。これには、板ポリゴンにテクスチャを貼り付けて正射影でレンダリングすることで対応します。そのあとトーラスのシルエットだけをレンダリングすることになりますが、ここで問題になるのが深度の問題です。普通にレンダリングしてしまうと、正射影でレンダリングした板ポリゴンが前面にきてしまい、トーラスのシルエットがうまくレンダリングされません。

そこで、正射影で板ポリゴンをレンダリングする際に、一度深度バッファの更新を切って無効化してしまう方法を使います。トーラスをレンダリングする際には、トーラス同士が重なる場面なども考えられますし、深度バッファの更新はきちんと有効にしておく必要がありますので注意しましょう。

javascript 側のソースの一部を抜粋

// フレームバッファを初期化
gl.clearColor(1.0, 1.0, 1.0, 1.0);
gl.clearDepth(1.0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

// 一度深度バッファへの描き込みを無効にする
gl.depthMask(false);

// 板ポリゴンをレンダリングしテクスチャを画面いっぱいに貼り付ける
set_attribute(vVBOList, oAttLocation, oAttStride);
gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, vIndex);
gl.uniformMatrix4fv(oUniLocation[0], false, ortMatrix);
gl.uniform1i(oUniLocation[1], 0);
gl.drawElements(gl.TRIANGLES, index.length, gl.UNSIGNED_SHORT, 0);

// 深度バッファへの描き込みを有効化する
gl.depthMask(true);

// 以下トーラスのレンダリングへと続く……

深度バッファへの描き込みを行わないようにするには depthMask メソッドを使います。引数には真偽値を渡すだけなので簡単ですね。

フレームバッファに背景とシルエットを描き込む際、さらには最終的なシーンで背景とカラーのトーラスをレンダリングする際の合計二回、同じように深度値の描き込みを無効化している場所がありますので、ソースを見る際には参考にしてみてください。

最終的に、ズームブラーを適用したフレームバッファのレンダリング結果を合成する場面でも、背景を画面いっぱいに貼り付ける場合とまったく同じ正射影用シェーダを使っています。ただし、最終合成の場面ではブレンドを有効化し、さらにブレンドファクターを正しく設定する必要があります。

ブレンドの有効化とブレンドファクターの設定部分を抜粋

// 加算合成するためにブレンドを有効化する
gl.enable(gl.BLEND);
gl.blendFuncSeparate(gl.SRC_ALPHA, gl.ONE, gl.ONE, gl.ONE);

ブレンドファクターについては過去のテキストで詳しく解説していますので、ここでは詳細には触れません。ただ blendFuncSeparate メソッドに上記のような指定をした場合には色は加算合成されるようになります。今回のように光を合成するような場面では比較的よく使いますので覚えておくといいかもしれません。

参考:アルファブレンディング

参考:ブレンドファクター

まとめ

さて、ゴッドレイフィルタの実装、いかがでしたでしょうか。今回のサンプルではシェーダよりもむしろメインプログラムのほうがちょっと難解かもしれませんが、深度バッファへの描き込みを無効化したり、ブレンドを有効化したりする部分さえ間違えなければ、ズームブラーのときとさして変わらないので簡単に実装できると思います。

途中でも触れたように、今回のサンプルにはガウシアンブラーは使っていませんが、必要に応じてそのほかのフィルタ処理と併用することで、さらに効果的にゴッドレイを演出できると思います。状況に応じて使い分けてみてください。また、勘がいい人は気がついたかもしれませんが、今回のゴッドレイフィルタは光を透過する部分がスクリーン上に見えている場合にしか使えません。例えば雲の隙間から太陽の光が差し込むようなシーンでは、場合によっては太陽がまったくスクリーン上に描かれない場合があります。こういったシーンでは別のアプローチが必要になりますので注意しましょう。

今回のサンプルも実際に動作するものを見たい場合には以下にリンクがあります。

entry

PR

press Z key