本系列文章由zhmxy555(毛星云)编写,转载请注明出处。
作者:毛星云(浅墨) 邮箱: happylifemxy@163.com
上个星期浅墨写的介绍三维摄像机的文章和示例程序放出以后,大家似乎都表现出了很高涨的热情,不少朋友评论或者给浅墨发邮件问什么时候讲地形和天空顶。本来浅墨是准备这个星期就开始讲可编程渲染流水线的,看大家这么强烈的要求,浅墨决定干脆把准备在后面讲的地形天空一气呵成,跟在摄像机后面一起讲了得了。所以,这篇文章就诞生了。
先上一张配套示例程序的截图:
修改顶点间距和缩放比例可以得到更广阔,更陡峭的山峰:
想创造出极具真实感的三维游戏世界,三维地形的模拟是必不可少,至关重要的。
三维地形模拟其实是一个很广阔的课题,它其实不仅仅局限于我们的游戏开发领域,在三维仿真,虚拟现实等领域都涉及。说起三维地形模拟,似乎有那么一丝神秘,其实,只要了解其实现原理了这所谓的地形系统模拟也就是纸老虎一只。这篇文章里我们就来揭开三维地形模拟的面纱,看看到底怎样利用一个C++类的书写,实现我们专属的三维地形系统,然后就只需几句代码,两幅图片,一个“活生生”的三维地形就跃然纸上了。
一、三维地形绘制思路分析
关于地形绘制的大体思路,其实非常简单,让我们先来看三幅图。
我们可以发现,以上的三幅图就概括了三维地形模拟的大体走向与思路。
首先是第一幅图,我们在图中可以看到,图中描绘的就是在同一平面上的三角形网格组成的一个大的矩形区域。在这里我们把他看做是一张大的均匀的同一平面上的“渔网”,显然它是一个二维的平面。图中的每一个顶点都可以用一个二维的坐标(x,y)来唯一表示。
然后第二幅图,我们就像“揠苗助长”一样,拉着第一幅图中的“渔网”的某些顶点往上提(或者往下压)。这里往上提一点,那里提一点,这样,我们就为每一个顶点都赋予了一个高度(就算有的顶点没有移动,它的高度就为0),第一幅图中的渔网就变形了,成了三维图形了。每个顶点就都有了一个高度值。用z坐标来表示这个高度值的话,那么现在三维空间中这个变形的“渔网”中的每个顶点都可以用(x,y,z)来唯一表示。
最后第三幅图,在第二幅图中的三维“渔网”的表面我们“镀上”纹理不尽相同的“薄膜”,也就是进行了一个纹理包装的过程。这样奇迹就发生了,逼真的雪原山川,奇峰怪石展现在了我们眼前。
所以,绘制三维地形的玄机,就被这三幅图联手一语道破了。
其中,第二幅图中的那个“揠苗助长”的过程可谓三维地形绘制的一招“妙棋”。
这招“妙棋”我们常常是借助高度图来完成。下面我们就来讲一讲什么是高度图。
二、关于高度图
高度图在三维地形模拟中扮演着非常重要的角色。下面让我们来一起探讨一下高度图的方方面面。
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位高度图中,每个字节都表示顶点的高度。
2.高度图的制作
高度图的制作一般有两种方式。
1、以某种算法为基础,写个程序生成。比较有名的是Fault Formation和Midpoint Displacement这两种算法。
2、通过图像编辑软件,三维建模软件,或者专业制作地形的软件来制作。
图像编辑软件首当其冲的当然是Photoshop,这个就是我们今天准备教大家的高度图生成方式。(先把后面两种介绍完,稍后就教大家怎么做高度图。)
三维建模软件就如我们之前介绍过的3DS Max和Maya了,地形制作也是三维建模界的一个分支。
然后专业制作地形的软件,比如一款叫Terragen。这款软件用起来也很方便,大家不妨google一下去下一个玩玩。
3、用Photoshop制作高度图
接下来,浅墨来教大家使用Photoshop生成高度图。
1.打开Photoshop(浅墨用的是Photoshop CS6),【Ctrl+N】或者依次点击菜单栏上的【文件】->【新建】,新建一个画布。如下图,我们的画布的大小取64x64像素。
2.创建完画布,接下来就是最关键的一步。依次点击菜单栏上的【滤镜】->【渲染】->【云彩】。
这时候,我们就可以发现,我们创建的空白画布上有了随机的灰度颜色值,如果你对这次生成的随机灰度图不满意的话,大可再次点击【滤镜】->【渲染】->【云彩】(或者【Ctrl+F】)来重新生成一次随机的灰度效果图,直到颜色分布满意为止。我也也可以用画笔来在图上涂抹,自己来设定高度。这是浅墨通过处理后得到的一张灰度图,这样后面如果我们用这张图作为高度图,得到的就是一个凹下去型的爱心地形图:
记得在用【云彩】滤镜的时候,最好把调色板的颜色前景色设为纯黑色,不然可能得到的随机灰度图效果出不来。即调色板中的颜色设置成如下图:
另外,我们可以通过对图片色阶的调整,来对生成的灰度图的整体颜色进行调节。比如想让地形整体来说高一些,就把灰度图整体调亮一些,反之,地形整体来说要显得低一些的话,就把绘图图整体调按。色阶对话框通过【图像】->【调整】->【色阶】打开,或者直接按快捷键【Ctrl+F】。
另外在点击【图像】->【调整】后弹出的对话框中还有曲线、色相、饱和度等等选项,大家不妨也试试。
制作完成,我们点击【文件】->【储存为…】或者直接按快捷键【Shift+Ctrl+S】来制作好的高度图进行保存。保存的格式随意,因为我们稍后写的一个地形类原则上支持几乎所有的图片格式高度图的导入,只不过对有些图片格式得到的效果图比较奇葩而已。
这里我们选择8位的raw格式:
点击确定后,会弹出如下导出raw的对话框,记得要把【通道储存在】这个选项改成非隔行顺序,如图:
其实,大家不想用Photshop的话,可以直接google一下“heighmap”,搜索结果中随便找就是一张现成的,然后改成raw格式就好了。原则上我们可以直接随便拿一张任意格式的图片来做高度图使用,只是可能做出来的地形显得怪异一点而已。
4.在程序中读取高度图
让我们针对使用最广泛的raw类型的高度图进行讲解。由于raw格式文件是按字节为单位保存图像中的每个像素的灰度值的,那么我们可以容易地读取保存在该文件中的高度信息。在这次的地形类的实现中,我们用到了C++中模板以及文件流的知识,如果对下面这段代码不太熟悉的话就去看看《C++Primer》的相应章节吧。好了,下面贴出详细注释的代码:
// 从文件中读取高度信息 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]之间取值。我们显然不高兴这样。所以,我们继续将读取的高度信息重新保存到一个浮点型的模板类型中,这样就能舒心地取到任何范围的高度值了。注意下面这段代码中vHeightInfo的定义是在类头文件中的:
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];
三、地形类轮廓的书写
在继续展开讲解之前,让我们先来把这个地形类的整体轮廓给勾勒出来。这个类我们取名为TerrainClass,它能通过载入二进制类型的文件(以raw格式为首)来得到地形的高度信息,通过载入图片得到地形所采用的纹理。载入文件的过程我们封装在一个名为LoadTerrainFromFile的函数中。
在上文中讲高度图的概念相关知识的时候我们就提到过,需要把高度图所传达的信息转化到顶点网格中去,这样才好绘制出来。所以在类中既是重点也是难点的就是这个“转化”的过程,这个过程我们放到一个名为InitTerrain的函数中。高度图到顶点的“转化”完成后,接下来当然需要把这些顶点配合着纹理都绘制出来,绘制的过程我们放在一个名为RenderTerrain的函数中。加上构造函数和析构函数,FVF顶点格式的定义以及若干必须的成员变量,我们就可以勾勒出TerrainClass类的轮廓如下,即下面贴出来的是Terrain.h头文件的全部代码:
//============================================================================= // Name: TerrainClass.h // Des: 一个封装了三维地形系统的类的头文件 // 2013年 3月17日 Create by 浅墨 //============================================================================= #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); //地形渲染函数 };
四、地形顶点的计算
下面我们来看看如何计算出地形中的每个顶点。
在计算顶点之前,还需要做一些准备工作。在创建地形时,需要通过指定地形的行数、列数以及顶点间的距离来指定地形的大小。上面我们在给类写轮廓的时候刚贴出来过,封装着地形顶点计算的InitTerrain函数的原型是这样的:
BOOLInitTerrain(INT nRows, INT nCols, FLOAT fSpace, FLOAT fScale); //地形初始化函数
其中前两个参数分别为地形的行数和列数,需要我们在初始化时指定。也就是说在计算地形的时候行数和列数是已知的,那么,地形在x方向和z方向上的顶点数也就明了了,也就是z方向上顶点数为地形的行数加1,而在x方向上的顶点数为地形列数加上1.
需要注意的是地形在x方向和z方向上的顶点数都不能大于与高度图对应的分辨率分量。因为高度图中的每个元素都描述地形型中某个顶点的高度值。如果高度图中只描述了128x128分辨率的地形信息,我们在初始化时InitTerrain函数的前两个参数都不能取超过128的值。以高度图的角度来想一下,既然我只跟你准备了128x128的高度信息,那么你就得在我规定的范围之内取顶点数,如果你取多了,我可管不了你这么多,等着内存溢出吧。
接着,第三个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();
已经逐行注释了,理解起来应该是没问题的吧。
五、地形索引的计算
顶点值算完了,当然还需要接着计算顶点的索引。顶点索引的计算关键是推导出一个用于求构成第i行,第j列的顶点处右下方两个三角形的顶点索引的通用公式。下面我们来看看这个公式如何推导,下图依旧解释得很清楚了:
对顶点缓存中的任意一点A,如果该点位于地形中的第i行、第j列的话,那么该点在顶点缓存中所对应的位置应该就是i*m+j(m为每行的顶点数)。如果A点在索引缓存中的位置为k的话,那么A点为起始点构成的三角形ABC中,B、C顶点在顶点缓存中的位置就为(i+1)x m+j和i x 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();
六、渲染出地形
没有渲染我们前面就算白忙活了。
地形的渲染我们封装在了一个名为RenderTerrain的函数中,注释很详细,我们直接上代码:
//-------------------------------------------------------------------------------------- // Name:TerrainClass::RenderTerrain() // Desc: 绘制出地形,可以通过第二个参数选择是否绘制出线框 //-------------------------------------------------------------------------------------- BOOLTerrainClass::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; }
七、完成地形类的设计
上面都是零零散散地说出了我们地形类的某些实现细节,下面我们把他们整合在一起,构成一个整体,完成地形类的设计,即贴出TerrainClass.cpp的全部代码:
//============================================================================= // Name:TerrainClass.cpp // Des: 一个封装了三维地形系统的类的源文件 // 2013年 3月17日 Create by 浅墨 //============================================================================= #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: 加载地形高度信息以及纹理 //-------------------------------------------------------------------------------------- BOOLTerrainClass::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: 初始化地形的高度, 填充顶点和索引缓存 //-------------------------------------------------------------------------------------- BOOLTerrainClass::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: 绘制出地形,可以通过第二个参数选择是否绘制出线框 //-------------------------------------------------------------------------------------- BOOLTerrainClass::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); }
一个地形类就这样被我们一步一步设计出来了。下面我们来看一下,这个类到底如何用。
八、详细注释的源代码欣赏
本次的配套程序有点科幻的味道,我们地形渲染得像水晶宝石山,然后载入了一个变形金刚中大黄蜂的模型,非常帅气。
源代码包含了8个文件,主要用于公共辅助宏定义的D3DUtil.h,用于封装了DirectInput输入控制API的DirectInputClass.h和DirectInputClass.cpp,以及封装了虚拟摄像机类的CameraClass.h和CameraClass.cpp,封装了地形系统的TerrainClass.h和TerrainClass.cpp,当还还有核心代码main.cpp。
DirectInputClass.h和DirectInputClass.cpp较之前的文章中依然没有任何修改,依然不再贴出,TerrainClass.cpp和TerrainClass.h在上面讲解的过程中以及贴出来了,这里也不贴出,我们依然只贴核心代码main.cpp,大家要看得爽的话,源代码在文章末尾有下载链接,下回去用VisualStuido看就行了。下面就是main.cpp的全部代码:
//***************************************************************************************** // //【Visual C++】游戏开发笔记系列配套源码四十八 浅墨DirectX教程十六 三维地形系统的实现 // VS2010版 // 2013年 3月17日 Create by 浅墨 //***************************************************************************************** //***************************************************************************************** // Desc: 宏定义部分 //***************************************************************************************** #define SCREEN_WIDTH 800 //为窗口宽度定义的宏,以方便在此处修改窗口宽度 #define SCREEN_HEIGHT 600 //为窗口高度定义的宏,以方便在此处修改窗口高度 #define WINDOW_TITLE _T("【Visual C++】游戏开发笔记系列配套示例程序四十八 浅墨DirectX教程十六 三维地形系统的实现") //为窗口标题定义的宏 //***************************************************************************************** // Desc: 头文件定义部分 //***************************************************************************************** #include <d3d9.h> #include <d3dx9.h> #include <tchar.h> #include <time.h> #include "DirectInputClass.h" #include "CameraClass.h" #include "TerrainClass.h" //***************************************************************************************** // Desc: 库文件定义部分 //***************************************************************************************** #pragma comment(lib,"d3d9.lib") #pragma comment(lib,"d3dx9.lib") #pragma comment(lib, "dinput8.lib") // 使用DirectInput必须包含的库文件,注意这里有8 #pragma comment(lib,"dxguid.lib") #pragma comment(lib, "winmm.lib") //***************************************************************************************** // Desc: 全局变量声明部分 //***************************************************************************************** LPDIRECT3DDEVICE9 g_pd3dDevice = NULL; //Direct3D设备对象 LPD3DXFONT g_pTextFPS =NULL; //字体COM接口 LPD3DXFONT g_pTextAdaperName = NULL; // 显卡信息的2D文本 LPD3DXFONT g_pTextHelper = NULL; // 帮助信息的2D文本 LPD3DXFONT g_pTextInfor= NULL; // 绘制信息的2D文本 float g_FPS= 0.0f; //一个浮点型的变量,代表帧速率 wchar_t g_strFPS[50] ={0}; //包含帧速率的字符数组 wchar_t g_strAdapterName[60] ={0}; //包含显卡名称的字符数组 D3DXMATRIX g_matWorld; //世界矩阵 LPD3DXMESH g_pMesh = NULL; // 网格对象 D3DMATERIAL9* g_pMaterials= NULL; // 网格的材质信息 LPDIRECT3DTEXTURE9* g_pTextures = NULL; // 网格的纹理信息 DWORD g_dwNumMtrls = 0; // 材质的数目 LPD3DXMESH g_cylinder = NULL; //柱子网格对象 D3DMATERIAL9 g_MaterialCylinder; //柱子的材质 DInputClass* g_pDInput = NULL; //DInputClass类的指针实例 CameraClass* g_pCamera = NULL; //摄像机类的指针实例 TerrainClass* g_pTerrain = NULL; //地形类的指针实例 //***************************************************************************************** // Desc: 全局函数声明部分 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ); HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance); HRESULT Objects_Init(); void Direct3D_Render( HWND hwnd); void Direct3D_Update( HWND hwnd); void Direct3D_CleanUp( ); float Get_FPS(); void HelpText_Render(HWND hwnd); //***************************************************************************************** // Name: WinMain( ) // Desc: Windows应用程序入口函数 //***************************************************************************************** int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance,LPSTR lpCmdLine, int nShowCmd) { //开始设计一个完整的窗口类 WNDCLASSEX wndClass = { 0 }; //用WINDCLASSEX定义了一个窗口类,即用wndClass实例化了WINDCLASSEX,用于之后窗口的各项初始化 wndClass.cbSize = sizeof( WNDCLASSEX ) ; //设置结构体的字节数大小 wndClass.style = CS_HREDRAW | CS_VREDRAW; //设置窗口的样式 wndClass.lpfnWndProc = WndProc; //设置指向窗口过程函数的指针 wndClass.cbClsExtra = 0; wndClass.cbWndExtra = 0; wndClass.hInstance = hInstance; //指定包含窗口过程的程序的实例句柄。 wndClass.hIcon=(HICON)::LoadImage(NULL,_T("icon.ico"),IMAGE_ICON,0,0,LR_DEFAULTSIZE|LR_LOADFROMFILE); //从全局的::LoadImage函数从本地加载自定义ico图标 wndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); //指定窗口类的光标句柄。 wndClass.hbrBackground=(HBRUSH)GetStockObject(GRAY_BRUSH); //为hbrBackground成员指定一个灰色画刷句柄 wndClass.lpszMenuName = NULL; //用一个以空终止的字符串,指定菜单资源的名字。 wndClass.lpszClassName = _T("ForTheDreamOfGameDevelop"); //用一个以空终止的字符串,指定窗口类的名字。 if( !RegisterClassEx( &wndClass ) ) //设计完窗口后,需要对窗口类进行注册,这样才能创建该类型的窗口 return -1; HWND hwnd = CreateWindow( _T("ForTheDreamOfGameDevelop"),WINDOW_TITLE, //喜闻乐见的创建窗口函数CreateWindow WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, SCREEN_WIDTH, SCREEN_HEIGHT, NULL, NULL, hInstance, NULL ); //Direct3D资源的初始化,调用失败用messagebox予以显示 if (!(S_OK==Direct3D_Init (hwnd,hInstance))) { MessageBox(hwnd, _T("Direct3D初始化失败~!"), _T("浅墨的消息窗口"), 0); //使用MessageBox函数,创建一个消息窗口 } PlaySound(L"雅尼 - 兰花.wav", NULL, SND_FILENAME | SND_ASYNC|SND_LOOP); //循环播放背景音乐 MoveWindow(hwnd,200,50,SCREEN_WIDTH,SCREEN_HEIGHT,true); //调整窗口显示时的位置,窗口左上角位于屏幕坐标(200,50)处 ShowWindow( hwnd, nShowCmd ); //调用Win32函数ShowWindow来显示窗口 UpdateWindow(hwnd); //对窗口进行更新,就像我们买了新房子要装修一样 //进行DirectInput类的初始化 g_pDInput = new DInputClass(); g_pDInput->Init(hwnd,hInstance,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE,DISCL_FOREGROUND | DISCL_NONEXCLUSIVE); //消息循环过程 MSG msg = { 0 }; //初始化msg while( msg.message != WM_QUIT ) //使用while循环 { if( PeekMessage( &msg, 0, 0, 0, PM_REMOVE ) ) //查看应用程序消息队列,有消息时将队列中的消息派发出去。 { TranslateMessage( &msg ); //将虚拟键消息转换为字符消息 DispatchMessage( &msg ); //该函数分发一个消息给窗口程序。 } else { Direct3D_Update(hwnd); //调用更新函数,进行画面的更新 Direct3D_Render(hwnd); //调用渲染函数,进行画面的渲染 } } UnregisterClass(_T("ForTheDreamOfGameDevelop"), wndClass.hInstance); return 0; } //***************************************************************************************** // Name: WndProc() // Desc: 对窗口消息进行处理 //***************************************************************************************** LRESULT CALLBACK WndProc( HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam ) //窗口过程函数WndProc { switch( message ) //switch语句开始 { case WM_PAINT: // 客户区重绘消息 Direct3D_Render(hwnd); //调用Direct3D_Render函数,进行画面的绘制 ValidateRect(hwnd, NULL); // 更新客户区的显示 break; //跳出该switch语句 case WM_KEYDOWN: // 键盘按下消息 if (wParam == VK_ESCAPE) // ESC键 DestroyWindow(hwnd); // 销毁窗口, 并发送一条WM_DESTROY消息 break; case WM_DESTROY: //窗口销毁消息 Direct3D_CleanUp(); //调用Direct3D_CleanUp函数,清理COM接口对象 PostQuitMessage( 0 ); //向系统表明有个线程有终止请求。用来响应WM_DESTROY消息 break; //跳出该switch语句 default: //若上述case条件都不符合,则执行该default语句 return DefWindowProc( hwnd, message, wParam, lParam ); //调用缺省的窗口过程来为应用程序没有处理的窗口消息提供缺省的处理。 } return 0; //正常退出 } //***************************************************************************************** // Name: Direct3D_Init( ) // Desc: 初始化Direct3D // Point:【Direct3D初始化四步曲】 // 1.初始化四步曲之一,创建Direct3D接口对象 // 2.初始化四步曲之二,获取硬件设备信息 // 3.初始化四步曲之三,填充结构体 // 4.初始化四步曲之四,创建Direct3D设备接口 //***************************************************************************************** HRESULT Direct3D_Init(HWND hwnd,HINSTANCE hInstance) { //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之一,创接口】:创建Direct3D接口对象, 以便用该Direct3D对象创建Direct3D设备对象 //-------------------------------------------------------------------------------------- LPDIRECT3D9 pD3D = NULL; //Direct3D接口对象的创建 if( NULL == ( pD3D = Direct3DCreate9( D3D_SDK_VERSION ) ) ) //初始化Direct3D接口对象,并进行DirectX版本协商 return E_FAIL; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之二,取信息】:获取硬件设备信息 //-------------------------------------------------------------------------------------- D3DCAPS9 caps; int vp = 0; if( FAILED( pD3D->GetDeviceCaps( D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, &caps ) ) ) { return E_FAIL; } if( caps.DevCaps & D3DDEVCAPS_HWTRANSFORMANDLIGHT ) vp = D3DCREATE_HARDWARE_VERTEXPROCESSING; //支持硬件顶点运算,我们就采用硬件顶点运算,妥妥的 else vp = D3DCREATE_SOFTWARE_VERTEXPROCESSING; //不支持硬件顶点运算,无奈只好采用软件顶点运算 //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之三,填内容】:填充D3DPRESENT_PARAMETERS结构体 //-------------------------------------------------------------------------------------- D3DPRESENT_PARAMETERS d3dpp; ZeroMemory(&d3dpp, sizeof(d3dpp)); d3dpp.BackBufferWidth = SCREEN_WIDTH; d3dpp.BackBufferHeight = SCREEN_HEIGHT; d3dpp.BackBufferFormat = D3DFMT_A8R8G8B8; d3dpp.BackBufferCount = 2; d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE; d3dpp.MultiSampleQuality = 0; d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD; d3dpp.hDeviceWindow = hwnd; d3dpp.Windowed = true; d3dpp.EnableAutoDepthStencil = true; d3dpp.AutoDepthStencilFormat = D3DFMT_D24S8; d3dpp.Flags = 0; d3dpp.FullScreen_RefreshRateInHz = 0; d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_IMMEDIATE; //-------------------------------------------------------------------------------------- // 【Direct3D初始化四步曲之四,创设备】:创建Direct3D设备接口 //-------------------------------------------------------------------------------------- if(FAILED(pD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, hwnd, vp, &d3dpp, &g_pd3dDevice))) return E_FAIL; //获取显卡信息到g_strAdapterName中,并在显卡名称之前加上“当前显卡型号:”字符串 wchar_t TempName[60]=L"当前显卡型号:"; //定义一个临时字符串,且方便了把"当前显卡型号:"字符串引入我们的目的字符串中 D3DADAPTER_IDENTIFIER9 Adapter; //定义一个D3DADAPTER_IDENTIFIER9结构体,用于存储显卡信息 pD3D->GetAdapterIdentifier(0,0,&Adapter);//调用GetAdapterIdentifier,获取显卡信息 int len = MultiByteToWideChar(CP_ACP,0, Adapter.Description, -1, NULL, 0);//显卡名称现在已经在Adapter.Description中了,但是其为char类型,我们要将其转为wchar_t类型 MultiByteToWideChar(CP_ACP, 0, Adapter.Description, -1, g_strAdapterName, len);//这步操作完成后,g_strAdapterName中就为当前我们的显卡类型名的wchar_t型字符串了 wcscat_s(TempName,g_strAdapterName);//把当前我们的显卡名加到“当前显卡型号:”字符串后面,结果存在TempName中 wcscpy_s(g_strAdapterName,TempName);//把TempName中的结果拷贝到全局变量g_strAdapterName中,大功告成~ if(!(S_OK==Objects_Init())) return E_FAIL; SAFE_RELEASE(pD3D) //LPDIRECT3D9接口对象的使命完成,我们将其释放掉 return S_OK; } 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, 6000.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, 500.0f, 60.0f); //四个值分别是顶点行数,顶点列数,顶点间间距,缩放系数 return S_OK; } void Direct3D_Update( HWND hwnd) { //使用DirectInput类读取数据 g_pDInput->GetInput(); // 沿摄像机各分量移动视角 if (g_pDInput->IsKeyDown(DIK_A)) g_pCamera->MoveAlongRightVec(-10.0f); if (g_pDInput->IsKeyDown(DIK_D)) g_pCamera->MoveAlongRightVec( 10.0f); if (g_pDInput->IsKeyDown(DIK_W)) g_pCamera->MoveAlongLookVec( 10.0f); if (g_pDInput->IsKeyDown(DIK_S)) g_pCamera->MoveAlongLookVec(-10.0f); if (g_pDInput->IsKeyDown(DIK_I)) g_pCamera->MoveAlongUpVec( 10.0f); if (g_pDInput->IsKeyDown(DIK_K)) g_pCamera->MoveAlongUpVec(-10.0f); //沿摄像机各分量旋转视角 if (g_pDInput->IsKeyDown(DIK_LEFT)) g_pCamera->RotationUpVec(-0.003f); if (g_pDInput->IsKeyDown(DIK_RIGHT)) g_pCamera->RotationUpVec( 0.003f); if (g_pDInput->IsKeyDown(DIK_UP)) g_pCamera->RotationRightVec(-0.003f); if (g_pDInput->IsKeyDown(DIK_DOWN)) g_pCamera->RotationRightVec( 0.003f); if (g_pDInput->IsKeyDown(DIK_J)) g_pCamera->RotationLookVec(-0.001f); if (g_pDInput->IsKeyDown(DIK_L)) g_pCamera->RotationLookVec( 0.001f); //鼠标控制右向量和上向量的旋转 g_pCamera->RotationUpVec(g_pDInput->MouseDX()* 0.001f); g_pCamera->RotationRightVec(g_pDInput->MouseDY() * 0.001f); //鼠标滚轮控制观察点收缩操作 static FLOAT fPosZ=0.0f; fPosZ += g_pDInput->MouseDZ()*0.03f; //计算并设置取景变换矩阵 D3DXMATRIX matView; g_pCamera->CalculateViewMatrix(&matView); g_pd3dDevice->SetTransform(D3DTS_VIEW, &matView); //把正确的世界变换矩阵存到g_matWorld中 D3DXMatrixTranslation(&g_matWorld, 0.0f, 0.0f, fPosZ); //以下这段代码用于限制鼠标光标移动区域 POINT lt,rb; RECT rect; GetClientRect(hwnd,&rect); //取得窗口内部矩形 //将矩形左上点坐标存入lt中 lt.x = rect.left; lt.y = rect.top; //将矩形右下坐标存入rb中 rb.x = rect.right; rb.y = rect.bottom; //将lt和rb的窗口坐标转换为屏幕坐标 ClientToScreen(hwnd,<); ClientToScreen(hwnd,&rb); //以屏幕坐标重新设定矩形区域 rect.left = lt.x; rect.top = lt.y; rect.right = rb.x; rect.bottom = rb.y; //限制鼠标光标移动区域 ClipCursor(&rect); } //***************************************************************************************** // Name: Direct3D_Render() // Desc: 进行图形的渲染操作 // Point:【Direct3D渲染五步曲】 // 1.渲染五步曲之一,清屏操作 // 2.渲染五步曲之二,开始绘制 // 3.渲染五步曲之三,正式绘制 // 4.渲染五步曲之四,结束绘制 // 5.渲染五步曲之五,翻转显示 //***************************************************************************************** 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); // 翻转与显示 } void HelpText_Render(HWND hwnd) { //定义一个矩形,用于获取主窗口矩形 RECT formatRect; GetClientRect(hwnd, &formatRect); //在窗口右上角处,显示每秒帧数 formatRect.top = 5; int charCount = swprintf_s(g_strFPS, 20, _T("FPS:%0.3f"), Get_FPS() ); g_pTextFPS->DrawText(NULL, g_strFPS, charCount , &formatRect, DT_TOP | DT_RIGHT, D3DCOLOR_RGBA(0,239,136,255)); //显示显卡类型名 g_pTextAdaperName->DrawText(NULL,g_strAdapterName, -1, &formatRect, DT_TOP | DT_LEFT, D3DXCOLOR(1.0f, 0.5f, 0.0f, 1.0f)); // 输出帮助信息 formatRect.left = 0,formatRect.top = 380; g_pTextInfor->DrawText(NULL, L"控制说明:", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(235,123,230,255)); formatRect.top += 35; g_pTextHelper->DrawText(NULL, L" W:向前飞翔 S:向后飞翔 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" A:向左飞翔 D:向右飞翔", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" I:垂直向上飞翔 K:垂直向下飞翔", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" J:向左倾斜 L:向右倾斜", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 上、下、左、右方向键、鼠标移动:视角变化 ", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" 鼠标滚轮:人物模型Y轴方向移动", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); formatRect.top += 25; g_pTextHelper->DrawText(NULL, L" ESC键 : 退出程序", -1, &formatRect, DT_SINGLELINE | DT_NOCLIP | DT_LEFT, D3DCOLOR_RGBA(255,200,0,255)); } //***************************************************************************************** // Name:Get_FPS()函数 // Desc: 用于计算帧速率 //***************************************************************************************** float Get_FPS() { //定义四个静态变量 static float fps = 0; //我们需要计算的FPS值 static int frameCount = 0;//帧数 static float currentTime =0.0f;//当前时间 static float lastTime = 0.0f;//持续时间 frameCount++;//每调用一次Get_FPS()函数,帧数自增1 currentTime = timeGetTime()*0.001f;//获取系统时间,其中timeGetTime函数返回的是以毫秒为单位的系统时间,所以需要乘以0.001,得到单位为秒的时间 //如果当前时间减去持续时间大于了1秒钟,就进行一次FPS的计算和持续时间的更新,并将帧数值清零 if(currentTime - lastTime > 1.0f) //将时间控制在1秒钟 { fps = (float)frameCount /(currentTime - lastTime);//计算这1秒钟的FPS值 lastTime = currentTime; //将当前时间currentTime赋给持续时间lastTime,作为下一秒的基准时间 frameCount = 0;//将本次帧数frameCount值清零 } return fps; } //***************************************************************************************** // Name: Direct3D_CleanUp() // Desc: 对Direct3D的资源进行清理,释放COM接口对象 //***************************************************************************************** void Direct3D_CleanUp() { //释放COM接口对象 for (DWORD i = 0; i<g_dwNumMtrls; i++) SAFE_RELEASE(g_pTextures[i]); SAFE_DELETE(g_pTextures); SAFE_DELETE(g_pMaterials); SAFE_DELETE(g_pDInput); SAFE_RELEASE(g_cylinder); SAFE_RELEASE(g_pMesh); SAFE_RELEASE(g_pd3dDevice); SAFE_RELEASE(g_pTextAdaperName) SAFE_RELEASE(g_pTextHelper) SAFE_RELEASE(g_pTextInfor) SAFE_RELEASE(g_pTextFPS) SAFE_RELEASE(g_pd3dDevice) }
然后是几张程序截图:
如果是嫌这个地形还不过瘾,不够大或者不够陡峭,我们可以修改顶点间的间距以及缩放系数,来得到陡峭而一望无际的山峰。下图对应的是在初始化地形时将顶点间的间距以及缩放系数调大一些的情况:
g_pTerrain->InitTerrain(200, 200, 2000.0f, 600.0f); //四个值分别是顶点行数,顶点列数,顶点间间距,缩放系数
按我们的飞行速度,达到这山峰的地图边界得飞几分钟。。。。不过这时候大黄蜂不见了,在地形整体下方了,此时摄像机初始位置也最好调一下,不然一出来也是在地形整体的下方。
文章最后,依旧是放出本篇文章配套源代码的下载:
本节笔记配套源代码请点击这里下载:
【浅墨DirectX提高班】配套源代码之十六下载
以上就是本节笔记的全部内容,更多精彩内容,且听下回分解。
浅墨在这里,希望喜欢游戏开发系列文章的朋友们能留下你们的评论,每次浅墨登陆博客看到大家的留言的时候都会非常开心,感觉自己正在传递一种信仰,一种精神。
文章最后,依然是【每文一语】栏目,今天的句子是:
这世上有两样东西是别人抢不走的:一是藏在心中的梦想,二是读进大脑的书。
下周一,让我们离游戏开发的梦想更近一步。
下周一,游戏开发笔记,我们,不见不散。
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。