第22章 钟灵毓秀的世界——三维地形的构建

章节导读
想创造出极具真实感的三维游戏世界, 三维地形的模拟是必不可少的,也是至关重要的。

三维地形模拟其实是一个很广阔的课题,它其实不仅仅局限于游戏开发领域,在三维仿真、虚拟现实等领域都涉及。说起三维地形模拟,似乎有那么一丝神秘,其实,只要了解其实现原理,这所谓的地形系统模拟也就是纸老虎一只。本章我们将揭开三维地形模拟的面纱,看看到底怎样利用一个C++类的书写,实现我们专属的三维地形系统,然后再用几句代码,两幅图片, 一个“活生生”的三维地形就跃然纸上了。

先上一张本章示例程序的截图:


22.1 三维地形绘制思路分析

关于地形绘制的大体思路,其实非常简单,让我们先来看三幅图。


我们可以发现,以上的3 幅图就概括了三维地形模拟的大体走向与思路。
首先是第1 幅图,我们可以看到,图中描绘的就是在同一平面上的三角形网格组成的一个大的矩形区域。这里我们把它看做是一张大面均匀的同一平面上的“渔网”,显然它是一个二维的平面。图中的每一个顶点都可以用一个二维的坐标(x,y )来唯一表示。
然后第2 幅图,我们就像“揠苗助长” 一样,拉着第一幅图中的“渔网”的某些顶点往上提(或者往下压〉。这里往上提一点,那里提一点,这样,我们就为每一个顶点都赋予了一个高度(有的顶点没有移动,它的高度为0 ),第1 幅图中的渔网就变形了,成了三维图形了。每个顶点就都有了一个高度值。如果引入z 坐标来表示这个高度值的话, 那么现在三维空间中这个变形的“渔网”中的每个顶点都可以用( x,y,z )来唯一表示。

最后第3 幅图,在第2 幅图中的三维“渔网”的表面我们“镀上”纹理不尽相同的“薄膜”,也就是进行了一个纹理包装的过程。这样奇迹就发生了,逼真的雪原山川、奇峰怪石展现在了我们眼前。
所以,绘制三维地形的玄机,就被这三幅图联手一语道破了。
其中, 第2 幅图中的那个“揠苗助长”的过程可谓三维地形绘制的一招“妙棋”。

这招“妙棋”常常是借助高度图来完成,下面我们就来讲一讲什么是高度图。

22.2 关于高度图

高度图在三维地形模拟中扮演着非常重要的角色。下面让我们来一起探讨一下高度图的方方面 面。

22.2.1 高度图的概念

高度图说白了其实就是一组连续的数组,这个数组中的元素与地形网格中的顶点一 一对应,且 每一个元素都指定了地形网格的某个顶点的高度值。当然,高度图至少还有一种实现方案,就是用 数值中的每一个元素来指定每个三角形栅格的高度值,而不是顶点的高度值。
高度图有多种可能的图形表示, 其中最常用的一种是灰度图(grayscale map)。地形中某一点 的海拔越高的话,相应地该点对应的灰度图中的亮度就越大。下面就是一幅灰度图:


我们通常只为每一个元素分配一个字节的存储空间,这样高度也就只能在0~255 之间取值。
因此,地形中最低点将用0表示,而最高点使用255 表示(当然,这样做可能会出现一些问题,比如地形中大部分区域的高度差别都不大,但是有少数地方高度差特别大,不过大多数情况下这个系统都能运行的很好)。
这个范围大体上来反应地形中的高度变化完全没问题,但是在实际运用中,为了匹配3D 世界的尺寸,可能需要对高度值进行比例变换,但是一进行比例变换,往往就可能超出上面的0~255这个区间。所以我们把高度数据加载到应用程序中时,需要重新分配一个整型或者浮点型的数组来
存储这些高度值,这样就不必拘泥于0~255 这个范围,就可以随心所欲地构建出我们心仪的三维世界了。

对于灰度图中的每个像素来说,同样使用0~255 之间的值来表示一个灰度。这样就能把不同的灰度映射为高度,并且用像素索引表示不同网格。
要从高度图创建一个地形,我们需要创建一个与高度图相同大小的顶点网格,并使用高度图上每个像素的高度值作为顶点的高度。例如,我们可以使用一张6 × 6 像素分辨率的高度图生成一个6 × 6 大小的顶点网格。
网格上的顶点不仅包含位置,还包含诸如法线和纹理坐标的信息。下图就是一个在xz 平面中的6 × 6 大小的顶点网格,其中每个顶点的高度对应在Y 坐标上。


另外我们在设计三维地形模拟系统的时候,会指定一下相邻顶点的距离(水平距离和垂直距离一样〉。这个距离在上图中用

“Block Scale”表示。这个距离如果取小一点的话,会使顶点间的高度过渡平滑,但是会减少网格也就是三维地形的整体大小: 反之,相邻间顶点的距离取大一点的话,顶点间的过渡会变得陡峭,同时网格也就是三维地形的整体尺寸会相对来说变大。在上图中,如果两个顶点间的距离我们设为1 米的话,那么所生产地形的大小就是25 平方米,很好理解吧。
最常用的灰度图格式是后缀名为RAW,我们在这里使用的高度图文件格式就是RAW,这个格式不包含诸如图像类型和大小信息的文件头,所以易于被读取。RAW 文件只是简单的二进制文件,只包含地形的高度数据。在一个8 位高度图中,每个字节都表示顶点的高度。


22.2.2 高度图的制作

高度图的制作一般有2 种方式。

  • 以某种算法为基础,写个程序生成。比较有名的是Fault Formation 和Midpoint Displacement 这两种算法。
  • 通过图像编辑软件、三维建模软件或者专业制作地形的软件来制作。

图像编辑软件首选的当然是Photoshop ,这个就是我们准备教大家的高度图生成方式。(先把后面两种介绍完,稍后就教大家怎么做高度图。〉
三维建模软件就如我们之前介绍过的3DS Max和Maya 了,地形制作也是三维建模领域的一个分支。
再就是专业制作地形的软件, 比如一款叫Terragen 。这款软件用起来也很方便,大家不妨Google一下去下一个看看。

22.2.3 用Photoshop 制作高度图

接下来, 来教大家使用Photoshop 生成高度图。
1: 打开Photoshop (作者用的是Photoshop CS6) , 【Ctrl+N)或者依次单击菜单栏上的【文件】→ 【新建】,新建一个画布。如下图,我们的画布的大小取64 × 64 像素。


2: 创建完画布, 接下来就是最关键的一步。依次单击菜单栏上的【滤镜】→ 【渲染】 → 【云彩】。

这时候就可以发现,我们创建的空白画布上有了随机的灰度颜色值,如果你对这次生成的随机灰度图不满意的话,大可再次单击【滤镜】→ 【渲染】→【云彩】或者按【Ctrl+F 】来重新生成一次随机的灰度效果图, 直到颜色分布满意为止。也可以用画笔来在图上涂抹,自己来设定高度。下面这是通过处理后得到的一张灰度图,这样,后面如果我们用这张图作为高度图,得到的就是一个
凹下去型的爱心地形图。
记得在用【云彩】滤镜的时候,最好把调色板的颜色前景色设为纯黑色,否则得到的随机灰度图效果可能出不来。即调色板中的颜色设置成如下右图:


另外,我们可以通过对图片色阶的调整,来对生成的灰度图的整体颜色进行调节。比如想让地形整体显得高一些, 就把灰度图整体调亮一些, 反之,地形整体来说要显得低一些的话,就把灰度图整体调暗。色阶对话框通过【图像】→ 【调整】→ 【色阶】打开,或者直接按快捷键【Ctrl +F 】。
另外, 在单击【图像】→ 【调整】后弹出的对话框中还有曲线、色相、饱和度等等选项, 大家不妨也试试。
制作完成,单击【文件】→ 【储存为】或者直接按快捷键【Shift+Ctrl+S 】来对制作好的高度图进行保存。保存的格式随意,因为我们稍后写的一个地形类原则上支持几乎所有的图片格式高度图的导入,只不过对有些图片格式得到的效果比较奇葩而己。
这里我们选择8 位的raw 格式,单击确定后,会弹出如下导出raw 的对话框,记得要把【通道储存在】这个选项改成非隔行顺序,如下图:


其实,大家不想用Photoshop 的话,可以直接Google 一下“ heighmap ”,搜索结果中随便找就是一张现成的,然后改成raw 格式就好了。原则上我们可以直接随便拿一张任意格式的图片来做高度图使用,只是可能做出来的地形显得怪异一点而已。

22.2.4 在程序中读取高度图

让我们针对使用最广泛的raw 类型的高度图进行讲解。由于raw 格式文件是按字节为单位保存图像中的每个像素的灰度值的,那么我们可以容易地读取保存在该文件中的高度信息。在这次的地形类的实现中,我们用到了C++中模板以及文件流的知识,下面贴出详细注释的代码:
  // 从文件中读取高度信息
    std::ifstream inFile;     
    inFile.open(pRawFileName, std::ios::binary);   //用二进制的方式打开文件

    inFile.seekg(0,std::ios::end);							//把文件指针移动到文件末尾
    std::vector<BYTE> inData(inFile.tellg());			//用模板定义一个vector<BYTE>类型的变量inData并初始化,其值为缓冲区当前位置,即缓冲区大小

    inFile.seekg(std::ios::beg);								//将文件指针移动到文件的开头,准备读取高度信息
    inFile.read((char*)&inData[0], inData.size());	//关键的一步,读取整个高度信息
    inFile.close();													//操作结束,可以关闭文件了
由于保存在raw 文件中的每个灰度数据只是用一个字节存储的,那么这样所表示的地形高度只能在[0, 255]之间取值。显然我们不喜欢这样。所以,我们继续将读取的高度信息重新保存到一个浮点型的模板类型中,这样就能舒心地取到任何范围的高度值了。注意下面这段代码中vHeightlnfo 的定义是在类头文件中的:

 std::vector<FLOAT>   m_vHeightInfo;			// 用于存放高度信息
 …
 m_vHeightInfo.resize(inData.size());					//将m_vHeightInfo尺寸取为缓冲区的尺寸
 //遍历整个缓冲区,将inData中的值赋给m_vHeightInfo
 for (unsigned int i=0; i<inData.size(); i++)		
     m_vHeightInfo[i] = inData[i];

22.3 地形类轮廓的书写

在继续展开讲解之前,我们先把这个地形类的整体轮廓给勾勒出来。这个类取名为 TerrainClass,它能通过载入二进制类型的文件〈以raw 格式为首〉来得到地形的高度信息,通过 载入图片得到地形所采用的纹理。载入文件的过程,我们封装在一个名为LoadTerrainFromFile 的 函数中。
在上文中讲高度图相关知识的时候我们就提到过,需要把高度图所传达的信息转化到顶点网格 中去,这样才好绘制出来。所以在类中既是重点也是难点的就是这个“转化”的过程,这个过程我 们放到一个名为lnitTerrain 的函数中。高度图到顶点的“转化”完成后,接下来当然需要把这些顶 点配合着纹理都绘制出来,绘制的过程我们放在一个名为RenderTerrain 的函数中。加上构造函数 和析构函数, FVF 顶点格式的定义以及若干必须的成员变量,我们就可以勾勒出TerrainClass 类的 轮廓,即下面贴出来的是Terrain.h 头文件的全部代码:

//=============================================================================
// Name: TerrainClass.h
//	Des: 一个封装了三维地形系统的类的头文件
//=============================================================================

#pragma once

#include <d3d9.h>
#include <d3dx9.h>
#include <vector>
#include <fstream>
#include  "D3DUtil.h"

class TerrainClass
{
private:
    LPDIRECT3DDEVICE9				m_pd3dDevice;			//D3D设备
    LPDIRECT3DTEXTURE9			m_pTexture;				//纹理
    LPDIRECT3DINDEXBUFFER9	m_pIndexBuffer;			//顶点缓存
    LPDIRECT3DVERTEXBUFFER9	m_pVertexBuffer;		//索引缓存

    int								m_nCellsPerRow;		// 每行的单元格数
    int								m_nCellsPerCol;			// 每列的单元格数
    int								m_nVertsPerRow;		// 每行的顶点数
    int								m_nVertsPerCol;			// 每列的顶点数
    int								m_nNumVertices;		// 顶点总数
    FLOAT						m_fTerrainWidth;		// 地形的宽度
    FLOAT						m_fTerrainDepth;		// 地形的深度
    FLOAT						m_fCellSpacing;			// 单元格的间距
    FLOAT						m_fHeightScale;			// 高度缩放系数
    std::vector<FLOAT>   m_vHeightInfo;			// 用于存放高度信息

	//定义一个地形的FVF顶点格式
    struct TERRAINVERTEX
    {
        FLOAT _x, _y, _z;
        FLOAT _u, _v;
        TERRAINVERTEX(FLOAT x, FLOAT y, FLOAT z, FLOAT u, FLOAT v) 
            :_x(x), _y(y), _z(z), _u(u), _v(v) {}
        static const DWORD FVF = D3DFVF_XYZ | D3DFVF_TEX1;
    };

public:
    TerrainClass(IDirect3DDevice9 *pd3dDevice); //构造函数
    virtual ~TerrainClass(void);		//析构函数

public:
    BOOL LoadTerrainFromFile(wchar_t *pRawFileName, wchar_t *pTextureFile);		//从文件加载高度图和纹理的函数
    BOOL InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale);  //地形初始化函数
    BOOL RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bDrawFrame=FALSE);  //地形渲染函数
};

