再探Direct3D流水线
Direct3D流水線大體圖:
局部坐標:
也就是3D實體在其局部坐標系中的坐標,即為當創建3D物體時,或從其他地方加載3D物體時,該物體通常都會有自己的一組坐標軸,物體的中心則位于局部坐標系的原點,如果不進行變化,那么程序加載物體就可能會看不見物體。如下圖定義了一個立方體,該立方體長為10個單位,中心坐標為(0,0,0),各個頂點坐標分別如下:
V0(10,10,10) V1(-10,10,10)
V2(-10,10,-10) V3(10,10,-10)
V4(10,-10,10) V5(-10,-10,10)
V6(-10,-10,-10) V7(10,-10,-10)
世界坐標:
當建立好所有的3D模型后,每個模型都位于自己的局部坐標系中,需要將其放置到世界坐標系中。世界坐標表示的是各種物體在游戲世界的實際位置,是絕對位置,而非相對位置。那這個世界有多大,會不會小于物體的局部坐標,這個取決于你設計游戲地圖的大小,以及物體處在這個世界中的什么地位(放大還是縮小)。知道了世界坐標后,怎么進行將局部坐標變換為世界坐標,記得上次寫過,在Direct3D中,可以通過IDirect3DDevice9::SetTransform()這個函數方法就可以加以應用,將里面的第一個參數改成自己想要變換的類型(如局部坐標到世界坐標的轉換,參數變為D3DTS_WORLD),即可完成坐標之間的轉換。不過也可以自己重載一個SetTransform()函數方法。就用剛才的立方體為例。 //定義3D坐標結構體 typedef strct Point {int x;int y;int z; }POINT3D;//那么立方體的頂點先用一個數組來存儲 POINT3D cube_model[8]={ {10,10,10},{-10,10,10}. {-10,10,-10},{10,10,-10}, {10,-10,10},{-10,-10,10}, {-10,-10,-10},{10,-10,-10}};當然也可以用面來存儲頂點數據,如立方體共6個面,每個面共4個頂點,所以再定義面的結構體。在把頂點包含進去就OK了。
我采用的是第一種,使用POINT3D來存儲立方體的各個頂點,要將局部坐標變換為世界坐標,只需知道要將物體的中心放置到游戲世界的什么位置,然后將局部坐標平移到這個位置即可,再符合游戲需求的進行放大或縮小。
POINT3D cube_world[8]; //用于存儲世界坐標 //每個頂點平移(_x,_y,_z) for(int vertex=0;vertex<8;vertex++) {cube_world[vertex].x=cube_model[vertex].x+_x;cube_world[vertex].y=cube_model[vertex].y+_y;cube_world[vertex].z=cube_model[vertex].z+_z; }這只是進行平移而已,可以用一個矩陣來進行表示。使用4D齊次坐標,平移矩陣表示為:
[1 0 0 0]
T= [0 1 0 0]
[0 0 1 0]
[_x _y _z 1]
既然轉換矩陣為4D齊次坐標表示,那么原坐標也用4D坐標來進行表示[x y z 1];那么從局部坐標到世界坐標的轉換就可以讓這兩個矩陣相乘。
[x y z 1]*T=[x+_x y+_y z+_z 1]
這相比剛才的for循環要簡便多了。
物體的放大縮小和旋轉也是一樣類似。
4D齊次坐標:
齊次坐標就是將一個n維的向量用一個n+1維向量來進行表示。如2D點[x y]的其次坐標表示為[x y z];3D點[x y z]的齊次坐標表示為[x y z w];其中w是用于其次化坐標,將齊次坐標轉換為常規坐標,就必須除以w。如3D點的其次坐標:[X Y Z W]常規坐標:[X/W Y/W Z/W]如果W=1,那么X,Y,Z的值將不會改變;如果W不為1,這種變換有何用途?W可以讓我們表示無窮大的概念;如當W趨于0時,X/W將趨近于無窮大。而且也方便計算機執行矩陣的運算。相機坐標:
局部坐標在物體自己的3D空間內定義了物體,坐標系是相對于這個物體的中心即(0,0,0),而世界坐標系定義了虛擬的3D空間,物體將被放置到這個空間中。3D游戲是圍繞著3D相機進行的,因此需要采取某種方式來觀察3D物體。所以才有相機坐標這個概念。
最近很火的“吃雞”(絕地求生)就是個典型的例子。
相機坐標系是以攝像機(屏幕中顯示的圖形就是虛擬攝像機拍攝在膠片上的景物)為攝像機位置為原點,攝像機觀察的方向向著Z軸而建立的坐標系。
不過要注意的是,該變化將攝像機變換至坐標系原點并使攝像機的方向向著Z軸正方向。空間中物體應隨著攝像機一同進行變換,這樣攝像機的視場才能保持不變。
在Direct3D中,提供了一些API函數使用:
還是一樣采用SetTransform()方法,不過要將第一個參數進行變換而已。
接下來跟著大師的步伐,學著自己重載一個。
1.視景體:
要觀察3D空間中的場景,最常用的方法是將一臺相機放置在世界的某個地方,然后通過特定的視景體觀察世界。這個視景體,它定義了相機能夠拍攝到的空間,也就是位于上面圖片中的遠裁剪面和近裁見面之間的范圍。
視景體的平面圖展示:
(1)遠、近裁剪面:
這兩個平面都垂直與觀察方向,決定了哪些物體將被渲染到圖像中。比遠裁剪面更遠或比近裁剪面更近的物體都不會被渲染。這也符合我們眼睛的視角。
2、世界坐標到相機坐標的變換:
兩步走,先平移,再旋轉。
(1)平移
這個可以采用上面局部坐標----->到世界坐標的方法,得知具體位置,采用for循環進行(加減)賦值,或者利用4D其次坐標(推薦,速度會更快)
4D其次矩陣
[1 0 0 0]
A= [0 1 0 0]
[0 0 1 0]
[-cam_x -cam_y -cam_z 1]
最后再將物體在世界的坐標位置乘上該矩陣,就可以得到了物體在相機坐標中的新位置。
新位置是有了,但得考慮下物體的朝向問題。
(2)旋轉
如上所示,經過第一步平移之后,相機位于世界坐標系原點,物體相對于相機的位置保持不變,但相機的觀察角仍為(0,ang_y,0)。要讓相機的朝向與+Z軸重合,只需將相機繞Y軸旋轉-ang_y,這是相機觀察角度(0,ang_y,0)的逆。
旋轉后,相機位于(0,0,0)處,觀察角度為(0,0,0),此時,物體與相機之間的相對位置和角度保持不變,變的只有物體在世界空間的絕對位置發生了改變。
那怎么設置旋轉呢?
相機朝向由3個角度指定(x,y,z),因此旋轉順序就會有6種:
xyz;
xzy;
yxz;
yzx;
zxy;
zyx;
無論采用哪種順序都可以,不過具體情況具體分析,加以分析,再進行抉擇,可能會達到事半功倍的效果。
應用例子(采用順序3吧)
yxz;先繞y軸(Ry),再繞x軸(Rx),最后再繞z軸(Rz)。
A仍為上面的世界坐標到相機坐標轉換的4D其次坐標表示
Tw=ARyRx*Rz
Train(練習):
//假定相機位于(cam_x,cam_y,cam_z)處 //觀察角度為(ang_x,ang_y,ang_z) //旋轉順序為yxz//平移矩陣 MATRIX4X4 Tcam_inv={{1,0,0,0},{0,1,0,0},{0,0,1,0},{-cam_x,-cam_y,-cam_z,1}};//現在計算繞x,y和z軸旋轉后的旋轉矩陣//繞x軸的逆旋轉矩陣 MATRIX4X4 Rcamx_inv{{1,0,0,0},{0,cos(-ang_x),sin(-ang_x),0},{0,-sin(-ang_x),cos(-ang_x),0},{0,0,0,1}}; //繞y軸的逆旋轉矩陣 MATRIX4X4 Rcamy_inv={{cos(-ang_y),0,-sin(-ang_y),0}, {0,1,0,0},{sin(-ang_y),0,cos(-ang_y),0},{9,0,0,1}}; //繞z軸的逆旋轉矩陣 MATRIX4X4 Rcamz_inv={{cos(-ang_z),sin(-ang_z),0,0},{-sin(-ang_z),cos(-ang_z),0,0},{0,0,1,0},{0,0,0,1}}; //計算出所有的逆旋轉矩陣,將它們相乘 //Tw=A*Ry*Rx*RzMATRIX4X4 Mtemp1,Mtemp2,Tcam; //注意下順序 Mat_Mul_4X4(&Tcam_inv,&Rcamy_inv,&Mtemp1); Mat_Mul_4X4(&Rcamx_inv,&Rcamz_inv,&Mtemp2); Mat_Mul_4X4(&Mtemp1,&Mtemp2,&Tcam);//開始執行世界坐標到相機坐標變換了 for(int vertex=0;vertex<8;vertex++) {Mat_Mul_VECTOR3D_4X4(world[vertex],&Tcam,_camera[vertex]); }如果再想將其速度再變快,可以手工進行計算,把計算結果存儲進去。
休息會o(▽)q,聽說最近無雙8快出了,畫質還不錯哦,戰場的自由度更高
消除、消隱
像上面的圖,如果不進行背面消除和消隱,那么看里面的物體的時候就會看見所有面,很別扭,而且也不符合現實生活中看物體的視覺變換,看物體的主視圖居然還能看見該物體的左視圖和俯視圖,這可真讓人摸不著頭腦。
所以背面消除和消隱就很重要了。
為了實現背面消除,Direct3D需要區分哪些多邊形是正面朝向的,哪些是背面朝向的。在默認狀態下,Direct3D認為頂點排列順序為順時針(相機坐標系)三角形單元是正面朝向的,頂點排列順序為逆時針,則三角形單元是背面朝向的。
D3D9提供一個API來進行背面消除,可以通過修改繪制狀態使用D3DRS_CULLMODE來達到效果。
繼續跟著“大師”進行重載
背面消除是測試在世界空間中進行的,測試還未執行世界坐標到相機坐標變換,用于避免對那些由于被遮住而從視點看不到的多邊形進行世界坐標到相機坐標變換。
工作原理:以某種方式(順時針或者逆時針)對構成每個物體的所有多邊形進行標記,然后計算每個多邊形的面法線n,并根據觀察向量對這條法線進行測試。當面法線和觀察向量之間的夾角<=90°,則多邊形對觀察者而言是可見的(不過,對于雙面多邊形,這個方法就不太管用了)。
采用點積來實現。
當且僅當向量u和v的夾角為90°(π/2弧度),uv=0;可見
當且僅當向量u和v的夾角<90°,uv>0;可見
當且僅當向量u和v的夾角>90°,u*v<0;不可見
現在已經解決了大多數多邊形的消除問題,接下來對剩下的物體進行世界坐標到相機坐標變換,之前的背面消除是建立在世界坐標系中的;現在執行包圍球測試。
包圍球測試的工作原理:對于世界空間中的每個物體,創建一個將其包圍起來的球體,如下圖所示,然后,只對球心(單個點)執行世界坐標到相機坐標變換,并判斷球體是否位于視景體內,如果不在視景體內,則丟棄它包圍的整個整體。當然如果球體的一部分在視景體內,這種測試就無法得出結論,需要再做其他測試。
假設有一個物體O,它包含一組頂點,要計算包圍球,可找出哪個頂點離物體中心最遠,該頂點與中心之間的距離就是包圍球的半徑。計算出半徑后,可以通過一些計算創建包圍球:給定物體中心p0的世界坐標(x0,y0,z0)及其半徑_radius,對中心點p0進行世界坐標到相機坐標變換:p1=p0*Tw。
Tw是上面所得出的從世界坐標到相機坐標的變換矩陣,p1是物體的包圍球球心的相機坐標,接下來定義6個點,它們與p1之間的連線分別平行于±x軸,±y軸和±z軸,到p1的距離都為_radius;這些點定義了一個以p1位球心的包圍球S,這樣,便可以通過一些簡單測試來判斷包圍球是否在視景體內。
光照系統
光源是在世界坐標系中定義的,但必須在相機坐標方可使用。在相機坐標系中,光源照亮場景中的物體,從而可以獲得較為逼真的顯示效果。
裁剪
在進行背面消除的同時,差點忘了考慮裁剪這一步驟,將位于視景體外的幾何體剔除掉。如有兩個點:p1(x1,y1,z1)和p2(x2,y2,z2),它們定義了一條線段,這條線段的一部分位于視景體內。
繪制該直線時,需要使用視景體對其進行裁剪。這一步很重要,因為所有完全位于視景體內的物體都將被投影和渲染,同樣所有完全位于視景體外的物體都將被剔除。然而,對于部分落在視景體內的幾何體,必須對其進行裁剪,就像在2D窗口中裁剪直線和位圖一樣。
就對這個直線進行裁剪,處理方式有兩種。
1、對直線執行投影變換(假設兩個端點的z坐標都大于0),到3D流水線的光柵化階段,再在2D空間內使用窗口對直線進行裁剪,再使用屏幕空間或視口對它們進行裁剪。
雖然易于實現,但這也極大地增加處理器的負載,(即便現在的CPU越來越強,還是能盡量減輕越好,這樣就可以把占用CPU的時間騰出來,讓CPU完成更為強大的工作)。
2、在物體空間(即物體或幾何體所在的數學空間)中進行。在對幾何體執行投影變換之前,要執行物體空間裁剪,必須在3D空間內使用視景體對所有不完全位于視景體內的幾何體(直線或多邊形)進行裁剪。
如圖所示,其中遠裁剪面為z=far_z,近裁剪面為z=near_z,投影面/視平面為d=1。
點p1~p6定義了視景體在x-z平面上的2D投影圖,它們的y坐標為0:
p1(-far_z,0,far_z)
p2(far_z,0,far_z)
p3(near_z,0,near_z)
p4(1,0,1)
p5(-1,0,1)
p6(-near_z,0,near_z)
d值為1,將這些點進行投影變換,即將每個分量乘以d再除以z坐標,得到(x-z平面):
p1(-far_z/far_z,0/far_z,far_z)=(-1,0,far_z)
p2(far_z/far_z,0/far_z,far_z)=(1,0,far_z)
p3(near_z/near_z,0/near_z,near_z)=(1,0,near_z)
p4(1/1,0/1,1)=(1,0,1)
p5(-1/1,0/1,1)=(-1,0,1)
p6(-near_z/near_z,0/near_z,near_z)=(-1,0,near_z)
現在繪制出新點p1~p6,變換后為矩形,這樣裁剪起來將很容易。在y-z平面進行一樣的分析,得到類似的結果,將結果合并起來,將得到一個長方體視景體。
投影
現在我們的目標是將所獲取的3D場景進行2D表示。 在Direct3D中,投影變換定義了視域體,并負責將視域體中的幾何體投影到投影窗口上,即可完成3D場景到2D窗口的表示。 根據視域體的描述信息創建一個投影矩陣 D3DXMATRIX *D3DXMatrixPerspectiveFOVLH(D3DXMATRIX &pOut,FLOAT fovY,FLOAT Aspect, //縱橫比FLOAT zn, //近裁面FLOAT zf); //遠裁面設置近裁面到坐標原點的距離是1,遠裁面到原點的距離是1000,采用如下方法進行設置:
D3DXMATRIX proj; D3DXMatrixPerspectiveFOVLH(&proj,PI*0.5f,(float)width/(float)height, 1.0f,1000.0f); Device->SetTransform(D3DTS_PROJECTION,&proj);學習重載函數:
投影面:
了解透視變換:指的是將物體的頂點投影到投影面上,給定視平面與投影點(相機視點)之間的距離d,可以很容易計算出物體上任何一點和視點之間的連線與視平面的交點。
將點p(y0,z0)投影到視平面上,后者離原點的距離為d,其平面方程為z=d。利用相似三角形原理。
d/z0=yp/y0
yp=dy0/z0
同樣在x-z平面上
d/z0=xp/x0
xp=dx0/z0
再將上述公式組合在一起,可以得到視平面z=d,視點位于(0,0,0)處時的投影變換:
x=dx/z;
y=dy/z;
再次總結下,創建一個完全基于矩陣的系統,還是采用4D齊次坐標來執行投影變換
[1 0 0 0]
Tp= [0 1 0 0]
[0 0 1 1/d]
[0 0 0 0]
進行驗證,使用相機坐標(xc,yc,zc,1)的點p來進行檢驗
pTp=[xc yc zc (zc/d)]
再將分量分別處以分量w(zc/d),將齊次坐標轉換為常規坐標,結果如下
[xdd/zc, ycd/zc, zcd/zc]
不考慮坐標z,則表示為
x=xcd/zc;
y=ycd/zc;
屏幕坐標:
終于快到終點了——屏幕坐標,也就是將頂點坐標從投影窗口轉換到屏幕的一個矩形區域中。 在Direct3D中,屏幕用結構D3DVIEWPORT9來表示 typedef struct D3DVIEWPORT9(DWORD X; //相對屏幕位置DWORD Y;DWORD Width; //矩形區域的大小DWORD Height;DWORD Minz; //深度緩存的值DWORD Maxz}D3DVIEWPORT9; //這樣來設置屏幕窗口 D3DVIEWPORT9 vp={0,0,640,480,0,1}; Device->SetViewport(&vp);這樣就可以完成屏幕坐標的轉換了。
最后一步光柵化,即可完成流水線操作。
總結
以上是生活随笔為你收集整理的再探Direct3D流水线的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 配置 hosts 浏览器访问仍然不生效
- 下一篇: 类似于萝卜书摘的书摘app推荐