射線和三角形的相交檢測(ray triangle intersection test)
概述
射線和三角形的相交檢測是遊戲程式設計中一個常見的問題,最典型的應用就是拾取(Picking),本文介紹一個最常見的方法,這個方法也是DirectX中採用的方法,該方法速度快,而且儲存空間少。先講述理論,然後給出對應的程式碼實現。
理論部分
一個直觀的方法
我想大多數人在看到這個問題時,可能都會想到一個簡單而直觀的方法:首先判斷射線是否與三角形所在的平面相交,如果相交,再判斷交點是否在三角形內。
但是,上面的方法效率並不很高,因為需要一個額外的計算,那就是計算出三角形所在的平面,而下面要介紹的方法則可以省去這個計算。
本文的方法
接下來會涉及到一些數學知識,不過沒關係,我會詳細解釋每一個步驟,不至於太晦澀,只要您不覺得煩就行了,好了開始!
射線的引數方程如下,其中O是射線的起點,D是射線的方向。
我們可以這樣理解射線,一個點從起點O開始,沿著方向D移動任意長度,得到終點R,根據t值的不同,得到的R值也不同,所有這些不同的R值便構成了整條射線,比如下面的射線,起點是P0,方向是u,p0 + tu也就構成了整條射線。
三角形的引數方程如下,其中V0,V1和V2是三角形的三個點,u, v是V1和V2的權重,1-u-v是V0的權重,並且滿足u>=0, v >= 0,u+v<=1。
確切的說,上面的方程是三角形及其內部所有點的方程,因為三角形內任意一點都可以理解為從頂點V0開始,沿著邊V0V1移動一段距離,然後再沿著邊V0V2移動一段距離,然後求他們的和向量。至於移動多大距離,就是由引數u和v控制的。
於是,求射線與三角形的交點也就變成了解下面這個方程-其中t,u,v是未知數,其他都是已知的
移項並整理,將t,u,v提取出來作為未知數,得到下面的線性方程組
現在開始解這個方程組,這裡要用到兩個知識點,一是克萊姆法則,二是向量的混合積。
令E1 = V1 - V0,E2 = V2 - V0,T = O - V0上式可以改寫成
根據克萊姆法則,可得到t,u,v的解分別是
將這三個解聯合起來寫就是
根據混合積公式
上式可以改寫成
令
得到最終的公式,這便是下面程式碼中用到的最終公式了,之所以提煉出P和Q是為了避免重複計算
程式碼部分
理論部分闡述完畢,開始上程式碼,這份程式碼來自DirectX SDK中的Demo,名字叫做Picking(拾取),該函式位於檔案Pick.cpp的最末尾。這個函式有一個特點,就是判斷語句特別多,因為對於一個頻繁被呼叫的函式來說,效率是最重要的,這麼多判斷就是為了在某個條件不滿足時,及時返回,避免後續不必要的計算。
1 // Determine whether a ray intersect with a triangle 2 // Parameters 3 // orig: origin of the ray 4 // dir: direction of the ray 5 // v0, v1, v2: vertices of triangle 6 // t(out): weight of the intersection for the ray 7 // u(out), v(out): barycentric coordinate of intersection 8 9 bool IntersectTriangle(const Vector3& orig, const Vector3& dir, 10 Vector3& v0, Vector3& v1, Vector3& v2, 11 float* t, float* u, float* v) 12 { 13 // E1 14 Vector3 E1 = v1 - v0; 15 16 // E2 17 Vector3 E2 = v2 - v0; 18 19 // P 20 Vector3 P = dir.Cross(E2); 21 22 // determinant 23 float det = E1.Dot(P); 24 25 // keep det > 0, modify T accordingly 26 Vector3 T; 27 if( det >0 ) 28 { 29 T = orig - v0; 30 } 31 else 32 { 33 T = v0 - orig; 34 det = -det; 35 } 36 37 // If determinant is near zero, ray lies in plane of triangle 38 if( det < 0.0001f ) 39 return false; 40 41 // Calculate u and make sure u <= 1 42 *u = T.Dot(P); 43 if( *u < 0.0f || *u > det ) 44 return false; 45 46 // Q 47 Vector3 Q = T.Cross(E1); 48 49 // Calculate v and make sure u + v <= 1 50 *v = dir.Dot(Q); 51 if( *v < 0.0f || *u + *v > det ) 52 return false; 53 54 // Calculate t, scale parameters, ray intersects triangle 55 *t = E2.Dot(Q); 56 57 float fInvDet = 1.0f / det; 58 *t *= fInvDet; 59 *u *= fInvDet; 60 *v *= fInvDet; 61 62 return true; 63 }
引數說明
輸入引數:前兩個引數orig和dir是射線的起點和方向,中間三個引數v0,v1和v2是三角形的三個頂點。
輸出引數:t是交點對應的射線方程中的t值,u,v則是交點的紋理座標值
程式碼說明
變數的命名方式:為了方便閱讀,程式碼中的變數命名與上面公式中的變數保持一致,如E1,E2,T等。
變數det表示矩陣的行列式值
27-35行用來確保det>0,如果det<0則令det = -det,並對T做相應的調整,這樣做是為了方便後續計算,否則的話需要分別處理det>0和det<0兩種情況。
第38行,注意浮點數和0的比較,一般不用 == 0的方式,而是給定一個Epsilon值,並與這個值比較。
第43行,這裡實際上u還沒有計算完畢,此時的值是Dot(P,T),如果Dot(P,T) > det, 那麼u > 1,無交點。
第51行,要確保u + v <= 1,也即 [Dot(P,T) + Dot(Q, D)] / det 必須不能大於1,否則無交點。
第57-60行,走到這裡時,表明前面的條件都已經滿足,開始計算t, u, v的最終值。
交點座標
根據上面程式碼求出的t,u,v的值,交點的最終座標可以用下面兩種方法計算
O + Dt
(1 - u - v)V0 + uV1 + vV2
後記
在本文開頭已經說了,射線和三角形的相交檢測最典型的應用就是拾取,比如在一個三維場景中用滑鼠選擇某個物體。那麼拾取是如何實現的呢?我們知道在物體的三維模型表示中,三角形是最小的幾何圖元,最小意味著不可再分,也就是說任何模型,無論它多麼複雜,都可以由若干個三角形組合而成。拾取過程實際是判斷拾取射線是否與模型相交,而這又可以轉化為-只要射線與模型中的任何一個三角形相交即可。下面是模型的線框表示法,可見如果想要判斷某條射線是否與這個茶壺相交,只要判斷該射線是否與茶壺模型中某個三角形相交即可。
需要注意的是,雖然射線和三角形的相交檢測可以用來實現拾取,但是大多數程式並不採用這個方法,原因是這個方法效率很低,我們可以設想,一個大型的3D線上遊戲,它的模型數量以及複雜程度都是很高的,如果用這種方法來判斷,需要對模型中每個三角形都做一次判斷,效率極其低下,一種可行的方案是,用包圍球或者包圍盒來代替,計算出能容納模型的最小球體或者矩形體,只要判斷出射線與包圍球或者包圍盒相交,我們就認為射線與模型相交,這樣效率會顯著提高,只是精確度上會有一定誤差,但是足以滿足多數程式的需要。
Happy Coding
== The End ==