絞り羽根フィルタ


~ Iris filter ~






■はじめに

今回は、カメラの絞りの形がついたフィルターを表現します。
右がフィルタリングした画像ですが、六角形の形をしているのがうっすらとわかると思います。
とりあえず、GeForceFX 月間?なので、GeForceFX ならではの強引な力技で勝負します(これがリアルタイムかは知りません)。

下のファイルがソースと実行ファイルです。

まぁ、いつものように適当にファイルが入っています。
今回は、中央値フィルタのシェーダを差し替えただけです。

fp.cgフラグメントプログラム。
main.cppメインループ。
CBitmap.hビットマップファイルの読み込み。
CBitmap.cppビットマップファイルの読み込み。
hinode.bmp

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

■絞り羽根フィルタとは

そもそも、カメラの絞り羽根とは、眼の虹彩(iris)を模したものです。 虹彩とは、角膜と水晶体の間にある膜で、目の色の付いた部分です。虹彩は暗いところでは広がり、明るいと所では縮まります。人間は、虹彩の伸び縮みによって入ってくる光の量を調節します。
また、虹彩の模様は、一人一人違うので、個人認証にも使われます。
カメラの絞り羽根は、虹彩と同じようにレンズの内径を調節する働きをします。写真を撮る時は、絞りを作る複数の薄い金属板の羽根が組み合わされた「絞り羽根」の絞り具合を光の量に応じて変えることで、フィルムに届く光量を調整します。
絞りの役割は、光量調節だけではなく、「ピントの深さ」にも関係しています。 絞りを開けることによって被写界深度は浅く、また、絞り込むことによって深くなります。例えば、背景をぼかして、対象物だけを浮き出させた写真は、絞りを大きく開くことによって撮影され、近景から遠景まですべてにピントが合っているものは絞りをきつくして撮影することによって得られます。
ということで、絞りの形は、カメラで撮影したときだけについて、人の目で見たときには現れません。ゲーム等で使用するときには、その点について納得してから導入しましょう。

さて、絞りの数学に関しては、「ボケ」た背景で包み込め@hirax.netが詳しいので、そちらを見ていただくことがお勧めですが、ここでも少し紹介しましょう。

ピンぼけとはどうしておきるのでしょうか。
それは、フィルムの1点に光が集まらないからです。 レンズで絞りきれなくてフィルムの周りの部分を感光してしまったのがピンぼけになります。
さて、カメラの場合は、絞り羽根を通ってレンズに光が入射するので、絞り羽根の形で光が入ってきます。
すなわち、光がぼけたときにはその形のままフィルムに感光されます。これが絞り羽根フィルタとして写される形の正体です。
したがって、各ピクセルの値を絞り羽根の形状でぼけた大きさで引き伸ばせは、絞り羽根フィルタの完成です。

但し、気をつける点が1つあります。それは、光のエネルギーと写真に写る色が違うということです。
2倍の明るさを再現するためには、光のエネルギー(CG業界で俗に考えられる「フォトン」の数)は2乗の個数が必要になります。つまり、エネルギーと色は指数の関係で結ばれています。
この事実はよく知られていて、各フィルムに対して、露光量とフィルムの濃度(光を受ければ受けるほど薄くなる。この薄さは直接写真の濃さになる)を片対数対数グラフでプロットすると、(写真としてよく使われる部分では)グラフは直線的になります。すなわち、それぞれの関係は指数的に結ばれています。
実は、指数変換すると、明るい部分がより強調されるので、絞り羽根の形状がよりくっきり出るというメリットがあります。
この方法は、SIGGRAPH 97におけるPaul Debevecの論文Recovering High Dynamic Range Radiance Maps from Photographsによって、CG業界にも広く知られることとなりました。

■フラグメントプログラム

シェーダプログラムですが、今回は、力技です。
下の図のように、中心の点から53個の近傍のテクセルを畳み込み積分としてサンプリングしました。 これらは、画像処理の方法として古くから行われている方法です。

   ■
  ■■■
 ■■■■■
■■■■■■■
■■■■■■■
■■■○■■■
■■■■■■■
■■■■■■■
 ■■■■■
  ■■■
   ■

やっている方法は本当に簡単です。
ずらしたテクスチャ座標を定数として持っておいて、 テクスチャ座標を動かしつつサンプリングし、 「exp」命令を使って、指数変換した後に足しこみます。
これを53回やって、最後に色の強さを調整するためにサンプリング数の53で割った後に、 「log」命令で対数変換をして、色情報に戻します。

