首页 > 代码库 > DirectX 9.0 (11) Terrain Rendering

DirectX 9.0 (11) Terrain Rendering

作者: 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>

                  我们先根据想要创建的地形尺寸的大小,调用genTriGrid()函数,来构建一个平面的网格模型。genTriGrid进行网格构建的方法很简单。我们先在XZ平面的(X,-Z)平面上构建好网格,然后根据网格的尺寸,对网格中的顶点进行平移,让网格的中心和世界坐标的中心对齐。

                  当我们有了平面的网格之后,我们就根据高度图中的数据,对网格中的顶点的y坐标进行改变,这样就能够创建一个高低起伏的地形网格出来了。

                   下面是我的模型的截图:

                                在使用Phong式着色模型进行点光源的照射,如下所示:


Multi-Texturing

                  在纹理映射技术中,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之后的图像:


好了,今天就到这里结束了。后会有期!!!

DirectX 9.0 (11) Terrain Rendering