レイトレース:影


~ Ray Tracing : Shadow ~







■はじめに

ただの陰影計算ではつまりません。 影をつけてみましょう。

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

まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
今回から、オブジェクトのためのクラスを別ファイル prim.h、prim.cpp に分けました。

render.cppレイトレ用の描画関数群
render.hレイトレ用の描画関数群
prim.cppオブジェクトのデータのための関数
prim.hオブジェクトのデータのための関数
mainDlg.cppダイアログを管理するクラスのメソッドが書かれたファイル
mainDlg.hダイアログを管理するクラスのヘッダ

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

■何やってるの?

今回の方法ですが、レイがぶつかった場所から再びレイを飛ばします。
但し、飛ばす方向は、光源の方向です。 光源の方向にレイを飛ばして、光源と飛ばした場所の間に物体があれば光は届かないので、その部分を影にします。

実際のプログラムは、次のようになります。
物体との交差が検出されたら、光源までの距離を計算して、その距離までの範囲で 交差点から出たレイが他の物体と交差しないか検出します。
他の物体の影に隠れることが分かったら、LN を0にして、拡散項の成分を落とします。
なお、LN<=0 となる、面が光源の方を向いていないときには、計算は無駄なので影の計算はしないようにしています。
今回は、鏡面反射項は何も手を加えていないのですが、ここも反射ベクトルなどを考えて何かしたほうがいいんでしょうねぇ

render.cpp
0606: D3DXVECTOR3 *GetColor(D3DXVECTOR3 *dest, D3DXVECTOR4 *x, D3DXVECTOR4 *v)
0607: {
0608:     D3DXVECTOR4 light_pos = D3DXVECTOR4(278.f, 548.8f, 279.5f,1);
0609:     
0610:     float t = CPrimitive::INFINTY_DIST;
0611:     CPrimitive *pObj = NULL;
0612:     D3DXVECTOR4 p, ptmp;
0613:     D3DXVECTOR4 n, ntmp;
0614:     
0615:     t = pShpereS   ->IsAcross(t, &n, &p, &pObj, x, v);
0616:     t = pShpereT   ->IsAcross(t, &n, &p, &pObj, x, v);
0617:     t = pRoom      ->IsAcross(t, &n, &p, &pObj, x, v);
0618:     t = pBlockSmall->IsAcross(t, &n, &p, &pObj, x, v);
0619:     t = pBlockTall ->IsAcross(t, &n, &p, &pObj, x, v);
0620: 
0621:     if( pObj ){
0622:         // 当たった!
0623: 
0624:         D3DXVECTOR4 l = light_pos-p;
0625:         float L2 = D3DXVec3Length((D3DXVECTOR3 *)&l);
0626:         D3DXVec3Normalize((D3DXVECTOR3 *)&l, (D3DXVECTOR3 *)&l);
0627: 
0628:         D3DXVECTOR3 dir, H;
0629:         // 視線の計算
0630:         camera.GetFrom(&dir);
0631:         dir = dir - *(D3DXVECTOR3 *)&p;
0632:         D3DXVec3Normalize(&dir, &dir);
0633:         // ハーフベクトル
0634:         H = dir+*(D3DXVECTOR3 *)&l;
0635:         D3DXVec3Normalize((D3DXVECTOR3 *)&H, (D3DXVECTOR3 *)&H);
0636: 
0637:         float LN = D3DXVec3Dot((D3DXVECTOR3 *)&l, (D3DXVECTOR3 *)&n);
0638:         float HN = D3DXVec3Dot((D3DXVECTOR3 *)&H, (D3DXVECTOR3 *)&n);
0639:         if(HN<0) HN=0;
0640:         if(0<LN){
0641:             // 当たったところから、光源が見えるかを判定
0642:             // 見えなければ、暗くする
0643:             bool bShadow = FALSE;
0644:             if(!bShadow) bShadow = pShpereS   ->IsAcross(L2, &p, &l);
0645:             if(!bShadow) bShadow = pShpereT   ->IsAcross(L2, &p, &l);
0646:             if(!bShadow) bShadow = pRoom      ->IsAcross(L2, &p, &l);
0647:             if(!bShadow) bShadow = pBlockSmall->IsAcross(L2, &p, &l);
0648:             if(!bShadow) bShadow = pBlockTall ->IsAcross(L2, &p, &l);
0649:             if(bShadow) LN*=0.0f;// difuseだけなくす
0650:         }else{
0651:             LN=0;
0652:         }
0653:         
0654:         pObj->GetColor(dest, LN, HN);
0655:         
0656:         // 光源の色の反映
0657:         D3DXVECTOR3 light_color = D3DXVECTOR3(10.f,9.0f,5.0f);
0658:         dest->x *= light_color.x;
0659:         dest->y *= light_color.y;
0660:         dest->z *= light_color.z;
0661:         
0662:         // 光の強さの適当な補正
0663:         *dest *= min(1.5f, 500000.0f/(10000.0f+L2)); // 距離による補正
0664:         *dest *= min(1, l.y+0.1f);                  // 光の向きをcosθの関数にする
0665:     }else{
0666:         // 外れ
0667:         *dest = BG_COLOR;
0668:     }
0669: 
0670: 
0671:     return dest;
0672: }

