继天空效果之后,这一节简单阐述一点地形生成的基本原理和方法~ 如果哪里说的不对,还望园子的前辈们多多拍砖 ^ ^
首先,我们准备两张图:
grass.dds heightdata.raw
(注:dds为directx支持的特定格式;raw可由photoshop自动生成,在无文件头标准下,其内部仅包含各点颜色值,不再含有其他冗余数据。)
左边这张草地的图我们作基本的地表纹理只用;右边这张怪模怪样的灰度图,其实就是日常大家口中的“高度图”了,我们以其特定点的颜色数据为依据来形成相应顶点的高度,以产生地形的高低错落感~
以下,我们来编写这个基本的地形类——CBaseTerrain:
代码清单:BaseTerrain.h
来自:http://www.cnblogs.com/kenkao
-------------------------------------*/
#include "D3DInit.h"
#pragma once
class CBaseTerrain
{
public:
CBaseTerrain(void);
~CBaseTerrain(void);
public:
bool Create( // 构建地形
int iSizeX, // 原始地形图宽
int iSizeY, // 原始地形图高
int iRate, // 放大倍率
char* szHeightRaw, // 原始地形图
char* szTexture // 地形纹理
);
void Draw(); // 绘制地形
void Release(); // 资源释放
float GetExactHeightAt(float xCoord, float zCoord); // 获得地形高度
private:
bool LoadHightData(int iSizeX, int iSizeY, char* szHeightRaw); // 加载地形图
float GetHeightData(int X, int Y){return (float)m_pHeightData[ m_SizeX * Y + X ];} // 获得地形图数据
void CreateTerrainVertices(); // 产生顶点缓冲
void CreateTerrainIndices(); // 产生索引缓冲
void GenerateNormals(VertexPosNorColorTex* vertices, int* indices); // 计算全部法线数据
private:
VertexPosNorColorTex* m_pVertices; // 顶点缓冲区
int* m_pIndices; // 索引缓冲区
IDirect3DTexture9* m_pTexture; // 地形纹理指针
unsigned char* m_pHeightData; // 地形数据缓冲
int m_SizeX; // 地形原始长
int m_SizeY; // 地形原始宽
int m_Rate; // 地形放大倍率
};
代码清单:BaseTerrain.cpp
来自:http://www.cnblogs.com/kenkao
-------------------------------------*/
#include "StdAfx.h"
#include "BaseTerrain.h"
#include "Ken3DGame.h"
#include "D3DCamera.h"
#include <fstream>
extern IDirect3DDevice9 *g_pD3DDevice;
extern CD3DCamera *g_pD3DCamera;
CBaseTerrain::CBaseTerrain(void) : m_pVertices(NULL),
m_pIndices(NULL),
m_pTexture(NULL),
m_pHeightData(NULL),
m_SizeX(0),
m_SizeY(0),
m_Rate(1)
{
}
CBaseTerrain::~CBaseTerrain(void)
{
}
void CBaseTerrain::Release()
{
delete[] m_pHeightData;
ReleaseCOM(m_pTexture);
delete[] m_pVertices;
delete[] m_pIndices;
}
bool CBaseTerrain::Create(int iSizeX, int iSizeY, int iRate, char* szHeightRaw, char* szTexture)
{
m_Rate = iRate;
// 加载高度数据
if(!LoadHightData(iSizeX,iSizeY,szHeightRaw))
return false;
// 产生顶点缓冲
CreateTerrainVertices();
// 产生索引缓冲
CreateTerrainIndices();
// 计算发现数据
GenerateNormals(m_pVertices,m_pIndices);
// 产生地形纹理
HRESULT hr = D3DXCreateTextureFromFile(g_pD3DDevice,szTexture,&m_pTexture);
if(FAILED(hr))
return false;
return true;
}
void CBaseTerrain::Draw()
{
// 地形绘制
g_pD3DDevice->SetTexture(0,m_pTexture);
g_pD3DDevice->SetFVF(VertexPosNorColorTex::FVF);
g_pD3DDevice->DrawIndexedPrimitiveUP(D3DPT_TRIANGLELIST, 0, m_SizeX * m_SizeY, (m_SizeX - 1) * (m_SizeY - 1) * 2, &m_pIndices[0],
D3DFMT_INDEX32, &m_pVertices[0], sizeof(VertexPosNorColorTex));
}
bool CBaseTerrain::LoadHightData(int iSizeX, int iSizeY, char* szHeightRaw)
{
m_SizeX = iSizeX;
m_SizeY = iSizeY;
m_pHeightData = new unsigned char[iSizeX * iSizeY];
memset(m_pHeightData, 0, iSizeX * iSizeY);
// 从.raw地形图读取高度数据
std::ifstream inFile(szHeightRaw, std::ios_base::binary);
if(!inFile)
{
delete[] m_pHeightData;
return false;
}
inFile.read((char*)&m_pHeightData[0], iSizeX * iSizeY);
inFile.close();
return true;
}
void CBaseTerrain::CreateTerrainVertices()
{
// 产生顶点缓冲
// 高度图中有多少数据便产生多少顶点,或者你也可以仅仅使用其中的一部分数据,这个很好理解 :)
m_pVertices = new VertexPosNorColorTex[m_SizeX * m_SizeY];
int i = 0;
for (int z = 0; z < m_SizeY; z++)
{
for (int x = 0; x < m_SizeX; x++)
{
float y = GetHeightData(x,z);
m_pVertices[i++] = VertexPosNorColorTex(
x * m_Rate, y, z * m_Rate, // 注意x、z坐标m_Rate倍率的使用,相应的y值即为由地形图获得的高度数据
0, 0, 0,
D3DXCOLOR_WHITE,
(float)x / 30.0f, (float)z / 30.0f);
}
}
}
void CBaseTerrain::CreateTerrainIndices()
{
// 产生全部的索引数据
/*
*---*---*
| / | / |
*---*---*
| / | / |
*---*---*
索引缓冲包含的元素个数 = 三角形个数 x 3
N 行顶点形成 N-1 行“三角形【对】”,每行三角形对个数为列数 M -1,则总的索引缓冲数 = ( N - 1 ) * ( M - 1 ) * 2 * 3
--------- --------- --- ---
行数 - 1 列数 - 1 三角形对 每个三角形三个顶点
*/
m_pIndices = new int[(m_SizeX-1)*(m_SizeY-1)*6];
int i = 0;
for (int z=0; z<m_SizeY-1; z++)
{
for (int x=0; x<m_SizeX-1; x++)
{
// 分别为每【对】三角形的顶点索引赋值,注意一下不要被“背面剔除”就可以了 ^ ^
m_pIndices[i++] = z * m_SizeX + x;
m_pIndices[i++] = (z + 1) * m_SizeX + x;
m_pIndices[i++] = z * m_SizeX + x + 1;
m_pIndices[i++] = (z + 1) * m_SizeX + x;
m_pIndices[i++] = (z + 1) * m_SizeX + x + 1;
m_pIndices[i++] = z * m_SizeX + x + 1;
}
}
}
void CBaseTerrain::GenerateNormals(VertexPosNorColorTex* pVertices, int* pIndices)
{
// 遍历每个三角形
for (int i = 0; i < (m_SizeX-1)*(m_SizeY-1)*2; i++)
{
// 获得第一条向量
D3DXVECTOR3 firstVec = D3DXVECTOR3(
pVertices[pIndices[i*3 + 1]]._x - pVertices[pIndices[i*3]]._x,
pVertices[pIndices[i*3 + 1]]._y - pVertices[pIndices[i*3]]._y,
pVertices[pIndices[i*3 + 1]]._z - pVertices[pIndices[i*3]]._z
);
// 获得第二条向量
D3DXVECTOR3 secondVec = D3DXVECTOR3(
pVertices[pIndices[i*3 + 2]]._x - pVertices[pIndices[i*3]]._x,
pVertices[pIndices[i*3 + 2]]._y - pVertices[pIndices[i*3]]._y,
pVertices[pIndices[i*3 + 2]]._z - pVertices[pIndices[i*3]]._z
);
// 两向量叉乘的该三角形片面的发现数据
D3DXVECTOR3 normal;
D3DXVec3Cross(&normal,&firstVec,&secondVec);
D3DXVec3Normalize(&normal,&normal);
// 所得法线数据单位化并累计到组成此片面的三个顶点的法线数据中
D3DXVECTOR3 nfirstVec = D3DXVECTOR3(
pVertices[pIndices[i * 3]]._nx += normal.x,
pVertices[pIndices[i * 3]]._ny += normal.y,
pVertices[pIndices[i * 3]]._nz += normal.z
);
D3DXVECTOR3 nsecondVec = D3DXVECTOR3(
pVertices[pIndices[i * 3 + 1]]._nx += normal.x,
pVertices[pIndices[i * 3 + 1]]._ny += normal.y,
pVertices[pIndices[i * 3 + 1]]._nz += normal.z
);
D3DXVECTOR3 nthirdVec = D3DXVECTOR3(
pVertices[pIndices[i * 3 + 2]]._nx += normal.x,
pVertices[pIndices[i * 3 + 2]]._ny += normal.y,
pVertices[pIndices[i * 3 + 2]]._nz += normal.z
);
}
// 遍历全部顶点
for (int i=0;i<m_SizeX * m_SizeY;i++)
{
// 单位化全部顶点的法线数据
D3DXVECTOR3 nVec;
D3DXVec3Normalize(&nVec,
&D3DXVECTOR3(pVertices[i]._nx, pVertices[i]._ny, pVertices[i]._nz)
);
pVertices[i]._nx = nVec.x;
pVertices[i]._ny = nVec.y;
pVertices[i]._nz = nVec.z;
}
// 以上,基本原理即基于所有三角形片面计算全部顶点法线数据而后求平均 :)
}
float CBaseTerrain::GetExactHeightAt(float xCoord, float zCoord)
{
// 还原倍率
xCoord /= m_Rate;
zCoord /= m_Rate;
// 非法判定
bool invalid = xCoord < 0;
invalid |= zCoord < 0;
invalid |= xCoord > m_SizeX - 1;
invalid |= zCoord > m_SizeY - 1;
if (invalid)
return 10; // 默认高度10
// 获得该点所在三角形【对】四点对应高度
int xLower = (int)xCoord;
int xHigher = xLower + 1;
float xRelative = (xCoord - xLower) / ((float)xHigher - (float)xLower);
int zLower = (int)zCoord;
int zHigher = zLower + 1;
float zRelative = (zCoord - zLower) / ((float)zHigher - (float)zLower);
float heightLxLz = GetHeightData(xLower, zLower);
float heightLxHz = GetHeightData(xLower, zHigher);
float heightHxLz = GetHeightData(xHigher, zLower);
float heightHxHz = GetHeightData(xHigher, zHigher);
// 判断该点位于三角形对中的哪个,并根据该三角形三点高度进行双线性插值计算最终高度
bool pointAboveLowerTriangle = (xRelative + zRelative < 1);
float finalHeight;
if (pointAboveLowerTriangle)
{
finalHeight = heightLxLz;
finalHeight += zRelative * (heightLxHz - heightLxLz);
finalHeight += xRelative * (heightHxLz - heightLxLz);
}
else
{
finalHeight = heightHxHz;
finalHeight += (1.0f - zRelative) * (heightHxLz - heightHxHz);
finalHeight += (1.0f - xRelative) * (heightLxHz - heightHxHz);
}
return finalHeight;
}
代码看起来挺长,不过其中并没有什么高深的机制 ^ ^
LoadHightData()用于加载高度图数据。
函数中使用的是最基本的C++读取文件二进制数据的方法,意在将raw存储的高度数据导入到内存的unsigned char型缓冲区中,已供生成顶点高度(也就是y坐标)之用~
CreateTerrainVertices()用于产生全部的顶点数据。
很明显,raw中有多少像素,我们这里便产生多少顶点即可。我在代码中提到的放大倍率,其实就是顶点与顶点间的间隔而已~
CreateTerrainIndices()用于产生全部的索引数据。我在代码中画的那个简陋的图图不知道大家看懂没有,呵呵~ 简单而言就是索引数据永远是三角形个数的3倍,而三角形对数永远是 (行数-1)*(列数-1)。各索引值取得皆有公式,龙书中有详细的阐述,大家看不懂的话可以留言~
GenerateNormals()用于为所有顶点生成法线数据。
要实现地形的光照效果,顶点法线是必不可少的数据成分。单个顶点的法线其实就是其所在三角形面的法线,如果一个顶点为多个三角形共有,则累加之后单位化即可(相当于求平均)~
GetExactHeightAt()用于求取某点的高度。
高度图中仅存有各顶点的高度数据,而地形中并不是每个点都位于顶点的位置。如果某点位于顶点与顶点之间,则我们需要采取双线性差值的方法求得改点合适的高度,其中的原理跟高中学过的相似三角形类似,挺简单~
有关双线性差值的相关理论,可以参看老师为大家翻译的这篇文章:http://shiba.hpe.sh.cn/jiaoyanzu/WULI/showArticle.aspx?articleId=473&classId=4
之后是主体代码:
代码清单:D3DGame.cpp
来自:http://www.cnblogs.com/kenkao
-------------------------------------*/
#include "StdAfx.h"
#include "D3DGame.h"
#include "D3DCamera.h"
#include "D3DEffect.h"
#include "CoordCross.h"
#include "SimpleXMesh.h"
#include "Texture2D.h"
#include "D3DSprite.h"
#include "Skybox.h"
#include "SpriteBatch.h"
#include "BaseTerrain.h"
#include <stdio.h>
//---通用全局变量
HINSTANCE g_hInst;
HWND g_hWnd;
D3DXMATRIX g_matProjection;
//---D3D全局变量
IDirect3D9 *g_pD3D = NULL;
IDirect3DDevice9 *g_pD3DDevice = NULL;
CMouseInput *g_pMouseInput = NULL;
CKeyboardInput *g_pKeyboardInput = NULL;
CD3DCamera *g_pD3DCamera = NULL;
CCoordCross *g_pCoordCross = NULL;
CSimpleXMesh *g_pSimpleXMesh = NULL;
CD3DEffect *g_pD3DEffect = NULL;
CD3DSprite *g_pD3DSprite = NULL;
CTexture2D *g_pTexture2D = NULL;
CSpriteBatch *g_SpriteBatch = NULL;
CTexture2D *g_pTexture2D2 = NULL;
CD3DEffect *g_pD3DEffect2 = NULL;
CSkybox *g_pSkybox = NULL;
CBaseTerrain *g_pBaseTerrain = NULL;
//---HLSL全局变量句柄
D3DXHANDLE g_CurrentTechHandle = NULL;
D3DXHANDLE g_matWorldViewProj = NULL;
D3DXHANDLE g_matWorld = NULL;
D3DXHANDLE g_vecEye = NULL;
D3DXHANDLE g_vecLightDir = NULL;
D3DXHANDLE g_vDiffuseColor = NULL;
D3DXHANDLE g_vSpecularColor = NULL;
D3DXHANDLE g_vAmbient = NULL;
D3DXHANDLE g_CurrentTechHandle2 = NULL;
D3DXHANDLE g_Scale = NULL;
// HLSL特效参数设置
void GetParameters();
void SetParameters();
void Initialize(HINSTANCE hInst, HWND hWnd)
{
g_hInst = hInst;
g_hWnd = hWnd;
InitD3D(&g_pD3D, &g_pD3DDevice, g_matProjection, hWnd);
g_pMouseInput = new CMouseInput;
g_pMouseInput->Initialize(hInst,hWnd);
g_pKeyboardInput = new CKeyboardInput;
g_pKeyboardInput->Initialize(hInst,hWnd);
g_pD3DCamera = new CD3DCamera;
}
void LoadContent()
{
g_pCoordCross = new CCoordCross;
g_pD3DCamera->SetCameraPos(D3DXVECTOR3(0.0f,0.0f,0.0f));
g_pSimpleXMesh = new CSimpleXMesh;
g_pSimpleXMesh->LoadXMesh("teapot.X");
g_pD3DEffect = new CD3DEffect;
g_pD3DEffect2 = new CD3DEffect;
g_pD3DEffect->LoadEffect("Light.fx");
g_pD3DEffect2->LoadEffect("Thunder.fx");
GetParameters();
g_pD3DSprite = new CD3DSprite(g_pD3DDevice);
g_SpriteBatch = new CSpriteBatch(g_pD3DDevice);
g_pTexture2D = new CTexture2D;
g_pTexture2D->LoadTexture("img.jpg");
g_pTexture2D2 = new CTexture2D;
g_pTexture2D2->LoadTexture("img2.jpg");
g_pSkybox = new CSkybox;
g_pSkybox->Create("Skybox_0.JPG","Skybox_1.JPG","Skybox_2.JPG"
,"Skybox_3.JPG","Skybox_4.JPG","Skybox_5.JPG");
g_pBaseTerrain = new CBaseTerrain;
g_pBaseTerrain->Create(128,128,10,"HeightData_128x128.raw","Grass.dds");
}
void Update()
{
g_pMouseInput->GetState();
g_pKeyboardInput->GetState();
// 更新摄影机高度
D3DXVECTOR3 CameraPos = g_pD3DCamera->GetCameraPos();
float roleHeight = 25.0f;
float Ty = g_pBaseTerrain->GetExactHeightAt(CameraPos.x,CameraPos.z) + roleHeight;
g_pD3DCamera->SetCameraPos(D3DXVECTOR3(
CameraPos.x,
Ty,
CameraPos.z));
g_pD3DCamera->Update();
// 更新g_pD3DEffect中的视点位置
D3DXVECTOR4 vecEye = D3DXVECTOR4(CameraPos.x,Ty,CameraPos.z,0.0f);
g_pD3DEffect -> GetEffect() -> SetVector(g_vecEye,&vecEye);
}
void Draw()
{
// 参数设定
SetParameters();
g_pD3DDevice->SetTransform(D3DTS_VIEW,&g_pD3DCamera->GetViewMatrix());
g_pD3DDevice->Clear(0, NULL, D3DCLEAR_TARGET | D3DCLEAR_ZBUFFER, D3DCOLOR_RGBA(100,149,237,255), 1.0f, 0);
if(SUCCEEDED(g_pD3DDevice->BeginScene()))
{
// 天空处在无限远处,因此必须最先绘制天空盒
g_pSkybox->Draw();
UINT numPasses;
// 开启特效
g_pD3DEffect->BeginEffect(numPasses);
for(UINT i=0;i<numPasses;i++)
{
// 开启路径
g_pD3DEffect->GetEffect()->BeginPass(i);
// 绘制地形
g_pBaseTerrain->Draw();
// 路径结束
g_pD3DEffect->GetEffect()->EndPass();
}
// 特效结束
g_pD3DEffect->EndEffect();
g_pD3DDevice->EndScene();
}
g_pD3DDevice->Present(NULL, NULL, NULL, NULL);
}
void UnloadContent()
{
ReleaseCOM(g_pBaseTerrain);
ReleaseCOM(g_pSkybox);
ReleaseCOM(g_pTexture2D2);
ReleaseCOM(g_pTexture2D);
ReleaseCOM(g_SpriteBatch);
ReleaseCOM(g_pD3DSprite);
ReleaseCOM(g_pD3DEffect2);
ReleaseCOM(g_pD3DEffect);
ReleaseCOM(g_pSimpleXMesh);
ReleaseCOM(g_pCoordCross);
}
void Dispose()
{
ReleaseCOM(g_pD3DCamera);
ReleaseCOM(g_pKeyboardInput);
ReleaseCOM(g_pMouseInput);
ReleaseCOM(g_pD3DDevice);
ReleaseCOM(g_pD3D);
}
void GetParameters()
{
// 获得HLSL中各个全局变量句柄
g_CurrentTechHandle = g_pD3DEffect -> GetEffect() -> GetTechniqueByName("SpecularLight");
g_matWorldViewProj = g_pD3DEffect -> GetEffect() -> GetParameterByName(0, "matWorldViewProj");
g_matWorld = g_pD3DEffect -> GetEffect() -> GetParameterByName(0, "matWorld");
g_vecEye = g_pD3DEffect -> GetEffect() -> GetParameterByName(0, "vecEye");
g_vecLightDir = g_pD3DEffect -> GetEffect() -> GetParameterByName(0, "vecLightDir");
g_vDiffuseColor = g_pD3DEffect -> GetEffect() -> GetParameterByName(0, "vDiffuseColor");
g_vSpecularColor = g_pD3DEffect -> GetEffect() -> GetParameterByName(0, "vSpecularColor");
g_CurrentTechHandle2 = g_pD3DEffect2 -> GetEffect() -> GetTechniqueByName("Technique1");
g_Scale = g_pD3DEffect2 -> GetEffect() -> GetParameterByName(0, "Scale");
}
void SetParameters()
{
// 设定当前技术
g_pD3DEffect -> GetEffect() -> SetTechnique(g_CurrentTechHandle);
// 设定HLSL中的各个参数
D3DXMATRIX worldMatrix;
D3DXMatrixTranslation(&worldMatrix,0.0f,0.0f,0.0f);
g_pD3DEffect -> GetEffect() -> SetMatrix(g_matWorldViewProj,&(worldMatrix*g_pD3DCamera->GetViewMatrix()*g_matProjection));
g_pD3DEffect -> GetEffect() -> SetMatrix(g_matWorld,&worldMatrix);
D3DXVECTOR4 vLightDirection = D3DXVECTOR4(-5.0f, -1.0f, 2.0f, 1.0f);
g_pD3DEffect -> GetEffect() -> SetVector(g_vecLightDir,&vLightDirection);
D3DXVECTOR4 vColorDiffuse = D3DXVECTOR4(0.0f, 0.0f, 0.0f, 1.0f);
D3DXVECTOR4 vColorSpecular = D3DXVECTOR4(1.0f, 1.0f, 1.0f, 1.0f);
D3DXVECTOR4 vColorAmbient = D3DXVECTOR4(0.0f, 0.0f, 0.0f, 1.0f);
g_pD3DEffect -> GetEffect() -> SetVector(g_vDiffuseColor,&vColorDiffuse);
// g_pD3DEffect -> GetEffect() -> SetVector(g_vSpecularColor,&vColorSpecular); //暂时不加入镜面高光 ^ ^
g_pD3DEffect -> GetEffect() -> SetVector(g_vAmbient,&vColorAmbient);
g_pD3DEffect2 -> GetEffect() -> SetTechnique(g_CurrentTechHandle2);
g_pD3DEffect2 -> GetEffect() -> SetFloat(g_Scale,0.8f);
}
我们使用light.fx为新生成的地形添加光照效果。这里我在其原有基础上作了改动。
首先添加一个纹理采样器:
sampler2D TextureSampler = sampler_state
{
magfilter=LINEAR;
minfilter=LINEAR;
mipfilter=LINEAR;
};
然后让像素着色器最终的输出结果累计地形纹理原有的颜色:
return vAmbient + vDiffuseColor * Diff + vSpecularColor * Specular + tex2D(TextureSampler, vtop.uv); //---最后的颜色 = 环境光 + 漫射光 + 镜面高光 + 本来颜色(纹理)
我们来看看效果:
山地跟沟壑~ 都是高度图的功劳,呵呵~
主体代码中我注释掉了镜面光的颜色赋值,因为按理说草地是不会反射阳光的。我们不妨换一张冷峻的岩石地表,然后再打开镜面光,试试看效果如何~
怎么样,很耀眼吧?呵呵 ^ ^
到此,以上阐述了地形生成方法中的诸多基本原理,但如果要用于生成大型的地形或者无边界地形的话,仅仅靠这样简单的方法是不堪重负的。不过大家无需担心,地形生成的基本原理大致都是相同的,只不过复杂的大地形在此基础上加入了一些特殊的算法和机制,以动态数据的方法减轻了CPU和显卡的负担。如果后续有机会深度研究这些,我会尽我所能,为大家一一道来~
谢谢~ ^ ^