ガウスフィルタ


~ Gaussian Filter ~







■はじめに

ATIのネットセミナーで紹介されていた、2パスを使ったガウス型のブラーフィルタです。
今回のプログラムは、15タップ30ピクセルのサンプリングと、ATIのものよりもサンプリング数が増えていることが特徴でしょうか(というか、なんでATIは少ないの?)。

まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。

hlsl.fxシェーダの入ったエフェクトファイル
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース
map.bmp:デカールテクスチャ

あと、実行ファイル、モデル及び、プロジェクトファイルが入っています。

■ガウスフィルタとは

ガウスフィルタとは、ガウス関数を使ったぼかしフィルタです。
ガウス関数とは、指数関数的な形をした関数で、次の形をしています。

       1         x2
f(x) = - exp(- ―― )
       N        2σ2

Nは規格化変数で、σ2は分散です。分散とは、平均<x>からのずれの2乗期待値

                   1         x2
σ2 = ∫dx (x-<x>)2 - exp(- ―― )
                   N        2σ2

で、例えば、分散σ2=52の規格化定数Nを除いたグラフは、次のようになります。

分散σ2は、おおよそのグラフの幅をあらわし、x=σの場所で、exp(-0.5)=0.6065になります。分散が大きければ大きいほど関数の山は緩やかになり、逆に小さくなれば、鋭い針のような形になります。

ガウス関数をフィルタリングに使うというのは、ピクセルを合成する時の重みを中心のピクセルからの距離に応じてガウス関数で決定するということです。
より、具体的には、ピクセルのx軸、y軸に対応して2次元の合成

           1            (xi-xa)2       (yj-yb)2
p(xa,yb) = ― ∑∑exp(- ――― ) exp(- ――― ) p(xi,yj)
           N2  i j        2σ22

の公式にしたがって、ピクセルが合成されます。
ここで、i,jにわたる和はピクセルの周辺の適当な個数のサンプリングで、規格化定数Nは、すべてのピクセルが同じ色だったら、ぼかした後も同じ色になるように

              (xi-xa)2        (yj-yb)2
N2 = ∑∑exp(- ――― ) exp(- ――― )
     i j        2σ22

になります。

ガウス関数による合成の特徴の1つは、2パスにきれいに処理を分離できることです。 式をよく見て分解すると、x軸方向と、y軸方向の和にきれいに分けることができます。

           1         (xi-xa)2
p(xa,yj) = - ∑exp(- ――― ) p(xi,yj)
           N  i        2σ2

           1         (yj-yb)2
p(xa,yb) = - ∑exp(- ――― ) p(xa,yj)
           N  j        2σ2

この式があるので、最初にx軸方向のサンプリングを行ってから、y軸方向のサンプリングを別に行うという方法が正当化されます。

もうひとつの特徴は、円形にサンプリングできるということです。
座標変換

x-xa = r cosθ
y-yb = r sinθ

をすると、合成の式は、

           1               r2
p(xa,yb) = ― ∑∑r exp(- ―― ) p(xi,yj)
           N2 θ r         2σ2

になり、重みの式自体には、角度θの係数が入らないので、中心のピクセルから同じ距離にあるピクセルは同じ重みで合成されます
逆の視点で見れば、一つの点がサンプリングされる時に、等しい距離にあるピクセルは同じ重みでサンプリングするので、ピクセルの色は円形に広がることになります。
このような関数は、重みをrの関数にすれば可能ですが、2パスにきれいに分けられる関数は少ないと思うので、ガウス関数は有用な関数の1つであると断定することができます。

具体的にグラフを見ても、重みが円形に広がっているのが確認できるでしょう。

■プログラム

では、プログラムを見ていきましょう。
頂点シェーダプログラムを最初に見ましょう。
最初は、x軸方向にぼかしをかけます
基本的には、中心のテクセル座標をピクセルシェーダに送るだけです。 そのときに、y軸を0.5テクセルだけ下げました。 これは、後々張ったときにテクセル中心がずれるのを防ぐためのものですが、 張るときに注意すれば、ずらさないほうが、よりぼけるのでいいかもしれません。