22.4 地形顶点的计算

下面我们来看看如何计算出地形中的每个顶点。
在计算顶点之前,还需要做一些准备工作。在创建地形时, 需要通过指定地形的行数、列数以及顶点间的距离来指定地形的大小。上面我们在给类写轮廓的时候刚给过代码,封装着地形顶点计算的InitTerrain 函数的原型是这样的:

BOOL InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale);  //地形初始化函数
其中前两个参数分别为地形的行数和列数, 需要我们在初始化时指定。也就是说在计算地形的 时候行数和列数是己知的,那么,地形在x 方向和z 方向上的顶点数也就明了了,也就是z 方向上 顶点数为地形的行数加1 ,而在x 方向上的顶点数为地形列数加上1。

需要注意的是,地形在x 方向和z 方向上的顶点数都不能大于与高度图对应的分辨率分量。因为高度图中的每个元素都描述地形中某个顶点的高度值。如果高度图中只描述了128 × 128 分辨率的地形信息,那我们在初始化时lnitTerrain 函数的前两个参数都不能取超过128 的值。以高度图的角度来想一下,既然我只跟你准备了128 × 128 的高度信息,那么你就得在我规定的范围之内取顶点数,如果你取多了,我可管不了你这么多, 等着内存溢出吧。

接着, 第三个fSpace 为顶点间的间隔, 第四个参数fScale 为缩放的系数。
关于顶点的计算思路,我们通过下面这幅图,就可以表示出来:


