レイトレース:薄いレンズのカメラ


~ Ray Tracing : Thin-lens cameras ~







■はじめに

Shirley の Realistic Ray Tracing を買ってみたのですが、その中で被写界深度を実現するための薄いレンズのモデルを紹介していました。 今回は、その方法を現在のプログラムに実装しようと思います。

上の画像の左側が今までのレンズを考慮しない(ピンホールカメラによる)画像で、 右側が今回の結果です。ピントを画面の奥のほうに合わせているので手前側がぼけています。現実にはありえないパラメータでレンダリングしたので、非常にぼけぼけです。

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

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

render.cppレイトレ用の描画関数群
render.hレイトレ用の描画関数群
prim.cppオブジェクトのデータのための関数
prim.hオブジェクトのデータのための関数
mainDlg.cppダイアログを管理するクラスのメソッドが書かれたファイル
mainDlg.hダイアログを管理するクラスのヘッダ
CBitmap.cppBMP保存用の関数
CBitmap.hBMP保存用の関数
math/MyTypes.h環境の依存性をなくすためのオレ用型定義
math/CRand.hMTによる乱数

あと、実行ファイル、リソースファイル、プロジェクトファイルが入っています。

■何やってるの?

カメラのカタログを見ると、焦点距離fとかF値とかのわけのわからない名前が列挙されています。 これらは、大体が次のようなレンズのモデルを仮定した値になっています。

物体とフィルムの間にレンズが置かれていて、ピントの合う場所からでた光はレンズの違う位置に入射したとしても フィルムの同じ位置を感光します。
このモデルでは、ピントが合う距離Lとフィルムが置かれた距離Vに関して、「光学定理」と呼ばれる1つの関係式が成り立ちます。

1   1   1
- + - = -
L   V   f

ここで、fが焦点距離と呼ばれる定数で、レンズの性能で決まります。
この関係式から、レンズの位置をフィルムからどれだけ離したかによってどの位置にピントが合うのか決定されます。
今回の場合には、ピントの合う位置が決められたときに、レンズをフィルムからどの程度離せばよいのかが大事になってきます。 その値は、上の関係式を逆に解いて次のように求められます。

      L f
V = -------
     L - f

また、カメラを制御するパラメータにF値があります。 これは、レンズの有効な直径2Rと焦点距離の比でこの値が小さなほど入射する光の量が増えて映像が明るくなります。

     f
F = --
    2R

F値はレンズ固有の性能に加えて絞りの大きさによって値が変わります。
上の式を逆に解けば、レンズの有効な半径は、焦点距離とF値で次のように書けます。

     f
R = --
    2F

さて、以上でカメラの基本は終了です。 では、これをいかにレイトレに乗せるのかが、これからの話です。
モンテカルロ法によるレイトレでは、パスごとにレイがフィルムに飛び込んでくる方向を変えることができます。 ただし、全てのレイはレンズを通過してきます。
ということで、各レイごとにレンズのどこを通過するかをランダムに与えて、その寄与を計算します。
レイが通過するレンズ上の点をSとすると、Sはレンズの中心Cと視線方向以外のカメラ座標系での基底ベクトルu,vを用いて次のように書けます。

S = C + r cosφ v + r sinφ u