■メッシュ

今回は、前回のプログラムを改良してたくさんの3角形を一度に扱うためのメッシュクラスを作りました。
メッシュクラスは、3角形等の数と、それらのポインタを格納する配列を持っています。
ここは、クラスのポインタを配列として持つのでなくて、データ配列へのポインタを持つようにするのも良いでしょう。
また、なんとなくですが、前回の CObject クラスは、CPrimitive クラスに名前を変えました。

prim.h
0103: class CMesh
0104: {
0105: public:
0106:     CMesh();
0107:     ~CMesh();
0108:     CPrimitive **m_ppPrim;
0109:     int         m_num;
0110: 
0111:     void  Init(OBJ_DATA *pData, int num);
0112:     void  Delete();
0113:     float IsAcross(float dist, D3DXVECTOR4 *n, D3DXVECTOR4 *p, CPrimitive **dest, const D3DXVECTOR4 *x, const D3DXVECTOR4 *v);
0114:     bool  IsAcross(float dist, const D3DXVECTOR4 *x, const D3DXVECTOR4 *v);
0115: };

メッシュクラスの各メソッドは、プリミティブの個数だけ初期化や交差判定の処理を回しています。
例えば、交差判定のプログラムは、次のようになっています。

prim.cpp
0219: bool CMesh::IsAcross(float dist, const D3DXVECTOR4 *x, const D3DXVECTOR4 *v)
0220: {
0221:     D3DXVECTOR4 ntmp, ptmp;
0222: 
0223:     for(int i = 0; i<m_num; i++){
0224:         float d;
0225:         switch(m_ppPrim[i]->m_type){
0226:         case OBJ_TYPE_SPHERE:
0227:             d = ((CSphere*)m_ppPrim[i])->IsAcross(&ntmp, &ptmp, x, v);
0228:             break;
0229:         case OBJ_TYPE_TRIANGLE:
0230:             d = ((CTriangle*)m_ppPrim[i])->IsAcross(&ntmp, &ptmp, x, v);
0231:             break;
0232:         }
0233:         if(0.001*dist<=d && d<0.99f*dist) return TRUE;
0234:     }
0235: 
0236:     return FALSE;
0237: }

■Windows っぽいプログラム

さて、今回からプログラムの実行時間が長くなってきました。
今までのように何も考えないで Render() 関数で処理をまわしっぱなしにしてしまうと、処理が終了するまでウィンドウの位置が動かせなかったりと、ちょいとしょぼいプログラムになってしまいます。
ここらへんで、処理付加を独り占めしない行儀のよいアプリケーションにしてみましょう。

手始めに、アプリケーションを変更して、描画関数を1ピクセルずつ処理する関数に変更します。

render.cpp
0581: int Render()
0582: {
0583:     if(STATE_IDLE == s_state) return 0;
0584:     
0585:     D3DXVECTOR3 col;
0586:     GetColor(&col, ((float)s_x+0.5f)/(float)RENDER_WIDTH
0587:                  , ((float)s_y+0.5f)/(float)RENDER_HEIGHT);
0588:     s_data[4*(s_y*RENDER_WIDTH+s_x)+0]=(char)(255.9*min(1,col.x));// R
0589:     s_data[4*(s_y*RENDER_WIDTH+s_x)+1]=(char)(255.9*min(1,col.y));// G
0590:     s_data[4*(s_y*RENDER_WIDTH+s_x)+2]=(char)(255.9*min(1,col.z));// B
0591:     
0592:     // 次のピクセルに移動
0593:     if(RENDER_WIDTH<=++s_x){
0594:         // 行を変える
0595:         s_x = 0;
0596:         if(RENDER_HEIGHT<=++s_y){
0597:             s_state = STATE_IDLE;// 終了
0598:             return 0;
0599:         }
0600:     }
0601: 
0602:     return -1;
0603: }

新しい描画関数では、(s_x, s_y)のピクセルに関してレイトレーシングしたら、 隣のピクセルに移動して、終了します。
一番右までピクセルが移動したら、次に下の段に移動します。
一番右下まで移動したら、本当にレイトレーシングは終了します。
描画中かどうかを判定するために、状態変数 s_state を用意しました。
今回は、s_state は、STATE_IDLE か STATE_RENDER の状態を取ります。
最初は、STATE_IDLEにいて、描画が始まるときにSTATE_RENDERに切り替わります。
描画が完全に終了したら、状態は再びSTATE_IDLEに戻って、待機します。
Render()関数は、STATE_IDLE の時に(処理をしていないことを意味する)0を返すことにします。

render.cpp
0057: static int s_x = 0;
0058: static int s_y = 0;
0059: static int s_state = 0;
0060: enum{
0061:     STATE_IDLE=0,
0062:     STATE_RENDER,
0063: };

Render() 関数が呼ばれる前の Begin() 関数の呼び出しで、初期状態やレイトレースするピクセルの初期位置を指定します。

