地形模拟演示Demo
地形渲染的首先是创建一个三角网络平面,然后调整平面顶点的y高度值,模拟地面的山丘和山谷,最后再绘制贴图效果。本文首先介绍如何生成三角网络平面。然后介绍如何通过高度图调整平面高度。以及使用BlendMap和3种材质绘制贴图效果的方法。最后演示如何调整摄像机位置和移动速度,在地面上行走。
生成三角网络平面
一个m*n个顶点的平面由2*(m-1)*(n-1)个三角形组成。行列的宽度分别为dx,dz。
首先生成顶点坐标,假设平面的中心在坐标原点。左上角的顶点坐标就为(-(m-1)*dx, 0, (n-1)*dz)。再生成索引,每个循环迭代中生成一个四边形的两个三角形的6个索引信息。如图所示。
∆ABC = {i·m+ j, i·m+j+1, (i+1)·m+ j}
∆CBD = {(i+1)·m+ j, i·m+ j+1, (i+1)·m+ j+1}
然后使用生成的顶点坐标和索引创建ID3DXMesh对象。顶点的Y值从下文介绍的高度图获取。顶点法向量由D3DXComputeNormals计算。完整代码如下。
void Terrain::GenTriGrid(int numVertRows, int numVertCols,
	float dx, float dz, 
	const D3DXVECTOR3& center, 
	std::vector<D3DXVECTOR3>& verts,
	std::vector<DWORD>& indices)
{
	int numVertices = numVertRows*numVertCols;
	int numCellRows = numVertRows-1;
	int numCellCols = numVertCols-1;
	int numTris = numCellRows*numCellCols*2;
	float width = (float)numCellCols * dx;
	float depth = (float)numCellRows * dz;
	//===========================================
	// Build vertices.
	// We first build the grid geometry centered about the origin and on
	// the xz-plane, row-by-row and in a top-down fashion.  We then translate
	// the grid vertices so that they are centered about the specified 
	// parameter 'center'.
	verts.resize( numVertices );
	// Offsets to translate grid from quadrant 4 to center of 
	// coordinate system.
	float xOffset = -width * 0.5f; 
	float zOffset =  depth * 0.5f;
	int k = 0;
	for(float i = 0; i < numVertRows; ++i)
	{
		for(float j = 0; j < numVertCols; ++j)
		{
			// Negate the depth coordinate to put in quadrant four.  
			// Then offset to center about coordinate system.
			verts[k].x =  j * dx + xOffset;
			verts[k].z = -i * dz + zOffset;
			verts[k].y =  0.0f;
			// Translate so that the center of the grid is at the
			// specified 'center' parameter.
			D3DXMATRIX T;
			D3DXMatrixTranslation(&T, center.x, center.y, center.z);
			D3DXVec3TransformCoord(&verts[k], &verts[k], &T);
			++k; // Next vertex
		}
	}
	//===========================================
	// Build indices.
	indices.resize(numTris * 3);
	// Generate indices for each quad.
	k = 0;
	for(DWORD i = 0; i < (DWORD)numCellRows; ++i)
	{
		for(DWORD j = 0; j < (DWORD)numCellCols; ++j)
		{
			indices[k]     =   i   * numVertCols + j;
			indices[k + 1] =   i   * numVertCols + j + 1;
			indices[k + 2] = (i+1) * numVertCols + j;
			indices[k + 3] = (i+1) * numVertCols + j;
			indices[k + 4] =   i   * numVertCols + j + 1;
			indices[k + 5] = (i+1) * numVertCols + j + 1;
			// next quad
			k += 6;
		}
	}
}
void Terrain::buildGeometry(IDirect3DDevice9 *pd3dDevice)
{
	HRESULT hr;
	//===============================================================
	// Create one large mesh for the grid in system memory.
	DWORD numTris  = (mVertRows-1)*(mVertCols-1)*2;
	DWORD numVerts = mVertRows*mVertCols;
	ID3DXMesh* mesh = 0;
	
	V(D3DXCreateMesh(numTris, numVerts, 
		D3DXMESH_SYSTEMMEM|D3DXMESH_32BIT, VertexPNT::Elements, pd3dDevice, &mesh));
	//===============================================================
	// Write the grid vertices and triangles to the mesh.
	VertexPNT* v = 0;
	V(mesh->LockVertexBuffer(0, (void**)&v));
	
	std::vector<D3DXVECTOR3> verts;
	std::vector<DWORD> indices;
	GenTriGrid(mVertRows, mVertCols, mDX, mDZ, D3DXVECTOR3(0.0f, 0.0f, 0.0f), verts, indices);
	float w = mWidth;
	float d = mDepth;
	for(UINT i = 0; i < mesh->GetNumVertices(); ++i)
	{
		// We store the grid vertices in a linear array, but we can
		// convert the linear array index to an (r, c) matrix index.
		int r = i / mVertCols;
		int c = i % mVertCols;
		v[i].pos   = verts[i];
		v[i].pos.y = mHeightmap(r, c);
		v[i].tex0.x = (v[i].pos.x + (0.5f*w)) / w;
		v[i].tex0.y = (v[i].pos.z - (0.5f*d)) / -d;
	}
	// Write triangle data so we can compute normals.
	DWORD* indexBuffPtr = 0;
	V(mesh->LockIndexBuffer(0, (void**)&indexBuffPtr));
	for(UINT i = 0; i < mesh->GetNumFaces(); ++i)
	{
		indexBuffPtr[i*3+0] = indices[i*3+0];
		indexBuffPtr[i*3+1] = indices[i*3+1];
		indexBuffPtr[i*3+2] = indices[i*3+2];
	}
	V(mesh->UnlockIndexBuffer());
	V(mesh->UnlockVertexBuffer());
	// Compute Vertex Normals.
	V(D3DXComputeNormals(mesh, 0));
	ReleaseCOM(mesh); // Done with global mesh.
}
使用高度图调整顶点Y值
高度图可以使用一个m*n的灰度图表示,每个像素值(范围[0-255])表示一个顶点的高度。使用图形编辑软件保存灰度图时,可以使用直接使用无头信息的RAW格式,读取的方法非常的简单。这样取得高度后,可以根据需求做必要的线性变换。height = value * heightScale + heightOffset。value为灰度图像素值。使用8位灰度图表示高度时,精度可能有所不足,顶点之间的高度变换不够平滑,因此我们使用3*3的过滤计算高度值。方法就是取得i,j元素和周围的8个元素的高度,再求平均,使用平均值作为i,j元素最终的高度。
float Heightmap::sampleHeight3x3(int i, int j)
{
	float accum = 0.0f;
	float num = 0.0f;
	for(int m = i-1; m <= i+1; ++m)
	{
		for(int n = j-1; n <= j+1; ++n)
		{
			if( inBounds(m,n) )
			{
				accum += mHeightMap(m,n);
				num += 1.0f;
			}
		}
	}
	return accum / num;
}
isBounds方法是为了避免i,j元素在边缘时,取到不在范围内的元素。
绘制地形贴图
本文例子中,使用3种贴图代表不同的地形表面。分别是dirt,grass和stone。这三种贴图的大小和地形的大小可以不相同。然后使用一个和地形相同大小的BlendMap的RGB三个通道表示三种材质在一个点上的比例是多少。具体的计算方法参考HLSL代码。代码中tiledTexC范围超过[0-1],重复使用地形表面贴图,nonTileTexC范围在[0-1],不重复BlendMap。
OutputVS TerrainVS(float3 posW : POSITION0,  // We assume terrain geometry is specified
                   float3 normalW : NORMAL0, // directly in world space.
                   float2 tex0: TEXCOORD0)
{
    // Zero out our output.
	OutputVS outVS = (OutputVS)0;
	
	// Just compute a grayscale diffuse and ambient lighting 
	// term--terrain has no specular reflectance.  The color 
	// comes from the texture.
    outVS.shade = saturate(max(0.0f, dot(normalW, gDirToSunW)) + 0.3f);
    
	// Transform to homogeneous clip space.
	outVS.posH = mul(float4(posW, 1.0f), gViewProj);
	
	// Pass on texture coordinates to be interpolated in rasterization.
	outVS.tiledTexC    = tex0 * gTexScale; // Scale tex-coord to tile.
	outVS.nonTiledTexC = tex0; // Blend map not tiled.
	
	// Done--return the output.
    return outVS;
}
float4 TerrainPS(float2 tiledTexC : TEXCOORD0, 
                 float2 nonTiledTexC : TEXCOORD1,
                 float shade : TEXCOORD2) : COLOR
{
	// Layer maps are tiled
    float3 c0 = tex2D(Tex0S, tiledTexC).rgb;
    float3 c1 = tex2D(Tex1S, tiledTexC).rgb;
    float3 c2 = tex2D(Tex2S, tiledTexC).rgb;
    
    // Blendmap is not tiled.
    float3 B = tex2D(BlendMapS, nonTiledTexC).rgb;
	// Find the inverse of all the blend weights so that we can
	// scale the total color to the range [0, 1].
    float totalInverse = 1.0f / (B.r + B.g + B.b);
    
    // Scale the colors by each layer by its corresponding weight
    // stored in the blendmap.  
    c0 *= B.r * totalInverse;
    c1 *= B.g * totalInverse;
    c2 *= B.b * totalInverse;
    
    // Sum the colors and modulate with the shade to brighten/darken.
    float3 final = (c0 + c1 + c2) * shade;
    
    return float4(final, 1.0f);
}
在地形中行走
为了在地形表面行走,只需要根据地形表面的Y值调整摄像机的位置以及移动方向即可。我们不能直接从地形顶点中直接获取高度Y值。当摄像机的处于三角形表面上时,这样获取的高度值不够精确。所有我们需要判断摄像机的xz值在哪一个三角形表面上,然后通过这个三角形的三个顶点线性插值计算摄像机的具体位置。
首先需要计算摄像机的世界坐标对应的平面顶点的row和col以及相对于这个顶点的位移s和t是多少。
// Transform from terrain local space to "cell" space. float c = (x + 0.5f*mWidth) / mDX; float d = (z - 0.5f*mDepth) / -mDZ; // Get the row and column we are in. int row = (int)floorf(d); int col = (int)floorf(c); // Where we are relative to the cell. float s = c - (float)col; float t = d - (float)row;
然后要根据s和t值,判断摄像机在row,col和row+1,col+1围成的四边形的哪个三角形上。如图所示,如果t < 1-s,则摄像机在上面一个三角形表面上,否则在下一个三角形表面上。
然后计算三角ABC两边的向量AC(Xc–Xa,Yc-Ya,Zc-Za)和AB(Xb–Xa,Yb-Ya,Zb-Za),那么摄像机的位置相对于顶点A的位移就为s * AB + s * AC。我们这里只关心Y值,所以,获取三个顶点A,B,C的高度值,即可计算出摄像机的高度值。如果在下面一个三角形上,计算方法类似。具体代码如下。
float Terrain::getHeight(float x, float z)
{
	// Transform from terrain local space to "cell" space.
	float c = (x + 0.5f*mWidth) /  mDX;
	float d = (z - 0.5f*mDepth) / -mDZ;
	// Get the row and column we are in.
	int row = (int)floorf(d);
	int col = (int)floorf(c);
	// Grab the heights of the cell we are in.
	// A*--*B
	//  | /|
	//  |/ |
	// C*--*D
	float A = mHeightmap(row, col);
	float B = mHeightmap(row, col+1);
	float C = mHeightmap(row+1, col);
	float D = mHeightmap(row+1, col+1);
	// Where we are relative to the cell.
	float s = c - (float)col;
	float t = d - (float)row;
	// If upper triangle ABC.
	if(t < 1.0f - s)
	{
		float uy = B - A;
		float vy = C - A;
		return A + s*uy + t*vy;
	}
	else // lower triangle DCB.
	{
		float uy = C - D;
		float vy = B - D;
		return D + (1.0f-s)*uy + (1.0f-t)*vy;
	}
}
如何摄像机的移动方向是平行于xz平面的,那么通过调整摄像机高度y值后,摄像机的移动速度将不再恒定。因此需要根据地形高度调整摄像机的移动方法。方法是,在一个时间点上,摄像机的移动方向是摄像机的look方法,在地形上的位置为P0。取得按这个方向移动一定距离后的点对应的地形高度值Y即得到这个点投影到地形上的点P。那么摄像机在地形表面移动的瞬时地形切线向量就为P-P0。具体代码如下。
// Find the net direction the camera is traveling in (since the
// camera could be running and strafing).
D3DXVECTOR3 dir(0.0f, 0.0f, 0.0f);
if( g_input->keyDown(DIK_W) )
	dir += mLookW;