fp.cg
0012: fragout main(vert2frag I
0013:             , uniform texobj2D SrcMap : texunit0
0014:     )
0015: {
0016:     fragout O;
0017:     
0018:     float3 col = {0,0,0};
0019:     
0020:     float2 shift00 = { ( 0.0f+0.5f)/256.0f, ( 0.0f+0.5f)/256.0f};
0021:     float2 shift01 = { ( 1.0f+0.5f)/256.0f, ( 0.0f+0.5f)/256.0f};
0022:     float2 shift02 = { ( 0.0f+0.5f)/256.0f, ( 1.0f+0.5f)/256.0f};
0023:     float2 shift03 = { (-1.0f+0.5f)/256.0f, ( 0.0f+0.5f)/256.0f};
0024:     float2 shift04 = { ( 0.0f+0.5f)/256.0f, (-1.0f+0.5f)/256.0f};
0025:     float2 shift05 = { ( 1.0f+0.5f)/256.0f, ( 1.0f+0.5f)/256.0f};
0026:     float2 shift06 = { (-1.0f+0.5f)/256.0f, ( 1.0f+0.5f)/256.0f};
0027:     float2 shift07 = { ( 1.0f+0.5f)/256.0f, (-1.0f+0.5f)/256.0f};
0028:     float2 shift08 = { (-1.0f+0.5f)/256.0f, (-1.0f+0.5f)/256.0f};
0029:     float2 shift09 = { ( 0.0f+0.5f)/256.0f, ( 2.0f+0.5f)/256.0f};
0030:     float2 shift10 = { ( 0.0f+0.5f)/256.0f, (-2.0f+0.5f)/256.0f};
0031:     float2 shift11 = { ( 0.0f+0.5f)/256.0f, ( 3.0f+0.5f)/256.0f};
0032:     float2 shift12 = { ( 0.0f+0.5f)/256.0f, (-3.0f+0.5f)/256.0f};
0033:     float2 shift13 = { ( 1.0f+0.5f)/256.0f, ( 2.0f+0.5f)/256.0f};
0034:     float2 shift14 = { ( 1.0f+0.5f)/256.0f, (-2.0f+0.5f)/256.0f};
0035:     float2 shift15 = { (-1.0f+0.5f)/256.0f, ( 2.0f+0.5f)/256.0f};
0036:     float2 shift16 = { (-1.0f+0.5f)/256.0f, (-2.0f+0.5f)/256.0f};
0037:     float2 shift17 = { ( 2.0f+0.5f)/256.0f, (-2.0f+0.5f)/256.0f};
0038:     float2 shift18 = { ( 2.0f+0.5f)/256.0f, (-1.0f+0.5f)/256.0f};
0039:     float2 shift19 = { ( 2.0f+0.5f)/256.0f, ( 0.0f+0.5f)/256.0f};
0040:     float2 shift20 = { ( 2.0f+0.5f)/256.0f, ( 1.0f+0.5f)/256.0f};
0041:     float2 shift21 = { ( 2.0f+0.5f)/256.0f, ( 2.0f+0.5f)/256.0f};
0042:     float2 shift22 = { (-2.0f+0.5f)/256.0f, (-2.0f+0.5f)/256.0f};
0043:     float2 shift23 = { (-2.0f+0.5f)/256.0f, (-1.0f+0.5f)/256.0f};
0044:     float2 shift24 = { (-2.0f+0.5f)/256.0f, ( 0.0f+0.5f)/256.0f};
0045:     float2 shift25 = { (-2.0f+0.5f)/256.0f, ( 1.0f+0.5f)/256.0f};
0046:     float2 shift26 = { (-2.0f+0.5f)/256.0f, ( 2.0f+0.5f)/256.0f};
0047:     float2 shift27 = { ( 0.0f+0.5f)/256.0f, ( 4.0f+0.5f)/256.0f};
0048:     float2 shift28 = { ( 0.0f+0.5f)/256.0f, (-4.0f+0.5f)/256.0f};
0049:     float2 shift29 = { ( 1.0f+0.5f)/256.0f, ( 3.0f+0.5f)/256.0f};
0050:     float2 shift30 = { ( 1.0f+0.5f)/256.0f, (-3.0f+0.5f)/256.0f};
0051:     float2 shift31 = { (-1.0f+0.5f)/256.0f, ( 3.0f+0.5f)/256.0f};
0052:     float2 shift32 = { (-1.0f+0.5f)/256.0f, (-3.0f+0.5f)/256.0f};
0053:     float2 shift33 = { ( 3.0f+0.5f)/256.0f, (-2.0f+0.5f)/256.0f};
0054:     float2 shift34 = { ( 3.0f+0.5f)/256.0f, (-1.0f+0.5f)/256.0f};
0055:     float2 shift35 = { ( 3.0f+0.5f)/256.0f, ( 0.0f+0.5f)/256.0f};
0056:     float2 shift36 = { ( 3.0f+0.5f)/256.0f, ( 1.0f+0.5f)/256.0f};
0057:     float2 shift37 = { ( 3.0f+0.5f)/256.0f, ( 2.0f+0.5f)/256.0f};
0058:     float2 shift38 = { (-3.0f+0.5f)/256.0f, (-2.0f+0.5f)/256.0f};
0059:     float2 shift39 = { (-3.0f+0.5f)/256.0f, (-1.0f+0.5f)/256.0f};
0060:     float2 shift40 = { (-3.0f+0.5f)/256.0f, ( 0.0f+0.5f)/256.0f};
0061:     float2 shift41 = { (-3.0f+0.5f)/256.0f, ( 1.0f+0.5f)/256.0f};
0062:     float2 shift42 = { (-3.0f+0.5f)/256.0f, ( 2.0f+0.5f)/256.0f};
0063:     float2 shift43 = { ( 2.0f+0.5f)/256.0f, ( 3.0f+0.5f)/256.0f};
0064:     float2 shift44 = { ( 1.0f+0.5f)/256.0f, ( 4.0f+0.5f)/256.0f};
0065:     float2 shift45 = { ( 0.0f+0.5f)/256.0f, ( 5.0f+0.5f)/256.0f};
0066:     float2 shift46 = { (-1.0f+0.5f)/256.0f, ( 4.0f+0.5f)/256.0f};
0067:     float2 shift47 = { (-2.0f+0.5f)/256.0f, ( 3.0f+0.5f)/256.0f};
0068:     float2 shift48 = { ( 2.0f+0.5f)/256.0f, (-3.0f+0.5f)/256.0f};
0069:     float2 shift49 = { ( 1.0f+0.5f)/256.0f, (-4.0f+0.5f)/256.0f};
0070:     float2 shift50 = { ( 0.0f+0.5f)/256.0f, (-5.0f+0.5f)/256.0f};
0071:     float2 shift51 = { (-1.0f+0.5f)/256.0f, (-4.0f+0.5f)/256.0f};
0072:     float2 shift52 = { (-2.0f+0.5f)/256.0f, (-3.0f+0.5f)/256.0f};
0073: 
0074:     float3 tex00 = f3tex2D( SrcMap, I.tcoords.xy+shift00 );
0075:     col.x += exp(tex00.x);
0076:     col.y += exp(tex00.y);
0077:     col.z += exp(tex00.z);
0078:     
0079:     float3 tex01 = f3tex2D( SrcMap, I.tcoords.xy+shift01 );
0080:     col.x += exp(tex01.x);
0081:     col.y += exp(tex01.y);
0082:     col.z += exp(tex01.z);
0083:     
0084:     float3 tex02 = f3tex2D( SrcMap, I.tcoords.xy+shift02 );
0085:     col.x += exp(tex02.x);
0086:     col.y += exp(tex02.y);
0087:     col.z += exp(tex02.z);
0088:     

中略

0329:     float3 tex51 = f3tex2D( SrcMap, I.tcoords.xy+shift51 );
0330:     col.x += exp(tex51.x);
0331:     col.y += exp(tex51.y);
0332:     col.z += exp(tex51.z);
0333:     
0334:     float3 tex52 = f3tex2D( SrcMap, I.tcoords.xy+shift52 );
0335:     col.x += exp(tex52.x);
0336:     col.y += exp(tex52.y);
0337:     col.z += exp(tex52.z);
0338:     
0339:     O.col.x = log(col.x/53.0f);
0340:     O.col.y = log(col.y/53.0f);
0341:     O.col.z = log(col.z/53.0f);
0342:     
0343:     return O;
0344: } 

ピクセルシェーダのレジスタの値の範囲が-2~2かと思い込んでいたのですが、 (少なくともエミュレータでは)きちんと動いたので、floatはまともな演算が使えるようです(halfを使うとだめかも)。

■最後に

やっぱり、この程度のぼけの大きさでは目立ちませんね。
といってもぼけの半径の2乗でサンプリング数は多くなっていくので、考え物です。
しかもそんなに綺麗に出ていないので、まだまだ改良が必要ですね。

次は、被写界深度への対応か・・・
ちょっと大変そう。





もどる

imagire@gmail.com