对每行的单元格数目、每列的单元格数目、单元格间的间距、高度缩放系数、地形的宽度、地形的深度、每行的顶点数、每列的顶点数、顶点总数各个击破,就写出了下面这几句代码:

    m_nCellsPerRow  = nRows;  //每行的单元格数目
    m_nCellsPerCol  = nCols;  //每列的单元格数目
    m_fCellSpacing  = fSpace;	//单元格间的间距
    m_fHeightScale  = fScale; //高度缩放系数
    m_fTerrainWidth = nRows * fSpace;  //地形的宽度
    m_fTerrainDepth = nCols * fSpace;  //地形的深度
    m_nVertsPerRow  = m_nCellsPerCol + 1;  //每行的顶点数
    m_nVertsPerCol  = m_nCellsPerRow + 1; //每列的顶点数
    m_nNumVertices  = m_nVertsPerRow * m_nVertsPerCol;  //顶点总数
另外, 在计算地形顶点前,还需要将地形的高度值乘以一个缩放系数,以便能够调整高度的整体变化幅度,就是下面这两句代码:
    // 通过一个for循环,逐个把地形原始高度乘以缩放系数,得到缩放后的高度
    for(unsigned int i=0; i<m_vHeightInfo.size(); i++)
        m_vHeightInfo[i] *= m_fHeightScale;
