*作者:蔡軍生
*出處:http://blog.csdn.net/caimouse/
************************************/
3D遊戲從入門到精通-16
4、 三角形帶列表
三角形顯示的方式總共分爲三種,前面已經學習最多的是三角形列表,現在再來看看三角形帶列表是什麼樣的,這樣有什麼優點呢?這裏顯示圖形如下:
上面的圖形可以看出,只有6個頂點就可以顯示4個三角形,而採用三角形列表的方式,只能顯示兩個三角形。因此採用這種方式就會大大提高渲染效率,減少佔用內存空間,減少佔用系統帶寬。
具體的程序如下:
HRESULT hr;
// 創建頂點緩衝區。
if( FAILED( hr = m_pd3dDevice->CreateVertexBuffer(
2*3 * sizeof(VT_CAIPRIMITIVE),
0, VT_CAIPRIMITIVE::dwFVF,
D3DPOOL_MANAGED, &pVB, NULL ) ) )
{
//創建頂點緩衝區失敗。
return DXTRACE_ERR( "CreateVertexBuffer", hr );
}
//
VT_CAIPRIMITIVE* pVertices;
if( FAILED( hr = pVB->Lock( 0, 0, (VOID**)&pVertices, 0 ) ) )
{
//鎖住頂點緩衝區。
return DXTRACE_ERR( "Lock", hr );
}
pVertices[0].vPosition = D3DXVECTOR3( -6.0f, -2.0f, 2.0f );
pVertices[0].crDiffuse = D3DCOLOR_COLORVALUE( 1.0, 0.0, 0.0, 1.0 );
pVertices[1].vPosition = D3DXVECTOR3( -4.0f, 2.0f, 2.0f );
pVertices[1].crDiffuse = D3DCOLOR_COLORVALUE( 1.0, 0.0, 0.0, 1.0 );
//
pVertices[2].vPosition = D3DXVECTOR3( -2.0f, -2.0f, 2.0f );
pVertices[2].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 1.0, 0.0, 1.0 );
pVertices[3].vPosition = D3DXVECTOR3( 0.0f, 2.0f, 0.0f );
pVertices[3].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 1.0, 0.0, 1.0 );
//
pVertices[4].vPosition = D3DXVECTOR3( 2.0f, -2.0f, 2.0f );
pVertices[4].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 0.0, 1.0, 1.0 );
pVertices[5].vPosition = D3DXVECTOR3( 4.0f, 2.0f, 2.0f );
pVertices[5].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 0.0, 1.0, 1.0 );
//解鎖頂點緩衝區。
pVB->Unlock();
這段程序先創建6個頂點的緩衝區,然後依次地設置6個頂點的座標和頂點混合的顏色。然後再調用下面的代碼來顯示:
m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
m_pd3dDevice->SetStreamSource( 0, m_pvbTriangleStrip, 0, sizeof(VT_CAIPRIMITIVE) );
m_pd3dDevice->SetFVF( VT_CAIPRIMITIVE::dwFVF );
m_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLESTRIP, 0, m_nTriangleStripCount );
第一行先設置顯示方式爲線框圖(D3DFILL_WIREFRAME),採用這種方式,很清楚地看到有多少個三角形被顯示出來。最後一行調DrawPrimitive來顯示,顯示方式是D3DPT_TRIANGLESTRIP方式顯示。
3D遊戲從入門到精通-17
5、 三角形扇形列表
最後一種三角形列表顯示是扇形排列的三角形列表。這種排列方式,有一個特別的地方,就是所有三角形共享一個頂點,這樣特別適合構成曲面的地方,比如球體和圓形的東西。下面看看這個例子顯示的圖:
通過上面這個圖,可以看到4個三角形共享一個頂點,這個頂點是紅顏色的,其它不同的頂點不同的顏色。下面來看看程序是怎麼樣實現它的顯示的:
HRESULT hr;
// 創建頂點緩衝區。
if( FAILED( hr = m_pd3dDevice->CreateVertexBuffer(
2*3 * sizeof(VT_CAIPRIMITIVE),
0, VT_CAIPRIMITIVE::dwFVF,
D3DPOOL_MANAGED, &pVB, NULL ) ) )
{
//創建頂點緩衝區失敗。
return DXTRACE_ERR( "CreateVertexBuffer", hr );
}
//
VT_CAIPRIMITIVE* pVertices;
if( FAILED( hr = pVB->Lock( 0, 0, (VOID**)&pVertices, 0 ) ) )
{
//鎖住頂點緩衝區。
return DXTRACE_ERR( "Lock", hr );
}
pVertices[0].vPosition = D3DXVECTOR3( 0.0f, 0.0f, 0.0f );
pVertices[0].crDiffuse = D3DCOLOR_COLORVALUE( 1.0, 0.0, 0.0, 1.0 );
pVertices[1].vPosition = D3DXVECTOR3( -2.0f, 0.0f, 0.0f );
pVertices[1].crDiffuse = D3DCOLOR_COLORVALUE( 1.0, 0.0, 0.0, 1.0 );
//
pVertices[2].vPosition = D3DXVECTOR3( -sqrt(2.0f), sqrt(2.0f), 0.0f );
pVertices[2].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 1.0, 0.0, 1.0 );
pVertices[3].vPosition = D3DXVECTOR3( 0.0f, 2.0f, 0.0f );
pVertices[3].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 1.0, 0.0, 1.0 );
//
pVertices[4].vPosition = D3DXVECTOR3( sqrt(2.0f), sqrt(2.0f), 0.0f );
pVertices[4].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 0.0, 1.0, 1.0 );
pVertices[5].vPosition = D3DXVECTOR3( 2.0f, 0.0f, 0.0f );
pVertices[5].crDiffuse = D3DCOLOR_COLORVALUE( 0.0, 0.0, 1.0, 1.0 );
//解鎖頂點緩衝區。
pVB->Unlock();
上面程序先創建6個頂點緩衝區空間,然後分別設置6個頂點的座標值。當使用D3DPT_TRIANGLEFAN顯示時,它的第一個頂點就是扇形的圓心,其它的頂點就相當圓周邊上的點了,並且每個三角形的頂點都是按順時針方向排列的,也就是按左手座標系的方式來排列的。
最後按下面的方式來顯示這4個三角形:
m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);
m_pd3dDevice->SetStreamSource( 0, m_pvbTriangleFan, 0, sizeof(VT_CAIPRIMITIVE) );
m_pd3dDevice->SetFVF( VT_CAIPRIMITIVE::dwFVF );
m_pd3dDevice->DrawPrimitive( D3DPT_TRIANGLEFAN, 0, m_nTriangleFanCount );
上面還是使用線框圖來渲染出來,這樣更容易分清楚它們是怎麼樣顯示的,總共有多少個三角形顯示。在DrawPrimitive函數裏使用D3DPT_TRIANGLEFAN方式顯示,就是扇形的方式顯示。
在三角形顯示中,D3D就提供三種方式顯示,那麼什麼時候使用它們是合適的呢?其實很多建模軟件都作出合適的選擇,減少頂點佔用空間,提高渲染效率。
3D遊戲從入門到精通-18
2.10.1矩陣變換
在3D遊戲裏,要表達不同的東西,每樣東西都在不同的位置。比如構造一個間教室,那麼就需要根據黑板、講臺、座位來不同位置來放置。由於所有模型座標都是局部座標,都需要變換到世界座標,才顯示出正確位置。還有遊戲裏的很多物體是動起來的,就是位置會變化,大小會變化。比如模擬一輛小車開過馬路,那麼這輛車就需要水平運動起來,車輪還需要旋轉起來。仔細看一下車輪,它不但作平移運動,還需要作自轉運動。要表達物體在不同的位置,需要使用到矩陣。要表達物體旋轉運動和平移運動,也需要使用矩陣。由n元線性方程組的系統數組成的m行n列的數表,就叫做矩陣。如下圖所示:
由於在三維裏的座標變換,都是線性變換,所以可以使用矩陣來表示三維變換。本來三維座標,只有三個座標軸,寫出來的線性方程也只有三個的。但爲了方便把所有的座標變換統一到一種矩陣表達方式,就使用了4×4的矩陣來表示變換,而不是採用3×3的矩陣變換。在D3D裏表示如下:
typedef struct _D3DMATRIX {
union {
struct {
float _11, _12, _13, _14;
float _21, _22, _23, _24;
float _31, _32, _33, _34;
float _41, _42, _43, _44;
};
float m[4][4];
};
} D3DMATRIX;
矩陣在3D遊戲裏是怎麼使用的呢?下面來看看這個例子,它已經把矩陣基本特性都使用了。在自然界裏,都多東西都是變化的,運動的。在我們生活的太陽系裏,在很多年前就知道太陽是自轉的,地球有自轉也有公轉,而月亮也自轉,還有繞着地球公轉和太陽公轉。要表達太陽的自轉,就需要使用矩陣來旋轉它的模型。而表達地球的自轉,也需要使用矩陣來旋轉它的模型,並且還需要使用平移的矩陣,然後再旋轉它,這樣纔會讓地球繞着太陽轉。而月亮是最複雜的,不但自己要自轉,還要繞着地球公轉和太陽公轉,這樣月亮就需要多次使用旋轉矩陣和平移矩陣。
在D3D裏可以使用矩陣實現物體的縮放、旋轉、平移。在三維座標裏,任何的點(x,y,z)變換到(x', y', z'),都可以使用4×4的矩陣來計算出來,像下面這樣計算它:
3D遊戲從入門到精通-19
縮放矩陣
模型比較大時,就需要把它縮小,這樣就需要使用到縮放矩陣。縮放矩陣如下所示:
其中的S就是縮放係數,如果要放大,就需要設置S大於0。如果要縮小,就要設置S小於1大於0。D3D裏已經準備好一個設置這樣縮放矩陣的函數,它就是
D3DXMATRIX * D3DXMatrixScaling(
D3DXMATRIX * pOut,
FLOAT sx,
FLOAT sy,
FLOAT sz
);
函數的第一個參數就是返回縮放矩陣,第二個參數是沿着X軸的縮放係數,第三個參數是沿着Y軸的縮放系統,第四個是沿着Z軸的縮放係數。如果三個縮放係數都相同的,就是等比例縮放,否則就是不等比例的縮放。比如需要設置一個等比例放大兩倍的矩陣,如下:
D3DXMATRIX mScale;
D3DXMatrixScaling(&mScale,2,2,2);
就可使用mScale來放大物體的模型了。
平移矩陣
在3D裏平移矩陣,應是使用最多的矩陣,由於每個物體相對位置,總是通過平移矩陣來變換出來。物體的整體移動,就是座標點的移動。比如從點(x,y,z)移動到新的位置(x', y', z'),就可以使用下面的矩陣乘法實現:
= ×
上面的表示了X軸,Y軸,Z軸的平移距離。在D3D提供了平移函數D3DXMatrixTranslation,使用這個函數就可以設置一個平移矩陣。它的具體參數如下:
D3DXMATRIX * D3DXMatrixTranslation(
D3DXMATRIX * pOut,
FLOAT x,
FLOAT y,
FLOAT z
);
第一個參數是返回平移矩陣,第二個參數是X軸的平移量,第三個參數是Y軸的平移量,第四個參數是Z軸的平移量。
比如要沿着Z軸的正方向平移5個單位,就需要按下面來設置平移矩陣:
D3DXMATRIX mTrans;
D3DXMatrixTranslation(&mTrans,0,0,5);
旋轉矩陣
在現實世界裏,有很多東西是轉動的。比如汽車的車輪是轉動的,電風扇也是轉動的。當你要3D世界裏顯示它們時,就需要使用旋轉矩陣。在3D世界裏,我們使用有三個座標軸的座標系,因此最簡單的旋轉就是繞着座標軸旋轉。它們也是通過矩陣變換來實現,具體如下:
這個矩陣變換就是繞着X軸旋轉,想要生這樣的矩陣,使用D3D的函數D3DXMatrixRotationX,就可生成上面矩陣。它的具體參數如下:
D3DXMATRIX * D3DXMatrixRotationX(
D3DXMATRIX * pOut,
FLOAT Angle
);
第一個參數是返回繞着X軸旋轉的矩陣,第二個參數是繞着X軸轉動的角度,這裏的單位是弧度,正的弧度是表示沿着X軸的正方向向原點看去時,物體是順時針方向旋轉。
下面是繞着Y軸旋轉的變換矩陣:
任何一個點只要通過這個矩陣變換,就是繞着Y軸旋轉。單位與方向判斷都是跟X軸一樣。
在D3D裏的具體函數如下:
D3DXMATRIX * D3DXMatrixRotationY(
D3DXMATRIX * pOut,
FLOAT Angle
);
第一個參數是返回繞着Y軸旋轉的矩陣。第二個參數是繞着Y軸旋轉角度。
下面是繞着Z軸旋轉的變換矩陣:
任何一個點只要通過這個矩陣變換,就是繞着Z軸旋轉。單位與方向判斷是跟X軸一樣的。
在D3D裏的具體函數如下:
D3DXMATRIX * D3DXMatrixRotationZ(
D3DXMATRIX * pOut,
FLOAT Angle
);
第一個參數是返回繞着Z軸旋轉的矩陣。第二個參數是繞着Z軸旋轉的角度。
上面三個矩陣只是實現了繞着三個座標軸的旋轉,如果要繞着任意軸的旋轉,還需要其它函數來構造變換矩陣,比如通過向量來構造,或者通過四元數計算後再變換爲矩陣。這些都很複雜的內容,以後再慢慢去掌握。
3D遊戲從入門到精通-20
有了上面繞着座標軸旋轉的矩陣,就可以輕鬆地構造我們的太陽系了。在我們太陽系中,太陽是自轉的,在這個例子裏要就要讓太陽繞着Y軸自轉。在讓太陽旋轉之前,就要先創建太陽這個物體,在這裏我採用一個大圓球代表太陽。創建太陽圓球的程序如下:
LPD3DXMESH m_pMeshSun; //太陽。
D3DXMATRIX m_mSunSpin; //自轉矩陣
這裏使用類成員變量m_pMeshSun保存太陽的三角形列表,類成員m_mSunSpin保存自轉矩陣。然後調用D3DXCreateSphere函數來創建太陽,這個函數就是創建一個球體的三角形網格。它具體參數如下:
HRESULT D3DXCreateSphere(
LPDIRECT3DDEVICE9 pDevice,
FLOAT Radius,
UINT Slices,
UINT Stacks,
LPD3DXMESH * ppMesh,
LPD3DXBUFFER * ppAdjacency
);
第一個參數pDevice是D3D設備。第二個參數Radius是球半徑長度。第三個參數是連接主軸兩端的線條數,也就是經度線的條數。第四個參數是緯度線的條數。第五個參數是創建返回的球體三角形列表。第六個參數是ID3DXBuffer指針。最後如下面調用它:
//創建太陽。
HRESULT hr = D3DXCreateSphere(m_pd3dDevice, 1.0f, 30, 30, &m_pMeshSun, NULL);
if (hr != D3D_OK)
{
return hr;
}
這裏創建弧度爲1.0,經度線是30條,緯度線是30條的球體。接着下來創建地球的模型,當然也是採用上面的球體,創建月亮也是一樣的。代碼如下:
//創建地球。
hr = D3DXCreateSphere(m_pd3dDevice,1.0f,20,20,&m_pMeshEarth,NULL);
if (hr != D3D_OK)
{
return hr;
}
//創建月亮.
hr = D3DXCreateSphere(m_pd3dDevice,1.0f,10,10,&m_pMeshMoon,NULL);
if (hr != D3D_OK)
{
return hr;
}
這裏創建地球和月亮是一樣的大小,但它們的經度線和緯度線的條數是不一樣。這樣表現出來的結果是地球的三角形總數多於月亮的三角形總數,看起來就更加像圓形。由於後面會把月亮縮小,這樣看起來就沒有太大的差別了。
太陽、地球和月亮都已經創建好模型,接着下來的事情,就是怎麼樣讓太陽自轉,怎麼樣讓地球繞着太陽轉和自轉,怎麼樣讓月亮繞着地球和太陽轉,以及它自己的自轉。下面先來看看怎麼實現地球自轉:
//
//顯示太陽爲自轉。
//
//縮放矩陣
D3DXMATRIX mSunScale;
D3DXMatrixScaling(&mSunScale,1.5,1.5,1.5);
//轉動90度。
D3DXMATRIX mSunRotationX;
D3DXMatrixRotationX(&mSunRotationX,D3DX_PI/2);
//自轉
D3DXMATRIX mSunRotation;
D3DXMatrixRotationY(&mSunRotation,fTime);
m_mSunSpin *= mSunRotation;
//組合所有矩陣。
D3DXMATRIX mSun = mSunScale*mSunRotationX*m_mSunSpin;
m_pd3dDevice->SetTransform( D3DTS_WORLD, &mSun );
m_pMeshSun->DrawSubset(0);
這段代碼不是很長,看下來很快的。由於上面創建的太陽模型太小了,那麼有什麼辦法可以把太陽放大呢?當然是有的,前面已經學習過使用矩陣可以縮放模型,那就先拿來試用一下,是否真的可以縮放物體呢?調用D3DXMatrixScaling函數,並且在X,Y,Z軸都進行放在1.5倍,這樣就構造好縮放矩陣mSunScale。又因爲創建的太陽的主軸是跟Z軸平行的,那麼在屏幕裏看的就是主軸的方向。我想把它轉到與Y軸平行,那麼就需要繞着X軸旋轉90度,這樣就可以與Y軸平行了。要實現太陽的自轉,也就是需要太陽繞着Y軸來旋轉,因此就需要構造一個繞着Y軸轉動的矩陣。調用函數D3DXMatrixRotationY,設置好返回矩陣mSunRotation,每次轉動的弧度fTime,這樣就可以構造出轉動矩陣。然而這個矩陣只是每次轉動的增量,那麼怎麼樣才能實現持續地轉動呢?這裏採用保存旋轉矩陣做法,m_mSunSpin成員變量就是保存每次轉動後的變換矩陣,只要每次改變這個矩陣轉動,就相當於太陽在自轉了。m_mSunSpin矩陣與mSunRotation矩陣相乘,就是把太陽每次都轉動一點。到這裏,已經把所有變換的矩陣構造出來,但是還沒有把所有變換組合到一起,其實非常簡單,只要把前面所有矩陣按順序地作乘法,就行了。但是要注意一點,就是矩陣的乘法不滿足交換律,不能隨便更換乘數的次序,否則結果就與想表達的內容大不一樣了。矩陣的組合有很多優點,可以把許多變換組合到一個矩陣裏,只作一次變換計算,就達到目標了。在程序後面,就是把這個複合矩陣mSun設置到世界變換矩陣裏,這樣調用DrawSubset顯示出來的太陽,就達到自轉的目的了。
接着下來,就需要計算地球的公轉和自轉。
//
//顯示地球的自轉和公轉。
//
D3DXMATRIX mEarthScale;
D3DXMatrixScaling(&mEarthScale,0.5f,0.5f,0.5f);
//自轉
D3DXMATRIX mEarthRotation;
D3DXMatrixRotationY(&mEarthRotation,1.5f*fTime);
m_mEarthSpin *= mEarthRotation;
//平移矩陣
D3DXMATRIX mEarthTranslation;
D3DXMatrixTranslation(&mEarthTranslation,0,0,5);
//繞太陽公轉矩陣
D3DXMATRIX mEarthRotSun;
D3DXMatrixRotationY(&mEarthRotSun,fTime);
m_mEarthRotSun *= mEarthRotSun;
D3DXMATRIX mEarth = mEarthScale*m_mEarthSpin*mEarthTranslation*m_mEarthRotSun;
m_pd3dDevice->SetTransform( D3DTS_WORLD, &mEarth );
m_pMeshEarth->DrawSubset(0);
由於地球的模型比較大,就得想辦法縮小它,這樣就需要使用D3D提供的縮放矩陣函數D3DXMatrixScaling。這個函數前面已經作了詳細的介紹,這裏是把地球模型各向同性地縮小一半。地球的大小解決了,那麼地球的自轉和公轉是先計算那個先的呢?由於公轉是繞着太陽轉的,也就是說需要繞着世界座標原點來轉動。而自轉只是繞着自己模型座標系裏的原點來轉動,因此,就需要把自轉放在前面,把公轉放在後面計算。上面使用函數D3DXMatrixRotationY實現自轉矩陣的計算,然後再這個矩陣旋轉變換累加起來,而這個累加就是使用矩陣乘法來實現。最後m_mEarthSpin矩陣就是保存了從模型座標系變換到世界座標的旋轉矩陣,這樣就構造完了自轉矩陣,其實是跟太陽的自轉矩陣是一樣的。
下面再來看看怎麼樣實現地球的公轉?由於地球的位置是在世界座標系的原點,而太陽的位置也是在世界座標系裏的原點,要想地球繞着太陽轉,就需要把地球位置平移到另外一個位置上,也就是說地球到原點的距離要大於太陽半徑與地球半徑之和。在這裏使用平移函數D3DXMatrixTranslation,並設置參數在Z軸的正方向上平移5個單位。接着再計算繞着原點旋轉矩陣,在這裏同樣是採用函數D3DXMatrixRotationY來構造繞着Y軸旋轉的矩陣mEarthRotSun,然後再累加到m_mEarthRotSun矩陣裏。最後把縮放矩陣mEarthScale、地球自轉矩陣m_mEarthSpin、地球平移矩陣mEarthTranslation、地球繞着太陽旋轉矩陣m_mEarthRotSun作乘法運算,就相當把所有變換都計算了,再設置到世界座標矩陣裏,然後顯示出來的地球,就會不斷旋轉和自轉。在這裏的變換一定搞清楚,如果矩陣順序搞錯了,就會顯示不同的結果。
最後來看看怎麼樣實現更加複雜的月亮轉動。實現代碼如下:
//
//顯示月亮。
//
D3DXMATRIX mMoonScale;
D3DXMatrixScaling(&mMoonScale,0.2f,0.2f,0.2f);
//自轉
D3DXMATRIX mMoonRotation;
D3DXMatrixRotationY(&mMoonRotation,3*fTime);
m_mMoonSpin *= mMoonRotation;
//離地球平移矩陣
D3DXMATRIX mMoonTranslationEarth;
D3DXMatrixTranslation(&mMoonTranslationEarth,0,0,1);
//繞地球公轉矩陣
D3DXMATRIX mMoonRotEarth;
D3DXMatrixRotationY(&mMoonRotEarth,2.5f*fTime);
m_mMoonRotEarth *= mMoonRotEarth;
//繞太陽公轉矩陣
m_mMoonRotSun *= mEarthRotSun;
D3DXMATRIX mMoon = mMoonScale*m_mMoonSpin*mMoonTranslationEarth*m_mMoonRotEarth;
mMoon *= mEarthTranslation*m_mMoonRotSun;
m_pd3dDevice->SetTransform( D3DTS_WORLD, &mMoon );
m_pMeshMoon->DrawSubset(0);
這裏跟地球的矩陣變換有很大一部分是相同的,由於月亮比地球小,因此就需要把月亮模型縮得比地球還要小一點。使用函數D3DXMatrixScaling各向性地縮放0.2倍,而地球是0.5倍,這樣月亮就比地球小一半以上。接着下來就是計算月亮的自轉矩陣m_mMoonSpin,後面接着計算繞着地球旋轉矩陣m_mMoonRotEarth,而月亮是跟地球一起繞着太陽公轉。因此採用地球繞着太陽旋轉的矩陣來計算月亮的公轉矩陣m_mMoonRotSun。但這裏還需要注意一點的,就是月亮同樣跟着地球作一個平移運算。在最後的組合矩陣變換時,就是月亮的縮放矩陣mMoonScale、自轉矩陣m_mMoonSpin、月亮與地球的平移矩陣mMoonTranslationEarth、月亮與地球公轉矩陣m_mMoonRotEarth、地球與太陽的平移矩陣mEarthTranslation、月亮繞着太陽公轉矩陣m_mMoonRotSun。通過這樣一系列的變換,就可以實現月亮運行。