轉載請註明出處:http://www.cnblogs.com/Ray1024

一、概述

Direct3D中很多複雜的幾何效果都是由基本的幾何體組合而成的,這篇文章中,我們來學習集中常見的基本幾何體的繪製方法。

二、準備工作

我們使用一個類來組織這些繪製基本幾何體的程式碼,以方便我們以後的使用。GeometryGenerator是一個工具類,用於生成諸如網格、球、圓柱體、盒子之類的幾何形狀,此係列的其他示例中都會用到這些形狀。這個類在系統記憶體中生成資料,我們必須將這些資料複製到頂點和索引緩衝中。GeometryGenerator這個類使用的資料結構如下:

class GeometryGenerator
{
public:
struct Vertex
{
Vertex(){}
Vertex(const XMFLOAT3& p, const XMFLOAT3& n, const XMFLOAT3& t, const XMFLOAT2& uv)
: Position(p), Normal(n), TangentU(t), TexC(uv){}
Vertex(
float px, float py, float pz,
float nx, float ny, float nz,
float tx, float ty, float tz,
float u, float v)
: Position(px,py,pz), Normal(nx,ny,nz),
TangentU(tx, ty, tz), TexC(u,v){} XMFLOAT3 Position;
XMFLOAT3 Normal;
XMFLOAT3 TangentU;
XMFLOAT2 TexC;
}; struct MeshData
{
std::vector<Vertex> Vertices;
std::vector<UINT> Indices;
};

};

GeometryGenerator建立的某些頂點資料在後面的學習中才會用到,這個本文中不會用到,所以也無需將這些資料複製到頂點緩衝中。MeshData結構體用於儲存頂點和索引的集合列表。Vertex結構體有四個成員,我們這篇文章中只使用第一個Position,其他的成員以後會介紹。

三、繪製基本幾何體

2.1 網格

首先來講解生成三角形網格的方法。網格是這些基本幾何體當中最重要的,其應用範圍很廣,這種幾何體在實現地形渲染和水體渲染時非常有用。

我們下面來建立xz平面上的網格。一個包含m×n個頂點的網格可以生成(m − 1)× (n− 1)個單元格,如下圖所示。每個多邊形由兩個三角形組成,一共2×(m − 1)× (n− 1)個三角形。設網格寬度為w、深度為d,則x軸、z軸方向上的單元格間距分別為為dx = w/(n-1)和dz=d/(m-1)。我們從左上角開始生成頂點,逐行計算每個頂點的座標。在xz平面上,第ij個網格頂點的座標為 vij= (−0.5w + j ∙ dx , 0.0 , 0.5d – i ∙ dz)。

我們可以生成網格頂點了,下面是程式碼:

void GeometryGenerator::CreateGrid(float width, float depth, UINT m, UINT n, MeshData& meshData)
{
UINT vertexCount = m*n;
UINT faceCount = (m-1)*(n-1)*2; //
// 建立頂點
// float halfWidth = 0.5f*width;
float halfDepth = 0.5f*depth; float dx = width / (n-1);
float dz = depth / (m-1); float du = 1.0f / (n-1);
float dv = 1.0f / (m-1); meshData.Vertices.resize(vertexCount);
for(UINT i = 0; i < m; ++i)
{
float z = halfDepth - i*dz;
for(UINT j = 0; j < n; ++j)
{
float x = -halfWidth + j*dx; meshData.Vertices[i*n+j].Position = XMFLOAT3(x, 0.0f, z);
meshData.Vertices[i*n+j].Normal = XMFLOAT3(0.0f, 1.0f, 0.0f);
meshData.Vertices[i*n+j].TangentU = XMFLOAT3(1.0f, 0.0f, 0.0f); // Stretch texture over grid.
meshData.Vertices[i*n+j].TexC.x = j*du;
meshData.Vertices[i*n+j].TexC.y = i*dv;
}
}
}

在完成頂點的計算之後,我們必須通過索引來定義網格三角形。我們再次從左上角開始逐行遍歷每個四邊形,通過計算索引來定義構成四邊形的兩個三角形。如下圖所示,對於一個由m×n個頂點構成的網格來說,兩個三角形的線性陣列索引為:

△ABC = (i∙n+j , i∙n + j + 1, (i + 1) ∙n + j)

△CBD = ((i +1) ∙n + j , i∙n + j + 1 ∙ (i + 1) ∙n + j + 1)

下面是對應的程式碼:

meshData.Indices.resize(faceCount*3); // 3 indices per face

