ブラーフィルターによるぼかし処理

実行結果

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

レンダリング結果のフィルタリング

前回はフレームバッファの基本的な使い方を解説しました。

フレームバッファを用いると、いわゆるオフスクリーンレンダリングを行うことが可能になり、バックグラウンドでレンダリングした結果を使って様々な処理を実現できます。

今回はフレームバッファを活用する一つの例として、ブラーフィルターをやってみます。ブラー( blur )というのは俗に言うぼかしのことで、今回のサンプルを用いるとフレームバッファにレンダリングした結果にぼかし処理が適用されます。

今回行なうぼかし処理のテクニック自体は、言うなれば[ フラグメントシェーダによるテクスチャのぼかし処理 ]なのですが、これにフレームバッファをプラスすることで一度レンダリングした結果にぼかしを適用することが可能になるわけですね。今回のサンプルで登場するいくつかの新しい概念もありますが、基本的には前回のフレームバッファを用いたサンプルの延長線上の処理が中心です。前回のサンプルの内容をしっかり理解できているという前提で解説をしていきます。

ぼかし処理の概念

ぼかし処理を行なうには、そもそもどのような処理を行なえばいいのでしょうか。

ぼかし処理とは、ある一つのピクセルの色を決めようとするとき、そのピクセルの周囲の色を参照しながら色を合成する処理だと考えることができます。

今回のぼかし処理では、対象となるピクセルの周囲 2 ピクセル分の範囲を参照し色を合成します。これを図にしてみると次のような感じになります。

周囲 2 ピクセル

上の図の青いピクセルが、今まさにレンダリングされようとしている対象ピクセルです。そのピクセルの周囲 2 ピクセル分の範囲を参照しながらぼかし処理を行ないます。周囲のピクセルを参照すると言っても、それぞれのピクセルから影響を受ける度合いは調節しなければなりません。

そのウェイトを表したのが以下の図ですね。

各ピクセルのウェイト

本来のピクセルの色の強度を 36 とすると、周囲 1 ピクセルの範囲を 4 に、周囲 2 ピクセル目の範囲を 2 として色を合成します。この合計 25 ピクセル分のウェイトを全て合算すると、ちょうど 100 になるようにウェイトを調整してあります。

ちなみに、ぼかし処理を行なう手法には様々なものがあり、今回行なうブラーフィルターの手法は当サイト独自のものですのであしからず。

マルチシェーダプログラム

ブラーを行なうための概念はなんとなく理解できたでしょうか。そんなに難しいことをするわけでもありませんので、落ち着いて考えてみてください。

さて、今回のサンプルで初めて登場するテクニックにマルチシェーダがあります。これは読んで字の如く、複数のシェーダを同時に使いこなす処理のことを言います。

今回のサンプルでは、まずフレームバッファに対して最初のレンダリングを行ないます。この時点では前回のサンプルで使ったライティングなどを行なうことが可能なシェーダを使います。フレームバッファへのレンダリングが済んだら、今度はブラーフィルターをかけるための専用のシェーダに切り替えて再度レンダリングを行ないます。つまり、頂点シェーダとフラグメントシェーダを二組用意しておき、目的に応じて切り替えながらレンダリングを行なうわけです。

当サイトのテキストでは今まで一組のシェーダしか使ってきませんでしたが、マルチシェーダプログラムであっても基本的にはやることは一緒です。シェーダのソースを HTML から吸出し、コンパイルしたあとプログラムオブジェクトによってリンクします。プログラムオブジェクトは以前も解説したとおり、頂点シェーダとフラグメントシェーダを取り持つ仲介役のような役割を持っていますが、このプログラムオブジェクトのうち、どのプログラムオブジェクトを利用するのかを WebGL に通知することで、結果的にシェーダを切り替えながらレンダリングを行うことが可能になります。

マルチシェーダに関連するサンプルのソースを一部抜粋してみます。

まずはライティングなどを行なう一つ目のシェーダに関連する部分です。

一組目のシェーダに関する処理

// フレームバッファ用シェーダ-start----------------------------------------

