Direct3D11学习:(九)绘制基本几何体

摘要:
};GeometryGenerator创建的某些顶点数据在后面的学习中才会用到,这个本文中不会用到,所以也无需将这些数据复制到顶点缓冲中。网格是这些基本几何体当中最重要的,其应用范围很广,这种几何体在实现地形渲染和水体渲染时非常有用。即stack为垂直方向上等分的个数,slice为在360度圆周上等分的个数。

转载请注明出处: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)。

Direct3D11学习:(九)绘制基本几何体第1张

我们可以生成网格顶点了,下面是代码:

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)

Direct3D11学习:(九)绘制基本几何体第2张

下面是对应的代码:

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)。如果还是不理解,可以看下图:

Direct3D11学习:(九)绘制基本几何体第3张

通过该图就可以直观地理解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绘制效果

Direct3D11学习:(九)绘制基本几何体第4张

三、结语

到这里,Direct3D基本几何体的绘制我们就学习完了,以后我们就可以使用这些基本的几何体来绘制一些复杂、有趣的图形了。

免责声明:文章转载自《Direct3D11学习:(九)绘制基本几何体》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇利用MSSQL排序规则,查询区分大小写的数据 枫.net c# 日期格式和常用处理下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

OpenGL实现多层绘制(Layered Rendering) [转]

http://blog.csdn.net/u010462297/article/details/50589991 引言 在某些情况下会需要用到多层绘制。FBO下有多个颜色挂接点(Color Attachment),可以用不同的挂接点挂接不同的纹理对象,实现绘制多张纹理(MRT),这在之前的文章里已经有所描述。但是有时候这种方法是不够好用的: - 当纹理非...

Shader编程学习笔记(九)—— Cg语言入门1

Cg入门一:输入输出和语义   本小节对Cg语言进行一个入门了解。   在以前的课程中,我们了解了渲染管线、顶点和片段程序和颜色计算等,在本小结中来了解一个简单的shader的顶点和片段程序的输入输出以及常用语义。   编写一个简单的顶点和片段着色器,代码如下: Shader "Lesson/vertex&fragment"{ SubSh...

Topology and Geometry in OpenCascade-Vertex

Topology and Geometry in OpenCascade-Vertex eryar@163.com 摘要Abstract:本文简要介绍了几何造型中的边界表示法(BRep),并结合程序说明OpenCascade中的边界表示的具体实现,即拓朴与几何的联系。对具有几何信息的拓朴结构顶点(vertex)、边(edge)、面(face)进行了详细说明...

并发编程概述--C#并发编程经典实例

优秀软件的一个关键特征就是具有并发性。过去的几十年,我们可以进行并发编程,但是难度很大。以前,并发性软件的编写、调试和维护都很难,这导致很多开发人员为图省事放弃了并发编程。新版.NET 中的程序库和语言特征,已经让并发编程变得简单多了。随着Visual Studio 2012 的发布,微软明显降低了并发编程的门槛。以前只有专家才能做并发编程,而今天,每一个...

实时数据集成

  企业应用集成 面向服务的体系结构 (SOA) 目前应该是一个很受欢迎的名词,中间件技术人员几乎到了言必称SOA的程度,数据集成当然也不例外,在Oracle openworld2008大会上,就推出了一堆数据集成的专场演讲,其中和SOA结合最紧密的就是实时数据集成 real time data integration。我总结了一下,实时数据集成一般分为两...

机器学习笔记——模型调参利器 GridSearchCV(网格搜索)参数的说明

GridSearchCV,它存在的意义就是自动调参,只要把参数输进去,就能给出最优化的结果和参数。但是这个方法适合于小数据集,一旦数据的量级上去了,很难得出结果。这个时候就是需要动脑筋了。数据量比较大的时候可以使用一个快速调优的方法——坐标下降。它其实是一种贪心算法:拿当前对模型影响最大的参数调优,直到最优化;再拿下一个影响最大的参数调优,如此下去,直到所...