render.cpp
0564: void Begin()
0565: {
0566:     s_state = STATE_RENDER;
0567:     
0568:     s_x = 0;
0569:     s_y = 0;
0570: 
0571:     // フレームバッファの初期化
0572:     for(int j=0; j<RENDER_HEIGHT; j++){
0573:     for(int i=0; i<RENDER_WIDTH ; i++){
0574:         s_data[4*(j*RENDER_WIDTH+i)+0]=(char)255;// R
0575:         s_data[4*(j*RENDER_WIDTH+i)+1]=(char)(i*256/RENDER_WIDTH );// G
0576:         s_data[4*(j*RENDER_WIDTH+i)+2]=(char)(j*256/RENDER_HEIGHT);// B
0577:     }
0578:     }
0579: }

次に、ボタンを押したときの描画関数の呼び出しプログラムを変更しましょう。
ボタンを押したときに、最初にBegin()関数を呼び出して、後は Render() 関数の返り値が0になるまで Render() 関数を呼び続けます。

mainDlg.cpp
0208: void CMainDlg::OnButtonRender() 
0209: {
0210:     bQuit = FALSE;
0211: 
0212:     // TODO: この位置にコントロール通知ハンドラ用のコードを追加してください
0213:     for(Render::Begin(); Render::Render(); this->OnPaint()){
0214:         for(int i=0; i<1000; i++) Render::Render();// 1回ずつだととても遅かった
0215:         // メッセージの処理
0216:         PumpMessages();
0217:         // 終了判定
0218:         if(bQuit) return;
0219:     }
0220:     this->OnPaint();
0221: }

Render() 関数が帰ってきたところで画面の再描画関数 OnPaint() を呼ぶことによって、現在どのピクセルまで処理を終えているかが一目でわかるようにしています。
あと、ループの中で再び Render() 関数を1000回ほどまわしています。これは、Render() を終えるたびにいちいち再描画していたら非常に遅かったので、再描画の回数を減らすための処理です。 1000回というのは、付加を見て決めた適当な数なので、タイマーを使って描画回数を制御したほうがよりよくなると思います。

ほかにも、強制終了のフラグ bQuit を追加しました。
外部のイベントで bQuit を TRUE にすると、Render()を途中で終了して、 プログラムを強制的に中断できるようにしています。

あと、PumpMessages() という関数も追加しています。
これは、Render() 関数の合間に、たまったイベントを吐き出させるためのものです。
PumpMessages() を追加したことによって、描画中のウィンドウの移動やプログラムの終了が可能になります。
PumpMessages() は、次のようになります。
これは決まった構文ですから、そのままコピーしてお使いください。

mainDlg.cpp
0199: void PumpMessages()
0200: {
0201:     MSG msg;
0202:     while (::PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
0203:         ::TranslateMessage(&msg);
0204:         ::DispatchMessage (&msg);
0205:     }
0206: }

終了フラグは、OKボタンやキャンセルボタンを押したときにたつようにします。
それは、あらかじめ用意されているボタンを押したときの関数 CMainDlg::OnOK() や CMainDlg::OnCancel() にフラグを立てるコードを追加します。

mainDlg.cpp
0223: // ---------------------------------------------------------------------------
0224: // OK ボタンを押したとき
0225: void CMainDlg::OnOK() 
0226: {
0227:     bQuit = TRUE;   // 終了フラグを立てる
0228:     
0229:     CDialog::OnOK();
0230: }
0231: 
0232: // ---------------------------------------------------------------------------
0233: // キャンセル ボタンを押したとき
0234: void CMainDlg::OnCancel() 
0235: {
0236:     bQuit = TRUE;   // 終了フラグを立てる
0237:     
0238:     CDialog::OnCancel();
0239: }

実際には、これらのコードが PumpMessages から呼び出されてフラグがたつことになります。

ウィンドウの右上の×ボタンを押したときも終了するようにしましょう。
終了するためには、終了イベントのときに呼ばれる関数を追加します。
この関数の追加をするためには、MFCの流儀としてクラスウィザードを使います。
メニューの「表示」-「ClassWizard」を選択するか、ctrl+wでクラスウィザードをひらきます。
「メッセージマップ」の「オブジェクトID」をダイアログのクラスにして、「メッセージ」の「WM_CLOSE」をダブルクリックします。

そうすると、下の「メンバ関数」のところに「OnClose」のイベント関数が作られるので、その部分をダブルクリックします。

すると、ダイアログのCPPファイルに関数が自動的に作られるので、ここにコードを追加します。
(.NETでは、リソースのデザインをしているときのダイアログのプロパティの「メッセージ」から関数を追加することができます)
今回は、レンダリングを強制終了するためのフラグ bQuit を立てます。

mainDlg.cpp
0241: // ---------------------------------------------------------------------------
0242: // ウィンドウを閉じるとき
0243: void CMainDlg::OnClose() 
0244: {
0245:     bQuit = TRUE;   // 終了フラグを立てる
0246: 
0247:     CDialog::OnClose();
0248: }

■最後に

全然、レイトレっぽくありませんね。
次は、反射、屈折を取り入れてみましょう。





もどる

imagire@gmail.com