接着, 就是顶点的正式计算时刻,我们按照之前专门讲解顶点缓存时用的四步曲, 并对照上 面的这幅图,下面的这些实现代码就很好理解了:

//---------------------------------------------------------------
    // 处理地形的顶点
	//---------------------------------------------------------------
	//1,创建顶点缓存
    if (FAILED(m_pd3dDevice->CreateVertexBuffer(m_nNumVertices * sizeof(TERRAINVERTEX), 
        D3DUSAGE_WRITEONLY, TERRAINVERTEX::FVF, D3DPOOL_MANAGED, &m_pVertexBuffer, 0)))
        return FALSE;
	//2,加锁
    TERRAINVERTEX *pVertices = NULL;
    m_pVertexBuffer->Lock(0, 0, (void**)&pVertices, 0);
	//3,访问,赋值
    FLOAT fStartX = -m_fTerrainWidth / 2.0f, fEndX =  m_fTerrainWidth / 2.0f;		//指定起始点和结束点的X坐标值
    FLOAT fStartZ =  m_fTerrainDepth / 2.0f, fEndZ = -m_fTerrainDepth / 2.0f;	//指定起始点和结束点的Z坐标值
    FLOAT fCoordU = 3.0f / (FLOAT)m_nCellsPerRow;     //指定纹理的横坐标值
    FLOAT fCoordV = 3.0f / (FLOAT)m_nCellsPerCol;		//指定纹理的纵坐标值

    int nIndex = 0, i = 0, j = 0;
    for (float z = fStartZ; z > fEndZ; z -= m_fCellSpacing, i++)		//Z坐标方向上起始顶点到结束顶点行间的遍历
    {
        j = 0;
        for (float x = fStartX; x < fEndX; x += m_fCellSpacing, j++)	//X坐标方向上起始顶点到结束顶点行间的遍历
        {
            nIndex = i * m_nCellsPerRow + j;		//指定当前顶点在顶点缓存中的位置
            pVertices[nIndex] = TERRAINVERTEX(x, m_vHeightInfo[nIndex], z, j*fCoordU, i*fCoordV); //把顶点位置索引在高度图中对应的各个顶点参数以及纹理坐标赋值给赋给当前的顶点
            nIndex++;											//索引数自加1
        }
    }
	//4,解锁
    m_pVertexBuffer->Unlock();

22.5 地形索引的计算

顶点值算完了,还需要接着计算顶点的索引。顶点索引的计算关键是推导出一个用于求解构成第i 行、第 j 列的顶点处右下方两个三角形的顶点索引的通用公式。下面我们来看看这个公式如何推导,下图解释得比较清楚:


对顶点缓存中的任意一点A,如果该点位于地形中的第i 行、第 j 列的话,那么该点在顶点缓存中所对应的位置应该就是i*m+j ( m 为每行的顶点数) 。如果A 点在索引缓存中的位置为k 的话,那么A 点为起始点构成的三角形ABC 中, B 、C 顶点在顶点缓存中的位置就为( i+ 1 )× m+j 和 i × m+ (j+1)。且B 点索引值为k+ 1, C点索引值为k+2 。这样, 公式就可以推导为如下:
    三角形ABC= 【i ×每行顶点数 + j, i ×每行顶点数+ ( j+ 1 ) , ( i+1)×行顶点数+j 】
    三角形CBD=【( i+1 )×每行顶点数 + j, i ×每行顶点数+ (j+ 1) , ( i+ 1)×行顶点数+ (j+1 )】
通过上面我们推导出的这个公式,就可以写出下面计算索引缓存的相关代码:

//---------------------------------------------------------------
    // 处理地形的索引
	//---------------------------------------------------------------
	//1.创建索引缓存
    if (FAILED(m_pd3dDevice->CreateIndexBuffer(m_nNumVertices * 6 *sizeof(WORD), 
        D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIndexBuffer, 0)))
        return FALSE;
	//2.加锁
    WORD* pIndices = NULL;
    m_pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);
	//3.访问,赋值
    nIndex = 0;
    for(int row = 0; row < m_nCellsPerRow-1; row++)   //遍历每行
    {
        for(int col = 0; col < m_nCellsPerCol-1; col++)  //遍历每列
        {
			//三角形ABC的三个顶点
            pIndices[nIndex]   =  row * m_nCellsPerRow + col;			//顶点A
            pIndices[nIndex+1] =  row * m_nCellsPerRow + col + 1;  //顶点B
            pIndices[nIndex+2] = (row+1) * m_nCellsPerRow + col;	//顶点C
			//三角形CBD的三个顶点
            pIndices[nIndex+3] = (row+1) * m_nCellsPerRow + col;		//顶点C
            pIndices[nIndex+4] =  row * m_nCellsPerRow + col + 1;		//顶点B
            pIndices[nIndex+5] = (row+1) * m_nCellsPerRow + col + 1;//顶点D
			//处理完一个单元格,索引加上6
            nIndex += 6;  //索引自加6
        }
    }
	//4、解锁
    m_pIndexBuffer->Unlock();

