轉載請注明出處: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基本幾何體的繪制我們就學習完了,以后我們就可以使用這些基本的幾何體來繪制一些復雜、有趣的圖形了。