DirectX 3D_基础之地形绘制基础 高度图 创建高度图 加载RAW文件 访问和修改高度图 创建地形的几何信息(顶点计算,索引计算,纹理映射,光照,着色,地形中行走)

来源:互联网 发布:淘宝拧盖器 编辑:程序博客网 时间:2024/05/29 02:46

                每日一语:

 

                我们是人,生活在这个社会上面,所以,肯定会有这样那样的牵绊。这也是谁都无法摆脱的。但不见得所有人都可以保持清醒的认识。知道自己最最重要的事情是什么。自己该做什么,不该做什么。IT这个行业,知识更新速度极快,逆水行舟,不进则退。既然,我们都在这条大船上面,谁都不想被推下船。大风大浪中的航行,肯定避免不了,有些人肯定会掉下船。掉下船的这些人大部分都是根基不牢。或者称下盘无力。侯杰说过,勿在浮沙筑高台,高台就是再高,下面是浮沙,所以终究会倒的。

             

                正文:
       
                地形绘制基础:


               地形网格(mesh)其实就是一系列三角形栅格,但是方格中的每个顶点都被赋予了一个高度值(即高度或海拔),这样该方格就可通过方格对应高度的平滑过度来模拟自然地形中的山脉到山谷的变化。当然,我们还需要对地形网格应用一些细致的纹理来表现沙滩,绿草如茵的丘陵和雪山。


                高度图:


                我们用高度图来描述地形中的丘陵和山谷。高度图其实就是一个数组,该数组的每个元素都指定了地形方格中某一特定顶点的高度值。(另外一种实现方式可能用该数组中的每个元素来指定每个三角形栅格的高度)。通常,我们将高度图视为一个矩阵,这样高度图中的元素就与地形栅格中的顶点意义对应。


               高度图被保存在磁盘中时,我们通常为其每个元素只分配一个字节的存储空间,这样高度只能在区间[0,255]内取值。该范围足以反映地形中的高度变化,但在实际应用中,为了匹配3D世界的尺度,可能需要对高度值进行比例变化,这样极有可能超出上述空间。例如,如果我们在3D世界中的度量单位为英寸,0到255就不足以表达我们目标值的高度。基于上述原因,当将高度图加载到应用程序时,我们重新分配一个整形或浮点型数组来存储这些高度值。


               高度图有多种可能的图形表示,其中一种是灰度图,地形中某一点的海拔越高,相应地该点在灰度图中的亮度就越大。


 

              创建高度图:

 

              高度图可自编程序来生成,也可用图像编辑软件来创建。此外,还可利用图像编辑软件提供的一些特殊功能比如过滤器(滤波器)等来创建一些有趣的高度图。注意,当我们创建一副图像时,便指定了一副灰度图。


              一旦,完成了高度图的创建,我们需要将其保存为8位的RAW文件。RAW文件仅连续存储了图像中以字节为单位的每个像素的灰度值。这就使得这类文件的读取操作非常容易。


              注意,高度信息不一定非要用RAW格式的文件来存储,可根据需要选择任意一种格式。RAW仅仅是由于读取简单。


              加载RAW文件:


              由于RAW文件本质上是一个连续的字节存储块,我们可用如下方法很容易的读取该字节块。注意,变量_heightmap是Terrain类的一个成员变量


                   std::vector<int> _heightmap;


                   bool Terrain::readRawFile(std::string filename)
                   {
                             std::vector<BYTE> in (_numVertices);


                             std::ifstream inFile(fileName.c_str(),std::ios_base::binary);


                             if (inFile == 0)
                                     return false;


                             inFile.read(
                                          (char*)&in[0],
                                          in.size());


                             inFile.close();
          
                             _heightmap.resize(_numVertices):


                            for(int i = 0; i < in.size();i++)
                                     _heightmap[i] = in[i];


                                     return true;
                   }


             注意,我们将字节型的向量复制到一个整型向量中。这样,我们就可以对高度值进行比例变换从而突破0到255的限制。

 

            该方法的唯一限制是所要读取的RAW文件中包含的字节数至少要与地形中的顶点总数一样多。所以,如果你要从一个256X256的RAW文件中读取数据,相应地只能创建一个至多有256X256个顶点的地形。
     
            访问和修改高度图:


            Terain类提供了下面两个函数用于访问和修改高度图中的指定项。


                     int Terrain::getHeightmapEntry(int row,int col)
                     {
                             return _heightmap[row * _numVertsPerRow + col];
                     }
                     void Terrain::setHeightmapEntry(int row,int col,int value)
                     {
                            _heightmap[row*_numVertsPerRow + col] = value;
                     }


             这些方法允许我们通过行和列的索引引用高度图中指定的项,这些方法隐藏了如何访问由线性数组来表示的矩阵。
     
             创建地形的几何信息:


             我们可通过指定每行和每列的顶点数以及单元间距来定义地形的尺寸。我们对Terrain类,还需要存传入与地形相关的设备指针,一个标识了存储高度图数据的文件的字符串以及一个用于对高度图中个元素实施比例变换的高度比例因子。
     
             顶点的计算:


             为了计算三角形栅格的各顶点,我们只需自顶点start起,逐行生成每个顶点,保持相邻顶点的行列间隔均为单元间距,直至到达顶点end为止。这样就给出了x和z坐标的定义。但是y坐标应怎样定义了,其实只要查询所加载的高度图数据结构中的相应项,就很容易地获知y坐标的值。


 

             为了计算纹理坐标,我们可以与地形中位于(i,j)的顶点相对应的纹理坐标(u,v)可由下列公式计算得到:


                          u = j . uCoordIncrementSize


                          v = i . vCoordIncrementSize


             其中:
                          uCoordIncrementSize = 1 / numCellCols;


                          vCoordIncrementSize = 1 / numCellRows;
       
             索引的计算:


            为计算三角形栅格各顶点的索引,我们只需要从左上角起直至右下角,依次遍历每个方格,并计算构成每个方格的三角形面片的顶点索引。


            计算的关键是推导出一个求出构成第i行,第j列的方格的两个面片的顶点索引的通用公式。


            我们发现,对于处在(i,j)位置的方格 有:


                   ABC = {i.numVertxPerRow + j i . numVertsPerRow +j + l(i.l).numVertsPerRow + j}
     
                   CBD = {(i+l).numVertsPerRow + j i.numVertsPerRow + j + l(i.l).numVertsPerRow + j + l}

 

            纹理映射:


 

            Terrain类为地形提供了两种纹理映射方式。一种容易想到的方式是,加载一个已创建好的纹理文件,然后再应用该纹理数据。下面这个由Terrain类实现的方法将纹理数据自文件加载到了_tex数据成员中,其中_tex是一个指向IDirect3DTexture9接口的指针。绘制地形之前,方法Terrain::draw首先在其内部对_tex进行了设置。

 
 
                 bool Terrain::loadTexture(std::string fileName);
     
                  bool Terrain::loadTexture(std::string fileName)
                  {
                            HRESULT hr = 0;
  
                            hr = D3DXCreateTextureFromFile(
                                                        _device,
                                                        fileName.c_str(),
                                                        &_tex
                            );


                            if(FALLED(hr))
                                   return false;


                             return true;
                    }
      
             一种过程化方法:


            另外一种对地形进行纹理映射的方法是按顺序逐个计算出纹理内容,即我们首先创建一个“空”纹理,然后基于一些已定义的参数计算出纹理元的颜色。在本例中,该参数为地形的高度。


            下面我们用Terrain::genTexture方法来顺序生成纹理数据。该方法首先用方法D3DXCreateTexture创建一个空纹理,然后在将顶层纹理(前面提到一个纹理对象可用多级渐进纹理,所以就有许多纹理)锁定。至此开始遍历每个纹理元并对其上色。上色的依据是坐标方格所对应的近似高度。思路是,地形中,海拔较低的部分上色为海滩色,中等海拔的部分上色为绿色的丘陵颜色,高海拔的部分上色为雪山的颜色。我们用坐标方格中左上角顶点的高度值来近似表示该方格的整体高度。


 

            为每个纹理元上色后,我们还想依据太阳光(用方向光来模拟)到达纹理元对应的坐标方格时的入射角来调整每个纹理元的明暗度。上述运算在函数Terrain::lightTerrain中完成。最后,Terrain::genTexture方法借助D3DXFilterTexture函数计算出比较低层的多级渐进纹理中的纹理元。
      
             光照:


             Terrain::genTexture方法中,调用了Terrain::lightTerrain,后者为地形添加光照以增强真实感。由于前面已经计算好了地形纹理的颜色。现在,我们只需计算地形中各部分在给定光源照射下应如何进行明暗调整的明暗因子(shade factor)。本节中我们将研究这样一项技术。你可能,会疑惑为什么我们不用Direct3D来添加光照而是自己手动计算。我们这样做,主要基于下述考虑:


 

            手工计算由于无需存储顶点法向量,所以可以节省大量的内存。


            由于地形是静态的,而且光源一般也不会移动,所以我们可以预先对光照进行计算,这样就节省了Direct3D实时照亮地形那部分计算时间。


            手动计算使得我们获得了将有关数学知识付诸实践的机会,并有助于我们加深对基本的光照概念的理解以及熟悉Direct3D中的一些函数。
       
            概述:


            我们在计算地形的明暗度时用到的光照技术很基本,也很常用,即漫射光光照(difiusing lighting)。给定一个平行光源,我们用“到达光源的方向(direction to the light)”(该光源发出的平行光的传播方向的反方向) 来描述该平行光源。例如,如果一组平行光线自空中沿着方向lightRaysDirection = (0,-1,0)向下照射,则到达光源的方向应与lightRaysDirection相反,即directionToLight = (0,1,0).注意,光的方向向量应为单位向量(unit vector)。


           接下来,为地形中的每个坐标方格计算光向量L和该方格的面法向量N之间的夹角。


 

           上述夹角越大,坐标方格的朝向偏离光源就越大,其所接收到的光照就越少。反之,接收的光照就越多。而且,一旦光向量与面法向量的夹角超过90度,方格表面便不接收任何光照。


 

           借助光向量和方格的面法向量之间的角度关系,我们可以构造一个位于区间[0,1]内的明暗因子,以表示方格表面所接收到的光照量,这样,我们就可以用一个接近于0的因子值来表示这两个向量的夹角很大。当一种颜色与该因子相乘时,颜色值就趋于0,从而呈现出较暗的视觉效果。反之,接近于1的因子值表示这两个向量的夹角很小,所以当一种颜色与该因子相乘时,该颜色基本保持了原来的亮度。

 

          坐标方格的明暗度计算:


 

          我们将光源的方向用一个单位向量L来表示。为了计算向量L与方格的法向量N之间的夹角,我们首先需要求出N。通过计算向量的叉积就很容易求出N.当我们必须首先找到与该方格共面的两个非零的互不平行的向量。


       
                  u = (cellSpacing,by-ay,0)


                  v = (0,cy-ay,-cellSpacing)

 


          只要求出向量u和v,该方格的法向量N就可由公式N = u x v求得。当然,还必须对向量N进行规范化处理。
                     
          现在来考虑如何求向量L和N之间的夹角,我们知道3D空间中的两个单位向量的点乘等于这两个向量夹角的余弦
          
                       L 点乘 N = S


            标量s位于区间[-1,1]内,由于位于[-1,0]内的s值对应与L和N之间的夹角大于90度的情形,此时,方格表面接收不到任何光照。所以当s位于[-1,0]内时,我们应将s钳位为0.
     
            对地形进行着色:


           一旦我们了解了如何对一个特定的坐标方格进行着色(shading,也称明暗处理),我们就可对地形中的所有方格着色了。我们只需要遍历每个坐标方格,计算其明暗因子,然后将该方格对应的纹理元的颜色与该因子相乘。

 

           在地形中行走:


           地形创建好以后,我们还想移动摄像机以模拟我们在场景中的行走过程。即,我们需要依据自身在地形中所处的位置不断调整摄像机的高度(y坐标)。为了实现这一点,我们首先需要依据给定的x和z坐标,找到我们所处的坐标方格。Terrain::getHeight函数可以实现该功能;它以摄像机的x和z坐标为参数,并返回摄像机应处在的高度。 

 
          首先,我们进行平移变换,将顶点start平移到坐标原点。然后,通过缩放因子为单元间隔的负倒数的比例变换将坐标方格的单元间隔归一化。这样,我们就转换到了一个新的参考系中,其中z轴方向向下。当然,程序代码并没有改变坐标系框架本身,我们只是将Z轴的正方向理解为向下。


 

          由于这里的变换非常复杂,需要很多图例,所以在后面再补齐。
      
      

 

原创粉丝点击