22.6 渲染出地形

没有渲染我们前面做的工作就算白忙活了。
地形的渲染我们封装在了一个名为RenderTerrain 的函数中,注释很详细,我们直接上代码:

//--------------------------------------------------------------------------------------
// Name: TerrainClass::RenderTerrain()
// Desc: 绘制出地形,可以通过第二个参数选择是否绘制出线框
//--------------------------------------------------------------------------------------
BOOL TerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame) 
{
    m_pd3dDevice->SetStreamSource(0, m_pVertexBuffer, 0, sizeof(TERRAINVERTEX));   ///把包含的几何体信息的顶点缓存和渲染流水线相关联  
    m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我们使用的灵活顶点格式的宏名称
    m_pd3dDevice->SetIndices(m_pIndexBuffer);//设置索引缓存  
    m_pd3dDevice->SetTexture(0, m_pTexture);//设置纹理

    m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);	//关闭光照
    m_pd3dDevice->SetTransform(D3DTS_WORLD, pMatWorld); //设置世界矩阵
    m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 
        m_nNumVertices, 0, m_nNumVertices * 2);		//绘制顶点

    m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE);  //打开光照
    m_pd3dDevice->SetTexture(0, 0);	//纹理置空

    if (bRenderFrame)  //如果要渲染出线框的话
    {
        m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式设为线框填充
        m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 
            m_nNumVertices, 0, m_nNumVertices * 2);	//绘制顶点  
        m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);	//把填充模式调回实体填充
    }
    return TRUE;
}

22.7 完成地形类的设计

上面都是零零散散地讲解了地形类的某些实现细节,本节我们把它们整合在一起,构成一个整体, 完成地形类的设计,即贴出TerrainClass.cpp 的全部代码:

//=============================================================================
// Name: TerrainClass.cpp
//	Des: 一个封装了三维地形系统的类的源文件
//=============================================================================

#include "TerrainClass.h"

//-----------------------------------------------------------------------------
// Desc: 构造函数
//-----------------------------------------------------------------------------
TerrainClass::TerrainClass(IDirect3DDevice9* pd3dDevice)
{
	//给各个成员变量赋初值
    m_pd3dDevice = pd3dDevice;
    m_pTexture = NULL;
    m_pIndexBuffer = NULL;
    m_pVertexBuffer = NULL;
    m_nCellsPerRow = 0;
    m_nCellsPerCol = 0;
    m_nVertsPerRow = 0;
    m_nVertsPerCol = 0;
    m_nNumVertices = 0;
    m_fTerrainWidth = 0.0f;
    m_fTerrainDepth = 0.0f;
    m_fCellSpacing = 0.0f;
    m_fHeightScale = 0.0f;
}


//--------------------------------------------------------------------------------------
// Name: TerrainClass::LoadTerrainFromFile()
// Desc: 加载地形高度信息以及纹理
//--------------------------------------------------------------------------------------
BOOL TerrainClass::LoadTerrainFromFile(wchar_t *pRawFileName, wchar_t *pTextureFile) 
{
    // 从文件中读取高度信息
    std::ifstream inFile;     
    inFile.open(pRawFileName, std::ios::binary);   //用二进制的方式打开文件

    inFile.seekg(0,std::ios::end);							//把文件指针移动到文件末尾
    std::vector<BYTE> inData(inFile.tellg());			//用模板定义一个vector<BYTE>类型的变量inData并初始化,其值为缓冲区当前位置,即缓冲区大小

    inFile.seekg(std::ios::beg);								//将文件指针移动到文件的开头,准备读取高度信息
    inFile.read((char*)&inData[0], inData.size());	//关键的一步,读取整个高度信息
    inFile.close();													//操作结束,可以关闭文件了

    m_vHeightInfo.resize(inData.size());					//将m_vHeightInfo尺寸取为缓冲区的尺寸
	//遍历整个缓冲区,将inData中的值赋给m_vHeightInfo
    for (unsigned int i=0; i<inData.size(); i++)		
        m_vHeightInfo[i] = inData[i];

    // 加载地形纹理
    if (FAILED(D3DXCreateTextureFromFile(m_pd3dDevice, pTextureFile, &m_pTexture)))
        return FALSE;

    return TRUE;
}

