今までの画像は、リアルタイムでも可能な画像でした。
ここで、こてこてのレイトレの画像で見かける、つるつるで透明な物体がおいてある画面を作って見ましょう。
今回のプログラムは、次のものです。
まぁ、いつものように適当にファイルが入っています。
APP WIZARD から出力されるフレームワークのファイルは紹介を省かせていただきます。
今回から、オブジェクトのためのクラスを別ファイル prim.h、prim.cpp に分けました。
render.cpp | レイトレ用の描画関数群 |
render.h | レイトレ用の描画関数群 |
prim.cpp | オブジェクトのデータのための関数 |
prim.h | オブジェクトのデータのための関数 |
mainDlg.cpp | ダイアログを管理するクラスのメソッドが書かれたファイル |
mainDlg.h | ダイアログを管理するクラスのヘッダ |
あと、実行ファイル、リソースファイル、プロジェクトファイルが入っています。
今回は、反射、屈折を追加しました。
視線からレイを飛ばしたときに、レイが衝突した物体が反射、もしくは屈折する物体だったら、衝突した点から反射、屈折の方向にレイを飛ばし、飛ばした先の色を最終的な色として出力します。
もちろん、反射、屈折してレイを飛ばした先が再び反射、屈折する物体だったら、その先でも反射、屈折したレイを飛ばします。
さて、反射、屈折の時のレイの方向ですが、反射ベクトルはこれまでも多く出てきているとおり、
r = v - 2 (n・v) n
です。
屈折ベクトルは、スネルの法則から導出します。
スネルの法則とは、入射角θ1で屈折率η1の物質から屈折率η2の物質に光が入射するときに、入射後の角度θ2には、
η1sinθ1 = η2sinθ2
の関係があるというやつです。
さて、以上の情報を元に、屈折ベクトルTを作ってみましょう。
屈折ベクトルTは、角度θ2で物体に入射するので、垂直方向の単位ベクトル(法線ベクトル)をN、水平方向の単位ベクトルをevとすると、
T = - N cosθ2 - ev sinθ2
になります。ここで、入射ベクトルLと垂直方向のベクトルの関係式
ev sinθ1 = L - (L・N)N
を使えば、屈折ベクトルは、
L - (L・N)N T = - N cosθ2 - ------------ sinθ2 sinθ1
とかけます。さらに、スネルの法則を使えば、
η1 T = - N cosθ2 - - [L - (L・N)N] η2
と変形できます。さらに、cosをsinで書くと
η1 T = - N (1 - sin2θ2)1/2 - - [L - (L・N)N] η2
となるのですが、さらに sinθ2 をスネルの法則と、cosθ1 = L・N を使って置き換えると、
η12 η1 T = - N (1 - - [1 - (L・N)2])1/2 - - [L - (L・N)N] η22 η2
になります。この式は、屈折率η1、η2と法線ベクトルN、入射ベクトルLという我々が扱うことのできる量から屈折ベクトルTを計算する式になります。
今回、反射、屈折を取り入れるために、質感情報の構造体 Material を拡張しました。
prim.h 0021: typedef struct{ 0022: float COLOR_AMB[4]; 0023: float COLOR_DIF[4]; 0024: float COLOR_SPE[4]; 0025: float speq_power; 0026: float reflection; // 鏡面反射率 0027: float refraction; // 屈折反射率 0028: float diffuse; // 拡散反射率 0029: }Material;
光が物体に当たったときに、鏡面反射率の割合で反射し、屈折反射率の割合で屈折します。また、拡散反射率の割合で物体に色がつきます。
1-reflection-refraction-diffuse の割合で光は物体に吸収されることになるので、reflection = refraction = diffuse = 0 にすると、物体は真っ黒になります。
拡散成分は、拡散反射率以外にも、拡散色 COLOR_DIF 等で色を制御することができるので、それらの色を掛けた値が各色に関する拡散反射率になります。
今回の球のデータは、下のようになります。
render.cpp 0064: // --------------------------------------------------------------------------- 0065: // 玉 0066: OBJ_DATA sphere_data[] = { 0067: {// 小さいのに乗ってるほう 0068: OBJ_TYPE_SPHERE, 0069: { 0070: {0.01f, 0.05f, 0.02f }, 0071: {0.01f, 0.05f, 0.02f }, 0072: {0.10f, 0.10f, 0.10f }, 0073: 16.0f, 0074: 0.2f, 0.8f, 0.0f, 0075: },{{0,0,0},{0,0,0},{0,0,0}},{ 0076: {185.5, 165+100,169}, 0077: 100.0f, 0078: } 0079: },{// 大きいのに乗ってるほう 0080: OBJ_TYPE_SPHERE, 0081: { 0082: {0.00f, 0.00f, 0.00f}, 0083: {0.00f, 0.00f, 0.00f}, 0084: {0.10f, 0.10f, 0.10f }, 0085: 64.0f, 0086: 0.90f, 0.00f, 0.10f, 0087: },{{0,0,0},{0,0,0},{0,0,0}},{ 0088: {368.5, 330+100,351}, 0089: 100.0f, 0090: } 0091: }};
両方とも完全に反射、屈折する物体ではなくて、透明な球はわずかに屈折、 つるつるな球はわずかに黒い色をつけています。
さて、色を得る関数 GetColor は、どのような変更を受けるでしょうか。
今回、反射、屈折をしたところで、この関数は再帰的に呼ばれることになります。
そこで、再帰された深さ depth を引数に追加しました。
関数が呼ばれた最初に再帰の深さを調べて、適当に設定した深さ DEPTH_MAX を超えたときには、光が届かなかったものとして、黒色を出力するようにしました。
render.cpp 0639: bool GetColor(D3DXVECTOR3 *dest, const D3DXVECTOR4 *x, const D3DXVECTOR4 *v, int depth) 0640: { 0641: // ----------------------------------------------------------------------- 0642: // 再帰しすぎた場合は光が届いていないものと考える 0643: // ----------------------------------------------------------------------- 0644: const int DEPTH_MAX = 3; 0645: if(DEPTH_MAX <= depth) {*dest = D3DXVECTOR3(0,0,0); return FALSE;}
次に行うのは、レイが衝突する物体を調べることです。
これは、今までと変わりありません。
全ての物体と交差判定して、一番近くで交差した物体と交差した場所、その法線を調べます。
衝突がなかったときには、外に突き抜けたということですから、背景色を出力します。
render.cpp 0646: 0647: // ----------------------------------------------------------------------- 0648: // 視線から交点を求める 0649: // ----------------------------------------------------------------------- 0650: float t = CPrimitive::INFINTY_DIST; 0651: CPrimitive *pObj = NULL; 0652: D3DXVECTOR4 p, n;// 交点の位置と法線 0653: 0654: t = pShpereS ->IsAcross(t, &n, &p, &pObj, x, v); 0655: t = pShpereT ->IsAcross(t, &n, &p, &pObj, x, v); 0656: t = pRoom ->IsAcross(t, &n, &p, &pObj, x, v); 0657: t = pBlockSmall->IsAcross(t, &n, &p, &pObj, x, v); 0658: t = pBlockTall ->IsAcross(t, &n, &p, &pObj, x, v); 0659: 0660: if( NULL == pObj ){ 0661: // 視線はオブジェクトから外れた! 0662: *dest = BG_COLOR; 0663: return TRUE; 0664: }
レイが物体と交差したときには、あたった点での色を計算します。
最初は、拡散色です。
拡散色は、今までにやってきたように、点光源が見えるか遮蔽チェックをして、Blinn-Phong の法則から色を求めます。
render.cpp 0666: // ----------------------------------------------------------------------- 0667: // 拡散光を算出する 0668: // ----------------------------------------------------------------------- 0669: float cd = pObj->m_material.diffuse; 0670: D3DXVECTOR3 diffuse_color; 0671: 0672: if(0.0f<cd){ 0673: D3DXVECTOR4 light_pos = D3DXVECTOR4(278.f, 548.8f, 279.5f,1); 0674: D3DXVECTOR4 l = light_pos-p; 0675: float L2 = D3DXVec3Length((D3DXVECTOR3 *)&l); 0676: D3DXVec3Normalize((D3DXVECTOR3 *)&l, (D3DXVECTOR3 *)&l); 0677: 0678: D3DXVECTOR3 dir, H; 0679: // 視線の計算 0680: camera.GetFrom(&dir); 0681: dir = dir - *(D3DXVECTOR3 *)&p; 0682: D3DXVec3Normalize(&dir, &dir); 0683: // ハーフベクトル 0684: H = dir+*(D3DXVECTOR3 *)&l; 0685: D3DXVec3Normalize((D3DXVECTOR3 *)&H, (D3DXVECTOR3 *)&H); 0686: 0687: float LN = D3DXVec3Dot((D3DXVECTOR3 *)&l, (D3DXVECTOR3 *)&n); 0688: float HN = D3DXVec3Dot((D3DXVECTOR3 *)&H, (D3DXVECTOR3 *)&n); 0689: if(HN<0) HN=0; 0690: if(0<LN){ 0691: // --------------------------------------------------------------- 0692: // 当たったところから、光源が見えるかを判定 0693: // --------------------------------------------------------------- 0694: // 見えなければ、暗くする 0695: bool bShadow = FALSE; 0696: if(!bShadow) bShadow = pShpereS ->IsAcross(L2, &p, &l); 0697: if(!bShadow) bShadow = pShpereT ->IsAcross(L2, &p, &l); 0698: if(!bShadow) bShadow = pRoom ->IsAcross(L2, &p, &l); 0699: if(!bShadow) bShadow = pBlockSmall->IsAcross(L2, &p, &l); 0700: if(!bShadow) bShadow = pBlockTall ->IsAcross(L2, &p, &l); 0701: if(bShadow) LN*=0.0f;// difuseだけなくす 0702: }else{ 0703: LN=0; 0704: } 0705: 0706: pObj->GetColor(&diffuse_color, LN, HN); 0707: 0708: // 光源の色の反映 0709: D3DXVECTOR3 light_color = D3DXVECTOR3(10.f,9.0f,5.0f); 0710: diffuse_color.x *= light_color.x; 0711: diffuse_color.y *= light_color.y; 0712: diffuse_color.z *= light_color.z; 0713: 0714: // 光の強さの適当な補正 0715: diffuse_color *= min(1.5f, 500000.0f/(10000.0f+L2)); // 距離による補正 0716: diffuse_color *= min(1, l.y+0.1f); // 光の向きをcosθの関数にする 0717: }else{ 0718: diffuse_color = D3DXVECTOR3(0,0,0); 0719: }
次に反射成分です。
交差した場所の法線ベクトル n から反射ベクトル r を求めて、交差した場所 p からGetColor 関数を再帰的に呼び出して出力する色とします。
ここで、計算誤差によって、衝突したオブジェクトの同じ場所に再び衝突するという判定を防ぐために、レイを飛ばす場所をあたった場所から少し 0.01 だけ法線方向にずらしました。
render.cpp 0721: // ----------------------------------------------------------------------- 0722: // 再帰的に光線を飛ばして、より高次の反射を考慮に入れる 0723: // ----------------------------------------------------------------------- 0724: // 反射 0725: float cr = pObj->m_material.reflection; 0726: D3DXVECTOR3 reflect_color; 0727: if(0.0f<cr){ 0728: // 反射ベクトル 0729: D3DXVECTOR4 r = *v - 2.0f*D3DXVec3Dot((D3DXVECTOR3 *)&n, (D3DXVECTOR3 *)v) * n; 0730: D3DXVECTOR4 pos = p + 0.01f*n;// ちょっと浮かす 0731: if(!GetColor( &reflect_color, &pos, &r, depth+1)){ 0732: cr = 0; 0733: } 0734: }else{ 0735: reflect_color=D3DXVECTOR3(0,0,0); 0736: }
さて、後は屈折です。
屈折が反射と違うところは、物体に入るときと、物体から出てくるときの2通りがあるということです。
入るときと出てくるときで、屈折率の比 η2/η1 は、逆数の関係になります。
今回は、入るとき、出てくるとき共に、物体でない側を真空(屈折率 1)にしました。
また、入るときと出るときで法線ベクトルの向きを反対にする必要もあります。
render.cpp 0738: // 屈折 0739: float cn = pObj->m_material.refraction; 0740: D3DXVECTOR3 refract_color=D3DXVECTOR3(0,0,0); 0741: if(0.0f<cn){ 0742: // 屈折ベクトル 0743: D3DXVECTOR4 t, pos; 0744: float VN = D3DXVec3Dot((D3DXVECTOR3 *)v, (D3DXVECTOR3 *)&n); 0745: float eta = 1.5f; 0746: if(VN<0){ 0747: // 入射 0748: float D = 1-(1+VN)*(1+VN)/(eta*eta); 0749: if(D<0){cn=0;goto no_refract;}// 全反射 0750: t = (-VN/eta-sqrtf(D))*n+(1/eta)*(*v); 0751: pos = p - 0.01f*n;// ちょっと浮かす 0752: }else{ 0753: // 出て行く 0754: float D = 1-(1-VN)*(1-VN)*(eta*eta); 0755: if(D<0){cn=0;goto no_refract;}// 全反射 0756: t = -(VN*eta-sqrtf(D))*n+eta*(*v); 0757: pos = p + 0.01f*n;// ちょっと浮かす 0758: } 0759: D3DXVec3Normalize( (D3DXVECTOR3 *)&t, (D3DXVECTOR3 *)&t ); 0760: if(!GetColor( &refract_color, &pos, &t, depth+1)){ 0761: cn = 0; 0762: } 0763: } 0764: no_refract:
以上で、それぞれの色成分が決定したら、後はそれぞれの反射率で色を合成して出力します。
render.cpp 0766: *dest = cd * diffuse_color + cr * reflect_color + cn * refract_color; 0767: 0768: return TRUE; 0769: }
計算の打ち切り回数 DEPTH_MAX を変えると、どのような結果になるのか見てみましょう。
|
1回目の衝突で計算を打ち切ると、…。反射や屈折しかしない球が真っ黒になりました。 |
|
2回目の衝突で計算を打ち切ると、…。反射の効果が見えます。 |
|
3回目の衝突で計算を打ち切ると、…。屈折の効果が出てきています。 |
| 4回目の衝突で計算を打ち切ると、反射する球に映った透明な球に関して、その裏側が透けています。 |
|
5回目の衝突で計算を打ち切ると、…。うーん違いがよくわかりませんね。 つまり、今回の場合5回以上にするのは意味がないということです。 シーンが複雑になってくると、この点も変わってくるでしょう。 |
さて、やっとレイトレっぽい画像になりましたが、やっぱり影の部分が不自然ですね。
もっとリアルな画像にするには、この部分を修正する必要がありますね。