ハロ


~ Lenticular halo ~







下の画像をクリックすると、UFOが動くアニメーションが見られます。


■はじめに

グレアといえば、masaさんが世界的にも有名ですが、そんなmasaさんが最近「DirectX9 HDR Lighting Demo!!」というデモを公開されています。
こちらでも、パクって 参考にして、グレアのエフェクトのひとつであるハロを実装してみました。

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

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

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

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

■何やってるの?

グレアといえば、コーネル大学のGreg Spencer らによる SIGGRAPH95 の 論文である Physically-Based Glare Effects for Digital Images が有名な論文です。
その論文には、目の仕組みを分析し、グレアの種類と原因を分析しています。
ハロは、目のレンズである水晶体の構造によって発生します。 水晶体はでこぼことした放射線状の構造をしていて、その模様の作り出す格子のパターンが 本来の像の外側に虹色のリングを作りだします。

ここで、プログラム的にリングを作り出す方法が今回のみそです。
円を作る方法は、ガウスフィルタを使えば可能です。
そこで、ガウスフィルタで大きくぼかした画像と、小さくぼかした画像を用意して、 それらの画像の差として、円環状のフィルタを作ります。

色をつける方法は、RGBのそれぞれの色成分に関して、異なる分散の値を与えて、 赤い色は広くぼかし、青い色は小さくしかぼかさないことによって実現します。

なにげに今回のパス数は多いです。
最初にシーンを描画します。この時に、浮動小数点バッファを利用して、鏡面反射による1.0よりも明るい色をテクスチャに記録します。
次に、縮小バッファに輝度を強調してコピーします。 小さなテクスチャに一度移しておく方が、ガウスフィルタをかけたときにより大きくかかるので、この操作を行ないます。
次に、2パスを2回使って、ガウスフィルタを別の2つのテクスチャにかけます。
最後にレンダリングしたシーンの画像に、大きなぼかしと小さなぼかしの差を加算合成して貼り付けます。

今回は、全てのバッファで浮動小数点バッファを使ったのですが、 双線型フィルタを使えないのが以外に痛くて、時間があれば、整数バッファで書き直したほうが、パフォーマンスや、ガウスフィルタのサンプル数の数で有利になるでしょう。
どうして浮動小数点バッファを使ったのかというと、鏡面反射光を保存したかったからですが、 これも、最初のパスでハロの強さをアルファ成分に記録しておけば可能でしょう。

■シーンの描画

こつこつとプログラムを見ていきましょう。
最初にシーンの描画をします。
描画するモデルは、テクスチャを張っていないただのメッシュです。
頂点シェーダでは、座標変換と環境色、拡散光の計算をします。
鏡面反射色は、ピクセルシェーダで行なうので、計算に必要な法線ベクトルと視線ベクトルをテクスチャ座標としてピクセルシェーダに流します。
ちなみに、これらのベクトルは、モデルのローカル座標系で計算しています。

hlsl.fx
0074: OUTPUT VS(
0075:       float4 Pos    : POSITION,          // モデルの頂点
0076:       float3 Normal : NORMAL             // モデルの法線
0077: ){
0078:     OUTPUT Out = (OUTPUT)0;        // 出力データ
0079:     float4  uv;
0080:     
0081:     // 座標変換
0082:     Out.Pos = mul(Pos, mWVP);
0083:     
0084:     // 色
0085:     Out.Color = vCol * (0.7*max( dot(vLightDir, Normal), 0)+0.3);
0086:     
0087:     Out.Normal = Normal;        // 色
0088:     Out.Eye    = Pos - vEyePos; // 視線
0089:     
0090:     return Out;
0091: }

ピクセルシェーダでは、法線ベクトルと視線ベクトルから反射ベクトルを計算して、適当な色をかけて鏡面反射色に足しこみます。
反射ベクトルの計算には、組み込み関数 reflect(e,n)=e-2(e,n)n を使いました。
浮動小数点バッファなので、1以上の値もテクスチャに保存されます。