// 遍歷所有四邊形並計算索引
UINT k = 0;
for(UINT i = 0; i < m-1; ++i)
{
for(UINT j = 0; j < n-1; ++j)
{
meshData.Indices[k] = i*n+j;
meshData.Indices[k+1] = i*n+j+1;
meshData.Indices[k+2] = (i+1)*n+j; meshData.Indices[k+3] = (i+1)*n+j;
meshData.Indices[k+4] = i*n+j+1;
meshData.Indices[k+5] = (i+1)*n+j+1; k += 6; // next quad
}
}

有了頂點和索引的集合,網格就生成了。

2.2 圓柱

接下來我們要生成一個圓柱。

為了構建一個圓柱,需要提供如下資訊:圓柱的上口半徑(topRadius),下口半徑(bottomRadius),高度(height)。此外,為了指定圓柱的精細度,還需要指定兩個引數,一個為沒高度方向上平均劃分的個數(stack),另一個為沿圓周方向等分的個數(slice)。如果還是不理解,可以看下圖:

通過該圖就可以直觀地理解stack和slice的意義了。即stack為垂直方向上等分的個數,slice為在360度圓周上等分的個數。等分地越多,尤其是圓周上,其越接近圓形,即表面越光滑。

先來構建頂點。我們可以發現,把圓柱沿垂直方向等分後,圓柱可以看成是stack+1行的一系列點,每一行的點位於一定半徑的圓周上。通過slice可以算出一行中每個點所在的角度theta,特定一行可以通過topRadius和bottomRadius插值算出其半徑tmpRadius。這樣頂點的位置就可以算出來了。

依然是二維的迴圈,外圍迴圈為逐行遍歷,內迴圈為一行的圓周上所有點的遍歷。程式碼如下:

	float stackHeight = height / stackCount;

	// Amount to increment radius as we move up each stack level from bottom to top.
float radiusStep = (topRadius - bottomRadius) / stackCount; UINT ringCount = stackCount+1; // Compute vertices for each stack ring starting at the bottom and moving up.
for(UINT i = 0; i < ringCount; ++i)
{
float y = -0.5f*height + i*stackHeight;
float r = bottomRadius + i*radiusStep; // vertices of ring
float dTheta = 2.0f*XM_PI/sliceCount;
for(UINT j = 0; j <= sliceCount; ++j)
{
Vertex vertex; float c = cosf(j*dTheta);
float s = sinf(j*dTheta); vertex.Position = XMFLOAT3(r*c, y, r*s); vertex.TexC.x = (float)j/sliceCount;
vertex.TexC.y = 1.0f - (float)i/stackCount; // This is unit length.
vertex.TangentU = XMFLOAT3(-s, 0.0f, c); float dr = bottomRadius-topRadius;
XMFLOAT3 bitangent(dr*c, -height, dr*s); XMVECTOR T = XMLoadFloat3(&vertex.TangentU);
XMVECTOR B = XMLoadFloat3(&bitangent);
XMVECTOR N = XMVector3Normalize(XMVector3Cross(T, B));
XMStoreFloat3(&vertex.Normal, N); meshData.Vertices.push_back(vertex);
}
}

然後就是生成索引了:

	// Add one because we duplicate the first and last vertex per ring
// since the texture coordinates are different.
UINT ringVertexCount = sliceCount+1; // Compute indices for each stack.
for(UINT i = 0; i < stackCount; ++i)
{
for(UINT j = 0; j < sliceCount; ++j)
{
meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1); meshData.Indices.push_back(i*ringVertexCount + j);
meshData.Indices.push_back((i+1)*ringVertexCount + j+1);
meshData.Indices.push_back(i*ringVertexCount + j+1);
}
}

此外,我們發現該圓柱不包含頂部和底部的蓋子。框架庫中提供了新增頂部、底部蓋子的函式。其實方法很簡單,頂部和底部分別是slice個三角形而已,共享一箇中心頂點。相關程式碼可以在原始碼中進行參考。

2.3 球體

繪製球體,基本引數只有一個半徑。此外,與圓柱一樣,為了指定其精細等級,也需要提供stack和slice兩個引數,意義也相似。只是這裡slice不是在垂直方向上的等分,而是從上極點沿球面到下極點的180度角進行等分。通過slice和stack可以得出頂點的球面座標,因此可以算出其直角座標。

球面頂點的生成與圓柱一樣也分為兩步(尤其與圓柱很類似,我只給出基本思路,可以通過研究程式碼來理解):

  1. 不考慮上下兩個極點,與圓柱計算方法類似,生成球面(與圓柱的柱面頂點計算一樣)

  2. 把兩個極點及相應三角形新增進來,也可以想像成新增蓋子(與圓柱新增蓋子過程一樣)

相關程式碼如下:

