概念介紹
Frustum裁剪是CLOD中很重要的一個算法,很多文章都是一句話就過去,或者直接給出代碼。但是數學推導很少給出,本文章的目的就是解釋大家看這些代碼中的疑問。
透視投影是將相機空間中的點從視錐體(view frustum)變換到規則觀察體(Canonical View Volume,CCV)中,即是世界空間的視錐體(view frustum)中的任何一個點,如果經過投影矩陣變換後,它必定規則觀察體(Canonical View Volume,CCV)中,也即是在(-1,-1,0) ~ (1,1,1)之間的值如圖1所示
這裏 我們定義
P0=(-1,-1,0)
P1=(1,-1,0)
P2=(1,-1,1)
P3=(-1,-1,1)
P4=(-1,1,0)
P5=(1,1,0)
P6=(1,1,1)
P7=(-1,1,1)
我們通過這8個點構建6個面,通過3點共面,假設面是Pos0,pos1,pos2構成,u=Pos1-Pos0,v=Pos2-Pos0,那麼法向量n=u×v。則d=-(n×Pos0)。得到每個平面的平面公式n,d,從而得到A,B,C,D,(n的xn,yn,zn,d,就是a,b,c,d)。這裏要注意構造面試後P點的順序,d3D是按順時針來構造面。法線都是由frustum裏到外。
Near: (P0,P4,P5) n=(0,0,-1),d=0 0x+0y-1z+0=0
Far: (P2,P6,P7) n=(0,0,1),d=-1 0x+0y+1z-1=0
Left: (P0,P3,P7) n=(-1,0,0),d=-1 -1x+0y+0z-1=0
Right: (P1,P5,P6) n=(1,0,0),d=-1 1x+0y+0z-1=0
Top: (P4,P7,P6) n=(0,1,0),d=-1 0x+1y+0z-1=0
Bottom: (P0,P1,P2) n=(0,-1,0),d=-1 0x-1y+0z-1=0
我們假設這六個平面中某個平面上存在一個點P’(x’,y’,c’,1),那它的平面方程爲A’x’ + B’y’+ C’z’+ D’= 0,在進行投影變換之前的座標爲P(x,y,z,1),在世界空間中的平面方程Ax + By + Cz + D = 0,如果P‘點在CVV中,那點P(x,y,z,1)必定在view Frustum中。
原理一
1.推理
由介紹我們可以得到下面的等式
|A| |A|
(x’,y’,c’,1) × |B| = 0 (x,y,z,1) × |B| = 0
|C| |C|
|D| |D|
同時(x,y,c,1) × Tvproj= (x‘,y’,z‘,1)
注意Tviewproj矩陣=Tview×Tproj(即是攝像機矩陣和投影矩陣相乘。是將世界座標轉換到視平面的變化矩陣)
結合着3個等式我們可以得到
|A| |A’|
|B| =Tviewproj × |B’| (等式1)
|C| |C’|
|D| |D’|
通過等式1,我們可以求出6個面在世界座標系下的平面方程Ax + By + Cz + D = 0
2代碼
void CFrustum::InitFrustum1(const D3DXMATRIX& aoViewMatrix,const D3DXMATRIX& aoProjMatrix)
{
D3DXMATRIX loComboMatrix;
D3DXMATRIX loInvComboMatrix;
D3DXMatrixMultiply(&loComboMatrix,&aoViewMatrix,&aoProjMatrix);
// 求得view * proj的逆矩陣.
D3DXMatrixInverse(&loInvComboMatrix, NULL, &loComboMatrix );
// 如果經過投影矩陣,所有的三維世界座標的點都變爲(-1,-1,0) ~ (1,1,1)之間的值.
// 將同次空間的臨界值填入moCVVPos.
moCVVPos[0].x = -1.0f; moCVVPos[0].y = -1.0f; moCVVPos[0].z = 0.0f;
moCVVPos[1].x = 1.0f; moCVVPos[1].y = -1.0f; moCVVPos[1].z = 0.0f;
moCVVPos[2].x = 1.0f; moCVVPos[2].y = -1.0f; moCVVPos[2].z = 1.0f;
moCVVPos[3].x = -1.0f; moCVVPos[3].y = -1.0f; moCVVPos[3].z = 1.0f;
moCVVPos[4].x = -1.0f; moCVVPos[4].y = 1.0f; moCVVPos[4].z = 0.0f;
moCVVPos[5].x = 1.0f; moCVVPos[5].y = 1.0f; moCVVPos[5].z = 0.0f;
moCVVPos[6].x = 1.0f; moCVVPos[6].y = 1.0f; moCVVPos[6].z = 1.0f;
moCVVPos[7].x = -1.0f; moCVVPos[7].y = 1.0f; moCVVPos[7].z = 1.0f;
//將cvv座標轉換回世界座標
for(int i = 0; i < 8; i++ )
D3DXVec3TransformCoord( &moCVVPos[i], &moCVVPos[i], &loInvComboMatrix );
// 通過得到的世界座標製作平截頭體平面.
// 向量由平截頭體內部指向外部的平面.
D3DXPlaneFromPoints(&moFrustumPlane[FRUSTUM_PLANE_FRONT], moCVVPos , moCVVPos+4, moCVVPos+5); // 近平面(near)
D3DXPlaneFromPoints(&moFrustumPlane[FRUSTUM_PLANE_BACK], moCVVPos+2, moCVVPos+6, moCVVPos+7); // 遠平面(far)
D3DXPlaneFromPoints(&moFrustumPlane[FRUSTUM_PLANE_LEFT], moCVVPos , moCVVPos+3, moCVVPos+7); // 左平面(left)
D3DXPlaneFromPoints(&moFrustumPlane[FRUSTUM_PLANE_RIGHT], moCVVPos+1, moCVVPos+5, moCVVPos+6); // 右平面(right)
D3DXPlaneFromPoints(&moFrustumPlane[FRUSTUM_PLANE_TOP], moCVVPos+4, moCVVPos+7, moCVVPos+6); // 上平面(top)
D3DXPlaneFromPoints(&moFrustumPlane[FRUSTUM_PLANE_BOTTOM], moCVVPos , moCVVPos+1, moCVVPos+2); // 下平面(bottom)
}
上面的代碼很容易理解,完全按原理來實現,但是當我們每次調用這個函數,我們每次都要求Tvproj矩陣的逆矩陣,同時還要重新用點來構造每個frustum平面,並且調用的是D3DXPlaneFromPoints函數。我們還有更好的方法嗎?下面我們接着優化我們的代碼,優化之前我們接着分析
原理二
1.推理
我們知道在P(x,y,z,1) 經過Tviewproj矩陣變換後得到點P’(x’,y’,z’,w’),這個點在前面的推導過程中,保證是在frustum的某個平面上的.這個時候我們知道如果對P的每個分量都除以w’就可以把P’歸一化到一個長方體的空間,即x’/w’, y’/w’在[-1,1]區間,z’/w’在[0,1]區間,所以如果有一個投影轉換後的點P1’,它的x1’,y1’在[-w’,w’]區間,z1’在[0,w’]區間,這個點肯定就在視錐體內.
|m11,m12,m13,m14|
假設Tviewproj = [v0,v1,v2,v3] = |m21,m22,m23,m24|
|m31,m32,m33,m34|
|m41,m42,m43,m44|
其中其中V0,v1,v2,v3是四個列向量.
P ’= P×Tviewproj = (P×v0, P×v1, P×v2, P×v3 ) = (x’,y’,z’,w’)
根據上面的x的範圍我們有:
-w’ <= x’ 就是 -P×v3 <= P×v0 就是 P×v0 + P×v3 >= 0 得到 P×(v0+v3) >= 0
把列向量換成矩陣的元素有
(x,y,z,1).(m_11 + m_14, m21 + m24, m31 + m34, m41 + m44 ) >= 0
就是
(m_11+m_14)*x + (m_21 + m_24)*y + (m_31 + m_34)*z + (m_41 + m_44)*1 >=0
簡單地看這是一個 A*x + B*y + C*z + D*1 >= 0 描述了一個半空間,就是平面A*x + B*y + C*z + D*1 = 0右邊的空間
所以我們知道是錐體的左裁減面爲
(m_11+m_14)*x + (m_21 + m_24)*y + (m_31 + m_34)*z + (m_41 + m_44)*1= 0.
相應地可以計算出 6個裁減面
Left= (v0 + v3).
Right =(v3 - v0)
Bottom =(v3 + v1)
Top = (v3 - v1 )
Near = (v2)
Far = (v3 - v2)
裁減的時候把點帶入公式Ax+By+Cz+Dw看大與0還是小與0就可以知道在平面的裏面還是外面,在實際計算中需要對A,B,C,D進行歸一化。因爲我們在處理座標的時候,點幾乎都是經過歸一化的。
2.代碼
void CFrustum::InitFrustum(const D3DXMATRIX& aoViewMatrix,const D3DXMATRIX& aoProjMatrix)
{
D3DXMATRIX loComboMatrix;
D3DXMatrixMultiply(&loComboMatrix,&aoViewMatrix,&aoProjMatrix);
// calculate the planes
// Near
D3DXPLANE* lpPlane = &moFrustumPlane[FRUSTUM_PLANE_NEAR];
lpPlane->a = loComboMatrix._14 + loComboMatrix._13;
lpPlane->b = loComboMatrix._24 + loComboMatrix._23;
lpPlane->c = loComboMatrix._34 + loComboMatrix._33;
lpPlane->d = loComboMatrix._44 + loComboMatrix._43;
D3DXPlaneNormalize(lpPlane,lpPlane);
// Far
lpPlane = &moFrustumPlane[FRUSTUM_PLANE_FAR];
lpPlane->a = loComboMatrix._14 - loComboMatrix._13;
lpPlane->b = loComboMatrix._24 - loComboMatrix._23;
lpPlane->c = loComboMatrix._34 - loComboMatrix._33;
lpPlane->d = loComboMatrix._44 - loComboMatrix._43;
D3DXPlaneNormalize(lpPlane,lpPlane);
//Left
lpPlane = &moFrustumPlane[FRUSTUM_PLANE_LEFT];
lpPlane->a = loComboMatrix._14 + loComboMatrix._11; // Left
lpPlane->b = loComboMatrix._24 + loComboMatrix._21;
lpPlane->c = loComboMatrix._34 + loComboMatrix._31;
lpPlane->d = loComboMatrix._44 + loComboMatrix._41;
D3DXPlaneNormalize(lpPlane,lpPlane);
// Right
lpPlane = &moFrustumPlane[FRUSTUM_PLANE_RIGHT];
lpPlane->a = loComboMatrix._14 - loComboMatrix._11;
lpPlane->b = loComboMatrix._24 - loComboMatrix._21;
lpPlane->c = loComboMatrix._34 - loComboMatrix._31;
lpPlane->d = loComboMatrix._44 - loComboMatrix._41;
D3DXPlaneNormalize(lpPlane,lpPlane);
// Top
lpPlane = &moFrustumPlane[FRUSTUM_PLANE_TOP];
lpPlane->a = loComboMatrix._14 - loComboMatrix._12;
lpPlane->b = loComboMatrix._24 - loComboMatrix._22;
lpPlane->c = loComboMatrix._34 - loComboMatrix._32;
lpPlane->d = loComboMatrix._44 - loComboMatrix._42;
D3DXPlaneNormalize(lpPlane,lpPlane);
// Bottom
lpPlane = &moFrustumPlane[FRUSTUM_PLANE_BOTTOM];
lpPlane->a = loComboMatrix._14 + loComboMatrix._12; // Bottom
lpPlane->b = loComboMatrix._24 + loComboMatrix._22;
lpPlane->c = loComboMatrix._34 + loComboMatrix._32;
lpPlane->d = loComboMatrix._44 + loComboMatrix._42;
D3DXPlaneNormalize(lpPlane,lpPlane); //norm = sqrt(pp->a * pp->a + pp->b * pp->b + pp->c * pp->c);
}
D3DXPLANE* WINAPI D3DXPlaneNormalize(D3DXPLANE *pout, CONST D3DXPLANE *pp)
{
FLOAT norm;
norm = sqrt(pp->a * pp->a + pp->b * pp->b + pp->c * pp->c);
if ( norm )
{
pout->a = pp->a / norm;
pout->b = pp->b / norm;
pout->c = pp->c / norm;
pout->d = pp->d / norm;
}
else
{
pout->a = 0.0f;
pout->b = 0.0f;
pout->c = 0.0f;
pout->d = 0.0f;
}
return pout;
}
第2種方法在沒有看見之間,先看見的是代碼http://www.racer.nl/reference/vfc_markmorley.htm ,自己想了好久都沒考慮清楚。最後看見解釋,鄙視自己的數學邏輯推理。3d看來困難的就在於思考方式,同樣的代碼處理,第二種方式明顯快很多。
判斷裁剪
我們已經有了裁剪體的方程,當我們需要裁剪一個頂點的時候,這六個方程已經足夠了。但是我們要判斷一個區域的可見性時,我們進行一些額外的計算。如圖2所示,一個物體和投影體的關係大致可以分爲:包圍、被包圍、相交和相離四種情況。圖中最大的淺藍色的矩形包圍了整個投影體。深綠色的小矩形則完全被投影體包圍。淺綠色的矩形和投影體相交。這三種情況下物體都是可以被看到的。剩下紅色的矩形則和投影體相離、只有它完全不可見
圖2
當處理節點的可見性的時候,由於節點的不規則性。我們還需要引入包圍體的概念。所謂的包圍體,就是用一個比較簡單的幾何體去度量另外一個比較複雜的幾何體,讓它剛好能包圍另外一個幾何體。比較合適的包圍體外形有矩形、正方形和球體。其中球體處理最爲簡單,但是近似度也最差。我們爲每一個節點都建立一個包圍體,只要測試這個包圍體,我們就可以決定一個節點的可見性,由於包圍體肯定大於這個節點,因此我們可以保證不會有任何可見的節點被裁剪在投影體之外
BOOL CFrustum::PointInFrustum( float afX, float afY, float afZ )
{
// A*x+B*y+C*z+D = 0 is in plane,
for(int i = 0; i < FRUSTUM_PLANE_COUNT; i++ )
{
D3DXPLANE &loPlane = moFrustumPlane[i];
//減少函數調用
//if(D3DXPlaneDotCoord(&moFrustumPlane[i], &D3DXVECTOR3(afX, afY, afZ)) < 0.0f)
if(loPlane.a * afX + loPlane.b * afY + loPlane.c * afZ + loPlane.d < 0.0f)
return FALSE;
}
return TRUE;
}
BOOL CFrustum::SphereInFrustum( float afX, float afY, float afZ, float AfRadius )
{
// A*x+B*y+C*z+D = -radius is in plane,
for(int i = 0; i < FRUSTUM_PLANE_COUNT; i++ )
{
D3DXPLANE &loPlane = moFrustumPlane[i];
//減少函數調用,直接用公式運算
//if(D3DXPlaneDotCoord(&moFrustumPlane[i], &D3DXVECTOR3(afX, afY, afZ)) < -AfRadius)
if( loPlane.a * afX + loPlane.b * afY + loPlane.c * afZ + loPlane.d <= -AfRadius )
{
return false;
}
}
return TRUE;
}
BOOL CFrustum::CubeInFrustum( float afX, float afY, float afZ, float aiSize,BOOL & abIsCompletelyContained )
{
float lfAlfaX = afX + aiSize;
float lfDeltaX = afX - aiSize;
float lfAlfaY = afY + aiSize;
float lfDeltaY = afY - aiSize;
float lfAlfaZ = afZ + aiSize;
float lfDeltaZ = afZ - aiSize;
DWORD ldwNumPointInFrustum = 0;
for(int i = 0; i < FRUSTUM_PLANE_COUNT; i++ )
{
int j = 8;
BOOL lbIsInAllPlanes = TRUE;
D3DXPLANE &loPlane = moFrustumPlane[i];
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfDeltaX,lfDeltaY, lfDeltaZ)) < 0.0f)
if(loPlane.a * lfDeltaX + loPlane.b * lfDeltaY + loPlane.c * lfDeltaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfAlfaX,lfDeltaY, lfDeltaZ)) < 0.0f)
if(loPlane.a * lfAlfaX + loPlane.b * lfDeltaY + loPlane.c * lfDeltaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfDeltaX,lfAlfaY, lfDeltaZ)) < 0.0f)
if(loPlane.a * lfDeltaX + loPlane.b * lfAlfaY + loPlane.c * lfDeltaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfAlfaX,lfAlfaY, lfAlfaZ)) < 0.0f)
if(loPlane.a * lfAlfaX + loPlane.b * lfAlfaY + loPlane.c * lfDeltaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfDeltaX,lfDeltaY, lfAlfaZ)) < 0.0f)
if(loPlane.a * lfDeltaX + loPlane.b * lfDeltaY + loPlane.c * lfAlfaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfAlfaX,lfDeltaY, lfAlfaZ)) < 0.0f)
if(loPlane.a * lfAlfaX + loPlane.b * lfDeltaY + loPlane.c * lfAlfaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfDeltaX,lfDeltaY, lfAlfaZ)) < 0.0f)
if(loPlane.a * lfDeltaX + loPlane.b * lfDeltaY + loPlane.c * lfAlfaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
//if(D3DXPlaneDotCoord(&loPlane[i], &D3DXVECTOR3(lfAlfaX,lfAlfaY, lfAlfaZ)) < 0.0f)
if(loPlane.a * lfAlfaX + loPlane.b * lfAlfaY + loPlane.c * lfAlfaZ + loPlane.d < 0.0f)
{
lbIsInAllPlanes = FALSE;
j--;
}
// if none contained, return FALSE.
if(0 == j)
return FALSE;
// update counter if they were all in front of plane.
if(lbIsInAllPlanes)
++ldwNumPointInFrustum;
}
abIsCompletelyContained = (BOOL)(ldwNumPointInFrustum == FRUSTUM_PLANE_COUNT);
return TRUE;
}