hlsl.fx
0093: float4 PS(OUTPUT In) : COLOR
0094: {   
0095:     float3 e = normalize(In.Eye);   // 視線ベクトル
0096:     float3 n = normalize(In.Normal);// 法線ベクトル
0097:     float3 r = reflect(e,n);        // 反射ベクトル
0098:     
0099:     float power = pow(max(0,dot(r,vLightDir)), 32); // Phone
0100:     float4 SpecCol = float4(5,5,5,0);           // 鏡面反射色
0101:     
0102:     return In.Color + SpecCol * power;
0103: }

■縮小バッファへのコピー

次に行なう操作は、縮小バッファへのコピーです。
16ボックスサンプリングを使って、4x4のテクセルを1つのピクセルに読みこきます。
縮小バッファのサイズも、もとの情報を漏らさない目いっぱいの大きさとして、 元のサイズの1/4のサイズのテクスチャにレンダリングしました。
縮小バッファへのコピーでは、頂点シェーダは使わずに、ピクセルシェーダだけで全て処理しました。
これは、面倒くさかったからだけで、入力頂点を8つめいっぱい使って、 ピクセルシェーダの負荷を軽くすると、少し高速化されるでしょう。
また、サンプリングした後に、色の指数をとって、明るい部分はより大きく、 暗い部分は色が小さくなるように変換してから出力します。
これで、白色とは違う本当に色の強い部分が抽出されます。

hlsl.fx
0108: float4 PS_Reduction(VS_OUTPUT In) : COLOR
0109: {   
0110:     float4 Out;
0111:     
0112:     // 16ボックスサンプリング
0113:     float2 t01 = float2( 0.0/512.0, 0.0/512.0);
0114:     float2 t02 = float2( 0.0/512.0, 1.0/512.0);
0115:     float2 t03 = float2( 0.0/512.0,-1.0/512.0);
0116:     float2 t10 = float2(-1.0/512.0,-2.0/512.0);
0117:     float2 t11 = float2(-1.0/512.0, 0.0/512.0);
0118:     float2 t12 = float2(-1.0/512.0, 1.0/512.0);
0119:     float2 t13 = float2(-1.0/512.0,-1.0/512.0);
0120:     float2 t20 = float2( 1.0/512.0,-2.0/512.0);
0121:     float2 t21 = float2( 1.0/512.0, 0.0/512.0);
0122:     float2 t22 = float2( 1.0/512.0, 1.0/512.0);
0123:     float2 t23 = float2( 1.0/512.0,-1.0/512.0);
0124:     float2 t30 = float2(-2.0/512.0,-2.0/512.0);
0125:     float2 t31 = float2(-2.0/512.0, 0.0/512.0);
0126:     float2 t32 = float2(-2.0/512.0, 1.0/512.0);
0127:     float2 t33 = float2(-2.0/512.0,-1.0/512.0);
0128: 
0129:     Out  =(tex2D( SrcSamp, In.Tex0 )   
0130:          + tex2D( SrcSamp, In.Tex0+t01 )
0131:          + tex2D( SrcSamp, In.Tex0+t02 )
0132:          + tex2D( SrcSamp, In.Tex0+t03 )
0133:          + tex2D( SrcSamp, In.Tex0+t10 )
0134:          + tex2D( SrcSamp, In.Tex0+t11 )
0135:          + tex2D( SrcSamp, In.Tex0+t12 )
0136:          + tex2D( SrcSamp, In.Tex0+t13 )
0137:          + tex2D( SrcSamp, In.Tex0+t20 )
0138:          + tex2D( SrcSamp, In.Tex0+t21 )
0139:          + tex2D( SrcSamp, In.Tex0+t22 )
0140:          + tex2D( SrcSamp, In.Tex0+t23 )
0141:          + tex2D( SrcSamp, In.Tex0+t30 )
0142:          + tex2D( SrcSamp, In.Tex0+t31 )
0143:          + tex2D( SrcSamp, In.Tex0+t32 )
0144:          + tex2D( SrcSamp, In.Tex0+t33 ))/16;
0145:     
0146:     // 明るい部分を強く書き込む
0147:     Out = 0.02f*exp(Out);
0148:     
0149:     return Out;
0150: }

