レイトレーシングを行うためには、物体と視線の交差判定ができなくては、話になりません。
ここでは、よくある物体として玉に関する交差判定を実装してみます。
今回のプログラムは、次のものです。
まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
render.cpp | レイトレ用の描画関数群 |
render.h | レイトレ用の描画関数群 |
mainDlg.cpp | ダイアログを管理するクラスのメソッドが書かれたファイル |
mainDlg.h | ダイアログを管理するクラスのヘッダ |
あと、実行ファイル、リソースファイル、プロジェクトファイルが入っています。
交差判定とは、何でしょうか?
レイトレーシングは、描画しようとするスクリーンから、
レイと呼ばれる視線を飛ばして、レイが物体と衝突したところで色を算出したり、
レイを反射させてさらにその先を追いかけて画面をレンダリングする手法です。
ここで、「レイが物体と衝突した」というのを検出するのが交差判定です。
交差判定は、基本的には3角ポリゴンとレイの判定ができれば不自由しないのですが、
ポリゴンをそのまま使うと計算量が莫大になってしまうので、
球や円柱のような簡単な物体の交差判定を使うことも、まま行われます。
ということで、簡単に見栄えのよい画像を作るために、最初に球とレイの交差判定を作ります。
さて、レイと球の交差判定を幾何学的に定式化してみましょう。
球は、中心の座標cと、半径rで表現することができます。
レイは、視点xと、視線vによって、視点から出るベクトルx+tvと書き下せます。
ここで、太字で書いてあるベクトルは、3次元のつもりで書きますが何次元でもかまいません。
2次元なら円、3次元なら球になります。
まぁ、3次元以外は使いませんが…
さて、交差判定は、このレイが球に衝突するかどうかを判定する作業ですが、 レイの方程式
X=x+tv
と、円の方程式
(X-c)(X-c)=r2
を連立して、交点を求める作業ともいえます。
これらの式を連立すると、レイの移動した量のパラメータtを求める計算になります。
連立した式は、
(x+tv-c)(x+tv-c)=r2
この式をtに関する式として変形すると、
|v|2t2 + 2v(x-c)t + |x-c|2-r2=0
になります。
この式は、解の公式を使えば簡単に解決して、
球と交差する時のレイに関するtの値は、
-v(x-c)±√D t = --------------- |v|2 D = {v(x-c)}2 - |v|2(|x-c|2-r2)
になります。
ここで、判別式Dは正でなくてはなりません。
判別式が負の時は解が虚数になるので、実際の世界では交点が観測されないことになります。
また、レイとして使えるパラメータtは、正でなくてはなりません。
解が負のレイは、視線の後ろに交点が存在することになるので、実際は交差しない解になります。
実際のプログラムでは、次のようになります。
このプログラムは、レイが球に衝突したら、黄色(RGB=(1, 0.9, 0.5))を返し、外れたらBG_COLORを返す関数です。
最初に判別式を計算して、判別式が負ならレイが外れたとみなして関数を終了します。
判別式が正の時には、解を計算して、どちらかの解が正ならば衝突したとみなします。
今回は、ベクトルのライブラリとして DirectX を使いました。デバイスの初期化などの面倒なことは全く必要とせずに使えるので手軽ないいやつです。
render.cpp 0112: D3DXVECTOR3 *GetColor(D3DXVECTOR3 *dest, D3DXVECTOR4 *x, D3DXVECTOR4 *v) 0113: { 0114: // 線分と球の判定 0115: D3DXVECTOR4 center = D3DXVECTOR4(0,1,0,1); // 中心位置 0116: FLOAT radius_sq = 1.0f*1.0f; 0117: 0118: D3DXVECTOR4 xc = (*x)-center;// 中心からの相対位置 0119: FLOAT xc2 = D3DXVec3Dot((D3DXVECTOR3 *)&xc, (D3DXVECTOR3 *)&xc); 0120: FLOAT vxc = D3DXVec3Dot((D3DXVECTOR3 *)v, (D3DXVECTOR3 *)&xc); 0121: FLOAT D = vxc*vxc-xc2+radius_sq;// 判別式 0122: 0123: *dest = D3DXVECTOR3(1,0.9,0.5); // 当たったときの色 0124: 0125: if(D<0) {*dest = BG_COLOR; return dest;} // 交点が存在しない外れた 0126: 0127: float tn = -vxc-sqrtf(D); 0128: float tp = -vxc+sqrtf(D); 0129: 0130: if(tn<0 && tp<0) {*dest = BG_COLOR; return dest;} 0131: 0132: return dest; 0133: }
球は、半径1で中心は(0,1,0)にしました。関数の最初に指定していますが、後々はデータとして分離していきます。
レイは、関数の引数として位置と視線方向を入力することによって指定します。
さて、後はレイの飛ばし方が問題になります。
個人的には、直接カメラの位置や向きを使ってレイを飛ばすのは、しょぼいと思います。
我々が普通にレンダリングするときには、カメラの位置は気にせず、
頂点座標はビュー行列や射影行列を使って行うように。
ここでは、ビュー行列や射影行列を使って、ワールド座標系で飛ばすレイの方向を、スクリーンに打たれるピクセルの位置から求めます。
ビュー行列は、ワールド座標系からカメラ座標系への変換行列です。
射影行列は、カメラ座標系から射影座標系への変換行列です。
そこで、射影座標系でのピクセルの位置をビュー行列と射影行列の逆行列にかければピクセルのワールド座標系での位置がわかります。
たとえば、z=0、w=1とした射影座標系の座標値をビュー行列と射影行列の逆行列にかければ、前方クリップ面のワールド座標系での位置が求まります。
ちなみに、スクリーン座標系から射影座標系への変換は、符号化スケーリングと拡大縮小で求めることができます。
また、視点ですが、カメラ座標系での原点(0,0,0,1)は、まさにカメラが置かれた場所なので、
(0,0,0,1)をビュー行列の逆行列で変換すれば、ワールド座標系でのカメラの位置が求まります。
後は、それぞれのベクトルの差をとって、規格化すれば、レイの方向ベクトルが求まります。
実際のプログラムは次のようになります。
ここで、cameraは、独自に作ったカメラクラスのオブジェクトで、
GetViewInverse()や、GetViewProjInverse() は、あらかじめ設定されたビュー行列やビュー射影行列の逆行列を返します。
この関数によって、0~1の範囲にスケールされたスクリーン行列の点(x,y)として与えられたピクセルの座標値からレイの方向が決定でき、
球と交差判定の結果、当たった点に色がつきます。
render.cpp 0135: D3DXVECTOR3 *GetColor(D3DXVECTOR3 *dest, float x, float y) 0136: { 0137: D3DXVECTOR4 ray_start_proj = D3DXVECTOR4(-2*x+1, -2*y+1, 0, 1);// 前方クリップ面 0138: D3DXVECTOR4 ray_eye = D3DXVECTOR4(0, 0, 0, 1);// ビュー空間でのカメラの位置 0139: D3DXVECTOR4 ray_start; 0140: D3DXVECTOR4 ray_to; 0141: D3DXVECTOR4 ray_dir; 0142: D3DXMATRIX mInv; 0143: 0144: // 視点を射影空間からワールド座標に変換する 0145: D3DXVec4Transform( &ray_start, &ray_eye, camera.GetViewInverse() ); 0146: D3DXVec4Transform( &ray_to, &ray_start_proj, camera.GetViewProjInverse() ); 0147: D3DXVec4Scale( &ray_to, &ray_to, 1.0f/ray_to.w );// w=1 の射影空間に落とす 0148: // 向きの計算 0149: ray_dir = ray_to - ray_start; 0150: D3DXVec4Normalize(&ray_dir, &ray_dir); 0151: 0152: // 色計算 0153: return GetColor(dest, &ray_start, &ray_dir); 0154: }
ライティングを何も行っていないので淡白な結果ですが、最初はこんなもんでしょう。