由于格式原因,粘貼出來的效果不好,可以下載pdf版本:
http://download.csdn.net/source/643603
World Craft中的無縫地形實現 v1.0
熊家煜,QY_TEK
Emile:Xjyhust@gmail.com
首頁:http://blog.csdn.net/xjyhust
簡單說明:
這份文檔不是論文,我稱之為筆記,或者.note;這份文檔free for all,你可以隨便使用。如果你想引用,請註明出處http://blog.csdn.net/xjyhust,以及作者 熊家煜。 Ok,由於這個文檔只是v1.0,所以缺少很多實際的測試結果,在後面的版本中我會加進來;但是裡面的技術是不會有太大的變化的。最後,希望這個文檔對你有用。
無縫地形技術簡介
mesh部分,使用分塊的高度圖來儲存資訊;紋理部分,每塊地形使用8張diffuser紋理,4張normal map紋理,使用調色盤紋理查詢權值進行混合。分別對待情境編輯器和引擎運行時的地形處理方法,使效率和靈活性都能得到保障。
無縫網格的實現
地形的實現,主要針對2D的高度圖,儲存的精度是float 32bit。為了支援超大的無縫地形,必須將地形進行分塊,在運行期間,記憶體中只儲存可視的一塊地區。
LOD的實現
地形分塊以後的最小單位,我們先稱之為brick。離攝像機較近的brick,有較為細節的lod,定義這個時候的lod為低lod;離攝像機較遠的brick,有較為粗略的lod,定義這個時候的lod為高lod。(不要弄錯了,近處lod低,遠處lod高。)
Lod的大致方法就是,lod為level 0時,渲染每個頂點;level為1時,每2個頂點渲染一個頂點;level 為i時,每(2的i次方)個頂點渲染一個頂點...... 具體參看:
Level i Level i + 1
為瞭解決地形“龜裂”的問題,需要對不同level之間缺少的頂點進行處理。有幾種不同的處理方式,我的方式是:
假設這個時候level i和level i+1相接,我將level i+1的brick和level i-1臨界的那條邊界上缺少的頂點加入原有的三角形列表中:
為瞭解決這個邊界“龜裂”的問題,我們需要有不同的三角帶。由於在World Craft中,lod的分布都是按照環形的:
所以每個lod的三角帶需要有這樣五種不同的情況,對於level 為i的brick。情況一,他只和level為i的brick相接;情況二至五,他有且僅有一側和level i-1的brick相接。
效能和最佳化
對於brick渲染的最佳化,我們引入了一個sheet的概念。Sheet是一個更大的劃分單位,在我的實踐過程中,sheet為4*4個brick或者8*8個brick組成(根據brick的大小不同),sheet共用一個vertex buffer,這樣可以使效率更高一些,雖然也是draw n次,但是不需要修改vertex buffer,只要修改index buffer,對待lod相同的brick,index buffer也是一樣的,只需要修改位移量。引入sheet,也是為了後面的texture的最佳化需要,不需要頻繁的切換material,也不用寫過於複雜的sort就可以達到最佳化的目的。
編輯器和引擎運行時的不同
有這樣幾個原因使我們有理由分開對待這兩種情況:
1. 編輯器需要及時的相應使用者的輸入,這就要求每一次對地形的改變在較短時間內完成
2. 引擎不需要靈活性,所以渲染的資料需要盡量的最佳化
編輯器中沒有sheet的實現(只有概念),即每個brick有單獨的vertex buffer,brick要儘可能的小,比如,17*17大小。這樣做大大減小了vertex buffer的修改範圍,使用者的修改會及時得到反饋,不會出現太長的“假死”。
而引擎運行時,sheet的大小為513*513或者257*257,每個brick的大小為65*65或者33*33。我比較傾向513*513的sheet和33*33的brick。(具體是多少比較合理,在這份.note裡面我沒有列出詳細的比較資料,在v2.0裡面會列出一些測試的實際資料)
World Craft的地形編輯
World Craft中,使用一個較大的height map來表示一個sheet的,比如513*513大小的height map,然後每個brick的大小為17*17,對應height map的某個17*17的地區,由於brick單獨使用vertex buffer,所以,相鄰的brick之間共用height map上的17個pixel。
以rise/lower地形筆刷為例,具體的流程如下:
1. 判斷滑鼠拾取點(和地形相交的點)
2. 計算拾取點所對應的height map上的點
3. 將筆刷(一樣也是32bit-float的位元影像)正確的操作到height map上
4. 更新所影響到的地區(只更新地區內的brick)
有幾點需要注意的地方:
1. 更新brick的時候,只更新頂點的position,而不更新normal,否則速度會很慢;normal的更新只在每次滑鼠抬起的時候進行;最後的效果非常不錯。
2. 塗刷height map的時候,引入一個筆刷密度的概念,表示每隔多少像素才進行一次塗刷;當滑鼠移動過快或者過慢的時候,不會產生不均勻的效果。大概的方法就是:記錄每次的圖刷點,如果圖刷點之間的累計距離超過了筆刷密度的定義,就在最近的塗刷路徑上的正確地方進行塗刷。
3. 頂點normal的更新,是自己寫得,沒有用到nvidia的nvMeshMender庫,不知道為什麼,用nvMeshMender的時候速度奇慢,也許是網格類型的mesh的頂點重複比較多。
頂點normal的計算
如,對於每個頂點vert(圖中的黑點),計算其周圍的6個三角形的normal,然後取平均值。我不知道這個是不是正確的演算法,反正最後的效果是正確的。 ;)
無縫紋理的實現紋理調色盤
紋理調色盤的做法就是:調色盤紋理上的點對應的rgba值作為權值,分別表示使用4張不同紋理的權值。如果調色盤上的點只有紅色,那麼就是表示最後地形上面的該點只使用了第一張紋理;如果調色盤的點有4中不同的顏色,分別為0.1,0.2,0.3,0.4,地形上最後的顏色就是0.1份紋理1,0.2份紋理2,0.3紋理3,0.4份紋理4的混合。具體的演算法後面有hlsl給出。
為什麼這樣做,而不是使用一張全域的紋理?具體原因我之前的一篇blog也說道了,這裡我直接粘貼過來:
簡單的重複紋理是現在最常用的方法,但是效果一般。使用全域的紋理,如果紋理不是超級大,效果也是很一般:一般高度圖中的兩個像素之間的距離就是對應真實地形中的一米,一米的世界,要用貼圖來表現細節,就是用512*512大小的貼圖,或者是256*256的,那麼,如果用不重複的紋理,實現一個1公裡*1公裡的世界,需要的貼圖大小是256k * 256k,如果效果稍微差一點,就算是一米內只有64個像素,那樣也是64k*64k的紋理。。。所以這樣算下來,使用全域的貼圖的唯一辦法就是用detailed map,而我覺得,這是個醜陋的做法,對於一個以室外情境為主的mmo的網遊來說,地形是給人印象最大的映像元素,而用統一的細節來欺騙效果的做法,給人的印象自然很糟糕。
使用八張不同的紋理
使用8張不同的紋理,進行混合,還需另外兩張不同的調色盤,加上8張normal map,一共加起來是18張紋理,這在某些硬體上面不能實現(好像是只有dx10的硬體才能實現),另外在設定寄存器的時候需要改變相當多的變數。
比較節約的做法就是使用texture atlas,參考nvidia的相應工具(還有代碼和文檔)。將每4張顏色紋理“打包”成一個紋理,這樣,最後就是2大張顏色紋理,兩大張normal map;兩張調色盤紋理,一共是6張紋理,在所有ps2_0以上的硬體上都可以實現。
使用atlas的時候,需要注意紋理座標的改變,原來的紋理座標是[0, 1],應該轉換到[0 +texOffset, 1 – texOffset],其中texOffset= 1 / ( 2*tex_width);
整合後的diffuse map 整合後的normal map
具體實現 pixel shader
texture g_palTex;
texture g_tex0; // Base color texture
sampler tex0 =
sampler_state
{
Texture = < g_tex0 >;
MipFilter = point;
MinFilter = point;
MagFilter = linear;
addressu =wrap;
addressv =wrap;
addressw =wrap;
};
sampler palTex =
sampler_state
{
Texture = < g_palTex >;
MipFilter = linear;
MinFilter = linear;
MagFilter = linear;
};
struct VS_OUTPUT
{
float4 position : POSITION;
float4 texCoord : TEXCOORD0;
float3 diffuse : TEXCOORD1;
float3 normal : TEXCOORD2;
};
float4 RenderWithTexturePS( VS_OUTPUT i ) : COLOR
{
float2 coord0 = i.texCoord.xy * g_fTexRepeat;
coord0 = frac( coord0 );
coord0 = coord0 * 0.5;
float2 coord1 = coord0 + float2( 0.5, 0 );
float2 coord2 = coord0 + float2( 0, 0.5 );
float2 coord3 = coord0 + float2( 0.5, 0.5 );
float4 palColor = tex2D( palTex, i.texCoord.zw ).rgba;
palColor = normalize( palColor );
float4 vDiffuse = tex2D( tex0, coord0 ) * palColor.x +
tex2D( tex0, coord1 ) * palColor.y +
tex2D( tex0, coord2 ) * palColor.z +
tex2D( tex0, coord3 ) * palColor.w;
return vDiffuse * float4( i.diffuse, 1 );
}
以上實現為:一張調色盤紋理,一大張diffuse map(4張不同的紋理整合成的)。頂點中的texCoord.xy為局部的diffuse 紋理的座標,texCoord.zw為全域的調色盤紋理座標。