if( g_input->keyDown(DIK_S) )
	dir -= mLookW;
if( g_input->keyDown(DIK_D) )
	dir += mRightW;
if( g_input->keyDown(DIK_A) )
	dir -= mRightW;
// Move at mSpeed along net direction.
D3DXVec3Normalize(&dir, &dir);
D3DXVECTOR3 newPos = mPosW + dir*mSpeed*dt;
if( terrain != 0)
{
	// New position might not be on terrain, so project the
	// point onto the terrain.
	newPos.y = terrain->getHeight(newPos.x, newPos.z) + offsetHeight;
	// Now the difference of the new position and old (current) 
	// position approximates a tangent vector on the terrain.
	D3DXVECTOR3 tangent = newPos - mPosW;
	D3DXVec3Normalize(&tangent, &tangent);
	// Now move camera along tangent vector.
	mPosW += tangent*mSpeed*dt;
	// After update, there may be errors in the camera height since our
	// tangent is only an approximation.  So force camera to correct height,
	// and offset by the specified amount so that camera does not sit
	// exactly on terrain, but instead, slightly above it.
	mPosW.y = terrain->getHeight(mPosW.x, mPosW.z) + offsetHeight;
}
else
{
	mPosW = newPos;
}
最终效果
本文的例子和代码参考《Introduction to 3D Game Programming with DirectX 9.0c—A Shader Approach》第17章Terrain Rendering ,有兴趣的同学可以阅读原文。
完整的代码请访问GoogleCode,使用你最喜欢的SVN客户端下载。
http://d3dexamples.googlecode.com/svn/trunk/
作者: devj 发表于 2011-10-18 11:47 原文链接
最新新闻:
· 苹果周二发布第三季度财报 iPhone成信心保障(2011-10-18 20:46)
· 求解斯芬克斯之谜——《X光下看腾讯》序(2011-10-18 20:44)
· 预告:Android 4.0 将于明日香港首发(2011-10-18 20:39)
· Twitter活跃用户量超1亿 超50%用户每天登陆(2011-10-18 20:36)
· 调查称近八成人每天必登录Facebook(2011-10-18 20:33)