hlsl.fx
0042: // -------------------------------------------------------------
0043: // 頂点シェーダプログラム
0044: // -------------------------------------------------------------
0045: VS_OUTPUT VS_pass1 (
0046:       float4 Pos    : POSITION,          // モデルの頂点
0047:       float4 Tex    : TEXCOORD0          // テクスチャ座標
0048: ){
0049:     VS_OUTPUT Out = (VS_OUTPUT)0;        // 出力データ
0050:     
0051:     // 位置座標
0052:     Out.Pos = Pos;
0053:     
0054:     Out.Tex = Tex + float2( 0, 0.5f/MAP_HEIGHT );
0055:     
0056:     return Out;
0057: }

ピクセルシェーダでいよいよサンプリングです。
今回は、テクセル中心をちょうどテクセルの真ん中にする位置でサンプリングしました。
15回のサンプリングで、30テクセルのサンプリングをします。
ガウス関数の重みは定数配列weightとして与えます。ちなみに、符号が異なる同じ距離にあるテクセルは同じ重みの値が使えるので、定数は8つ用意すれば事足ります。

hlsl.fx
0059: // -------------------------------------------------------------
0060: // ピクセルシェーダプログラム
0061: // -------------------------------------------------------------
0062: float4 PS_pass1(VS_OUTPUT In) : COLOR
0063: {   
0064:     float4 Color;
0065:     
0066:     Color  = weight[0] *  tex2D( SrcSamp, In.Tex );
0067:     Color += weight[1]
0068:      * (tex2D( SrcSamp, In.Tex + float2( + 2.0f/MAP_WIDTH, 0 ) )
0069:      +  tex2D( SrcSamp, In.Tex + float2( - 2.0f/MAP_WIDTH, 0 ) ));
0070:     Color += weight[2]
0071:      * (tex2D( SrcSamp, In.Tex + float2( + 4.0f/MAP_WIDTH, 0 ) )
0072:      +  tex2D( SrcSamp, In.Tex + float2( - 4.0f/MAP_WIDTH, 0 ) ));
0073:     Color += weight[3]
0074:      * (tex2D( SrcSamp, In.Tex + float2( + 6.0f/MAP_WIDTH, 0 ) )
0075:      +  tex2D( SrcSamp, In.Tex + float2( - 6.0f/MAP_WIDTH, 0 ) ));
0076:     Color += weight[4]
0077:      * (tex2D( SrcSamp, In.Tex + float2( + 8.0f/MAP_WIDTH, 0 ) )
0078:      +  tex2D( SrcSamp, In.Tex + float2( - 8.0f/MAP_WIDTH, 0 ) ));
0079:     Color += weight[5]
0080:      * (tex2D( SrcSamp, In.Tex + float2( +10.0f/MAP_WIDTH, 0 ) )
0081:      +  tex2D( SrcSamp, In.Tex + float2( -10.0f/MAP_WIDTH, 0 ) ));
0082:     Color += weight[6]
0083:      * (tex2D( SrcSamp, In.Tex + float2( +12.0f/MAP_WIDTH, 0 ) )
0084:      +  tex2D( SrcSamp, In.Tex + float2( -12.0f/MAP_WIDTH, 0 ) ));
0085:     Color += weight[7]
0086:      * (tex2D( SrcSamp, In.Tex + float2( +14.0f/MAP_WIDTH, 0 ) )
0087:      +  tex2D( SrcSamp, In.Tex + float2( -14.0f/MAP_WIDTH, 0 ) ));
0088:     
0089:     return Color;
0090: }

y軸方向のぼかしは、同じ定数を使って、ずらす方向をx軸からy軸に変更するだけです。

