http://hi.baidu.com/macsofft/blog/item/0b05dbd4069e7500a18bb79f.html
有一篇更原始的文章,有機會了我找找在哪,
不過,現在發現,我不適合在三維世界中思索,我還是要好好的,專研2D圖形影像處理,現在,對3D的定位,更務實點,就是,懂得圖形學原理,能找到相應的函數即可.
3D互動圖形應用程式中,常常要用滑鼠去選擇圖形,其實現的機制基於滑鼠拾取演算法。本文主要講述如何在D3D中實現圖元的滑鼠拾取。為了討論簡單,本文假定讀者理解D3D 座標變換流程和基本的圖形學知識,如果閱讀有困難請參考相關資料。
1、什麼是拾取,拾取能做什嗎?
首先,拾取操作指當我們在螢幕上用滑鼠點擊某個圖元應用程式能返回該圖元的一個標誌和某些相關資訊。有圖形程式設計經驗的人都知道,有這些資訊就表示我們有了對該圖元的控制權,我們可以刪除,可以編輯,可以任意對待該圖元,至於你到底想幹什麼,就是閣下自己的事了^_^。
2、拾取操作的步驟和實現
拾取演算法的思想很簡單:得到滑鼠點擊處的螢幕座標,通過投影矩陣和觀察矩陣把該座標轉換為通過視點和滑鼠點擊點的一條射入情境的光線,該光線如果與情境模型的三角形相交(本文只處理三角形圖元),則擷取該相交三角形的資訊。本文講述的方法除可以得到三角形的一個索引號以外還可以得到相交點的重心座標。
從數學角度來看,我們只要得到射線的方向向量和射線的出射點,我們就具備了判斷射線與空間一個三角面是否相交的條件,本文主要討論如何獲得這些條件,並描述了射線三角面相交判斷演算法和D3D的通常實現方法。
根據拾取操作的處理順序,大概可以依次分為以下幾個步驟
2.1. 變換並獲得通過視點和螢幕上點擊點的射線向量(Dir)
詳細介紹之前,為了大家方便理解,我們要先簡單說一下d3d座標轉換的大概流程,如:
所以我們要通過一系列的反變換,得到我們關心的值在全局座標中的表示。
2.1.1 確定滑鼠選取點的螢幕座標
這一步是非常簡單的Windows給我們提供了API來完成螢幕座標的擷取,使用GetCursorPos獲得滑鼠指標位置,然後再利用ScreenToClient轉換座標到客戶區座標系(以視窗視區左上方為座標原點,單位為像素),設該座標為(POINT screenPt)。
2.1.2 得到Dir在觀察座標空間內的表示
在觀察座標系中,Dir是一條從觀察座標原點出發的射線,所以我們只需要再確定一個該射線經過的點,就可以得到它在觀察座標系中的表示。假設我們要求的射線上的另外一點為該射線與透視投影平截頭體近剪下面的交點,針對最普遍的透視投影而言,透視投影平截頭體經投影變換後,變成一個1/2立方體(請允許我這麼叫^_^,因為它的大小為一個正方體的一半,x,y方向邊長為2,z方向為1)
投影座標系以近剪下面中心為座標原點,該立方體從z軸負向看過去與圖形程式視區相對應,最終近剪下面(前剪下面)上一點與螢幕座標之間的對應關係如所示:
projPt.y = (screenPt.y-screenHeight/2)/screenHeight*2; (公式2)
projPt.z =0;(實際該值可任意取,不影響最終結果。為了處理簡單,我們取改值為0,表示該點取在近剪下面上)得到projPt後,我們需要做的是把該點座標從投影空間轉換到觀察空間(view space),根據透視投影的定義,
可假設點(projPt.x,projPt.y,projPt.z)
對應的其次座標為 (projPt.x*projPt.w,projPt.y*projPt.w,projPt.z*projPt.w,projPt.w)
我們可以通過 GetTransform( D3DTS_PROJECTION, &ProjMatrix)函數獲得投影矩陣ProjMatrix,則根據觀察空間到投影空間的變換關係則
(projPt.x*projPt.w,projPt.y*projPt.w,projPt.z*projPt.w,projPt.w)= (viewPt.x,viewPt.y,viewPt.z, 1)*pProjMatrx;
根據定義和圖形學原理
根據比例關係,screenPt與投影空間上的點projPt之間的關係為
假設圖形程式視窗的寬為screenWidth,高為screenHeight,
projPt.x = (screenPt.x-screenWidth/2)/screenWidth*2; (公式1)
所以,
(projPt.x*projPt.w,projPt.y*projPt.w,projPt.z*projPt.w,projPt.w)
= ( viewPt.x*ProjMatrix._m11,
viewPt.y*ProjMatrix._m22,
viewPt.z*Q-QZn,
viewPt.z)
所以
projPt.x*projPt.w = viewPt.x*ProjMatrix._m11
projPt.y*projPt.w = viewPt.y*ProjMatrix._m22
projPt.z*projPt.w = viewPt.z*Q-QZn (注意projPt.z = 0)
projPt.w = viewPt.z;
解得
viewPt.x = projPt.x*Zn/ ProjMatrix._m11;
viewPt.y = projPt.y*Zn/ ProjMatrix._m22;
viewPt.z = Zn;
好了,到這裡為止我們終於求出了射線與近剪下面交點在觀察座標系中的座標,現在我們擁有了射線的出發點(0,0,0)和射線方向上另外一點(viewPt.x,viewPt.y,viewPt.z),則該射線的方向向量在觀察空間中的表示可確定為(viewPt.x-0,viewPt.y-0,viewPt.z-0),化簡一下三個分量同除近剪下面z座標Zn,該方向向量可寫作
DIRview = (projPt.x/projMatrix._m11,projPt.y/projMatrix._m22,1)
代入公式1,公式2
DIRview.x = (2*screenPt.x/screenWidth-1)/projMatrix._m11;
DIRview.y = (2*screenPt.y/screenHeight-1)/projMatrix._m22;
DIRview.z = 1;
其中screenWidth和screenHeight可以通過映像顯示的backBuffer的目標表面(D3DSURFACE_DESC)來獲得,該表面在程式初始化時由使用者建立。
2.1.3 轉換Dir到全局座標空間,並得到觀察點在全局座標系中的座標
由於最終的運算要在全局座標空間中進行,所以我們還需要把向量DIRview從觀察空間轉換為全局座標空間中的向量DIRworld。
因為
DIRview = DIRworld*ViewMatrix;
其中ViewMatrix為觀察矩陣,在D3D中可以用函數GetTransform( D3DTS_VIEW, &ViewMatrix )得到。
所以DIRworld = DIRview * inverse_ViewMatrix,其中inverse_ViewMatrix為
ViewMatrix的逆矩陣。
觀察點在觀察座標系中座標為OriginView(0,0,0,1),所以其在全局座標系中的座標同樣可以利用ViewMatrix矩陣,反變換至全局座標系中,事實上我們可以很簡單的判斷出,其在全局座標系中的表示為:
OriginWorld = (inverse_ViewMatrix._41,
inverse_ViewMatrix._42,
inverse_ViewMatrix._43,
1);
到這裡為止,判斷射線與三角面是否相交的條件就完全具備了。
2.2 使用射線向量對情境中的所有三角形圖元求交,獲得三角形索引值和重心座標。
這一步驟地實現由兩種途徑:
第一種方法非常簡單,利用D3D提供的擴充函數D3DXIntersect可以輕鬆搞定一切。見2.1
第二種方法就是我們根據空間解析幾何的知識,自己來完成射線三角形的求交演算法。一般來講,應用上用第一種方法就足夠了,但是我們如果要深入的話,必須理解相交檢測的數學演算法,這樣才能自由的擴充,面對不同的需求,內容見2.2
下面分別講解兩種實現途徑:
2.2.1 D3D擴充函數實現求交
這種方法很簡單也很好用,對於應用來說應儘力是用這種方式來實現,畢竟效率比自己寫得要高得多。
實際上其實沒什麼好講的,大概講一下函數D3DXIntersect吧
D3D SDK該函式宣告如下
HRESULT D3DXIntersect(
LPD3DXBASEMESH pMesh,
CONST D3DXVECTOR3 *pRayPos,
CONST D3DXVECTOR3 *pRayDir,
BOOL *pHit,
DWORD *pFaceIndex,
FLOAT *pU,
FLOAT *pV,
FLOAT *pDist,
LPD3DXBUFFER *ppAllHits,
DWORD *pCountOfHits
);
l pMesh指向一個ID3DXBaseMesh的對象,最簡單的方式是從.x檔案獲得,描述了要進行相交檢測的三角面元集合的資訊,具體規範參閱direct9 SDK
l pRayPos 指向射線發出點
l pRayDir 指向前面我們辛辛苦苦求出的射線方向的向量
l pHit 當檢測到相交圖元時,指向一個true,不與任何圖元相交則為假
l pU 用於返回重心座標U分量
l pV返回重心座標V分量
l pDist 返回射線發出點到相交點的長度
注意:以上紅色字型部分均指最近的一個返回結果(即*pDist最小)
l ppAllHits用於如果存在多個相交三角面返回相交的所有結果
l pCountOfHits 返回共有多少個三角形與該射線相交
補充:重心座標的概念
其中pU和pV用到了重心座標的概念,下面稍作描述
一個三角形有三個頂點,在迪卡爾座標系中假設表示為V1(x1,y1,z1),V2(x2,y2,z2),V3(x3,y3,z3),則三角形內任意一點的座標可以表示為 pV = V1 + U(V2-V1) + V(V3-V1),所以已知三個頂點座標的情況下,任意一點可用座標(U,V)來表示,其中 參數U控制V2在結果中佔多大的權值,參數V控制V3佔多大權值,最終1-U-V控制V1佔多大權值,這種座標定義方式就叫重心座標。
2..2.2射線三角面相交的數學演算法
使用d3d擴充函數,畢竟有時不能滿足具體需求,掌握了該方法,我們才能夠獲得最大的控制自由度,任意修改演算法。
已知條件: 射線源點orginPoint,三角形三個頂點 v1,v2,v3,射線方向 Dir
(均以三維座標向量形式表示)
演算法目的: 判斷射線與三角形是否相交,如果相交求出交點的重心座標(U,V)和射線原點到交點的距離T。
我們可先假設射線與三角形相交則交點(注以下均為向量運算,*數乘,dot(X,Y) X,Y 點乘,cross(X,Y)X,Y叉乘;U,V,T為標量)
則:
IntersectPoint = V1 + U*(V2-V1) + V*(V3-V1) ;
IntersectPoint = originPoint + T*Dir;
所以
orginPoint + T*Dir = V1 + U*(V2-V1) + V*(V3-V1);
整理得:
這是一個簡單的線性方程組,若有解則行列式[-Dir,V2-V1,V3-V1] 不為0。
根據T,U,V的含義當T>0, 0<U<1,0<V<1,0<U+V<1時該交點在三角形內部,
解此方程組即可獲得我們關心的值,具體解法不再贅述,克萊姆法則就夠了(詳細見線性代數):射線原點到相交點的距離T,和交點的中心座標(U,V)。
下面給出Direct 9 SDK樣本程式中的實現代碼
IntersectTriangle( const D3DXVECTOR3& orig,
const D3DXVECTOR3& dir, D3DXVECTOR3& v0,
D3DXVECTOR3& v1, D3DXVECTOR3& v2,
FLOAT* t, FLOAT* u, FLOAT* v )
{
// 算出兩個邊的向量
D3DXVECTOR3 edge1 = v1 - v0;
D3DXVECTOR3 edge2 = v2 - v0;
D3DXVECTOR3 pvec;
D3DXVec3Cross( &pvec, &dir, &edge2 );
// 如果det為0,或接近於零則射線與三角面共面或平行,不相交
//此處det就相當於上面的 ,
FLOAT det = D3DXVec3Dot( &edge1, &pvec );
D3DXVECTOR3 tvec;
if( det > 0 )
{
tvec = orig - v0;
}
else
{
tvec = v0 - orig;
det = -det;
}
if( det < 0.0001f )
return FALSE;
// 計算u並測試是否合法(在三角形內)
*u = D3DXVec3Dot( &tvec, &pvec );
if( *u < 0.0f || *u > det )
return FALSE;
// Prepare to test V parameter
D3DXVECTOR3 qvec;
D3DXVec3Cross( &qvec, &tvec, &edge1 );
//計算u並測試是否合法(在三角形內)
*v = D3DXVec3Dot( &dir, &qvec );
if( *v < 0.0f || *u + *v > det )
return FALSE;
/*計算t,並把t,u,v放縮為合法值(注意前面的t,v,u不同於演算法描述中的相應量,乘了一個係數det),注意:由於該步運算需要使用除法,所以放到最後來進行,避免不必要的運算,提高演算法效率*/
*t = D3DXVec3Dot( &edge2, &qvec );
FLOAT fInvDet = 1.0f / det;
*t *= fInvDet;
*u *= fInvDet;
*v *= fInvDet;
return TRUE;
}
2.2.3 拾取完成根據獲得的中心座標計算我們關心的常見量。
根據重心座標(U,V),我們可以很容易的算出各種相關量比如紋理座標和交點的差值顏色,假設以紋理座標為例設V1,V2,V3的紋理座標分別為T1(tu1,tv1),T2(tu2,tv2),T3(tu3,tv3)則交點的座標為
IntersectPointTexture = T1 + U(T2-T1) + V(T3-T1)