ここで、r(0<=r

さて、フィルムからでてSを通った光はどこへ向かって飛べばいいのでしょうか?
ここで、光学定理をおもいだすと、レンズを通る全てのレイは、ピントの合う距離で同じ位置Pを通ります。ということで、ピントの合う点Pを求めて、この点へ向かう方向をレイの進む方向とします。
Pを求めましょう。フィルムの点Xから出てレンズの中心Cを通る光はピントの合う位置Pへ一直線に向かいます。フィルムの点Xからレンズの中心Cへの向きをeとすると、Pは、次の式で求められます(図を見てください)。

         L             L
P = C + ---- e = C + ----- e
        cosθ        (e.l)

レンズの中心までの向きは、ベクトルを正規化する関数normalizeを用いて次のように書けます。

e = normalize( C - X )

フィルム上の点は、フィルムのサイズを(w,h)、ピクセルの解像度を(W,H)として、求めようとするピクセルの位置を(x,y)とすれば、次の式で計算できます。

                x       y
X = C - V l + w - u + h - v
                W       H

これで、全ての値が与えられた量から求められるようになります。

ちなみに、Sからでたレイが進む方向rは、次の式で求められます。

r = normalize( P - S )

■レイの設定

さて、具体的な実装方法ですが、カメラのクラスを変更しました。
視野角やアスペクト比といったおなじみのパラメータは影を潜めて、 焦点距離やF値、フィルムのサイズといったものに変わっています。
2次的なパラメータとして、レンズの半径やレンズからフィルムまでの距離というものも(パラメータが決定し次第計算して)保持します。
また、ビュー行列もそれらの基底ベクトル u, v, lに変更しました。

0023: class CCamera
0024: {
0025: private:
0026:     D3DXVECTOR3 m_vFrom;
0027:     D3DXVECTOR3 m_vLookat;
0028:     D3DXVECTOR3 m_vUp;
0029: 
0030:     float   m_fFilmSize[2];  // フィルムの実寸0.036*0.024[m*m]0.025*0.025(Cornell Box)
0031:     float   m_fFocalLength;  // 焦点距離 0.035(Cornell Box )
0032:     float   m_fFNumber;      // F値
0033:     float   m_fFocusDistance;// ピントが合う位置までの距離
0034: 
0035:     float   m_fLensRadius;   // レンズの半径(f/2F)
0036:     float   m_fFilmDistance; // フィルムまでの距離 Lf/(L-f)
0037: 
0038:     D3DXVECTOR3 u, v, l;
0052: };

実際にレイを計算する関数は、かなりべたに実装しました。
最初にレンズの何所から飛ばすか決定するための乱数を作って、その位置がワールド座標系でどこになるのか計算します。
次に、ピクセルの位置をフィルムの大きさ等からワールド座標に変換して、レンズがあたるべき位置を算出し、向きを決定します。

0491: void CCamera::GetRay( D3DXVECTOR3 *o, D3DXVECTOR3 *dir, float x, float y)
0492: {
0493:     D3DXVECTOR3 ds, e;
0494:     D3DXVECTOR3 tmp, tmp1;
0495:     float z1 = frand();
0496:     float z2 = frand();
0497: 
0498:     float r = m_fLensRadius * sqrtf(z1);
0499:     float theta = 2.0f * D3DX_PI * z2;
0500:     
0501:     // レイを飛ばす位置
0502:     D3DXVec3Scale( &tmp, &v, cosf(theta) );
0503:     D3DXVec3Scale( &ds,  &u, cosf(theta) );
0504:     D3DXVec3Add( &ds, &ds, &tmp );
0505:     D3DXVec3Scale( &ds, &ds, r );
0506:     D3DXVec3Add( o, &ds, &m_vFrom );
0507: 
0508:     // レンズの中心からピントの当たる位置へのベクトル
0509:     D3DXVec3Scale( &e,    &l,   m_fFilmDistance );
0510:     D3DXVec3Scale( &tmp,  &v,   m_fFilmSize[0] * (x-0.5f) );
0511:     D3DXVec3Scale( &tmp1, &u, - m_fFilmSize[1] * (y-0.5f) );
0512:     D3DXVec3Add( &e, &e, &tmp );
0513:     D3DXVec3Add( &e, &e, &tmp1 );
0514:     D3DXVec3Normalize( &e, &e );
0515: 
0516:     D3DXVec3Scale( dir, &e, m_fFocusDistance / D3DXVec3Dot( &e, &l ) );
0517:     D3DXVec3Subtract( dir, dir, &ds );
0518:     D3DXVec3Normalize( dir, dir );
0519: }

■マルチスレッド化

Hyper-Threading とかマルチCPUとかが流行の今日この頃、 普通にプログラムを組んだだけでは、CPUの性能を使い切ることはできません。
レイトレースは、各ピクセルごとに計算するので、マルチスレッド化しやすいアプリケーションということもあるので、 今回マルチスレッド化してみました。

マルチスレッド化するには、_beginthreadex とかを使ってスレッドを作ります。
気をつけなくてはならないのは、別のスレッドで同じメモリを書き込まないことです。 グローバル変数を用意して、各スレッドがその変数に書き込むようにするとどのスレッドが書き込むか 不定になるので、そのような状況は避けなくてはなりません。
また、各スレッドが起動、終了するタイミングもばらばらなので、 スレッドの順番に依存するプログラムを組むときには注意が必要になります。
また、変数とかの寿命で、うかつにauto変数とかを使ってスレッドを呼び出すと スレッドを呼び出した直後に変数が無効になり、呼ばれたスレッドでは値が使えないことがあります。

さて、今回は、レンダリングする画面を BLOCK_X x BLOCK_Y(2x2=4) のブロックに分けて、 それぞれを独自スレッドとして計算させます。
各スレッドの違いは描画する画面の位置なので、描画範囲を引数としてスレッドを生成します。
描画範囲は、普通に四角形の構造体 rect に範囲を入れておきます。

0624:     static rect r[BLOCK_Y][BLOCK_X];
0625:     HANDLE  hThread[BLOCK_Y][BLOCK_X];
0626:     unsigned int  dummy;
0627:     
0628:     for( s32 i = 0; i < BLOCK_Y; i++ )
0629:     {
0630:         for( s32 j = 0; j < BLOCK_X; j++ )
0631:         {
0632:             r[i][j].left   = (j+0)*RENDER_WIDTH /BLOCK_X;
0633:             r[i][j].right  = (j+1)*RENDER_WIDTH /BLOCK_X;
0634:             r[i][j].top    = (i+0)*RENDER_HEIGHT/BLOCK_Y;
0635:             r[i][j].bottom = (i+1)*RENDER_HEIGHT/BLOCK_Y;
0636:         }
0637:     }

スレッドは、呼び出す関数 RayTrace や、引数 r[i][j] を指定して_beginthreadex を呼び出せばよいです。 失敗したときに 0 が返ってくるようなので、そのときには後から再挑戦するように bFinished[][] という変数を用意しておいて 終わったかどうかを覚えて置くようにしました (ところで、どのくらい失敗するのでしょうねぇ。こんな大げさに対応しなくてもいいのかな?)。
なお、Windows では、_beginthreadex の返り値をハンドルにでき、 WaitForSingleObject でスレッドが終了したかどうかも判定できるようなので、 これを利用して、一通りスレッドを作り終えた後に、作成した全てのスレッドが終了するのを待っています。

0639:     bool bFinished[BLOCK_Y][BLOCK_X]; // 終わった?
0640:     
0641:     // 終了フラグ
0642:     for( s32 i = 0; i < BLOCK_Y; i++ )
0643:         for( s32 j = 0; j < BLOCK_X; j++ )
0644:             bFinished[i][j] = false;
0645:     
0646:     for( u32 n = BLOCK_X * BLOCK_Y; 0 < n; )
0647:     {
0648:         // スレッドを生成する
0649:         for( s32 i = 0; i < BLOCK_Y; i++ )
0650:         {
0651:             for( s32 j = 0; j < BLOCK_X; j++ )
0652:             {
0653:                 if( !bFinished[i][j] )
0654:                 {
0655:                     hThread[i][j] = (HANDLE)_beginthreadex( NULL, 0, &RayTrace, (LPVOID)&r[i][j], 0, &dummy );
0656:                 }
0657:                 if(0==hThread[i][j])
0658:                 {
0659:                     hThread[i][j]=0;
0660:                 }
0661:             }
0662:         }
0663:         
0664:         // スレッドを待つ
0665:         for( s32 i = 0; i < BLOCK_Y; i++ )
0666:         {
0667:             for( s32 j = 0; j < BLOCK_X; j++ )
0668:             {
0669:                 if( !bFinished[i][j] && 0 != hThread[i][j] )
0670:                 {
0671:                     WaitForSingleObject( hThread[i][j], INFINITE );  // 受信スレッド終了待ち
0672:                     CloseHandle( hThread[i][j] );                    // ハンドルを閉じる
0673:                     bFinished[i][j] = true;
0674:                     n--;
0675:                 }
0676:             }
0677:         }
0678:     }

呼び出されるスレッドの関数 RayTrace は、次のようになります。
与えられた矩形の中を走査してレイを飛ばしまくり、結果をグローバルなバッファ s_total に格納します。
GetColor は、各ピクセルの位置からレイを飛ばして、最終的な色の結果を得る関数です。

0593: unsigned int WINAPI RayTrace ( LPVOID pParam )
0594: {
0595:     rect *pRect = (rect *)pParam;
0596:     D3DXVECTOR3 col;
0597:     
0598: for(int i=0;i<10;i++)
0599:     for( s32 y = pRect->top; y < pRect->bottom; y++ )
0600:     {
0601:         for( s32 x = pRect->left; x < pRect->right; x++ )
0602:         {
0603:             GetColor(&col, ((float)x+0.5f)/(float)RENDER_WIDTH
0604:                          , ((float)y+0.5f)/(float)RENDER_HEIGHT);
0605:             int no = 4*(y*RENDER_WIDTH+x);
0606:             s_total[no+0]+=col.x;// R
0607:             s_total[no+1]+=col.y;// G
0608:             s_total[no+2]+=col.z;// B
0609:         }
0610:     }
0611: 
0612:     return 0;
0613: }

最初、各ピクセル1回のレイを飛ばすごとにスレッドを生成していたのですが、 オーバーヘッドが大きかったので、各ピクセルごとに10回のレイを飛ばすようなスレッドを生成しています。

■最後に

ぶっちゃけ、Shirley の本を読んで、プログラムを書き直したかっただけなのですが、 その道のりは遠そうです…

あと、Fナンバーを使ってカメラを設定するのは、本物のカメラを使う人にはよいと思うのですが、 自分的には調整しづらいと感じました。
パラメータの吟味はしないといけないでしょうねぇ





もどる

imagire@gmail.com