1. 程式人生 > >射線和三角形的相交檢測(ray triangle intersection test)

射線和三角形的相交檢測(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 ==