// 頂点シェーダとフラグメントシェーダ、プログラムオブジェクトの生成
var v_shader = create_shader('vs');
var f_shader = create_shader('fs');
var prg = create_program(v_shader, f_shader);

// attributeLocationを配列に取得
var attLocation = new Array();
attLocation[0] = gl.getAttribLocation(prg, 'position');
attLocation[1] = gl.getAttribLocation(prg, 'normal');
attLocation[2] = gl.getAttribLocation(prg, 'color');
attLocation[3] = gl.getAttribLocation(prg, 'textureCoord');

// attributeの要素数を配列に格納
var attStride = new Array();
attStride[0] = 3;
attStride[1] = 3;
attStride[2] = 4;
attStride[3] = 2;

// 球体モデル
var earthData     = sphere(64, 64, 1.0, [1.0, 1.0, 1.0, 1.0]);
var ePosition     = create_vbo(earthData.p);
var eNormal       = create_vbo(earthData.n);
var eColor        = create_vbo(earthData.c);
var eTextureCoord = create_vbo(earthData.t);
var eVBOList      = [ePosition, eNormal, eColor, eTextureCoord];
var eIndex        = create_ibo(earthData.i);

// uniformLocationを配列に取得
var uniLocation = new Array();
uniLocation[0] = gl.getUniformLocation(prg, 'mMatrix');
uniLocation[1] = gl.getUniformLocation(prg, 'mvpMatrix');
uniLocation[2] = gl.getUniformLocation(prg, 'invMatrix');
uniLocation[3] = gl.getUniformLocation(prg, 'lightDirection');
uniLocation[4] = gl.getUniformLocation(prg, 'useLight');
uniLocation[5] = gl.getUniformLocation(prg, 'texture');

// フレームバッファ用-end--------------------------------------------------

こちらのシェーダでは、シェーダに送る頂点属性として、ライティングを行なうための法線に関する情報やテクスチャ座標などが必要になります。uniform 変数としては座標変換行列のほか、ライトに関するデータなどが必要になりますね。このフレームバッファに描き込む際に利用するシェーダは前回のサンプルとほとんど同じものです。

続いてはブラーフィルターを掛ける際に利用するシェーダに関するソースです。

二組目のシェーダに関する処理

// ブラーフィルター用シェーダ-start----------------------------------------

// 頂点シェーダとフラグメントシェーダ、プログラムオブジェクトの生成
v_shader = create_shader('bvs');
f_shader = create_shader('bfs');
var bPrg = create_program(v_shader, f_shader);

// attributeLocationを配列に取得
var bAttLocation = new Array();
bAttLocation[0] = gl.getAttribLocation(bPrg, 'position');
bAttLocation[1] = gl.getAttribLocation(bPrg, 'color');

// attributeの要素数を配列に格納
var bAttStride = new Array();
bAttStride[0] = 3;
bAttStride[1] = 4;

// 頂点の位置
var position = [
	-1.0,  1.0,  0.0,
	 1.0,  1.0,  0.0,
	-1.0, -1.0,  0.0,
	 1.0, -1.0,  0.0
];

// 頂点色
var color = [
	1.0, 1.0, 1.0, 1.0,
	1.0, 1.0, 1.0, 1.0,
	1.0, 1.0, 1.0, 1.0,
	1.0, 1.0, 1.0, 1.0
];

// 頂点インデックス
var index = [
	0, 1, 2,
	3, 2, 1
];

// VBOとIBOの生成
var vPosition     = create_vbo(position);
var vColor        = create_vbo(color);
var vVBOList      = [vPosition, vColor];
var vIndex        = create_ibo(index);

// uniformLocationを配列に取得
var bUniLocation = new Array();
bUniLocation[0] = gl.getUniformLocation(bPrg, 'mvpMatrix');
bUniLocation[1] = gl.getUniformLocation(bPrg, 'texture');
bUniLocation[2] = gl.getUniformLocation(bPrg, 'useBlur');

// ブラーフィルター用-end--------------------------------------------------

こちらは必要な頂点属性として頂点位置と頂点色の二つのみを準備します。また、uniform 変数として送るデータも座標変換行列とテクスチャユニット番号、そしてブラーフィルターを適用するかどうかを表す真偽値のみです。