■ガウスフィルタ

次は、ガウスフィルタです。
このフィルタは、縦横2パスを使い、さらに大きくぼかすのと小さくぼかす2回のセットで描画します。
ガウスフィルタは、前にやったとおりに頂点シェーダで8つのテクスチャ座標を与えてピクセルシェーダで重みを付けて合成します。
今回は、浮動小数点バッファを使うので、双線型フィルタを効かすことができないので、 テクセルは1つのサンプリングで1つづつ、計16個のテクスチャをサンプリングします。
ちなみに、縦方向の頂点シェーダは次のようになります。

hlsl.fx
0154: VS_OUTPUT VS_pass1 (
0155:       float4 Pos    : POSITION,          // モデルの頂点
0156:       float4 Tex    : TEXCOORD0          // テクスチャ座標
0157: ){
0158:     VS_OUTPUT Out = (VS_OUTPUT)0;        // 出力データ
0159:     
0160:     // 位置座標
0161:     Out.Pos = Pos;
0162:     
0163:     Out.Tex0 = Tex + float2( -1.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0164:     Out.Tex1 = Tex + float2( -2.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0165:     Out.Tex2 = Tex + float2( -3.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0166:     Out.Tex3 = Tex + float2( -4.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0167:     Out.Tex4 = Tex + float2( -5.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0168:     Out.Tex5 = Tex + float2( -6.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0169:     Out.Tex6 = Tex + float2( -7.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0170:     Out.Tex7 = Tex + float2( -8.0f/MAP_WIDTH, 0.0f/MAP_HEIGHT );
0171:     
0172:     return Out;
0173: }

ピクセルシェーダでは、前と同じプログラムで重みを付けて足し合わせます。
ところが、一つ大きな違いがあって、weight[i] はfloat4の4次元ベクトルです(前はfloatのスカラーでした)。
今回は、RGBの各成分ごとに広げる量を変えたかったので、 重み weight[i] をベクトルにして、それぞれの色で円の広がる半径を調整しました。

hlsl.fx
0175: float4 PS_pass1(VS_OUTPUT In) : COLOR
0176: {   
0177:     float4 Color;
0178:     
0179:     Color  = weight[0] * (tex2D( SrcSamp, In.Tex0 )
0180:                         + tex2D( SrcSamp, In.Tex7 + offsetX ));
0181:     Color += weight[1] * (tex2D( SrcSamp, In.Tex1 )
0182:                         + tex2D( SrcSamp, In.Tex6 + offsetX ));
0183:     Color += weight[2] * (tex2D( SrcSamp, In.Tex2 )
0184:                         + tex2D( SrcSamp, In.Tex5 + offsetX ));
0185:     Color += weight[3] * (tex2D( SrcSamp, In.Tex3 )
0186:                         + tex2D( SrcSamp, In.Tex4 + offsetX ));
0187:     Color += weight[4] * (tex2D( SrcSamp, In.Tex4 )
0188:                         + tex2D( SrcSamp, In.Tex3 + offsetX ));
0189:     Color += weight[5] * (tex2D( SrcSamp, In.Tex5 )
0190:                         + tex2D( SrcSamp, In.Tex2 + offsetX ));
0191:     Color += weight[6] * (tex2D( SrcSamp, In.Tex6 )
0192:                         + tex2D( SrcSamp, In.Tex1 + offsetX ));
0193:     Color += weight[7] * (tex2D( SrcSamp, In.Tex7 )
0194:                         + tex2D( SrcSamp, In.Tex0 + offsetX ));
0195:     
0196:     return Color;
0197: }

重みを求める計算は次のようになります。
重みは m_tbl[j][i]の4次元ベクトルに記録しますが、 それぞれ適当な大きさ size[n][m] を分散として、成分ごとに規格化などの調整をします。
size[][] の上の段と下の段の違いは、加算の円と減算の円の違いで、 大きく広がる上の段のガウス分布から、より小さい下の段のガウス分布の分散の色を最後に引きます。
ちなみに、w成分に1をいれたのは、おまけで、たいした意味はありません。

main.cpp
0204: VOID CMyD3DApplication::UpdateWeight( )
0205: {
0206:     DWORD i, j;
0207:     FLOAT size[2][3] = {
0208:                         // 赤     緑     青
0209:                         { 30.0f, 17.5f, 9.8f},// 外側
0210:                         { 20.0f, 11.2f, 7.5f},// 内側
0211:                     };
0212:     
0213:     for(j=0;j<2;j++){
0214:         FLOAT total[3]={0,0,0};
0215:         for( i=0; i<WEIGHT_MUN; i++ ){
0216:             FLOAT pos = 1.0f+2.0f*(FLOAT)i;
0217:             // ガウス重みを計算する
0218:             m_tbl[j][i].x = expf(-0.5f*(FLOAT)(pos*pos)/size[j][0]);
0219:             m_tbl[j][i].y = expf(-0.5f*(FLOAT)(pos*pos)/size[j][1]);
0220:             m_tbl[j][i].z = expf(-0.5f*(FLOAT)(pos*pos)/size[j][2]);
0221:             // 規格化のための重みの総和を計算する
0222:             total[0] += 2.0f*m_tbl[j][i].x;
0223:             total[1] += 2.0f*m_tbl[j][i].y;
0224:             total[2] += 2.0f*m_tbl[j][i].z;
0225: 
0226:             m_tbl[j][i].w = 1;
0227:         }
0228:         // 規格化
0229:         for( i=0; i<WEIGHT_MUN; i++ ){
0230:             m_tbl[j][i].x /= total[0];
0231:             m_tbl[j][i].y /= total[1];
0232:             m_tbl[j][i].z /= total[2];
0233:         }
0234:     }
0235: }

■最後の合成

さて、いよいよ最後のパスです。
最後は、大きなぼかしたと小さなぼかしの差分を取って、円を作ります。
ただし、そのままでは縮小バッファのを拡大した四角形の形が見えてしまうので、 中心のテクセルと上下左右のテクセルの5点をサンプリングして平均化します。
また、リングの強さの調整もここでできるので、見た目で調整(今回は10倍です)して、出力します。
出力した結果は、フレームバッファに加算合成で合成します。

hlsl.fx
0248: float4 PS_pass3(VS_OUTPUT In) : COLOR
0249: {   
0250:     float4 Color;
0251:     
0252:     float2 t0 = float2( 0.5/MAP_WIDTH, 0.0/MAP_HEIGHT);// 右
0253:     float2 t1 = float2(-0.5/MAP_WIDTH, 0.0/MAP_HEIGHT);// 左
0254:     float2 t2 = float2( 0.0/MAP_WIDTH, 0.5/MAP_HEIGHT);// 下
0255:     float2 t3 = float2( 0.0/MAP_WIDTH,-0.5/MAP_HEIGHT);// 上
0256:     
0257:     // 大きくぼかした画像と小さくぼかした画像の差を取る
0258:     Color  = tex2D( SrcSamp, In.Tex0    ) - tex2D( SrcSamp2, In.Tex1    );
0259:     Color += tex2D( SrcSamp, In.Tex0+t0 ) - tex2D( SrcSamp2, In.Tex1+t0 );
0260:     Color += tex2D( SrcSamp, In.Tex0+t1 ) - tex2D( SrcSamp2, In.Tex1+t1 );
0261:     Color += tex2D( SrcSamp, In.Tex0+t2 ) - tex2D( SrcSamp2, In.Tex1+t2 );
0262:     Color += tex2D( SrcSamp, In.Tex0+t3 ) - tex2D( SrcSamp2, In.Tex1+t3 );
0263:     
0264:     return 10.0f*Color;
0265: }

■最後に

グラデーションをつけると綺麗ですね。
浮動小数点バッファを使うと、双線型フィルタが使えないなどいろいろとできないことが増えるので、 一概に浮動小数点バッファを使うほうが綺麗ではないことを今回痛感しました。
しかし、masaさんのはきれいだなぁ~





もどる

imagire@gmail.com