hlsl.fx
0112: // -------------------------------------------------------------
0113: // ピクセルシェーダプログラム
0114: // -------------------------------------------------------------
0115: float4 PS_pass2(VS_OUTPUT In) : COLOR
0116: {   
0117:     float4 Color;
0118:     
0119:     Color  = weight[0] *  tex2D( SrcSamp, In.Tex );
0120:     Color += weight[1]
0121:      * (tex2D( SrcSamp, In.Tex + float2( 0, + 2.0f/MAP_HEIGHT) )
0122:      +  tex2D( SrcSamp, In.Tex + float2( 0, - 2.0f/MAP_HEIGHT) ));
0123:     Color += weight[2]
0124:      * (tex2D( SrcSamp, In.Tex + float2( 0, + 4.0f/MAP_HEIGHT) )
0125:      +  tex2D( SrcSamp, In.Tex + float2( 0, - 4.0f/MAP_HEIGHT) ));
0126:     Color += weight[3]
0127:      * (tex2D( SrcSamp, In.Tex + float2( 0, + 6.0f/MAP_HEIGHT) )
0128:      +  tex2D( SrcSamp, In.Tex + float2( 0, - 6.0f/MAP_HEIGHT) ));
0129:     Color += weight[4]
0130:      * (tex2D( SrcSamp, In.Tex + float2( 0, + 8.0f/MAP_HEIGHT) )
0131:      +  tex2D( SrcSamp, In.Tex + float2( 0, - 8.0f/MAP_HEIGHT) ));
0132:     Color += weight[5]
0133:      * (tex2D( SrcSamp, In.Tex + float2( 0, +10.0f/MAP_HEIGHT) )
0134:      +  tex2D( SrcSamp, In.Tex + float2( 0, -10.0f/MAP_HEIGHT) ));
0135:     Color += weight[6]
0136:      * (tex2D( SrcSamp, In.Tex + float2( 0, +12.0f/MAP_HEIGHT) )
0137:      +  tex2D( SrcSamp, In.Tex + float2( 0, -12.0f/MAP_HEIGHT) ));
0138:     Color += weight[7]
0139:      * (tex2D( SrcSamp, In.Tex + float2( 0, +14.0f/MAP_HEIGHT) )
0140:      +  tex2D( SrcSamp, In.Tex + float2( 0, -14.0f/MAP_HEIGHT) ));
0141:     
0142:     return Color;
0143: }

ちなみに、重みの計算ですが、アプリケーション側で初期化時や設定が変更されたときにしています。m_tbl[i]にそれぞれの重みをガウス関数から計算し、最後に規格化定数で割って正規化します。
重みは、中央の1つだけが2テクセルのサンプリングに使われ、他は4テクセルのサンプリングをするので、規格化のための係数の重みを考えなくてはなりません。

main.cpp
0173: //-------------------------------------------------------------
0174: // Name: UpdateWeight()
0175: // Desc: 重みの計算
0176: //-------------------------------------------------------------
0177: VOID CMyD3DApplication::UpdateWeight( FLOAT dispersion )
0178: {
0179:     DWORD i;
0180: 
0181:     FLOAT total=0;
0182:     for( i=0; i<WEIGHT_MUN; i++ ){
0183:         m_tbl[i] = expf(-0.5f*(FLOAT)(i*i)/dispersion);
0184:         if(0==i){
0185:             total += m_tbl[i];
0186:         }else{
0187:             // 中心以外は、2回同じ係数を使うので2倍
0188:             total += 2.0f*m_tbl[i];
0189:         }
0190:     }
0191:     // 規格化
0192:     for( i=0; i<WEIGHT_MUN; i++ ) m_tbl[i] /= total;
0193: 
0194:     if(m_pEffect) m_pEffect->SetFloatArray(m_hafWeight
0195:                                         , m_tbl, WEIGHT_MUN);
0196: 
0197: }

■ついでにフレア

今回のフィルタを使って、擬似フレアエフェクトを作成しました。
フレアの大きさを大きくするために、4分の1の縮小バッファに明るい部分を抽出してぼかしました。

■最後に

お、おもい・・・
使ってよいのかわからない重さですね。

■高速化 (2003 Feb. 11追加)

Masaさんの掲示板で、
「私が今使った感じでは、
ガウスフィルタ自体は特に重くなっていませんね。」
いわれてしまったので、高速化してみました。
前回80FPS程度だったものが、今回は120FPS程度になりました。

hlsl.fxシェーダの入ったエフェクトファイル
main.hアプリケーションのヘッダ
main.cppアプリケーションのソース

