mosaic フィルタ

実行結果

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

いろんなところで目にします

前回は、後光表面下散乱を解説しました。後光表面下散乱を用いると、モデルの裏側から当たったライトの光をぼんやりと透過したような効果が得られます。表面下散乱には様々な実装がありますので前回解説した表面下散乱はそのうちの一例ということになりますが、必要に応じてプログラムに組み込んでみてください。

さて、前回の後光表面下散乱では実に四組ものシェーダを豪勢に使いましたが、今回はもう少しシンプルなフィルタ処理をフラグメントシェーダを活用してやってみます。今回は mosaic (モザイク)フィルタです。

モザイクというと、テレビやそのほかのメディアでよく使われている技術を真っ先に思い浮かべるという方が多いでしょう。そもそもモザイクの語源となったのはフランス語のようです。寄木細工のことを表す言葉のようですが、確かにモザイクの掛かった状態はいくつかの断片が寄せ集まったように見えますよね。今回はフラグメントシェーダを用いてレンダリング結果に動的にモザイクフィルタを適用します。リアルタイムなモザイク処理、どんなふうに実装すればいいでしょうか。

モザイクフィルタの概念

モザイクフィルタを適用してレンダリングするためには、いくつかの方法が考えられます。

まず思いつくのは、レンダリングされるスクリーン上をある程度の大きさのボックスに区切り、そのボックスの左上など、特定のピクセルの色をボックス内の全てのピクセルに適用する方法です。

左上のピクセルを参照するイメージ図

左上のピクセルを参照するイメージ

この方法は、これからレンダリングしようとしているピクセルの位置さえわかれば、そこから逆算してボックスの左上座標を参照すればいいだけなのでコストパフォーマンスに優れています。ただし、ボックスが大きくなった場合やそもそもスクリーン自体が狭い場合には不自然な結果になりがちです。

そこで今回は、単純に特定のピクセルを参照するのではなく、ボックス内の全体の色を平均化することで精度を上げる方法を採用します。

ボックス内の平均値を算出するイメージ図

ボックス内の平均値を算出するイメージ

この方法では、ある程度ボックスが大きくなっても問題なく精度の高いモザイクフィルタが適用できますが、その代わりにデメリットとして、当然と言えば当然ですが先程の方法に比べて負荷が高い処理になります。先程の方法では座標の逆算という計算は必要になるものの、参照するピクセルさえ特定できれば処理が完結できるのに対し、こちらの方法では全てのピクセルの加算処理が発生するので、ボックスとして区切る領域が大きくなればなるほど高負荷になります。

今回のサンプルでは、ボックスのサイズを 8 x 8 の領域に設定します。それでもひとつのピクセルの色を決定するために 64 回のテクセルの参照と加算処理が必要になるわけですから、それなりに高負荷になります。

シェーダの記述

さて、それではシェーダのソースから見ていきます。

以下はモザイクフィルタの肝となるフラグメントシェーダのソースです。

モザイクフィルタのフラグメントシェーダ

precision mediump float;

uniform sampler2D texture;
varying vec2      vTexCoord;

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

void main(void){
	vec4  destColor = vec4(0.0);
	vec2  fc = vec2(gl_FragCoord.s, 512.0 - gl_FragCoord.t);
	float offsetX = mod(fc.s, 8.0);
	float offsetY = mod(fc.t, 8.0);
	
	for(float x = 0.0; x <= 7.0; x += 1.0){
		for(float y = 0.0; y <= 7.0; y += 1.0){
			destColor += texture2D(texture, (fc + vec2(x - offsetX, y - offsetY)) * tFrag);
		}
	}
	gl_FragColor = destColor * nFrag;
}

フラグメントシェーダでは for ステートメントを使ってループ処理することで、モザイクフィルタを実装します。

ポイントとなるのは float 型の値である offsetX offsetY です。このふたつの変数には GLSL のビルトイン関数である mod を使って何かしらの値を取得しているのがわかりますね。

ここで使われている mod は除算の余剰を返します。今から処理しようとしているテクセルがボックス内のどの位置にあるのかを算出するために、事前にこれを計算しています。先述のとおり、今回はスクリーン上を区切るボックスのサイズを 8 x 8 に設定しています。ですから mod 関数の第二引数に 8.0 を指定しているのですね。最終的に、ここで得られたオフセットを元にテクセルを参照しながら、ループ処理で色を全て加算していきます。

最終的に色を出力する際に正規化するために、事前に定数 nFrag を計算しておくこともポイントのひとつでしょうか。

それほど難しいことはしていないので、ソースコードを見ればおおよそ何をやっているのかはわかると思います。

GLSL の for ステートメント

今回のサンプルでは、当サイトのサンプルとしては初めて for ステートメントによる繰り返し処理を使っています。

実は、当初サンプルを作成するに当たり、モザイク処理に使うボックスのサイズは可変にできるようにするつもりでした。しかし、GLSL の for ステートメントは、コンパイルの際に不定の値、つまり変数などを用いた初期化ができないようです。具体的には、次のように for(i = x; i < y; i++){ ... } というような変数を用いた記述はエラーになります。スカラー値を用いてコーディングしなければならないというのは、いろんな意味でちょっと困りますね。

今回のサンプルでは float 型の値をカウンタに使っていますが for ステートメント自体は int 型をカウンタに用いても問題なく動作します。今回は vec2 型のデータとして処理する際に都合がよかったのでカウンタは float 型としました。

また ++ などのインクリメント演算子も GLSL では普通に使えます。状況に応じてうまく for ステートメントを活用したいものです。

メインプログラムの実装とまとめ

今回のサンプルでは、トーラスを一度フレームバッファに描き込み、これにモザイクフィルタを適用します。また、あらかじめ画像を読み込んでおき、これに対してフィルタを適用することもできるようにしてあります。

メインプログラムとなる javascript 側では、特別難しいことはしていません。しいて挙げるなら、フレームバッファに描き込んだリアルタイムなレンダリング結果をテクスチャとして用いるか、あるいは読み込んだ画像をテクスチャとして用いるか、これらを分岐している点くらいでしょうか。基本的に、今まで解説してきたフィルタ系のプログラムと同じロジックを使っています。フレームバッファに描きこんで、正射影でスクリーンに投影する、これが基本的な流れです。

さて、モザイクフィルタ、いかがでしたでしょうか。

見た目はそれほど派手さがない割に、結構重い処理になってしまうというのがネックですね。それなりに大きなスクリーンの全体にモザイクをかける必要性がある場合などでは、特定のピクセルの色をボックス内に適用する簡易版モザイクフィルタを用いたほうがコストパフォーマンス的にいいかもしれません。このあたりは実装の状況によりけりで使い分ければいいと思います。

特定のピクセルを参照してボックス内に適用する場合でも、今回のサンプルを参考にしつつ mod をうまく使えば簡単に実装できると思います。ケースバイケースで使い分けてみていただければと思います。

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

entry

PR

press Z key