作者: i_dovelemon
来源:CSDN
日期:2014 / 9 / 24
关键字:Multi-texturing, Terrian Rendering, Height Map
在很多游戏中,我们遇到的并不是一望无际的平原,而是有着高低不平的地形。这一节,我们将要了解,如何建立地形。
地形有高低之分,在3D图形中,我们使用高度图来表述一个地形的高低起伏。高度图是一个像素矩阵,它保存了地形网格中每一个顶点的高度值。我们所要做的就是,根据地形的尺寸,创建适合大小的高度图,并且设定不同的高度值,来描述地形。有很多的工具都够产生高度图,比如Terragen这个工具还有Bryce 5.5也能够创建出高度图出来。高度图保存之后,是一个灰度图像,它仅仅是作为高度数据来使用,并不是用来进行纹理映射的纹理图。我们可以将高度图保存为任何我们想要保存的格式。这里为了讲述上的方便,我们使用不带文件头的raw文件来保存高度图。在这个文件中,每一个字节表示相对应的顶点的高度值,它不含有RGB值,仅仅一个8位的值,用来保存高度值而已。为了能够读取这样的文件,我们编写如下的类,用于读取这样的高度图:
<span style="font-family:Microsoft YaHei;">//------------------------------------------------------------------------------------------- // declaration : Copyright (c), by XJ , 2014. All right reserved . // brief : This file will define the Height Map model. // file : HeightMap.h // author : XJ // date : 2014 / 9 / 23 // version : 1.0 //-------------------------------------------------------------------------------------------- #pragma once #include<iostream> using namespace std ; class HeightMap { public: HeightMap(); HeightMap(int x, int y); HeightMap(int x, int y, const std::string& fileName, float heightScale, float heightOffset ); ~HeightMap(); void recreate(int x, int y); void loadRaw(int x, int y, const std::string& fileName, float heightScale, float heightOffset); int numRows() const ; int numCols() const ; //For non-const objects float& operator()(int i, int j); //For const object const float& operator()(int i, int j)const ; private: bool inBounds(int i, int j); float sampleHeight3x3(int i, int j); void filter3x3(); private: std::string m_sFileName ; float *m_pHeightMap; float m_fHeightScale; float m_fHeightOffset; unsigned int m_uiNumCols; unsigned int m_uiNumRows; };// end for class</span>
<span style="font-family:Microsoft YaHei;">#include"HeightMap.h" #include<fstream> using namespace std ; /** * Constructor */ HeightMap::HeightMap() { m_pHeightMap = NULL ; m_fHeightScale = 0 ; m_fHeightOffset = 0 ; m_uiNumRows = 0 ; m_uiNumCols = 0 ; } HeightMap::HeightMap(int x, int y) { m_pHeightMap = NULL ; m_fHeightScale = 0 ; m_fHeightOffset = 0 ; m_uiNumRows = x ; m_uiNumCols = y ; recreate(x,y); } HeightMap::HeightMap(int x, int y, const std::string& fileName, float heightScale, float heightOffset) { m_sFileName = fileName ; m_pHeightMap = NULL ; m_fHeightScale = heightScale ; m_fHeightOffset = heightOffset ; m_uiNumRows = x ; m_uiNumCols = y ; loadRaw(x,y, m_sFileName, m_fHeightScale, m_fHeightOffset); } /** * Destructor */ HeightMap::~HeightMap() { if(m_pHeightMap) delete[]m_pHeightMap; m_pHeightMap = NULL ; } /** * Create an m*n heightmap with heights initializa zero */ void HeightMap::recreate(int x, int y) { m_pHeightMap = new float[x * y]; memset(m_pHeightMap, 0, sizeof(float) * (x * y)); }// end for recreate /** * Load the heightmap from the .raw file which does not have the file header */ void HeightMap::loadRaw(int x, int y, const std::string& fileName, float heightScale, float heightOffset) { //open the file ifstream input; input.open(fileName,ios_base::binary); if(input.fail()) return ; unsigned char * buffer = new unsigned char[x * y]; input.read((char*)&buffer[0], (streamsize)(x * y) * sizeof(unsigned char)); input.close(); //allocate the memory the map data m_pHeightMap = new float[x * y]; //scale and offset the height value for(int i = 0 ; i < y ; i ++) { for(int j = 0 ; j < x ;j ++) { m_pHeightMap[i * m_uiNumCols + j] = (float)buffer[i * m_uiNumCols + j] * m_fHeightScale + m_fHeightOffset; }// end for j }// end for i delete []buffer ; //filter3x3(); }// end for loadRaw /** * Sample the specific height value according the box filter */ float HeightMap::sampleHeight3x3(int x,int y) { float sum = 0 , avg = 0 ; unsigned int num = 0 ; for( int i = x -1 ; i <= x + 1 ; i ++) { for( int j = y - 1 ; j <= y + 1 ; j ++) { if(inBounds(i,j)) { sum += m_pHeightMap[i * m_uiNumCols + j]; num ++ ; } }// end for j }// end for i avg = sum / num ; return avg ; }// end for sampleHeight3x3 /** * Fileter */ void HeightMap::filter3x3() { float *buffer = new float[m_uiNumCols * m_uiNumRows]; memset(buffer, 0, sizeof(float)*(m_uiNumCols * m_uiNumRows)); for(int i = 0 ; i < m_uiNumRows ; i ++) { for(int j = 0 ; j < m_uiNumCols ; j ++) { buffer[i * m_uiNumCols + j] = sampleHeight3x3(i,j); }// end for j }// end for i memcpy(m_pHeightMap, buffer, sizeof(float) * (m_uiNumCols * m_uiNumRows)); }// end for filter3x3 /** * Check if the coordinate is in the range */ bool HeightMap::inBounds(int i, int j) { if( i < 0 || i > m_uiNumCols) return false ; if( j < 0 || j > m_uiNumRows) return false ; return true ; }// end for inBounds /** * Return the num of raws */ int HeightMap::numRows() const { return m_uiNumRows ; }// end for numRows /** * Return the num of cols */ int HeightMap::numCols() const { return m_uiNumCols ; }// end for numCols /** * Operator */ float& HeightMap::operator()(int i, int j) { return m_pHeightMap[j * m_uiNumCols + i] ; }// end for () const float& HeightMap::operator()(int i, int j) const { return m_pHeightMap[j * m_uiNumCols + i]; }// end for ()</span>
在上面的函数中,有一个函数filter3x3,需要说明下。我们知道,如果只用一个字节来表示高度的话,那么就是说我们只能够表示256个高度级别。因为你不管对高度值进行怎样的放大缩小,不同高度值的种类总是只有256种,也就是说一个字节的高度值,只能够表示256个级别的高度变化。所以,当我们想要的高度很高的时候,比如说0-256000的时候,那么将0-256000使用高度图进行变换的话,即使两个高度值是连续的,它们两个高度之间也相差1000个单位。这样看上去很不平滑。我们这里使用简单的box filter来对高度值进行滤波处理。使用围绕当前采样的高度值所在的8个值,进行平均运算,得到一个平均值来表示当前的高度值。使用这样的方法,就能够出现较平滑的高度渐进了。如果想要更加平滑的效果,那么可以使用加权的box filter,进行高度值的滤波处理。下图是不使用box filter与使用box filter进行高度采样的区别:
上图是使用box filter进行滤波处理之后的图,下图是没有进行box filter滤波处理之后的图:
从上图的比较可以看出,使用box filter之后,图像显得平滑了很多,不那么尖锐。
在我们对高度图进行了处理之后,我们就需要根据高度图来进行地形网格的构造了。下面的函数是进行地形网格构造的方法:
<span style="font-family:Microsoft YaHei;">void CubeDemo::genCube() { //Create the height map m_pMap = new HeightMap(129, 129, "heightmap17_129.raw", 0.25f, 0.0f); //Build the grid geometry std::vector<D3DXVECTOR3> verts ; std::vector<WORD> indices ; int vertRows = 129 ; int vertCols = 129 ; float dx = 1.0f , dz = 1.0f ; genTriGrid(vertRows, vertCols, dx, dz, verts, indices); //Calculate the number of vertices int numVerts = vertRows * vertCols ; //Calculate the number of faces int numTris = (vertRows - 1) * (vertCols - 1) * 2 ; //Create the mesh D3DVERTEXELEMENT9 elems[MAX_FVF_DECL_SIZE]; UINT numElements = 0 ; HR(VertexPNT::_vertexDecl->GetDeclaration(elems, &numElements)); HR(D3DXCreateMesh(numTris, numVerts, D3DXMESH_MANAGED,elems, m_pDevice,&m_TerrianMesh)); //Lock the vertex buffer VertexPNT* v = NULL ; HR(m_TerrianMesh->LockVertexBuffer(0, (void**)&v)); //Calculate the width and depth float w = (vertCols - 1) * dx ; float d = (vertRows - 1) * dz ; //Write the vertex for(int i = 0 ; i < vertRows ; i ++) { for(int j = 0 ; j < vertCols ; j ++) { DWORD index = i * vertCols + j ; v[index]._pos = verts[index]; v[index]._pos.y = (float)(*m_pMap)(j, i) ; v[index]._normal = D3DXVECTOR3(0.0f, 1.0f, 0.0f); v[index]._tex.x = (v[index]._pos.x + 0.5f * w) / w ; v[index]._tex.y = (v[index]._pos.z - 0.5f * d) / (-d) ; } } //Unlock the vertex buffer HR(m_TerrianMesh->UnlockVertexBuffer()); //Write the indices and attribute buffer WORD* k = 0 ; HR(m_TerrianMesh->LockIndexBuffer(0, (void**)&k)); DWORD * attr = 0 ; HR(m_TerrianMesh->LockAttributeBuffer(0, (DWORD**)&attr)); //Compute the index buffer for the grid for(int i = 0 ; i < numTris ; i ++) { k[i * 3 + 0] = (WORD)indices[i * 3 + 0]; k[i * 3 + 1] = (WORD)indices[i * 3 + 1]; k[i * 3 + 2] = (WORD)indices[i * 3 + 2]; attr[i] = 0 ; //Always subset 0 } //Unlock the index buffer HR(m_TerrianMesh->UnlockIndexBuffer()); HR(m_TerrianMesh->UnlockAttributeBuffer()); //Generate normals and then opimize the mesh HR(D3DXComputeNormals(m_TerrianMesh,0)); DWORD* adj = new DWORD[m_TerrianMesh->GetNumFaces() * 3]; HR(m_TerrianMesh->GenerateAdjacency(1.0f, adj)); HR(m_TerrianMesh->OptimizeInplace(D3DXMESHOPT_VERTEXCACHE| D3DXMESHOPT_ATTRSORT, adj, 0, 0, 0)); delete[]adj; }</span>
<span style="font-family:Microsoft YaHei;">void CubeDemo::genTriGrid(int raws, int cols, float dx, float dz, std::vector<D3DXVECTOR3>& v, std::vector<WORD>& indices) { //Generate the vertices for(int i = 0 ; i < raws ; i ++) { for(int j = 0 ; j < cols ; j ++) { v.push_back(D3DXVECTOR3(j * dx , 0, -i * dz)); } } //Generate the indices for(int i = 0 ; i < raws - 1 ; i ++) { for(int j = 0 ; j < cols - 1 ; j ++) { //Face 1 indices.push_back(i * cols + j); indices.push_back(i * cols + j + 1); indices.push_back((i + 1) * cols + j + 1 ); //Face 2 indices.push_back(i * cols + j); indices.push_back((i + 1) * cols + j + 1); indices.push_back((i + 1) * cols + j); } } //Translate the vertices for(int i = 0 ; i < raws * cols ; i ++) { v[i].x -= (cols - 1) * dx * 0.5f; v[i].z += (raws - 1) * dz * 0.5f; } }// end for genTriGrid</span>
当我们有了平面的网格之后,我们就根据高度图中的数据,对网格中的顶点的y坐标进行改变,这样就能够创建一个高低起伏的地形网格出来了。
下面是我的模型的截图:
在使用Phong式着色模型进行点光源的照射,如下所示:
在纹理映射技术中,Multi-Texturing用于融合不同的纹理,从而构成一个纹理效果。在本次实例中,我们使用三种不同的纹理,分别是草地纹理,岩石纹理,和路面纹理。在有了这个三个纹理图之后,我们还需要确定,最终形成的纹理图中各个纹理所占的比例分别是多少了?所以,还有另外一个纹理图blendmap。这个纹理图,仅仅是提供一个混合的参数数据,让我们能够在Pixel Shader中判断,上面三个纹理图分别所占的比例。也就是说,在构建第Pij个像素的时候,我们分别从三个纹理图中获取C0ij, C1ij, C2ij。同时获取Blendmap里面的Bij。然后我们根据如下的公式来构建最后的Pij:
Pij = w0 * C0ij + w1 * C1ij + w2 * C2ij ;
式中w0 = Bij.R/(Bij.R + Bij.G + Bij.B) ;
w1 = Bij.G/(Bij.R + Bij.G + Bij.B) ;
w2ij = Bij.B/(Bij.R + Bij.G + Bij.B) ;
这样,我们就能够进行多纹理映射,从而形成比较丰富的图形效果。下图是进行Multi-Texturing之后的图像:
好了,今天就到这里结束了。后会有期!!!