• (转)【D3D11游戏编程】学习笔记十一:基本几何体绘制


    (注:【D3D11游戏编程】学习笔记系列由CSDN作者BonChoix所写,转载请注明出处:http://blog.csdn.net/BonChoix,谢谢~)

           这次我们来学习几种常见的基本几何体的绘制方法,包含网格、球、圆柱等。很多复杂的几何图形都是由众多这些基本几何体组成的。而且,在水面渲染、地形渲染当中,都要使用到网格,因此掌握网格的基本生成方法很有必要。此外,有了这么多种几何体的绘制方法,我们在后面的程序中(学习导入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)。有了这些参数,整个网格上的顶点坐标就很容易生成了。我们可以通过一个二维的循环来实现,从左上角开始,向右、向下进行。

    [cpp] view plain copy
    1. //每行顶点数、每列顶点数  
    2. UINT nVertsRow = m + 1;  
    3. UINT nVertsCol = n + 1;  
    4. //起始x、z坐标  
    5. float oX = -width * 0.5f;  
    6. float oZ = height * 0.5f;  
    7. //每一格纹理坐标变化  
    8. float dx = width / m;  
    9. float dz = height /n;  
    10. for(UINT i=0; i<nVertsCol; ++i)  
    11. {  
    12.     float tmpZ = oZ - dz * i;  
    13.     for(UINT j=0; j<nVertsRow; ++j)  
    14.     {  
    15.         UINT index = nVertsRow * i + j;  
    16.         mesh.vertices[index].pos.x = oX + dx * j;  
    17.         mesh.vertices[index].pos.y = 0.f;  
    18.         mesh.vertices[index].pos.z = tmpZ;  
    19.     }  
    20. }  

           这样网格的顶点就生成了,下面是索引生成方法:

           索引是为了组合成三角形而用的,因此我们的思路即逐个三角形进行。在网格中只存在一个个的矩形,我们需要把每个矩形拆成两个三角形。显然,沿对角线拆开即可。如下图所示:

    我们依然用一个二维的循环,逐个遍历矩形(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的索引了,代码如下:

    [cpp] view plain copy
    1. //总格子数量:m * n  
    2. //因此总索引数量: 6 * m * n  
    3. UINT nIndices = m * n * 6;  
    4. mesh.indices.resize(nIndices);  
    5. UINT tmp = 0;  
    6. for(UINT i=0; i<n; ++i)  
    7. {  
    8.     for(UINT j=0; j<m; ++j)  
    9.     {  
    10.         mesh.indices[tmp] = i * nVertsRow + j;  
    11.         mesh.indices[tmp+1] = i * nVertsRow + j + 1;  
    12.         mesh.indices[tmp+2] = (i + 1) * nVertsRow + j;  
    13.         mesh.indices[tmp+3] = i * nVertsRow + j + 1;  
    14.         mesh.indices[tmp+4] = (i + 1) * nVertsRow + j + 1;  
    15.         mesh.indices[tmp+5] = (i + 1) * nVertsRow + j;  
    16.           
    17.         tmp += 6;  
    18.     }  
    19. }  

    该代码中即逐个矩形进行遍历,每个矩形包括两个三角形,共六个索引值。

           有了顶点和索引的集合,网格就生成了。当然每个顶点除了位置坐标外还可以包括其他信息,比如法线、切线(用于后面的Bump Mapping),纹理坐标等,这些信息的生成可以参考附带的源代码。

           2.2 圆柱

           圆柱的生成其实和网格有一定的相似,毕竟,把圆柱的柱面展开后,其实就是一个网格。因此它们的索引构建基本是一样的。为了构建一个圆柱,需要提供如下信息:圆柱的上口半径(topRadius),下口半径(bottomRadius),高度(height)。此外,为了指定圆柱的精细度,还需要指定两个参数,一个为没高度方向上平均划分的个数(stack),另一个为沿圆周方向等分的个数(slice)。如果还是不理解,可以看下图:

    通过该图就可以直观地理解stack和slice的意义了。即stack为垂直方向上等分的个数,slice为在360度圆周上等分的个数。等分地越多,尤其是圆周上,其越接近圆形,即表面越光滑。

           先来构建顶点。我们可以发现,把圆柱沿垂直方向等分后,圆柱可以看成是stack+1行的一系列点,每一行的点位于一定半径的圆周上。通过slice可以算出一行中每个点所在的角度theta,特定一行可以通过topRadius和bottomRadius插值算出其半径tmpRadius。这样顶点的位置就可以算出来了。

           依然是二维的循环,外围循环为逐行遍历,内循环为一行的圆周上所有点的遍历。代码如下:

    [cpp] view plain copy
    1. //从上到下每个stack半径变化量:dRadius  
    2. float dRadius = (bottomRadius - topRadius) / stack;  
    3. //每个stack高度:dHeight  
    4. float dHeight = height / stack;  
    5. //每个圆周上顶点数量:slice+1  
    6. int vertsPerRow = slice + 1;  
    7. //顶点行数:stack+1  
    8. int nRows = stack + 1;  
    9. //总顶点数  
    10. int nVerts = vertsPerRow * nRows;  
    11. //总索引数  
    12. int nIndices = slice * stack * 6;  
    13. mesh.vertices.resize(nVerts);  
    14. mesh.indices.resize(nIndices);  
    15. //顶部Y坐标  
    16. float topY = height * 0.5f;  
    17. for(int i=0; i<nRows; ++i)  
    18. {  
    19.     float tmpY = topY - dHeight * i;  
    20.     float tmpRadius = topRadius + i * dRadius;  
    21.   
    22.     for(int j=0; j<vertsPerRow; ++j)  
    23.     {  
    24.         float theta = XM_2PI * j / slice;  
    25.         int index = i * vertsPerRow + j;  
    26.         mesh.vertices[index].pos = XMFLOAT3(tmpRadius*cos(theta),tmpY,tmpRadius*sin(theta));  
    27.     }  
    28. }  

           下面是索引构建,由于与网格高度相似,直接放出代码:

    [cpp] view plain copy
    1. UINT tmp(0);  
    2. for(int i=0; i<stack; ++i)  
    3. {  
    4.     for(int j=0; j<slice; ++j)  
    5.     {  
    6.         mesh.indices[tmp] = i * vertsPerRow + j;  
    7.         mesh.indices[tmp+1] = (i + 1) * vertsPerRow + j + 1;  
    8.         mesh.indices[tmp+2] = (i + 1) * vertsPerRow + j;  
    9.         mesh.indices[tmp+3] = i * vertsPerRow + j;  
    10.         mesh.indices[tmp+4] = i * vertsPerRow + j + 1;  
    11.         mesh.indices[tmp+5] = (i + 1) * vertsPerRow + j + 1;  
    12.   
    13.         tmp += 6;  
    14.     }  
    15. }  

           此外,我们发现该圆柱不包含顶部和底部的盖子。框架库中提供了添加顶部、底部盖子的函数。其实方法很简单,顶部和底部分别是slice个三角形而已,共享一个中心顶点。相关代码可以在源代码中进行参考。

           2.3 球

           绘制球体,基本参数只有一个半径。此外,与圆柱一样,为了指定其精细等级,也需要提供stack和slice两个参数,意义也相似。只是这里slice不是在垂直方向上的等分,而是从上极点沿球面到下极点的180度角进行等分。通过slice和stack可以得出顶点的球面坐标,因此可以算出其直角坐标。

           球面顶点的生成与圆柱一样也分为两步(尤其与圆柱很类似,我只给出基本思路,可以通过研究代码来理解):

           1. 不考虑上下两个极点,与圆柱计算方法类似,生成球面(与圆柱的柱面顶点计算一样)

           2. 把两个极点及相应三角形添加进来,也可以想像成添加盖子(与圆柱添加盖子过程一样)

           相关代码如下:

    [cpp] view plain copy
    1. int vertsPerRow = slice + 1;  
    2. int nRows = stack - 1;  
    3.   
    4. for(int i=1; i<=nRows; ++i)  
    5. {  
    6.     float phy = XM_PI * i / stack;  
    7.     float tmpRadius = radius * sin(phy);  
    8.     for(int j=0; j<vertsPerRow; ++j)  
    9.     {  
    10.         float theta = XM_2PI * j / slice;  
    11.         UINT index = (i-1)*vertsPerRow+j;  
    12.   
    13.         float x = tmpRadius*cos(theta);  
    14.         float y = radius*cos(phy);  
    15.         float z = tmpRadius*sin(theta);  
    16.         //位置坐标  
    17.         mesh.vertices[index].pos = XMFLOAT3(x,y,z);  
    18.     }  
    19. }  

           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中绘制函数即可,绘制函数原型如下:

    [cpp] view plain copy
    1. void DrawIndexed(  
    2.   [in]  UINT IndexCount,  
    3.   [in]  UINT StartIndexLocation,  
    4.   [in]  INT BaseVertexLocation  
    5. );  

           IndexCount为相应物体索引个数,对应上面立方体的:numBoxIndices;

           StartIndexLocation对应上面立方体的:firstBoxIndex;

           BaseVertexLocation对应上面立方体的:firstBoxVertexPos。

           因此,只要在构建顶点、索引缓冲区时记录下每个物体的顶点起始位置、索引起始位置、索引总数,即可在绘制时通过指定不同的变换矩阵、纹理等随意绘制它。

           4. 最新框架代码+示例程序

           本节完,以下最最新的框架代码及本节的示例程序:

           操作方法:鼠标左键按下拖动旋转场景,右键按下拖动调整镜头的远近。

           最新框架、示例程序代码

  • 相关阅读:
    利用余数选择特殊位置元素
    CSS hack
    css选择器
    按yyyy-mm-dd格式输入一个日期,判断这是这一年的第几天
    输入不同year,month,打印月历
    java学习之多线程(二)
    java学习之多线程
    剑指offer--第一个只出现一次的字符
    剑指offer--两个链表的第一个公共结点
    剑指offer--最小的k个数
  • 原文地址:https://www.cnblogs.com/wodehao0808/p/6603901.html
Copyright © 2020-2023  润新知