前回のプログラムがピクセルシェーダ命令数で 15(texture) + 59(arithmetic) 命令だったところを16 + 25 命令にしました。
今回は、テクセル座標をずらして、1サンプルで4テクセル読み込むようにして、 かつ、中心のタップもずらして2つ読むようにしています。

ほとんどわかりませんが、よりぼけているはずです。

プログラムの高速化ですが、ピクセルシェーダプログラムを頂点シェーダやアプリケーションのプログラムに押し込む形で進めていきました。
ps.2.0では、8つのテクスチャ座標を渡せるので、片方のテクスチャ座標を渡します。

hlsl.fx
0052: // -------------------------------------------------------------
0053: // 頂点シェーダプログラム
0054: // -------------------------------------------------------------
0055: VS_OUTPUT VS_pass1 (
0056:       float4 Pos    : POSITION,          // モデルの頂点
0057:       float4 Tex    : TEXCOORD0          // テクスチャ座標
0058: ){
0059:     VS_OUTPUT Out = (VS_OUTPUT)0;        // 出力データ
0060:     
0061:     // 位置座標
0062:     Out.Pos = Pos;
0063:     
0064:     Out.Tex0 = Tex + float2( - 1.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0065:     Out.Tex1 = Tex + float2( - 3.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0066:     Out.Tex2 = Tex + float2( - 5.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0067:     Out.Tex3 = Tex + float2( - 7.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0068:     Out.Tex4 = Tex + float2( - 9.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0069:     Out.Tex5 = Tex + float2( -11.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0070:     Out.Tex6 = Tex + float2( -13.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0071:     Out.Tex7 = Tex + float2( -15.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0072:     
0073:     return Out;
0074: }

ピクセルシェーダでは、頂点シェーダから入力されたテクセル座標と、定数を足しこんで求めた反対側からのテクセル座標からサンプリングして重みを掛け合わせて足し込みます。
テクスチャ座標をずらす定数は、テクスチャの広さから求まるのですが、命令数を減らすために、ピクセルシェーダで計算しないで、アプリケーション側から設定して、命令数を稼ぎました。
また、定数を足してテクスチャ座標をずらしたので、外側にあったピクセルは内側に移動したことに注意してください。

hlsl.fx
0076: // -------------------------------------------------------------
0077: // ピクセルシェーダプログラム
0078: // -------------------------------------------------------------
0079: float4 PS_pass1(VS_OUTPUT In) : COLOR
0080: {   
0081:     float4 Color;
0082:     
0083: //    offsetX = float2( 16.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0084:     
0085:     Color  = weight[0] * (tex2D( SrcSamp, In.Tex0 )
0086:                         + tex2D( SrcSamp, In.Tex7 + offsetX ));
0087:     Color += weight[1] * (tex2D( SrcSamp, In.Tex1 )
0088:                         + tex2D( SrcSamp, In.Tex6 + offsetX ));
0089:     Color += weight[2] * (tex2D( SrcSamp, In.Tex2 )
0090:                         + tex2D( SrcSamp, In.Tex5 + offsetX ));
0091:     Color += weight[3] * (tex2D( SrcSamp, In.Tex3 )
0092:                         + tex2D( SrcSamp, In.Tex4 + offsetX ));
0093:     Color += weight[4] * (tex2D( SrcSamp, In.Tex4 )
0094:                         + tex2D( SrcSamp, In.Tex3 + offsetX ));
0095:     Color += weight[5] * (tex2D( SrcSamp, In.Tex5 )
0096:                         + tex2D( SrcSamp, In.Tex2 + offsetX ));
0097:     Color += weight[6] * (tex2D( SrcSamp, In.Tex6 )
0098:                         + tex2D( SrcSamp, In.Tex1 + offsetX ));
0099:     Color += weight[7] * (tex2D( SrcSamp, In.Tex7 )
0100:                         + tex2D( SrcSamp, In.Tex0 + offsetX ));
0101:     
0102:     return Color;
0103: }

これぐらいの高速化では、やっぱり重いと感じてしまう・・・
ガウスフィルタかけないと400FPSになっているので、使うときは縮小バッファを使わなくては駄目ではないでしょうか。





もどる

imagire@gmail.com