ズームブラーフィルタ
今回のサンプルの実行結果
動きのあるエフェクト
前回はモザイクフィルタを扱いました。テレビなどでよく目にするモザイクは実装方法によっては意外にも高負荷になる場合があったのでしたね。割と簡易なプログラムでありながら、ちょっとした高負荷な処理になってしまうということで、なんとなく割に合わない微妙なフィルタでした。自分で解説しておいてこんなこと言ってていいのだろうか……
さて、今回もフィルタ系処理を扱ってみます。前回同様、今回も仕事をするのは主にフラグメントシェーダです。今回のフィルタ処理はズームブラー(zoom blur)フィルタです。
フォトレタッチソフトなどでもおなじみのズームブラー。一昔前は、ロールプレイングゲームの戦闘開始時の画面遷移などにもこのズームブラーが使われたりしていました。
ズームブラーの例
元画像
モンスターと遭遇した瞬間に、フィールド画面からズームブラーを使って戦闘画面に移る―― WebGL を使ってゲームなどを作ろうと思ったら、こういったエフェクトも使い方によっては非常に効果的です。WebGL なら、フレームバッファに描き込んだシーンに、リアルタイムにズームブラーを適用することも可能ですので、ぜひ参考にしてみてください。
さて、そんなズームブラーですが実装はどのようにすればいいのでしょうか。
今回のサンプルでは、前回のモザイクフィルタなどと同様に、フレームバッファを用意してオフスクリーンレンダリングを行った後、そのシーンに対してフィルタ処理を施します。オフスクリーンにレンダリングするのはおなじみのトーラスたちで、グーローシェーディングによって普通にライティングしたシーンを準備しておきます。それに加え、いくつかの画像を読み込んでおきテクスチャとして利用できるようにしておきます。ここまでは前回のサンプルと大差ありません。
フラグメントシェーダ内を見てみる
今回のサンプルでも肝となるのはフラグメントシェーダです。早速、そのソースを見てみましょう。
フラグメントシェーダのソース
precision mediump float;
uniform sampler2D texture;
uniform float strength;
varying vec2 vTexCoord;
const float tFrag = 1.0 / 512.0;
const float nFrag = 1.0 / 30.0;
const vec2 centerOffset = vec2(256.0, 256.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 - centerOffset;
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);
}
さて、なかなか文章量の多いシェーダのソースになりましたが、順を追って見ていきます。
まず、冒頭部分の変数宣言ですが、今回は uniform 変数としてふたつの変数が宣言されています。まず一つ目はテクスチャのユニット番号を受け取るための変数である texture
ですね。これは説明不要でしょう。二つ目の float
型の変数 strength
は、ズームブラーをかける強度を指定するための変数です。
あらかじめ HTML に埋め込まれた range タイプの input エレメントから、この strength
に値が送られてくるようにプログラムを組みます。この strength
の値の範囲は今回のサンプルでは 0 ~ 30 の間になりますが、このあたりはケースバイケースで調整してしまって問題ありません。
ほかには、冒頭部分でいくつかの定数を定義しているのがわかりますね。
フラグメントシェーダの定数定義
const float tFrag = 1.0 / 512.0;
const float nFrag = 1.0 / 30.0;
const vec2 centerOffset = vec2(256.0, 256.0);
今回のサンプルでは、ズームの中心点を canvas の中心に指定します。canvas のサイズは 512 ピクセルになっていますので、それに応じたいくつかの定数をあらかじめ宣言しているのが上記のコードになります。最初の tFrag
は、シェーダ内で正しくテクセルを参照するための正規化に使います。一方もうひとつの float
型定数である nFrag
は、シェーダ内の for
文による繰り返し処理で正規化を行うために利用します。
さらに vec2
型で宣言されている centerOffset
は、もう読んで字の如くの意味で canvas の中心位置を正しく計算するために使います。というのも、思い出してみてほしいのですが GLSL におけるテクスチャ座標系では原点が左下隅になります。このままだとブラーの中心位置が左下隅を基準に計算されてしまいますので、原点をテクスチャの中心に持ってくるために centerOffset
が必要になります。後述するシェーダの main
関数のなかで、どのように利用されているのか注意して見てみてください。
さて、定数宣言に続いて登場するのが float
型の値を返す怪しげな関数 rnd
です。
rnd 関数
float rnd(vec3 scale, float seed){
return fract(sin(dot(gl_FragCoord.stp + seed, scale)) * 43758.5453 + seed);
}
この関数は、簡易な乱数ジェネレータで、ベクトルとシード値を与えると 0 ~ 1 の範囲で乱数を返します。ズームブラーフィルタでは、実際参照するべきテクセルよりも、中心にめり込んだ内側のテクセルを参照します。単純にズームの強さを適用して本来の位置より内側のテクセルを参照しただけでは、単なるズームにしかなりません。そこで、乱数を使ってどの程度まで内側にめり込んだ位置を参照するのか、ランダムに調整するようにするわけです。この rnd
関数はフラグメントシェーダ内で毎回呼び出されますので、極力軽量な実装になっていることが望ましいです。ただ、ほかの乱数生成ロジックを使ってもまったく問題はありません。
ズームブラーのロジック
さて、それでは続いてズームブラーの核となる main
関数の内部を見てみましょう。再度、抜粋してコードを掲載します。
main 関数のソース
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 - centerOffset;
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);
}
まず最初に最終的に出力する色を RGB で保持するための destColor
と、先程の rnd
関数から乱数を取得する random
が出てきます。続けて fc
という vec2
型の変数にこれから参照しようとしている本来の座標を取得しています。
この本来の座標は canvas のサイズと同様にピクセル単位で取得されます。ここから定数の centerOffset
を減算して得られる fcc
は、canvas の中心位置 (256.0, 256.0) から fc
が相対的にどの程度の位置にあるのかを示すベクトルになります。つまり、この fcc
をうまく利用することによって、canvas の中心位置に向かって放射状にエフェクトをかけることが可能になるわけです。
フラグメントシェーダ内の for
文による繰り返し処理では、先だって取得した乱数とカウンタとなる変数 i
を使って、対象ピクセルの重さなどを計算します。ここで定数 nFrag
が登場してきますが、この nFrag
の中身が[ 1.0 / 30.0 ]
の計算結果だったというのがポイントです。繰り返し処理を行う上で、値が大きくなり過ぎないように正規化するために nFrag
が必要なのですね。もし for
文による繰り返し処理の回数を 50 回にするのなら、定数 nFrag
の中身は[ 1.0 / 50.0 ]
になるように調整しましょう。
最終的に、繰り返し処理が終わったあとに destColor
を正規化すれば、晴れてズームブラー用のシェーダの完成になります。
今回の場合、繰り返し処理の中身がちょっと難解かもしれませんが vec2
型の変数 t
への代入処理の部分で、パーセンテージとズームブラー強度に応じた参照テクセルを算出しています。変数 t
に代入される値は、全て掛け算(乗算)ですので、仮に HTML から送られてくる strength
が 0 であれば、ズームブラーは一切掛かりません。落ち着いて、どのような計算が行われているのか考えてみてください。
まとめ
さて、今回はフラグメントシェーダをうまく利用したエフェクト処理のひとつとして、ズームブラーフィルタを解説しました。ひとつのフラグメントを出力するために 30 回もの繰り返し処理が行われますが、そこまで負荷は高くならないように感じます。ゲームの画面遷移など、使い方によっては非常に効果的なエフェクトになりえるフィルタ処理だと思いますので、ぜひ習得していただければと思います。
今回のサンプルも前回までのフィルタ系サンプルと同様に、トーラスのリアルタイムレンダリング結果のほか、2種類の画像に対してエフェクトをかけることが可能です。実際に動作するサンプルは以下にリンクがありますので、気になる方はソースコードなども覗いてみるといいのではないでしょうか。