本文主要講的是《天龍八部》游戲的地形和一部分場景的具體實現,使用C++, Ogre1.6,我摸索了段時間,可能方法用的并不是最好的,但好歹實現了。文章可能講得有點羅嗦,很多簡單的東西都講了。我是修改了ETM(Editable Terrain Manager)實現的地形,其實單單實現天龍八部的地形場景等的載入根本不需要使用ETM,直接用Ogre的頂點->索引->紋理就可以搞定地形,但我要做的是可以實時編輯的,所以用了ETM,場景其由于很重要的粒子和model等部分我還沒去看,所以等以后看了再詳細寫關于場景的部分,但這個Demo已經實現了基本的場景的載入。光,霧,環境,靜態物等都能載入。
修改過的ETM和這個場景的Demo代碼可以通過文章底下的鏈接下載。
Demo截圖如下:(少林)
這個Demo比較簡單,只能移動攝像機看看場景。
我研究這些的動機是當前在學校做一個網游項目,想做得類似于《Second Life》,苦于沒有游戲美工,最近有馬上要二期驗收了,為了讓游戲看上去光鮮一點,無奈之下只好借《天龍八部》的資源來用了。看了不少大牛的博客,將得感覺都有點不是很詳細,只是大概把文件格式講了一下而已,具體怎么實現說得不多(可能是覺得實現太容易,懶得多說了吧...)最主要的是,似乎沒看到有人發完整的代碼。
實際項目中用的程序代碼我就不放出來了,場景部分差不多,只是多了個內建的編輯器,人物移動和網絡通信部分等。
編輯器的截圖曬一下,功能還不全 :-)
言歸正傳,先簡單地說一下載入一個天龍八部場景的大致過程:
- 讀取.Scene文件
- 根據<Texture>讀取.Terrain文件
- 讀取地磚大小(<tileSize>) 地形大小(xsize, ysize),縮放值(<scale>),地圖中心坐標(<center>)。
- 讀取所有要用的地形貼圖(<textures>中各項)。
- 讀取.gridinfo 文件,此文件中存放著每個格子對應的紋理坐標。
- 根據3,4,5步的信息用修改過的ETM創建Terrain。
- 讀取lightmap, 是png格式的預處理的場景陰影圖。
- 讀取場景中的各種模型等,并插入到場景Root中。
(注:天龍八部的場景包含很多個文件,用“劒蚩”的資源提取工具提取出來,文件夾下的基本都是,但我暫時不考慮尋路,碰撞等,所以就地形來講只研究.Terrain文件,.Gridinfo文件。 資源提取的問題可訪問http://www.cnitblog.com/sword/category/5167.htmlScene )
下面我分幾個部分來具體講如何實現天龍八部的場景Demo。
讀取高度圖
做地形首先肯定是要讀取高度圖,《天龍八部》的高度圖是保存在.Heightmap文件中,讀取的方法是跳過前面8個字節,讀地形的width和height,然后讀取width*height個float型數據,上面說到.Terrain文件中有地形大小(xsize, ysize),縮放值(<scale>),地圖中心坐標(<center>),<scale>中有xyz 3個值(一般情況下是100,100,100),分別是x,y,z軸的放大系數,用ETM創建地形的時候,直接用讀取到的float型數據作為高度圖數據,然后再用上面那些值作為參數,定義地形的大小,縮放值,和偏移。
這是讀取高度圖的代碼,heightMapData是float型的數組,存放原始的高度圖信息。
void TileTerrainInfo::LoadHightMap( const char* fileName, const char* type )
{
FILE* pf = fopen( fileName, "rb" );
fseek( pf, 8, SEEK_SET );
int height, width;
fread( &width, 4,1, pf );
fread( &height, 4,1, pf );
assert( height = this->height+1 );
assert( width == this->width+1 );
if( heightMapData )
delete []heightMapData;
heightMapData = new float[height*width];
for( int i = 0; i < height; ++i )
{
for( int j = 0; j < width; ++j )
{
float data;
fread( &data, 4,1,pf );
heightMapData[i*width+j] = data;
}
}
fclose( pf );
}
材質文件的分析
我想先講一下地形的材質,因為用別人的資源,首先要知道怎么用這些資源,一般情況下材質信息可以明顯地反映出如何使用紋理資源(不排除有可能用代碼動態生成材質)。
在每個.Terrain文件的最下面,有這些內容。
<materials>
<template material="Terrain/OneLayer" name="OneLayer"/>
<template material="Terrain/OneLayerLightmap" name="OneLayerLightmap"/>
<template material="Terrain/TwoLayer" name="TwoLayer"/>
<template material="Terrain/TwoLayerLightmap" name="TwoLayerLightmap"/>
<fog_replacement exp="Terrain/OneLayer_ps%fog_exp" exp2="Terrain/OneLayer_ps%fog_exp2" linear="Terrain/OneLayer_ps%fog_linear" none="Terrain/OneLayer_ps"/>
<fog_replacement exp="Terrain/TwoLayer_ps%fog_exp" exp2="Terrain/TwoLayer_ps%fog_exp2" linear="Terrain/TwoLayer_ps%fog_linear" none="Terrain/TwoLayer_ps"/>
<fog_replacement exp="Terrain/OneLayerLightmap_ps%fog_exp" exp2="Terrain/OneLayerLightmap_ps%fog_exp2" linear="Terrain/OneLayerLightmap_ps%fog_linear" none="Terrain/OneLayerLightmap_ps"/>
<fog_replacement exp="Terrain/TwoLayerLightmap_ps%fog_exp" exp2="Terrain/TwoLayerLightmap_ps%fog_exp2" linear="Terrain/TwoLayerLightmap_ps%fog_linear" none="Terrain/TwoLayerLightmap_ps"/>
</materials>
定義了一些材質模板。
我沒有深究其他的,只考慮TwoLayerLightmap這個材質。
不記得是在哪個文件夾下,有一個文件FairyTerrain.material,其中就是地形的材質。
我修改了一些內容,將<lightmap>設tex_coord = 0. <layer0>設tex_coord=1,<layer1>設tex_coord=2。這是因為我想讓ETM原有的地形和天龍八部的地形共存,而原有地形紋理坐標剛好和<lightmap>紋理坐標相符合,所以設為同一層。
這是我改過的材質
material Terrain/TwoLayerLightmap
{
technique
{
pass
{
fragment_program_ref Terrain/TwoLayerLightmap_ps
{
}
texture_unit
{
texture_alias <layer0>
texture <layer0>
tex_address_mode clamp
tex_coord_set 1
}
texture_unit
{
texture_alias <layer1>
texture <layer1>
tex_address_mode clamp
tex_coord_set 2
}
texture_unit
{
texture_alias <lightmap>
texture <lightmap>
tex_address_mode clamp
tex_coord_set 0
}
}
}
}
<layer0>,<layer1>,<lightmap>是一個pass中的3個texture_unit.也就是3層紋理。顧名思義<layer0>是第一層紋理,<layer1>是第二層紋理,<lightmap>是光照圖紋理(陰影圖),具體如何使用,如何使天龍八部的地形貼圖資源對應到layer0,layer1,我下面會講到。
從FairyTerrain.cg中我們可以找到對應的shader。
void TwoLayerLightmap_ps(
in float2 uv0 : TEXCOORD0,
in float2 uv1 : TEXCOORD1,
in float2 uvLightmap : TEXCOORD2,
in uniform sampler2D layer0,
in uniform sampler2D layer1,
in uniform sampler2D lightmap,
in float4 diffuse : COLOR0,
in float4 specular : COLOR1,
out float4 oColour : COLOR)
{
float4 c0 = tex2D(layer0, uv0);
float4 c1 = tex2D(layer1, uv1);
float3 texturedColour = lerp(c0.rgb, c1.rgb, c1.a);
float4 lightmapColour = tex2D(lightmap, uvLightmap);
float4 baseColour = diffuse * lightmapColour;
float3 finalColour = baseColour.rgb * texturedColour + specular.rgb * (1-c0.a) * (1-c1.a) * lightmapColour.a;
float3 resultColour = Fogging(finalColour);
oColour = float4(finalColour, baseColour.a);
}
很容易看出其大致思路是<layer1>的alpha值控制<layer0>和<layer1>進行混合。
可見,天龍八部的地形應該是部分像魔獸一樣的格子式地形,部分權重圖地形,也就是ETM原有的那種貼圖模式,很多層紋理,然后又1-2層手動生成的紋理數據控制各層紋理的alpha值,達到混合的效果,只不過這里是只有一個alpha通道來控制紋理混合。
兩層紋理的效果比單獨一層紋理好的多,我用OneLayerLightmap材質試過,效果比較赫人...
地形紋理的實現
<lightmap>紋理很明顯,是一整張紋理貼到整個地形,沒什么好說的。
但<layer0> <layer1>這兩層地形紋理應該怎么貼上去呢?
對于材質中的這兩層紋理,有兩種可能。
1.<layer0>,<layer1>本身只是材質模板中紋理的名字,沒有實際意義,在實際的程序中會為每一塊地形從材質模板繼承一個模板,然后修改材質中紋理的名稱。
2.在程序中手動創建<layer0>,<layer1>。
先說兩種不能實現的方法:
1. 在程序中手動創建<layer0>,<layer1>, 為極大極大的貼圖(和真實的地形一樣大),該貼圖根據.Terrain和.Gridinfo中的信息來組成,和lightmap一樣,整個貼到地形上。
在小游戲,只有可能一個屏幕那么大的地形,也許可以用,而且效果可能不錯,但在這種地形相對較大的游戲中是不可能的,首先,極大的浪費資源,一個地磚的紋理,可能被用到幾十次幾百次,那么在這個大紋理中,就會有幾百個地磚紋理的拷貝,其次,不可能創建這么大的紋理(硬件不支持?反正我試過創建不出來..)
2. 像ETM一樣,將所有要用到的紋理(假設有n張)一個一個作為texture_unit放在材質里面,然后用n/4張手動生成的紋理去控制這些紋理的alpha值。這個方法不是對于天龍八部的地形不是很現實,一般每個天龍八部的地形有大概十幾個不同的紋理,如果用這個方法,每個pass一般支持8個texture_unit,十幾個紋理,加n/4張控制紋理需要3-4個pass,效率似乎... 而且我們通過天龍的材質文件可以看出,游戲應該不是用的這個方法來實現的。
3. 每一個格子都有自己獨自的材質,修改每個格子材質中的<layer0>, <layer1>, 改為它需要的材質文件,如 "05武當\褐色土地底層.jpg” 相當于將每一個格子作為單獨的mesh。這個是可以實現的,我試過,將ETM的TileSize設為1,然后生成每個Tile的時候修改其材質,成功了,地形也顯示出來了,完全正確,但幀率..... 呵呵,debug模式下fps 大于0小于1... 到release也許可以到十幾吧,我沒試,顯然是不能這樣搞的...
我最后實現地形貼圖用的是texture atlas,手動創建一張紋理,將所有要用到的地形紋理組合成一張大紋理,然后每一個頂點設基于這張大紋理的UV坐標,texture atlas比每個格子設材質更好的原因很顯而易見,具體可以參考附件中所帶的文章,《“Batch, Batch, Batch:”What Does It Really Mean?》中的第30頁:Batch Breaker: Texture Change.
下圖就是將wudang.Terrain中
<textures>
<textures>
<texture filename="03南海/巖石海礁01.jpg" type="image"/>
<texture filename="03南海/巖石海礁03.jpg" type="image"/>
<texture filename="05武當/褐色土地底層.jpg" type="image"/>
<texture filename="05武當/褐色土地上層.tga" type="image"/>
<texture filename="05武當/青磚地底層.tga" type="image"/>
<texture filename="13鏡湖/鏡湖桃花瓣.tga" type="image"/>
... ...
</textures>
所定義的所有紋理組合成的一張大紋理。
可以發現,天龍八部中的地形貼圖大小是不同的,但最大是256x256(就我目前所知),所以我干脆將每一格劃為256x256,共可容納有ROW_SIZExCOL_SIZE張小貼圖,這樣大貼圖的大小應該是256*COL_SIZE x 256*ROW_SIZE。
我這臺機器支持的最大紋理大小似乎為4096x4096,那么理論上因該可以最多容納16*16張小貼圖,綽綽有余了。這樣雖然浪費一點空間,但可以很方便地通過ID索引貼圖坐標。
比如 <pixmap bottom="0.2480469" left="0.00390625" right="0.4960938" textureId="2" top="0.001953125"/> 通過這樣一塊pixmap的定義,我們可以根據textureId=2找到它所所在的位置。
其所在行為textureId/COL_SIZE,所在列為textureId%COL_SIZE。如上面那張大紋理的COLE_SIZE = 8(一行有8張小貼圖)
所以textureId=2的這張小貼圖所在行row=0,坐在列col=2.
我們知道紋理坐標范圍為0.0f-1.0f,所以textureId=2的小貼圖左上角的UV坐標為U = (float)col/COL_SIZE = 0.25f , V = (float)row/ROW_SIZE = 0.0f.
再根據pixmap中的信息left ,right, top, bottom 可以計算出小貼圖四個點的坐標。在創建頂點時將紋理坐標附上即可。
具體的過程應該是
1.手動創建名字為<layer0>的texture
代碼如下:
TexturePtr layer0 = TextureManager::getSingletonPtr()->createManual(
"<layer0>", "General",TEX_TYPE_2D,
layerTextureWidth,layerTextureHeight, 1, 3, PF_BYTE_RGBA, TU_WRITE_ONLY );
2. 將材質中的texture_unit <layer1>中的texture_name 由<layer1>改為<layer0>,因為我們兩層用的是同一張紋理,沒必要復制一遍,直接改名指向同一張紋理就行了。
代碼如下:
MaterialPtr material (MaterialManager::getSingleton().getByName("Terrain/TwoLayerLightmap"));
material->getTechnique(0)->getPass(0)->getTextureUnitState( 1 )->setTextureName( "<layer0>");
3.讀取.Texture文件,將要用到的紋理拼接為大紋理,如上面那張圖。
地形的頂點與索引
若地圖為192x192,它就是應該有192*192個格子。一般情況下的做法下,它應該有193*193個頂點,織成一個網狀,但由于我用的atlas,
可以知道,每一個非邊緣的頂點將會有4個紋理坐標(左上,右上,左下,右下 )
如下圖
中間的頂點要同時負責A塊的右下,B塊的左下,C塊的右上,D塊的左上。
話說一個頂點確實是同時又多個紋理坐標的,只要設置不同的tex_coord。但天龍八部地形貼圖一般有3層,<layer0>,<layer1>,<lightmap>,分別是兩層地形,一層預處理的陰影。
一層<lightmap>不用多說的,就是一張大紋理,每個頂點的坐標是u=col/terrainColSize, v=row/terrainRowSize.
另外兩層就是我們需要考慮的,因為有兩層,這樣每個點不止同時負責4塊,要同時負責兩層共8塊,這樣這個pass的8個texture_unit都滿了,必須再用一個pass來做<lightmap>那一層,效率不行。
所以只好用另一方法,就是在非邊緣的每一個位置,將4個頂點重合在一起,這4個頂點的紋理坐標不同,但位置相同,即每一個格子都有四個獨立的頂點,相鄰的兩個格子有兩個點重合。
也就是說192x192的地圖,需要有192*192*4個頂點。索引方式還是差不多,每一個格子需要6個索引,所以一共要192*192*6個索引。
這樣,ETM中生成頂點和索引的部分代碼都需要改,生成頂點的代碼在void Tile::createVertexData(size_t startx, size_t startz)中,生成索引的代碼在void Tile::createIndexData()中。
天龍八部的.Terrain文件一般有這么一行<scale x="100" y="100" z="100"/>,說明地形在3個方向都是放大100倍,x,z本是一格大小為1x1為單位的,放大后即為100x100,
一個192x192的地形實際游戲中的大小應該為19200*19200,而天龍的坐標系是正中間坐標為.Terrain文件中的<center>的值,若不存在則中心為(0,0), 正方向為正,負方向為負,所以當<center>值為(0,0),192x192的地形實際坐標范圍應該是 (-9600,-9600)到(9600,9600)。
要注意的是不是將所有頂點作為一個mesh,而是應該根據.Terrain文件中的<tileSize>規定每一個TerrainTile的大小,每個TerrainTile中包含tileSize x tileSize 個地形網格,一個TerrainTile作為一個Entity插入到一個場景節點。
頂點位置和索引考慮完了,該是要考慮每個頂點的紋理坐標的問題了。
要給每個頂點設UV必須用到.Gridinfo文件中的信息,該文件中定義了每一個格子對應的紋理信息。
具體的文件格式可參考,我在這就不贅述了。
http://www.shnenglu.com/mybios/archive/2009/07/26/91267.html
此處是正解,其他地方似乎多多少少都有錯,特別是op=8的時候,要注意是與對角線兩邊的兩個點(不是對角線上的點)從上面復制到下面。
如圖1應該是將左上角的頂點紋理坐標復制到右下角
圖2應該是將右上角的紋理坐標復制到左下角。

圖1 圖2
但還有一處我有不同,op=4的時候,我覺得應該是順時針轉90度,測試下來似乎沒問題。
這是我的根據op操作UV坐標的代碼,op=4的時候我貌似確實是在順時針轉吧……
void changeGridInfoUV( AutoTexCoord& leftTop, AutoTexCoord& rightTop, AutoTexCoord& leftBottom, AutoTexCoord& rightBottom, uchar state, bool bIndex )
{
//0 不變
//1 圖片水平翻轉
//2 圖片垂直翻轉
//4 順時針旋轉度
//8 對角線上方頂點紋理坐標復制到對角線下方頂點。(與對角線垂直的兩個頂點)
uchar res1 = state&1;
uchar res2 = state&2;
uchar res3 = state&4;
uchar res4 = state&8;
if( res1 != 0 )
{
leftTop.Exchange( rightTop );
leftBottom.Exchange( rightBottom );
}
if( res2 != 0 )
{
leftTop.Exchange( leftBottom );
rightTop.Exchange( rightBottom );
}
if( res3 != 0 )
{
leftTop.Exchange( rightTop );
leftBottom.Exchange( rightTop );
rightBottom.Exchange( rightTop );
}
if( res4 != 0 )
{
// 非正常索引
if( bIndex ) {
(leftBottom.setX( rightTop.getX) );
leftBottom.setY( rightTop.getY() );
}
// 正常索引
else {
rightBottom.setX( leftTop.getX() );
rightBottom.setY( leftTop.getY() );
}
}
}
讀取場景環境與模型
先階段我讀取了一部分場景,包括環境和一些模型,粒子等部分還沒看,所以這個場景是不完整的,不過大概的輪廓都出來了。
讀取場景其實就是用TinyXML讀取.Scene中的各種XML項,然后根據讀取的數據創建相應的場景節點,或設置相應的場景環境,如霧,skydome等。
具體代碼下載附件看吧,有點無聊,都是switch-case語句。
但有一點一定要注意,在讀取資源前一定要先調用一個函數
setlocale( LC_CTYPE, "" );
不然中文路徑或文件名的.mesh文件是讀不了的。
地形和場景都搞定了,可以看看結果了!然而,悲劇出現了!
遠景的效果圖,很明顯,地形一格一格像有裂縫一樣……

近處的效果圖, 近了以后,就沒地形的裂縫了……
地形裂縫問題
問了一下我的一個學長,自己也思考些時間。估計是由于是用的atlas texture, 然后一定距離后的mipmap和texture filtering,產生了裂縫的問題。
通過附件中的《Improve Batching Using Texture Atlases 》在Applying Texture Filtering To Atlases節可以看討論到texture filtering對texture atlas造成的影響,但是解決方案只是理論上的,并沒有實現,一個是寫shader,在不同的mipmap下調整紋理坐標,另一個是預留紋理,就是在已有紋理上加一圈和邊緣相同的像素。文章中還提到:enabling anisotropic filtering minimizes these errors,我試了一下,設置了filtering anisotropic , 并且將anisotropic_max 設為最大,結果卻是略有好轉,但幀率損失了50fps左右… 也許是我的顯卡太水了?我最后還是沒采用這個解決方案。
我最后的解決方案是
1,手動創建紋理的時候,設置其mipmap的級別最多為3,這樣就不會有更高級別的mipmap,導致更嚴重的失真。
2,手動創建mipmap,而不是讓其自動生成。比如原來是1024*1024大小的貼圖,1級mipmap的大小應該為512*512,默認它是自動生成的,我設為手動生成,先把個小貼圖縮小為50%,再把他們組成一張512x512的貼圖作為原來的貼圖的mipmap,一次類推手動生成3層mipmap.這樣情況稍微有了點改善。
3,小貼圖合成大貼圖之前,將他們統一resize到256*256的大小,在組合成大貼圖,這樣后感覺裂縫問題好轉了不少。
4,預留紋理坐標,雖然天龍八部開始就預留了紋理坐標,可以.Terrain文件中的紋理坐標很多都不是絕對的0.0f 0.25f 0.5f 等,都是一些 0.2480469 ,0.00390625,0.4960938等,我不知道它本是出于什么原因。但我用了這樣的坐標還是不行,還是要把它設地更加靠內才能避免裂縫。
所以讀取紋理坐標的時候加上了一個fixFloat的過程,這個過程很不科學,但我試了一下似乎有點用處就用上了。
static void fixFloat( float& f )
{
if( f < 0.01f )
f = 0.005f;
else if( f > 0.24f && f < 0.25f )
f = 0.245f;
else if( f > 0.25f && f < 0.26f )
f = 0.255f;
else if( f > 0.49f && f < 0.50f )
f = 0.495f;
else if( f > 0.5f && f < 0.51f )
f = 0.505f;
else if( f > 0.74f && f < 0.75f )
f = 0.745f;
else if( f > 0.75f && f < 0.76f )
f = 0.755f;
else if( f > 0.99f )
f = 0.995f;
}
這樣也在一定程度上改善了裂縫的問題,但有的貼圖看上去會不太吻合,但總比裂縫好。
隨后地形基本上看不出有裂縫了,但還是有一點痕跡。

這個地形裂縫的問題困擾了我許久,最后的解決方案我也覺得不甚滿意,不知道有哪位有好點的解決方案請告訴我。