//--------------------------------------------------------------------------------------
// Name: TerrainClass::InitTerrain()
// Desc: 初始化地形的高度, 填充顶点和索引缓存
//--------------------------------------------------------------------------------------
BOOL TerrainClass::InitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale) 
{
    m_nCellsPerRow  = nRows;  //每行的单元格数目
    m_nCellsPerCol  = nCols;  //每列的单元格数目
    m_fCellSpacing  = fSpace;	//单元格间的间距
    m_fHeightScale  = fScale; //高度缩放系数
    m_fTerrainWidth = nRows * fSpace;  //地形的宽度
    m_fTerrainDepth = nCols * fSpace;  //地形的深度
    m_nVertsPerRow  = m_nCellsPerCol + 1;  //每行的顶点数
    m_nVertsPerCol  = m_nCellsPerRow + 1; //每列的顶点数
    m_nNumVertices  = m_nVertsPerRow * m_nVertsPerCol;  //顶点总数

    // 通过一个for循环,逐个把地形原始高度乘以缩放系数,得到缩放后的高度
    for(unsigned int i=0; i<m_vHeightInfo.size(); i++)
        m_vHeightInfo[i] *= m_fHeightScale;
	//---------------------------------------------------------------
    // 处理地形的顶点
	//---------------------------------------------------------------
	//1,创建顶点缓存
    if (FAILED(m_pd3dDevice->CreateVertexBuffer(m_nNumVertices * sizeof(TERRAINVERTEX), 
        D3DUSAGE_WRITEONLY, TERRAINVERTEX::FVF, D3DPOOL_MANAGED, &m_pVertexBuffer, 0)))
        return FALSE;
	//2,加锁
    TERRAINVERTEX *pVertices = NULL;
    m_pVertexBuffer->Lock(0, 0, (void**)&pVertices, 0);
	//3,访问,赋值
    FLOAT fStartX = -m_fTerrainWidth / 2.0f, fEndX =  m_fTerrainWidth / 2.0f;		//指定起始点和结束点的X坐标值
    FLOAT fStartZ =  m_fTerrainDepth / 2.0f, fEndZ = -m_fTerrainDepth / 2.0f;	//指定起始点和结束点的Z坐标值
    FLOAT fCoordU = 3.0f / (FLOAT)m_nCellsPerRow;     //指定纹理的横坐标值
    FLOAT fCoordV = 3.0f / (FLOAT)m_nCellsPerCol;		//指定纹理的纵坐标值

    int nIndex = 0, i = 0, j = 0;
    for (float z = fStartZ; z > fEndZ; z -= m_fCellSpacing, i++)		//Z坐标方向上起始顶点到结束顶点行间的遍历
    {
        j = 0;
        for (float x = fStartX; x < fEndX; x += m_fCellSpacing, j++)	//X坐标方向上起始顶点到结束顶点行间的遍历
        {
            nIndex = i * m_nCellsPerRow + j;		//指定当前顶点在顶点缓存中的位置
            pVertices[nIndex] = TERRAINVERTEX(x, m_vHeightInfo[nIndex], z, j*fCoordU, i*fCoordV); //把顶点位置索引在高度图中对应的各个顶点参数以及纹理坐标赋值给赋给当前的顶点
            nIndex++;											//索引数自加1
        }
    }
	//4,解锁
    m_pVertexBuffer->Unlock();

	//---------------------------------------------------------------
    // 处理地形的索引
	//---------------------------------------------------------------
	//1.创建索引缓存
    if (FAILED(m_pd3dDevice->CreateIndexBuffer(m_nNumVertices * 6 *sizeof(WORD), 
        D3DUSAGE_WRITEONLY, D3DFMT_INDEX16, D3DPOOL_MANAGED, &m_pIndexBuffer, 0)))
        return FALSE;
	//2.加锁
    WORD* pIndices = NULL;
    m_pIndexBuffer->Lock(0, 0, (void**)&pIndices, 0);
	//3.访问,赋值
    nIndex = 0;
    for(int row = 0; row < m_nCellsPerRow-1; row++)   //遍历每行
    {
        for(int col = 0; col < m_nCellsPerCol-1; col++)  //遍历每列
        {
			//三角形ABC的三个顶点
            pIndices[nIndex]   =  row * m_nCellsPerRow + col;			//顶点A
            pIndices[nIndex+1] =  row * m_nCellsPerRow + col + 1;  //顶点B
            pIndices[nIndex+2] = (row+1) * m_nCellsPerRow + col;	//顶点C
			//三角形CBD的三个顶点
            pIndices[nIndex+3] = (row+1) * m_nCellsPerRow + col;		//顶点C
            pIndices[nIndex+4] =  row * m_nCellsPerRow + col + 1;		//顶点B
            pIndices[nIndex+5] = (row+1) * m_nCellsPerRow + col + 1;//顶点D
			//处理完一个单元格,索引加上6
            nIndex += 6;  //索引自加6
        }
    }
	//4、解锁
    m_pIndexBuffer->Unlock();

    return TRUE;
}

//--------------------------------------------------------------------------------------
// Name: TerrainClass::RenderTerrain()
// Desc: 绘制出地形,可以通过第二个参数选择是否绘制出线框
//--------------------------------------------------------------------------------------
BOOL TerrainClass::RenderTerrain(D3DXMATRIX *pMatWorld, BOOL bRenderFrame) 
{
    m_pd3dDevice->SetStreamSource(0, m_pVertexBuffer, 0, sizeof(TERRAINVERTEX));   ///把包含的几何体信息的顶点缓存和渲染流水线相关联  
    m_pd3dDevice->SetFVF(TERRAINVERTEX::FVF);//指定我们使用的灵活顶点格式的宏名称
    m_pd3dDevice->SetIndices(m_pIndexBuffer);//设置索引缓存  
    m_pd3dDevice->SetTexture(0, m_pTexture);//设置纹理

    m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, FALSE);	//关闭光照
    m_pd3dDevice->SetTransform(D3DTS_WORLD, pMatWorld); //设置世界矩阵
    m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 
        m_nNumVertices, 0, m_nNumVertices * 2);		//绘制顶点

    m_pd3dDevice->SetRenderState(D3DRS_LIGHTING, TRUE);  //打开光照
    m_pd3dDevice->SetTexture(0, 0);	//纹理置空

    if (bRenderFrame)  //如果要渲染出线框的话
    {
        m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME); //把填充模式设为线框填充
        m_pd3dDevice->DrawIndexedPrimitive(D3DPT_TRIANGLELIST, 0, 0, 
            m_nNumVertices, 0, m_nNumVertices * 2);	//绘制顶点  
        m_pd3dDevice->SetRenderState(D3DRS_FILLMODE, D3DFILL_SOLID);	//把填充模式调回实体填充
    }
    return TRUE;
}

