https://blog.csdn.net/bonchoix/article/details/8394916
这次我们来学习几种常见的基本几何体的绘制方法,包含网格、球、圆柱等。很多复杂的几何图形都是由众多这些基本几何体组成的。而且,在水面渲染、地形渲染当中,都要使用到网格,因此掌握网格的基本生成方法很有必要。此外,有了这么多种几何体的绘制方法,我们在后面的程序中(学习导入3D模型之前)就可以绘制各种有趣图形了,而不是每次都是单调、乏味的立方体。。。
1. 程序框架新增内容
首先说下继上次实现的基本程序框架后,这次新增的内容。
当然,最大的更新显示是常见的几何体的绘制,包括立方体、网格、圆柱、球,实现了这些绘制算法,每次通过简单的一句调用,即可生成各种大小、不同的几何体了。
其次是鼠标的操作,为了对我们绘制的场景有更加直观的观察,我们从现在开始加入了鼠标的操作,通过鼠标可以方便地旋转场景、伸缩镜头来从各个角度、各个距离来观察绘制的场景。尽管这些功能依然很有限,但相比之前只能从一个角度观察场景,已经舒服多了。在后面我们会学习自己来实现一个第一人称的照相机,到时即可随意地在场景里面走动、观察了。
2. 基本几何体绘制方法
下面进入本文的重点,即这些几何体的绘制算法。
2.1 网格
网格应该是这些几何体当中最重要的一个了,其应用范围很广,包括水面、地形等。一般来讲,在生成一个网格之前,我们要指定该网格的宽和高(width、height),以及在宽、高上划分的格子数(m、n)。默认情况,也是绝大多数情况下,该网格位于x、z平面上,坐标系原点位于网格正中心。如下图所示:
图中宽为w,高为d。相应格子数为m、n。这样,每行顶点数为m+1,每列顶点数为n+1。格子宽、高分别为dx=width/m,dz=height/n。左上角顶点处坐标为(-0.5*width,0.5*height)。有了这些参数,整个网格上的顶点坐标就很容易生成了。我们可以通过一个二维的循环来实现,从左上角开始,向右、向下进行。
//每行顶点数、每列顶点数UINT nVertsRow = m + 1;UINT nVertsCol = n + 1;//起始x、z坐标float oX = -width * 0.5f;float oZ = height * 0.5f;//每一格纹理坐标变化float dx = width / m;float dz = height /n;for(UINT i=0; i<nVertsCol; ++i){ float tmpZ = oZ - dz * i; for(UINT j=0; j<nVertsRow; ++j) { UINT index = nVertsRow * i + j; mesh.vertices[index].pos.x = oX + dx * j; mesh.vertices[index].pos.y = 0.f; mesh.vertices[index].pos.z = tmpZ; }}
这样网格的顶点就生成了,下面是索引生成方法:
索引是为了组合成三角形而用的,因此我们的思路即逐个三角形进行。在网格中只存在一个个的矩形,我们需要把每个矩形拆成两个三角形。显然,沿对角线拆开即可。如下图所示:
我们依然用一个二维的循环,逐个遍历矩形(m*n个),如图为针对i行j列处的矩形,沿B、C拆成两个三角形。因此我们要构建的索引即针对ABC和BDC两个三角形。通过简单的数学计算,我们知道,对于 i 行 j 列(我们用[i,j]来表示)的矩形ABDC来说,四个顶点的坐标可以表示成:A[i,j]、B[i,j+1],C[i+1,j],D[i+1,j+1]。这样就可以算出各顶点在顶点缓存中所在的位置了:A(i * nVertsRow + j),B(i * nVertsRow + j + 1),C((i + 1)
* nVertsRow + j),D((i + 1) * nVertsRow + j + 1)。这样就可以创建出三角形ABC和BDC的索引了,代码如下:
//总格子数量:m * n//因此总索引数量: 6 * m * nUINT nIndices = m * n * 6;mesh.indices.resize(nIndices);UINT tmp = 0;for(UINT i=0; i<n; ++i){ for(UINT j=0; j<m; ++j) { mesh.indices[tmp] = i * nVertsRow + j; mesh.indices[tmp+1] = i * nVertsRow + j + 1; mesh.indices[tmp+2] = (i + 1) * nVertsRow + j; mesh.indices[tmp+3] = i * nVertsRow + j + 1; mesh.indices[tmp+4] = (i + 1) * nVertsRow + j + 1; mesh.indices[tmp+5] = (i + 1) * nVertsRow + j; tmp += 6; }}
该代码中即逐个矩形进行遍历,每个矩形包括两个三角形,共六个索引值。
有了顶点和索引的集合,网格就生成了。当然每个顶点除了位置坐标外还可以包括其他信息,比如法线、切线(用于后面的Bump Mapping),纹理坐标等,这些信息的生成可以参考附带的源代码。
2.2 圆柱
圆柱的生成其实和网格有一定的相似,毕竟,把圆柱的柱面展开后,其实就是一个网格。因此它们的索引构建基本是一样的。为了构建一个圆柱,需要提供如下信息:圆柱的上口半径(topRadius),下口半径(bottomRadius),高度(height)。此外,为了指定圆柱的精细度,还需要指定两个参数,一个为没高度方向上平均划分的个数(stack),另一个为沿圆周方向等分的个数(slice)。如果还是不理解,可以看下图:
通过该图就可以直观地理解stack和slice的意义了。即stack为垂直方向上等分的个数,slice为在360度圆周上等分的个数。等分地越多,尤其是圆周上,其越接近圆形,即表面越光滑。
先来构建顶点。我们可以发现,把圆柱沿垂直方向等分后,圆柱可以看成是stack+1行的一系列点,每一行的点位于一定半径的圆周上。通过slice可以算出一行中每个点所在的角度theta,特定一行可以通过topRadius和bottomRadius插值算出其半径tmpRadius。这样顶点的位置就可以算出来了。
依然是二维的循环,外围循环为逐行遍历,内循环为一行的圆周上所有点的遍历。代码如下:
//从上到下每个stack半径变化量:dRadiusfloat dRadius = (bottomRadius - topRadius) / stack;//每个stack高度:dHeightfloat dHeight = height / stack;//每个圆周上顶点数量:slice+1int vertsPerRow = slice + 1;//顶点行数:stack+1int nRows = stack + 1;//总顶点数int nVerts = vertsPerRow * nRows;//总索引数int nIndices = slice * stack * 6;mesh.vertices.resize(nVerts);mesh.indices.resize(nIndices);//顶部Y坐标float topY = height * 0.5f;for(int i=0; i<nRows; ++i){ float tmpY = topY - dHeight * i; float tmpRadius = topRadius + i * dRadius; for(int j=0; j<vertsPerRow; ++j) { float theta = XM_2PI * j / slice; int index = i * vertsPerRow + j; mesh.vertices[index].pos = XMFLOAT3(tmpRadius*cos(theta),tmpY,tmpRadius*sin(theta)); }}
下面是索引构建,由于与网格高度相似,直接放出代码:
UINT tmp(0);for(int i=0; i<stack; ++i){ for(int j=0; j<slice; ++j) { mesh.indices[tmp] = i * vertsPerRow + j; mesh.indices[tmp+1] = (i + 1) * vertsPerRow + j + 1; mesh.indices[tmp+2] = (i + 1) * vertsPerRow + j; mesh.indices[tmp+3] = i * vertsPerRow + j; mesh.indices[tmp+4] = i * vertsPerRow + j + 1; mesh.indices[tmp+5] = (i + 1) * vertsPerRow + j + 1; tmp += 6; }}
此外,我们发现该圆柱不包含顶部和底部的盖子。框架库中提供了添加顶部、底部盖子的函数。其实方法很简单,顶部和底部分别是slice个三角形而已,共享一个中心顶点。相关代码可以在源代码中进行参考。
2.3 球
绘制球体,基本参数只有一个半径。此外,与圆柱一样,为了指定其精细等级,也需要提供stack和slice两个参数,意义也相似。只是这里slice不是在垂直方向上的等分,而是从上极点沿球面到下极点的180度角进行等分。通过slice和stack可以得出顶点的球面坐标,因此可以算出其直角坐标。
球面顶点的生成与圆柱一样也分为两步(尤其与圆柱很类似,我只给出基本思路,可以通过研究代码来理解):
1. 不考虑上下两个极点,与圆柱计算方法类似,生成球面(与圆柱的柱面顶点计算一样)
2. 把两个极点及相应三角形添加进来,也可以想像成添加盖子(与圆柱添加盖子过程一样)
相关代码如下:
int vertsPerRow = slice + 1;int nRows = stack - 1; for(int i=1; i<=nRows; ++i){ float phy = XM_PI * i / stack; float tmpRadius = radius * sin(phy); for(int j=0; j<vertsPerRow; ++j) { float theta = XM_2PI * j / slice; UINT index = (i-1)*vertsPerRow+j; float x = tmpRadius*cos(theta); float y = radius*cos(phy); float z = tmpRadius*sin(theta); //位置坐标 mesh.vertices[index].pos = XMFLOAT3(x,y,z); }}
2.4 立方体
最后一个,也是最简单的一个,即立方体。一个立方体只需要提供三维方向上的长度即可,即width(X方向)、height(Y方向)、depth(Z方向)。有一点与之前绘制彩色立方体时不一样的是,我们这里构建立方体用到24个顶点(每个面4个)。而之前彩色立方体只用到了8个顶点(每个顶点被3个面共享)。这是因为在后面学习过程中我们需要顶点的法线坐标,而一个顶点相对于其连接的3个面来说,法线完全不同,因此无法共享顶点。之前的例子由于只需要颜色信息,我们让其3个面在该顶点处共享了颜色值,因此只需要8个顶点即可。
索引创建与彩色立方体例子一样,共36个索引值(每个面包含两个三角形,共6个索引值)。
由于立方体构建十分容易,代码就不在这里列出了。
3. 本节的场景绘制
有了以上几种基本几何体的绘制方法,我们现在来关注本节当中的示例场景。在该场景中,我们放置了一个网格,作为地面;中心一个立方体,上面放置了一个圆球;对称的四个角落,分别摆放了四个圆柱,圆柱上分别放置一个圆球。场景截图如下:
该程序一方面是为了展示一下我们构建几何体的效果,更重要的是学习D3D11中如何让多个物体共享同一个顶点/索引缓冲区。
在该程序中,我们一共构建了四种几何体:网格、立方体、圆柱、球。这四种物体的顶点全部放置于同一个缓冲区中,索引也一样。这样在绘制相应的物体时,我们就需要在缓冲区中找到其对应的位置。在D3D11中,为了在顶点、索引缓冲区中找到一个物体对应的位置,我们使用三个参数:该物体在顶点缓冲区中的起始位置(VStart),索引缓冲区中的起始位置(IStart),以及索引总数(totalIndices)。
如下图所示:
在该图示例中,球、立方体、圆柱三种物品共享顶点缓冲区和索引缓冲区。在Global Vertex Buffer中,我们可以看到各自的起始位置(VStart)及顶点个数,在下面可以看到相应的索引起始位置(IStart)及索引个数。比如我们要绘制立方体,我们需要的三个参数即为:firstBoxVertexPos,firstBoxIndex和numBoxIndices。通过这三个参数来调用D3D11中绘制函数即可,绘制函数原型如下:
void DrawIndexed( [in] UINT IndexCount, [in] UINT StartIndexLocation, [in] INT BaseVertexLocation);
IndexCount为相应物体索引个数,对应上面立方体的:numBoxIndices;
StartIndexLocation对应上面立方体的:firstBoxIndex;
BaseVertexLocation对应上面立方体的:firstBoxVertexPos。
因此,只要在构建顶点、索引缓冲区时记录下每个物体的顶点起始位置、索引起始位置、索引总数,即可在绘制时通过指定不同的变换矩阵、纹理等随意绘制它。