マルチシェーダプログラムを組む際にポイントとなるのは、attribute 変数や uniform 変数のインデックスを取得する際に、対象となるプログラムオブジェクトを正しく選択することです。今回の例で言えば、最初のシェーダをリンクしたプログラムオブジェクトは prg という名前の変数入っています。二組目のシェーダをリンクしたプログラムオブジェクトは bPrg という変数に入ってますね。ここをあやふやにしてコードを書くとうまくいきませんので注意しましょう。もちろん、取得した uniformLocation や attributeLocation がごっちゃにならないように気をつけることも忘れないように。

ブラーフィルターシェーダ

さて、マルチシェーダとは言え、一組目のシェーダは前回とほぼ同じ内容です。ですから一組目のシェーダに関する解説はここでは割愛します。今回のサンプルにおける肝であるブラーフィルターを掛けるシェーダの内容を、ここからは見ていきましょう。

まずはブラーフィルター用の頂点シェーダです。

頂点シェーダ

attribute vec3 position;
attribute vec4 color;
uniform   mat4 mvpMatrix;
varying   vec4 vColor;

void main(void){
	vColor = color;
	gl_Position = mvpMatrix * vec4(position, 1.0);
}

なんとも簡素ですね。

拍子抜けするかもしれませんが、ブラーフィルターはそのほとんどの処理をフラグメントシェーダによって行ないます。頂点シェーダでするべきことは、頂点の座標変換と、頂点色をフラグメントシェーダに送ることのみなのです。

さあそれでは問題のフラグメントシェーダのソースです。

こちらはなかなかすごいことになってます。

フラグメントシェーダ

precision mediump float;

uniform sampler2D texture;
uniform bool      useBlur;
varying vec4      vColor;

void main(void){
	vec2 tFrag = vec2(1.0 / 256.0);
	vec4 destColor = texture2D(texture, gl_FragCoord.st * tFrag);
	if(useBlur){
		destColor *= 0.36;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-1.0,  1.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 0.0,  1.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 1.0,  1.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-1.0,  0.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 1.0,  0.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-1.0, -1.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 0.0, -1.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 1.0, -1.0)) * tFrag) * 0.04;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-2.0,  2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-1.0,  2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 0.0,  2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 1.0,  2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 2.0,  2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-2.0,  1.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 2.0,  1.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-2.0,  0.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 2.0,  0.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-2.0, -1.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 2.0, -1.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-2.0, -2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2(-1.0, -2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 0.0, -2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 1.0, -2.0)) * tFrag) * 0.02;
		destColor += texture2D(texture, (gl_FragCoord.st + vec2( 2.0, -2.0)) * tFrag) * 0.02;
	}
	gl_FragColor  = vColor * destColor;
}

なんじゃこりゃー……って感じですね。

ここで最も重要となるポイントは、初めて登場する gl_FragCoord という組み込み変数の使い方です。

この変数にはこれから描かれようとしているフラグメントのピクセル単位の座標があらかじめ入っています。つまり、canvas タグに対して設定したサイズが縦横 300 ピクセルだと仮定すると、組み込み変数 gl_FragCoord の s 要素と t 要素は 0 ~ 300 の範囲を取ることになります。

ここで得られる数値は、左下を原点とすることに注意しましょう。また、今回のサンプルの場合、一度フレームバッファにレンダリングした結果に対してブラーフィルターを適用することになるため、このフレームバッファのサイズ(つまりフレームバッファにアタッチしたテクスチャのサイズ = 256 ピクセル)と、canvas タグのサイズを同じにするようにしています。こうすることで、ブラーのオン・オフを切り替えたときに、その結果がわかりやすくなるからですね。

さて、それでは具体的にフラグメントシェーダ内で何をやっているのか見ていきます。

まず main 関数の一行目、ここでは何かの値を変数 tFrag に代入していますね。

vec2 型の変数 tFrag への値の代入

vec2 tFrag = vec2(1.0 / 256.0);