//-----------------------------------------------------------------------------
// Desc: 析构函数
//-----------------------------------------------------------------------------
TerrainClass::~TerrainClass(void)
{
	SAFE_RELEASE(m_pTexture);
	SAFE_RELEASE(m_pIndexBuffer);
	SAFE_RELEASE(m_pVertexBuffer);
}
一个地形类就这样被我们一步一步设计出来了。下一节来看一下这个类到底如何使用。

22.8 示例程序D3Ddemo17

本节的示例程序有点科幻的味道,我们地形渲染得像水晶宝石山,再载入了一个变形金刚中大黄蜂的模型,非常帅气。
源代码包含了8 个文件,主要用于公共辅助宏定义的D3DUtil.h , 用于封装了Directlnput 输入控制API 的DirectlnputClass.h 和DirectlnputClass.cpp ,以及封装了虚拟摄像机类的CameraClass.h和CameraClass.cpp ,封装了地形系统的TerrainClass.h 和TerrainClass.cpp ,当然还有核心代码main.cpp 。


DirectlnputClass.h 和DirectlnputClass.cpp 较之前的示例没有做任何修改,依然不再贴出,TerrainClass.cpp 和TerrainClass.h 在上面讲解的过程中以经贴出来了,这里也不再贴出, 这里只给出main.cpp 中的核心代码:

//-----------------------------------【Object_Init( )函数】--------------------------------------
//	描述:渲染资源初始化函数,在此函数中进行要被渲染的物体的资源的初始化
//--------------------------------------------------------------------------------------------------
HRESULT Objects_Init()
{
	//创建字体
	D3DXCreateFont(g_pd3dDevice, 36, 0, 0, 1000, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, _T("Calibri"), &g_pTextFPS);
	D3DXCreateFont(g_pd3dDevice, 20, 0, 1000, 0, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"华文中宋", &g_pTextAdaperName); 
	D3DXCreateFont(g_pd3dDevice, 23, 0, 1000, 0, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"微软雅黑", &g_pTextHelper); 
	D3DXCreateFont(g_pd3dDevice, 26, 0, 1000, 0, false, DEFAULT_CHARSET, 
		OUT_DEFAULT_PRECIS, DEFAULT_QUALITY, 0, L"黑体", &g_pTextInfor); 


	// 从X文件中加载网格数据
	LPD3DXBUFFER pAdjBuffer  = NULL;
	LPD3DXBUFFER pMtrlBuffer = NULL;

	D3DXLoadMeshFromX(L"bee.X", D3DXMESH_MANAGED, g_pd3dDevice, 
		&pAdjBuffer, &pMtrlBuffer, NULL, &g_dwNumMtrls, &g_pMesh);
	// 读取材质和纹理数据
	D3DXMATERIAL *pMtrls = (D3DXMATERIAL*)pMtrlBuffer->GetBufferPointer(); //创建一个D3DXMATERIAL结构体用于读取材质和纹理信息
	g_pMaterials = new D3DMATERIAL9[g_dwNumMtrls];
	g_pTextures  = new LPDIRECT3DTEXTURE9[g_dwNumMtrls];
	for (DWORD i=0; i<g_dwNumMtrls; i++) 
	{
		//获取材质,并设置一下环境光的颜色值
		g_pMaterials[i] = pMtrls[i].MatD3D;
		g_pMaterials[i].Ambient = g_pMaterials[i].Diffuse;

		//创建一下纹理对象
		g_pTextures[i]  = NULL;
		D3DXCreateTextureFromFileA(g_pd3dDevice, pMtrls[i].pTextureFilename, &g_pTextures[i]);
	}
	SAFE_RELEASE(pAdjBuffer)
	SAFE_RELEASE(pMtrlBuffer)



	//创建柱子
	D3DXCreateCylinder(g_pd3dDevice, 8000.0f, 100.0f, 50000.0f, 60, 60,  &g_cylinder, 0);
	g_MaterialCylinder.Ambient  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);  
	g_MaterialCylinder.Diffuse  = D3DXCOLOR(1.0f, 0.0f, 0.0f, 1.0f);  
	g_MaterialCylinder.Specular = D3DXCOLOR(0.5f, 0.0f, 0.3f, 0.3f);  
	g_MaterialCylinder.Emissive = D3DXCOLOR(0.0f, 0.0f, 0.0f, 1.0f);

	// 设置光照  
	D3DLIGHT9 light;  
	::ZeroMemory(&light, sizeof(light));  
	light.Type          = D3DLIGHT_DIRECTIONAL;  
	light.Ambient       = D3DXCOLOR(0.7f, 0.7f, 0.7f, 1.0f);  
	light.Diffuse       = D3DXCOLOR(1.0f, 1.0f, 1.0f, 1.0f);  
	light.Specular      = D3DXCOLOR(0.9f, 0.9f, 0.9f, 1.0f);  
	light.Direction     = D3DXVECTOR3(1.0f, 1.0f, 1.0f);  
	g_pd3dDevice->SetLight(0, &light);  
	g_pd3dDevice->LightEnable(0, true);  
	g_pd3dDevice->SetRenderState(D3DRS_NORMALIZENORMALS, true);  
	g_pd3dDevice->SetRenderState(D3DRS_SPECULARENABLE, true);

	// 创建并初始化虚拟摄像机
	g_pCamera = new CameraClass(g_pd3dDevice);
	g_pCamera->SetCameraPosition(&D3DXVECTOR3(0.0f, 12000.0f, -30000.0f));  //设置摄像机所在的位置
	g_pCamera->SetTargetPosition(&D3DXVECTOR3(0.0f, 10000.0f, 0.0f));  //设置目标观察点所在的位置
	g_pCamera->SetViewMatrix();  //设置取景变换矩阵
	g_pCamera->SetProjMatrix();  //设置投影变换矩阵

	// 创建并初始化地形
	g_pTerrain = new TerrainClass(g_pd3dDevice);   
	g_pTerrain->LoadTerrainFromFile(L"heighmap.raw", L"green.jpg");		//从文件加载高度图和纹理
	g_pTerrain->InitTerrain(200, 200, 300.0f, 70.0f);  //四个值分别是顶点行数,顶点列数,顶点间间距,缩放系数

	return S_OK;
}
然后是渲染有关的代码, Direct3D_Render()函数的实现代码:

