こんにちは、クライエントエンジニアの中島龍清(ナカシマリュウセイ)です。
だいぶ久しぶりの投稿となってしまいました。
今回はフィルタリングについて調べていたうちにどれが何なのか分からなくなってきたため、備忘録代わりにひとまず簡単に実装ができた平均化フィルタリングについて解説していこうと思います。
平均化フィルタリングとは
ある一点のピクセルとその周辺のピクセルを用いて計算をおこなう画像処理『空間フィルタリング(またはフィルタ)』の一種です。
平均化フィルタリングは、その名の通り各ピクセルの平均値を利用したフィルタリングとなります。
仕組み
例として、平均化フィルタリングでもっとも単純な3x3の場合の仕組みを解説します。
まず、基準となるピクセルとその周辺の8ピクセルの値を取得します。
下図は分かりやすいように各ピクセルの値を0~1で例えたものです。
中心の四角が基準となるピクセルとして、周囲8マスと合わせて計9ピクセルの平均値を算出します。
今回の場合、 (1 + 1 + 1 + 0 + 0 + 0 + 0 + 0 + 0) / 9 = 3 / 9 = 0.333…となるため約0.3とします。
その値を基準となるピクセルに上書きします。
平均化フィルタリングの基本的な処理は以上です。
かなり大雑把となりますが、全てのピクセルに適用すると下図のようになります。
平均化フィルタリングにより周囲のピクセルとの差が減ったことで、明るい箇所から暗い箇所に向かって徐々に色が変化するようになりました。
これを画像全体やアウトライン等の特定の箇所に適用してぼかしを表現しているという仕組みです。
メリット、デメリット
平均化フィルタリングの主なメリット、デメリットはおおよそ以下の通りです。
メリット
・単純な処理であるため実装が比較的簡単
・軽くぼかしたい場合に負荷が少ない
デメリット
・ぼかしきれない場合がある
・強くぼかすと負荷が高くなる可能性がある
実装
3x3の場合の処理をマテリアルノードで作成すると下図のようになります。
かなり複雑に見えますが、内容としてはシンプルです。
TexCoordノードは当マテリアルをアタッチしたメッシュのUVテクスチャ座標を出力するノードです。
TextureObjectParamノードは使用するテクスチャを出力するノードで、TextureSampleノードは入力されたテクスチャとUV座標からRGBA値を出力するノードです。仕組み解説の際に0~1の値で例えていたもので、今回はカラーのテクスチャなのでRGB値(0~1の値を持った3チャンネルのベクター)となります。
TexelSizeノードは入力されたテクスチャオブジェクトのテクセルサイズを出力するノードです。テクスチャのみにフィルタを適用する場合、ピクセルではなくテクセル基準に計算する必要があるためこちらのノードを利用しています。周囲のテクセルを取得する際にテクセルサイズを乗算したオフセット値を加算することで1テクセル分ずらすことが可能です。
最後に、こちらの処理は基準となるテクセルとその周囲のテクセルのRGB値を取得した後、その平均値を算出しているものとなります。つまり、こちらの処理が平均化フィルタリングのメイン処理そのものです。単純なノードだけで組むと3x3の場合でもかなり複雑な見た目となってしまうのが難点です。
実装結果
平均化フィルタリングを適用したものと適用していないものを比較すると下図のようになります。
フィルタなしと比較してフィルタありは細かい箇所がはっきりと描画されずに少しぼかされています。3x3の場合だとこれぐらいの差となります。
応用
カスタムノードを活用する
単純なノードだけで組むと、処理内容はシンプルなのに見た目が複雑になり視認性が悪くなることが多々あります。そこでカスタムノードを活用します。
カスタムノードはシェーダーコードを直接書くことができるノードで、シェーダーコードの知識がある程度必要にはなりますが、処理内容によっては簡単なコードのみで視認性を遥かに改善することができます。
早速カスタムノードにシェーダーコードを書いていきましょう。
まず、こちらの処理部分ですが全く同じノードを使っており、入力される値も参照テクセルをずらす為の値が違うだけでそれ以外はほとんど同じであることが分かります。(中央はずらす値が[0, 0]であるためにずらす処理が省略されています)
ずらす値は以下のループ文を活用することで表現が可能です。
for(int v = -1; v <= 1; v++) { for(int u = -1; u <= 1; u++) { // (u, v)がずらす値となります } }
また、TextureSampleノードは、Texture2DSample関数で同じ処理を表現できます。
そして、右側の演算は平均値の算出をするために9つの値を加算した後9で除算しているだけであるため、コード化すると(A+B+C+D+E+F+G+H+I)/9 となりだいぶ簡略化できます。
各ノードの処理内容を基にシェーダーコード化すると以下のようになります。
float3 outValue = float3(0,0,0); for(int v = -1; v <= 1; v++) { for(int u = -1; u <= 1; u++) { // 各テクセルのRGB値を加算する outValue += Texture2DSample(inTexture, inTextureSampler, inUV + float2(u, v) * inTexelSize).rgb; } } // 平均値を出力する return outValue / 9;
テクスチャオブジェクト(inTexture)、テクセルサイズ(inTexelSize)、UV(inUV)を入力し、基準となるテクセルと周囲のテクセルの平均値を出力するノードが完成しました。
※テクスチャサンプラー(inTextureSampler)はテクスチャオブジェクトをカスタムノードに入力すると自動で追加され、変数名はテクスチャオブジェクトの変数名の後にSamplerが付くものとなります
カスタムノード化したあとの全体図とカスタムノード側の設定は下図のようになっています。※マテリアルはUnlitに変更しています
単純なノードを利用していた時よりもだいぶ見た目が綺麗になりました。
3x3以外を実装する
先ほど作成したカスタムノードを改良することで3x3以外の平均化フィルタリングの実装が可能です。参照する範囲を決めているのはループの部分なので、こちらの数値を変数にして範囲を変更可能にしていきます。
ループの範囲はどちらも-1~1なので、範囲指定用の入力値[inRange]を追加し -inRange ~ inRange という形に変更します。
平均値を算出するために使用している値である 9 はpow(inRange*2 + 1, 2) で求めることができるので 9 をその式に変更します。
それぞれの変更を加えると以下のようになります。
float3 outValue = float3(0,0,0); // 小数は切り捨てにする int r = floor(inRange); for(int v = -r; v <= r; v++) { for(int u = -r; u <= r; u++) { // 各テクセルのRGB値を加算する outValue += Texture2DSample(inTexture, inTextureSampler, inUV + float2(u, v) * inTexelSize).rgb; } } // 平均値を出力する return outValue / pow(r * 2 + 1, 2);
inRangeにカスタムノードの外側からパラメーターを接続すればマテリアルパラメーターで平均化フィルタリングの範囲が調整可能となります。
inRange追加後の全体図およびカスタムノード設定は以下のようになっています。
試しに3x3以外でフィルタリングした場合、どういった描画になるか検証してみましょう。
リアルタイムでパラメーターを調整すると下図のようになっています。((Range*2+1) x (Range*2+1) が範囲となります。 例:1の場合、1*2 +1 = 3 なので3x3)
範囲が大きくなればなるほどぼかしが強くなることが分かると思います。ぼかしを強くしたい場合は参照範囲を大きくすれば良いですが、その分ループ処理が増えるので取り扱いには注意が必要です。
まとめ
今回は平均化フィルタリングというフィルタについて紹介しましたが、フィルタは色々な種類が存在します。用途にあったフィルタを活用することが大切なので、私もフィルタについて改めて勉強していこうと思います。