この tFrag という変数は gl_FragCoord を参照して得られた値を、テクスチャ座標に変換するために使います。思い返してみてください。テクスチャ座標とは常に 0 ~ 1 の範囲で参照するものでしたよね。組み込み変数 gl_FragCoord は、先ほども書いたようにビューポート(つまり canvas)のサイズまでの範囲を取りますので、それを 0 ~ 1 の範囲に収まるように変換することで、正しくテクスチャを参照することができるようになります。

割り算は掛け算に比べ、若干ですが負荷の大きな処理です。フラグメントシェーダのような、各ピクセルに対して動作するプログラムでは、この小さな負荷の差が大きな影響を及ぼすこともあるので、最初に tFrag に値を設定する一度だけ割り算を使い、あとは掛け算だけで処理できるようにしています。

続いて main 関数の二行目では、テクスチャを参照して色を抜き出しています。

テクスチャの参照

vec4 destColor = texture2D(texture, gl_FragCoord.st * tFrag);

先ほども書いたように gl_FragCoord tFrag を掛けることで値を 0 ~ 1 の範囲に変換し、テクスチャ座標を正しく参照できるようにしています。

さて問題はここからですね。この後、三行目では uniform 変数として入ってきたフラグを元に、ブラーフィルターを掛けるかどうかによって処理が分岐します。仮に、ブラーフィルターを適用しない場合には、二行目で得られた色をそのまま gl_FragColor に出力します。

ブラーフィルターを適用する場合には、冒頭で登場した図に示したとおり 25 個分のピクセルへの参照を行い色を合成します。本来描かれるはずのピクセルはウェイト 36 でしたよね。その周囲 1 ピクセルの範囲は ウェイト 4 で、さらに一回り外側のピクセルのウェイトは 2 でした。

シェーダ内では、色を表す数値も 0 ~ 1 の範囲で表しますので、ウェイトは 100 分の 1 に変換して掛けていますが、これは直感的に理解できるでしょう。

サンプルの補足とまとめ

今回のサンプルでは、フレームバッファにアタッチするテクスチャの、テクスチャパラメータに gl.CLAMP_TO_EDGE を追加しています。

テクスチャパラメータの付加

gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);

これはブラーフィルターを適用する際、端にあるピクセルが周囲のピクセルを参照するときに、WebGL におけるテクスチャの既定値である gl.REPEAT のままだと反対側の端にあるピクセルを参照してしまい、色が不自然に合成されてしまうためです。

javascript プログラムの内部では、座標変換行列を生成する際に、透視法による射影変換ではなく、正射影による座標変換を行なっています。これは当サイトオリジナルのライブラリである minMatrixb.js に含まれる matIV.ortho メソッドで実現できます。

正射影変換行列の生成

m.ortho(-1.0, 1.0, 1.0, -1.0, 0.1, 1, pMatrix);

正射影変換がどういうものなのかは、テキストが長くなるのでここでは説明しませんが、要は canvas にぴったりと収まるように板ポリゴン(ブラーの掛かったフラグメントを適用するポリゴン)をレンダリングするのに、正射影変換を用いたほうが都合がいいのでこのようになっています。

また、今回のサンプルではブラーを掛けるかどうか HTML 側で選択できるようにするため、HTML 内に input タグによるチェックボックスを設けています。javascript コード内でこのチェックボックスの値を参照しながら、ブラーを掛けるかどうか判断し、最終的に uniform 変数としてシェーダにプッシュするようになっています。

さて、ブラーフィルター、いかがでしたか。実際に動作するサンプルを見ると、ぼんやりとぼかしが掛かる様子がわかると思います。

フラグメントシェーダの内容がすごく長文なのでびっくりするかもしれませんが、組み込み変数 gl_FragCoord の使い方さえ掴めれば、あとは対象ピクセルの周囲を参照して色を合成しているだけなので、すんなり理解できると思います。

サンプルはいつものように下記にあるリンクから実物を参照できます。

今回のサンプルは、非常にシェーダを使いこなしている感の強いものだと思います。このあたりの処理が楽しくなってくると、いろんな発想が生まれてきて WebGL がさらに面白くなってくるのではないかと思います。

さて次回ですが、バンプマッピングをやる予定です。

entry

PR

press Z key