//-----------------------------------【Direct3D_Render( )函数】-------------------------------
//	描述:使用Direct3D进行渲染
//--------------------------------------------------------------------------------------------------
void Direct3D_Render(HWND hwnd)
{
	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之一】:清屏操作
	//--------------------------------------------------------------------------------------
	g_pd3dDevice->Clear(0, NULL, D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER|D3DCLEAR_STENCIL, D3DCOLOR_XRGB(0, 108, 255), 1.0f, 0);

	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之二】:开始绘制
	//--------------------------------------------------------------------------------------
	g_pd3dDevice->BeginScene();                     // 开始绘制
	
	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之三】:正式绘制
	//--------------------------------------------------------------------------------------

	//绘制大黄蜂
	D3DXMATRIX mScal,mRot1,mRot2,mTrans,mFinal;   //定义一些矩阵,准备对大黄蜂进行矩阵变换
	D3DXMatrixScaling(&mScal,20.0f,20.0f,20.0f);  
	D3DXMatrixTranslation(&mTrans,0,8000,0);
	D3DXMatrixRotationX(&mRot1, D3DX_PI/2); 
	D3DXMatrixRotationY(&mRot2, D3DX_PI/2); 
	mFinal=mScal*mRot1*mRot2*mTrans*g_matWorld;
	g_pd3dDevice->SetTransform(D3DTS_WORLD, &mFinal);//设置模型的世界矩阵,为绘制做准备
	// 用一个for循环,进行模型的网格各个部分的绘制
	for (DWORD i = 0; i < g_dwNumMtrls; i++)
	{
		g_pd3dDevice->SetMaterial(&g_pMaterials[i]);  //设置此部分的材质
		g_pd3dDevice->SetTexture(0, g_pTextures[i]);//设置此部分的纹理
		g_pMesh->DrawSubset(i);  //绘制此部分
	}

	//绘制地形
	g_pTerrain->RenderTerrain(&g_matWorld, false);  //渲染地形,且第二个参数设为false,表示不渲染出地形的线框
	
	//绘制柱子
	D3DXMATRIX TransMatrix, RotMatrix, FinalMatrix;
	D3DXMatrixRotationX(&RotMatrix, -D3DX_PI * 0.5f);
	g_pd3dDevice->SetMaterial(&g_MaterialCylinder);
	for(int i = 0; i < 4; i++)
	{
		D3DXMatrixTranslation(&TransMatrix, -10000.0f, 0.0f, -15000.0f + (i * 20000.0f));
		FinalMatrix = RotMatrix * TransMatrix ;
		g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix);
		g_cylinder->DrawSubset(0);

		D3DXMatrixTranslation(&TransMatrix, 10000.0f, 0.0f, -15000.0f + (i * 20000.0f));
		FinalMatrix = RotMatrix * TransMatrix ;
		g_pd3dDevice->SetTransform(D3DTS_WORLD, &FinalMatrix);
		g_cylinder->DrawSubset(0);
	}

	//绘制文字信息
	HelpText_Render(hwnd);


	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之四】:结束绘制
	//--------------------------------------------------------------------------------------
	g_pd3dDevice->EndScene();                       // 结束绘制
	//--------------------------------------------------------------------------------------
	// 【Direct3D渲染五步曲之五】:显示翻转
	//--------------------------------------------------------------------------------------
	g_pd3dDevice->Present(NULL, NULL, NULL, NULL);  // 翻转与显示
	 
}
编译并运行这个程序, 我们就可以看到如下图所示非常炫酷的游戏场景:


如果嫌这个地形不够大或者不够陡峭,我们可以修改顶点间的间距以及缩放系数,来得到陡峭而一望无际的山峰。即修改如下这句代码中的参数:

g_pTerrain->InitTerrain(200, 200, 300.0f, 70.0f);  //四个值分别是顶点行数,顶点列数,顶点间间距,缩放系数

22.9 章节小憩

嗯,在本章中我们一起探究了基本的三维地形的创建方法,其实, 三维地形要讲解深入的话,需要基本上的容量。在本书附录中专门为大家列举了地形和LOD 相关的进阶书籍,大家如果有兴趣可以根据提供的书名去购买或者找到电子版阅读和学习。



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值