第十三章
地形网格(mesh)其实就是一系列三角形栅格(grid),但是方格中的每个顶点都被赋予了一个高度值(即高度或海拔), 这样该方格就可通过方格对应高度的平滑过渡来模拟自然地形中三脉到三谷的变化,当然,还需要对地形网格应用一些细致的纹理来表现沙滩,绿草如茵的丘陵和雪山
用高度图(heightmap)来描述地形中的丘陵和山谷,高度图其实是一个数组,该数组的每个元素都指定了地形方格中某一特定顶点的高度值,(另外一种实现方式可能用该数组中的每个元素来指定每个三角形栅格的高度)通常,将高度图视为一个矩阵,这样高度图中的元素就与地形栅格中的顶点一一对应
高度图被保存在磁盘中,通常为其每个元素只分配一个字节的存储空间,这样高度只能在区间[0,255]内取值,该范围足以反映地形中的高度变化,但在实际应用中,为了匹配3D世界的尺度,可能需要对高度值进行比例变换,这样就极可能超出上述区间。例,如果在3D世界中的度量单位为英尺,则0-255之间的高度值尚不足以表达任何感兴趣的目标点的高度,基于上述原因,当将高度数据加载到应用程序中,重新分配一个整形或浮点型数组来存储这些高度值,这样就不必拘泥于0-255的范围,从而可很好第匹配任何尺度
高度图有多种可能的图形表示,其中一种是灰度图(grayscale map),地形中某一点的海拔越高,相应地该点在灰度图中的亮度就越大。
高度图可自编程序来生成,也可用图像编辑软件(如Adobe Photoshop)来创建,使用图像编辑软件可能是最简捷的方式,因为这类软件往往提供了一种可视化的交互环境,用户可随心所欲地创建自己所需的地形,此外还可利用图像编辑软件提供的一些特殊功能如过滤器(滤波器)等来创建一些有趣的高度图。
一旦完成了高度图的创建,需要将其保存为8位的RAW文件,RAW文件仅连续存储了图像中以字节为单位的每个像素的灰度值,这就使得这类文件的读取操作非常容易,你使用的图像编辑软件可能会询问你是否要为RAW文件增加一个文件头,选择"否".
可以根据需要选择任意一种格式
例:加载RAW文件
std::vector<int> _heightmap;
bool Terrain::readRawFile(std::string fileName)
{
// Restriction:RAW file dimensions must be >= to the
// dimensions of the terrain that is a 128 * 128 RAW file
// can only be used with a terrain constructed with at most
// 128 * 128 vertices
// A height for each vertex
std::vector<BYTE> in(_numVertices);
std::ifstream inFile(fileName.c_str(),std::ios_base::binary);
if(inFile == 0)
return false;
inFile.read(
(char*)&in[0],// buffer
in.size());// number of butes to read into buffer
inFile.close();
// copy BYTE vector to int vector
_heightmap.resize(_numVertices);
for(int i = 0 ; i < in.size();i++)
_heightmap[i] = in[i] ;
return true;
}
注意,将字节向量复制到一个整型向量中,这样就可对高度值进行比例变换从而突破0-255的限制
该方法的唯一限制是所要读取的RAW文件中包含的字节数至少要与地形中的顶点数一样多,所以,如果你要从一个256*256的RAW文件中读取数据,相应地您只能创建一个之多有256*256个顶点的地形
例:访问和修改高度图
int getHeightmapEntry(int row,int col)
{
return _heightmap[row * _numVertsPerRow + col];
}
int setHeightmapEntry(int row,int col,int value)
{
_heightmap[row * _numVertsPerRow + col] = value;
}
这些方法允许通过行和列索引引用高度图中指定的项,这些方法隐藏了如何访问由线性数组表示的矩阵
class Terrain
{
public:
Terrain(
IDirect3DDevice9* device,
std::string heightmapFileName,
int numVertsPerRow,
int numVertsPerCol,
int cellSpacing,// space between cells
float heightScale);
...methods snipped
private:
.. device / vertex buffer etc .snipped
int _numVertsPerRow;
int _numVertsPerCol;
int _cellSpacing;
int _numCellsPerRow;
int _numCellsPerCol;
int _width;
int _depth;
int _numVertices;
int _numTriangles;
float _heightScale;
};
可由构造函数的传入参数计算出地形的其他变量
_numCellsPerRow = _numVertsPerRow - 1;
_numCellsPerCol = _numVertsPerCol - 1;
_width = _numCellsPerRow * _cellSpacing;
_depth = _numCellsPerCol * _cellSpacing;
_numVertices = _numVertsPerRow * _numVertsPerCol;
_numTriangles = _numCellsPerRow * _numCellsPerCol * 2;
该地形的顶点结构定义;
struct TerrainVertex
{
TerrainVertex(){}
TerrainVertex(float x,float y ,float z ,float u float v)
{
_x = x; _y = y ;_z = z;_u = u ;_v = v;
}
float _x,_y ,_z ;
float _u,_v;
static const DWORD FVF ;
};
const DWORD Terrain::TerrainVertex::FVF = D3DFVF_XYZ | D3DFVF_TEX1;
TerrainVertex是嵌套类(nested class),之所以这样定义是因为TerrainVertex只在Terrain类的内部使用
顶点计算
为了计算三角形栅格的各顶点,只需自顶点start起,逐行生成每个顶点,保持相邻顶点的行列间隔均为单元间距(cell spacing),直至到达顶点end为止,这样就给出了x和z坐标的定义,y坐标只有查询所加载的高度数据结构中的相应项,就很容易获知
由于硬件的限制,3D设备都预设了最大图元和最大顶点索引值,可通过检查D3DCAPS9结构的MaxPrimitiveCount和MaxVertexIndex成员变量来获知你的图形设备为上述值所指定的上限
计算纹理坐标:
u = j*uCoordIncrementSize
v = i*vCoordIncrementSize
其中
uCoordIncrementSize = 1/numCellCols
vCoordIncrementSize = 1/numCellRows
下面是生成个顶点的代码
bool Terrain::computeVertices()
{
HRESULT hr = 0;
hr = _device ->CreateVertexBuffer(_numVertices * sizeof(TerrainVertex),
D3DUSAGE_WRITEONLY,
TerrainVertex::FVF,
D3DPOOL_MANAGED,
&_vb,
0);
if(FAILED(hr))
return false;
//coordinates to start generating vertices at
int endX = _width / 2 ;
int endZ = _depth / 2 ;
// compute the increment sizeof the texture coordinates
// from one vertex to the next
float uCoordIncreamentSize = 1.0f / (float)_numCellsPerRow;
float vCoordIncrementSize = 1.0f / (float)_numCellsPerCol;
TerrainVertex* v = 0;
_vb->Lock(0,0,(void**)&v,0);
int i =0;
for(int z = startZ; z >= endZ ; z -= _cellSpacing)
{
int j = 0;
for(int x = startX; x<= endX ; x += _cellSpacing)
{
// compute the correct index into the vertex buffer and heightmap
// based on where we are in the nested loop
int index = i * _numVertsPerRow +j;
v[index] = TerrainVertex(
(float)x,
(float)_heightmap[index],
(float)z,
(float)j*uCoordIncrementSize,
(float)i*vCoordIncrementSize);
j++;//next column
}
i++;//next row
}
_vb->Unlock();
return true;
}
为计算三角形栅格各顶点的索引,只需依次遍历每个方格,并计算构成每个方格的三角面片的顶点索引
计算的关键是推导出一个用于求出构成第i行,第j列的方格的两个面片的顶点索引的通用公式
下面计算索引的代码:
bool Terrain::computeIndices()
{
HRESULT hr = 0;
hr = _device ->CreateIndexBuffer(
_numTriangles* 3 * sizeof(WORD),// 3 indices per triangle
D3DUSAGE_WRITEONLY,
D3DFMT_INDEX16,
D3DPOOL_MANAGED,
&_ib,
0);
if(FAILED(hr))
return false;
WORD* indices = 0;
_ib ->Lock(0,0,(void**)&indices,0);
//index to start of a group of 6 indices that describe the
// two triangles that make up a guad
int baseIndex = 0;
// loop through and compute the triangles of each guad
for(int i = 0 ; i< _numCellsPerCol;i++)
{
for(int j = 0; j < _numCellsPerRow ; j++)
{
indices[baseIndex] = i * _numVertsPerRow + j;
indices[baseIndex + 1] = i * _numVertsPerRow + j + 1;
indices[baseIndex + 2] = (i+1) * _numVertsPerRow + j ;
indices[baseIndex + 3] = (i+1) * _numVertsPerRow + j;
indices[baseIndex + 4] = i * _numVertsPerRow + j + 1;
indices[baseIndex + 5] = (i+1) * _numVertsPerRow + j + 1;
// next guad
baseIndex += 6 ;
}
}
_ib ->Unlock();
return true;
}
例 ,纹理映射
bool Terrain::loadTexture(std::string fileName)
{
HRESULT hr = 0;
hr = D3DXCreateTextureFromFile(
_device,
fileName.c_str(),
&_tex);
if(FAILED(hr))
return false;
return true;
}
另外一种对地形进行纹理映射的方法是按顺序逐个计算出纹理内容,即首先创建一个"空"纹理,然后再基于一些一定义的参数计算出纹理元的颜色,在下例中,该参数为地形高度
该方法首先用D3DXCreateTexture创建一个空纹理,然后再将顶层纹理(一个纹理对象可能有多级渐进纹理,所以就有许多层纹理)锁定,至此开始遍历每个纹理元并对其上色(color),上色的依据是坐标方格所对应的近似高度,思路是:地形中海拔较低的部分上色为沙滩色,中等海拔的部分上色为绿色的丘陵颜色,高海拔的部分上色为雪山的颜色
用坐标方格中左上角顶点的高度值近似表示该方格的整体高度
// 用D3DXFilterTexture函数计算出较低层的多级渐进纹理中的纹理元
bool Terrain::genTexture(D3DXVECTOR3* directionToLight)
{
// Method fills the top surface of a texture procedurally then
// lights the top surface .Finally ,it fills the other mipmap
// surfaces based on the top surface data using D3DXFilterTexure
HRESULT hr = 0;
// texel for each quad cell
int texWidth = _numCellsPerRow;
int texHeight = _numCellsPerCol;
//create an empty texture
hr = D3DXCreateTexture(
_device,
texWidth,texHeight,
0,// create a complete mipmap chain
0,// usage
D3DFMT_X8R8G8B8,// 32bit XRGB format
D3DPOOL_MANAGED,&_tex) ;
if(FAILED(hr))
return false ;
D3DSURFACE_DESC textureDesc;
_tex->GetLevelDesc(0,&textureDesc);
// make sure we got the requested format because our code
// that fills the texture is hard coded to a 32 bit pixel depth
if(textureDesc.Format != D3DFMT_X8R8G8B8)
return false;
D3DLOCKED_RECT lockedRect;
_tex->LockRect(0/*lock top surface*/,&lockedRect,0/*lock entire rex*/,0/*flags*/);
DWORD *imageData = (DWORD*)lockedRect.pBits;
for(int i = 0 ; i < texHeight ;i++)
{
for (int j = 0 ; j < texWidth ; j++)
{
D3DXCOLOR c;
// get height of upper left vertex of quad
float height = (float)getHeightmapEntry(i,j);// _heightScale
if((height) < 42.5f) c = d3d::BEACH_SAND;
else if((height) < 85.5f) c = d3d::LIGHT_YELLOW_GREEN;
else if((height) < 127.5f) c = d3d::PUREGREEN;
else if((height) < 170.0f) c = d3d::DARK_YELLOW_GREEN;
else if((height) < 212.5f) c = d3d::DARKBROWN;
else c = d3d::WHITE;
// fill locked data,note we divide the pitch by four because the
// pitch is given in bytes and there are 4 butes per DWORD
imageData[i * lockedRect.Pitch / 4 +j] = (D3DCOLOR)c;
}
}
_tex->UnlockRect(0) ;
if (!lightTerrain(directionToLight))
{
::MessageBox(0,"lightTerrain()- FAILED",0,0);
return false;
}
hr = D3DXFilterTexture(
_tex,
0,// default palette
0,// use top level as source level
D3DX_DEFAULT);// default filter
if (FAILED(hr))
{
::MessageBox(0,"D3DXFilterTexture() - FAILED",0,0);
return false;
}
return true;
}
计算地形中各部分在给定光源照射下应如何进行明暗调整的明暗因子(shade factor)
不用Direct3D来添加光照而是手动计算,主要基于下述考虑:
1 手工计算由于无需存储顶点法向量,所以可以节省大量内存
2 由于地形是静态的,而且光源一般也不发生移动,所以可以预先对光照进行计算这样就节省了Direct3D实时照亮地形那部分计算时间
3 可以学习和巩固基础知识
在计算地形的明暗时用到的关照技术很基本,也很常用,即漫射光光照(diffusing lighting),给定一个平行光源,用"到达光源的方向(direction to the light)"(该光源发出的平行光的传播方向的反方向)来描述该平行光源。
例如:一组平行光线自空中沿着方向lightRaysDirection = (0,-1,0)向下照射,则到达光源的方向应于lightRaysDirection相反,即directionTolight=(0,1,0)。注意,光的方向向量应为单位向量(unit vector)
虽然指定光的出射方向好像更符合直觉,但指定到达光源的方向更适合漫射光光照的计算
要为地形中的每个坐标方格计算光向量L^和该方格的面法向量N^之间的夹角
一旦光向量与面法线的夹角超过90度,方格表面便接收不到任何光照
借助光向量和方格的面法向量之间的角度关系,可以构造一个位于区间[0,1]内的明暗因子(shading scalar),以表示方格表面所接收到的光照量,这样,就可用一个接近于0的因子值来表示这两个向量的夹角很大,当一种颜色与该因子相乘时,颜色值就接近0,从而呈现出较暗的视觉效果,反之,接近于1的因子值表示这两个向量夹角很小,所以当一种颜色与该因子相乘时,该颜色基本保持了原来的亮度
坐标方格的明暗计算,将光源的方向用一个单位向量L^来表示,为了计算向量L^与方格的法向量N^之间的夹角,首先需要求出N^, 通过计算向量的叉积可很容易地求出 N^,但必须首先找到该方格共面的两个非零的互不平行的向量。
例:计算某个特定坐标方格的明暗因子
float Terrain::computeShade(int cellRow,int cellCol,D3DXVECTOR3* directionToLight)
{
//get heights of three vertices on the quad
float heightA = getHeightmapEntry(cellRow,cellCol);
float heightB = getHeightmapEntry(cellRow,cellCol + 1);
float heightC = getHeightmapEntry(cellRow + 1,cellCol);
// build two vectors on the quad
D3DXVECTOR3 u(_cellSpacing,heightB-heightA,0.0f);
D3DXVECTOR3 v(0.0f,heightC-heightA,-_cellSpacing);
// find the normal by taking the cross product of two
// vectors on the quad
D3DXVECTOR3 n;
D3DXVector3Cross(&n,&u,&v);
D3DXVector3Normalize(&n,&n);
float cosine = D3DXVec3Dot(&n,directionToLight);
if(cosine < 0.0f)
cosine = 0.0f;
return cosine;
}
//获取摄像机应处在的高度
float Terrain::getHeight(float x,float z)
{
// translate on xz-plane by the transformation that takes
// the terrain START point to the origin
x = ((float)_width/2.0f) + x;
z = ((float)_depth/2.0f) - z;
// Scale down by the transformation that makes the
// cellspacing equal to one this is given by
// 1/cellspacing since ;cellspacing* 1 / cellspacing = 1
x /= (float) _cellSpacing;
z /= (float) _cellSpacing;
//首先,进行平移变换,将顶点start平移至坐标原点,然后,通过缩放因子为单元间隔的负倒数的比例变换将坐标方格的单元间隔归一化,这样,就转换到了一个新的参考系中,其中z轴方向向'下',当然,程序代码并没有改变坐标系框架本身,当然,程序代码并没有改变坐标系框架本身,只是将z轴正方向理解为向下
经过变换的坐标系与矩阵的顺序保持了一致,即左上角为原点,列索引和行索引分别沿着右方向和下方向递增,这样,由于目前单元间距为1,便可迅速求出当前所处的坐标方格的行列索引
float col = ::floorf(x);
float row = ::floorf(z);
//即,列索引等于x坐标的整数部分,行索引等于z坐标的整数部分,这里floor(t)函数的功能是求取不小于t的最大整数
既然知道了当前所处的坐标方格,就可求出构成该方格的4个顶点的高度
// get the height of the quad we're in
// A B
//*----*
// | \ |
//*----*
// C D
float A = getHeightmapEntry(row,col);
float B = getHeightmapEntry(row,col + 1);
float C = getHeightmapEntry(row + 1,col);
float D = getHeightmapEntry(row + 1,col + 1);
//此时,当前所处的方格位置以及构成该方格的4个顶点的高度均已知,现在来求摄像机位于任意位置(x,z)时,坐标方格单元的高度。初看起来,有些棘手,因为该方格单元可能沿着多个方向发生倾斜
}
为了求取该高度,首先需要判断摄像机当前处于哪个坐标方格内,注意,记坐标方格都是由两个三角面片的组合来绘制的,为了求出摄像机当前所处的三角形,需要对当前所处的坐标方格进行变换,使其左上角的顶点与坐标原点重合
由于col和row描述了当前坐标方格的左上角顶点的位置,必须沿x轴平移-col个单位,沿z轴平移-row个单位
float dx = x - col;
float dz = z - row;
如果dz < 1.0 -dx 当前就位于上三角形面片中,否则就在下三角形面片中
当在上三角形面片中,沿着该三角形AB,AC两个边,以向量 q= (qx,A,qz)为始端分别构造两个向量u = (cellSpacing,B-A,0),V = (0,C-A,-cellSpacing),然后沿着u轴对dx点进行线性差值,然后再沿着v轴对dy点进行线性插值,给定x坐标和z坐标,向量(q +dxu + dzv)的y分量就是该点所处的高度值
注意,由于只关心高度插值,所以可以只对y分量进行插值,这样指定点的高度就可由公式A+dxu+dzv求出
float height = 0.0f;
if(dz < 1.0f - dx)// upper triangle ABC
{
float uy = B-A;// A->B
float vy = C-A;// A->C
height = A + d3d::Lerp(0.0f,uy,dx) + d3d::Lerp(0.0f,vy,dz);
}
else // lower triangle DCB
{
float uy = C-D;// D->C
float vy = B-D;// D->B
height = D + d3d::Lerp(0.0f,uy,1.0f-dx) +d3d::Lerp(0.0f,vy,1.0f - dz);
}
其中Lerp函数可沿着ID直线做线性插值
float d3d::Lerp(float a,float b,float t)
{
return a-(a*t) +(b*t);
}
函数D3DXSplitMesh将地形网格分割为若干较小的子网格
void D3DXSplitMesh(
LPD3DXMESH pMeshIn,
CONST DWORD *pAdjacencyIn,
CONST DWORD MaxSize,
CONST DWORD Options,
DWORD * pMeshesOut,
LPD3DXBUFFER* ppMeshArrayOut,
LPD3DXBUFFER* ppAdjacencyArrayOut,
LPD3DXBUFFER* ppFaceRemapArrayOut,
LPD3DXBUFFER* ppVertRemapArrayOut);
#pMeshIn用于接收源网格,然后该函数可将源网格分割成若干较小的网格
#pAdjacencyIn是一个指向源网格邻接数组的指针
#MaxSize 用于指定分割的网格允许拥有的最大顶点数
#Option标记指定了分割所得的网格创建的选项
#pMeshesOut返回所创建的网格数组,而所创建的网格被保存在参数ppMeshArrayOut指向的数组缓存中
后3个参数可选(NULL表示忽略这些参数),并分别返回邻接数组,面片重绘信息以及每个新创建的网格的顶点重绘信息