第十三章
地形網格(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),上色的依據是座標方格所對應的近似高度,思路是:地形中海拔較低的部分上色為沙灘色,中等海拔的部分上色為綠色的丘陵顏色,高海拔的部分上色為雪山的顏色
用座標方格中左上方頂點的高度值近似表示該方格的整體高度
// 用D3DXFilterTextureFunction Compute出較低層的多級漸進紋理中的紋理元
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表示忽略這些參數),並分別返回鄰接數組,面片重繪資訊以及每個新建立的網格的頂點重繪資訊