void GeometryGenerator::CreateSphere(float radius, UINT sliceCount, UINT stackCount, MeshData& meshData)
{
meshData.Vertices.clear();
meshData.Indices.clear(); // 計算頂端的極端點,並且向下移動堆
// // 極端點:注意貼圖座標可能會扭曲,因為正方形貼圖對映到球體導致沒有合適的位置對映到極端點。
Vertex topVertex(0.0f, +radius, 0.0f, 0.0f, +1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 0.0f);
Vertex bottomVertex(0.0f, -radius, 0.0f, 0.0f, -1.0f, 0.0f, 1.0f, 0.0f, 0.0f, 0.0f, 1.0f); meshData.Vertices.push_back( topVertex ); float phiStep = XM_PI/stackCount;
float thetaStep = 2.0f*XM_PI/sliceCount; // 計算每個棧環的頂點(不將極端點視為環)
for(UINT i = 1; i <= stackCount-1; ++i)
{
float phi = i*phiStep; // 環的頂點
for(UINT j = 0; j <= sliceCount; ++j)
{
float theta = j*thetaStep; Vertex v; // 球面到笛卡爾座標系
v.Position.x = radius*sinf(phi)*cosf(theta);
v.Position.y = radius*cosf(phi);
v.Position.z = radius*sinf(phi)*sinf(theta); // Partial derivative of P with respect to theta
v.TangentU.x = -radius*sinf(phi)*sinf(theta);
v.TangentU.y = 0.0f;
v.TangentU.z = +radius*sinf(phi)*cosf(theta); XMVECTOR T = XMLoadFloat3(&v.TangentU);
XMStoreFloat3(&v.TangentU, XMVector3Normalize(T)); XMVECTOR p = XMLoadFloat3(&v.Position);
XMStoreFloat3(&v.Normal, XMVector3Normalize(p)); v.TexC.x = theta / XM_2PI;
v.TexC.y = phi / XM_PI; meshData.Vertices.push_back( v );
}
} meshData.Vertices.push_back( bottomVertex ); //
// 計算堆的索引。堆頂是頂點快取第一個資料,並且連線頂端的極端點到第一個環。
// for(UINT i = 1; i <= sliceCount; ++i)
{
meshData.Indices.push_back(0);
meshData.Indices.push_back(i+1);
meshData.Indices.push_back(i);
} //
// 計算內堆的索引。(不包括極端點) // 第一個頂點到第一個環的索引偏移
// 這裡僅僅跳過頂端的極端頂點
UINT baseIndex = 1;
UINT ringVertexCount = sliceCount+1;
for(UINT i = 0; i < stackCount-2; ++i)
{
for(UINT j = 0; j < sliceCount; ++j)
{
meshData.Indices.push_back(baseIndex + i*ringVertexCount + j);
meshData.Indices.push_back(baseIndex + i*ringVertexCount + j+1);
meshData.Indices.push_back(baseIndex + (i+1)*ringVertexCount + j); meshData.Indices.push_back(baseIndex + (i+1)*ringVertexCount + j);
meshData.Indices.push_back(baseIndex + i*ringVertexCount + j+1);
meshData.Indices.push_back(baseIndex + (i+1)*ringVertexCount + j+1);
}
} //
// 計算底堆的索引。底堆是最後寫到頂點快取的,並且連線低端的極端點和底端環
// // 南極端頂點是最後新增的
UINT southPoleIndex = (UINT)meshData.Vertices.size()-1; // 第一個頂點到最後一個環的偏移索引
baseIndex = southPoleIndex - ringVertexCount; for(UINT i = 0; i < sliceCount; ++i)
{
meshData.Indices.push_back(southPoleIndex);
meshData.Indices.push_back(baseIndex+i);
meshData.Indices.push_back(baseIndex+i+1);
}
}

2.4 立方體

最後一個,也是最簡單的一個,即立方體。一個立方體只需要提供三維方向上的長度即可,即width(X方向)、height(Y方向)、depth(Z方向)。有一點與之前繪製彩色立方體時不一樣的是,我們這裡構建立方體用到24個頂點(每個面4個)。而之前彩色立方體只用到了8個頂點(每個頂點被3個面共享)。這是因為在後面學習過程中我們需要頂點的法線座標,而一個頂點相對於其連線的3個面來說,法線完全不同,因此無法共享頂點。之前的例子由於只需要顏色資訊,我們讓其3個面在該頂點處共享了顏色值,因此只需要8個頂點即可。

索引建立與彩色立方體例子一樣,共36個索引值(每個麵包含兩個三角形,共6個索引值)。

由於立方體構建十分容易,程式碼就不在這裡列出了。

2.5 繪製效果

三、結語

到這裡,Direct3D基本幾何體的繪製我們就學習完了,以後我們就可以使用這些基本的幾何體來繪製一些複雜、有趣的圖形了。