Direct3D11學習:(九)繪制基本幾何體


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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM