絵画調レンダリング


~ Painterly Rendering with summed area table ~







下の画像をクリックすると、リアルタイムに絵画調でレンダリングするアニメーションが見られます。


■はじめに

SAT(Summed Area Table:エリア総和テーブル)は、とても面白い技法です。
画面全体を平均化してぼかすような操作が簡単に行なえます。
SATの順当な使い道は被写界深度ですが、それでは月並みなので別のネタを考えてみました。

今回のプログラムは、次のものです。

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

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

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

■何やってるの?

単純なSATの手法はぼかすだけですが、少し応用するだけで、ぼかし方を変化させられます。
色をぼかすときの平均を取る数式は、

     1
t = ---(t11 + t12 + ... + tab)
    a*b

です。この和は全てのテクセルに関して同じ重みで平均を取っています。
この式を一般化することを考えると、その1つとして、全てのテクセルの重みを変えてサンプリングすることが考えられます。

     c11t11 + c12t12 + ... + cabtab
t = ---------------------------
        c11 + c12 + ... + cab

係数cijは、自由に決めることができますが、今回は乱数をテクスチャに仕込んだものを使います。

乱数がまばらにちりばめられているときには、 SATを使って、回りのテクセルをサンプリングするときに、非常に少ないテクセルの影響しか受けません。
例えば、下の図のように、サンプルする領域にテクセルが1つしかないときには、 そのテクセルの色になります。
色の付いたテクセルの周辺は、ほとんどがそのような1色のピクセルになるので、 ひとつの色が広がった絵画風の画像になります。

ただし、サンプリングする範囲が狭すぎると、その中にあるテクセルの数が0になって、計算する分母が発散するので、注意が必要です。
そのようなピクセルは値が不定になるので、次のような絵になります。

実際の方法ですが、普通にレンダリングした画像と、乱数をマッピングしたテクスチャを用意しておきます。
色成分にレンダリングした画像と乱数を乗算した画像を入れ、アルファ成分に乱数を格納します。
次にエリア総和テーブルを作成します。 これは、前にやったように横に和をとった後に、縦に和を取ります。
後は、色成分とアルファ成分でエリア総和テーブルの計算をした後に、色成分をアルファ成分で割って平均値を求めます。

■プログラム

それでは、シェーダプログラムを見ていきましょう。
テクスチャの合成とエリア総和テーブルの横方向の合成は同時に行います。
頂点シェーダで対応するテクセルとその左側のテクセルをテクスチャ座標に指定します。

flsl.fx
0062: // -------------------------------------------------------------
0063: // 頂点シェーダプログラム
0064: // -------------------------------------------------------------
0065: VS_OUTPUT VS_sat_x (
0066:       float4 Pos    : POSITION,          // モデルの頂点
0067:       float4 Tex    : TEXCOORD0          // テクスチャ座標
0068: ){
0069:     VS_OUTPUT Out = (VS_OUTPUT)0;        // 出力データ
0070:     
0071:     // 位置座標
0072:     Out.Pos = Pos;
0073:     
0074:     // テクスチャ座標// 中心
0075:     Out.Tex0 = Tex + float2( 0.5f/MAP_WIDTH, 0.5f/MAP_HEIGHT );
0076:     Out.Tex1 = Tex + float2(-0.5f/MAP_WIDTH, 0.5f/MAP_HEIGHT );// 左
0077:     
0078:     return Out;
0079: }

ピクセルシェーダでは、乱数のテクスチャ"WeightSamp"を読み込んで、 中心のテクセルに乗算することによって元の色と乱数の乗算を計算します。
また、元のテクスチャのアルファ成分には1を入れておくことによって、 アルファ成分への乱数の格納を自然に行います。

flsl.fx
0080: // -------------------------------------------------------------
0081: // ピクセルシェーダプログラム
0082: // -------------------------------------------------------------
0083: float4 PS_sat_x(VS_OUTPUT In) : COLOR
0084: {   
0085:     float4 Color;
0086:     float4 weight = tex2D( WeightSamp, In.Tex0 );
0087:     
0088:     Color  = tex2D( SrcSamp, In.Tex0 ) * weight
0089:            + tex2D( SrcSamp, In.Tex1 );
0090:            
0091:     return Color;
0092: }

縦方向の合成はエリア総和テーブルの普通の作成方法になります。
頂点シェーダで中心とその上のテクセルを指定します。

flsl.fx
0094: // -------------------------------------------------------------
0095: // 頂点シェーダプログラム
0096: // -------------------------------------------------------------
0097: VS_OUTPUT VS_sat_y (
0098:       float4 Pos    : POSITION,          // モデルの頂点
0099:       float4 Tex    : TEXCOORD0          // テクスチャ座標
0100: ){
0101:     VS_OUTPUT Out = (VS_OUTPUT)0;        // 出力データ
0102:     
0103:     // 位置座標
0104:     Out.Pos = Pos;
0105:     
0106:     // テクスチャ座標
0107:     Out.Tex0 = Tex + float2( 0.5f/MAP_WIDTH, 0.5f/MAP_HEIGHT );
0108:     Out.Tex1 = Tex + float2( 0.5f/MAP_WIDTH,-0.5f/MAP_HEIGHT );// 上
0109:     
0110:     return Out;
0111: }

ピクセルシェーダでは、単純にその2つを足します。

flsl.fx
0113: // -------------------------------------------------------------
0114: // ピクセルシェーダプログラム
0115: // -------------------------------------------------------------
0116: float4 PS_sat_y(VS_OUTPUT In) : COLOR
0117: {   
0118:     float4 Color;
0119:     
0120:     Color  = tex2D( SrcSamp, In.Tex0 )
0121:            + tex2D( SrcSamp, In.Tex1 );
0122:            
0123:     return Color;
0124: }

最後の合成では、エリア総和テーブルの式を使って、アルファ成分と色成分を計算して、色成分のほうはアルファ成分で割って、ピクセルの値を求めます。

flsl.fx
0152: // -------------------------------------------------------------
0153: // ピクセルシェーダプログラム
0154: // -------------------------------------------------------------
0155: float4 PS_out(VS_OUTPUT In) : COLOR
0156: {   
0157:     float4 Color;
0158:     float  weight;
0159:     
0160:     // 重みの総和を求める
0161:     weight =  tex2D( SrcSamp, In.Tex0 ).a
0162:             - tex2D( SrcSamp, In.Tex1 ).a
0163:             - tex2D( SrcSamp, In.Tex2 ).a
0164:             + tex2D( SrcSamp, In.Tex3 ).a;
0165:             
0166:     // SATの値を重みの総和で割って、色を求める
0167:     Color  =( tex2D( SrcSamp, In.Tex0 )
0168:             - tex2D( SrcSamp, In.Tex1 )
0169:             - tex2D( SrcSamp, In.Tex2 )
0170:             + tex2D( SrcSamp, In.Tex3 ))/weight;
0171: 
0172:     return Color;
0173: }

■最後に

エリア総和テーブルの応用をしてみました。
積分計算ができるので、かなりのポストエフェクトが実行できそうですね。
ただ、アニメの画像を見ると良く分かるのですが、総和の値が大きい右下の部分で 誤差による縞模様が見えています。
エリア総和テーブルを使う時は、この縞が見えないように調整するのが大事になるでしょう。





もどる

imagire@gmail.com