背景 html
在介紹層次細節算法以前,先來看兩幅圖片。 linux
圖一 算法
圖二 函數
這兩幅圖片是用層次細節算法也即LOD算法繪製的地形網格。爲了更清晰的看清地形網格的結構,我沒有給其貼上紋理。這兩幅圖片看上去給人第一感受就是分辨率不一樣,圖一分辨率較低,圖二分辨率很高。圖一圖二是由同一個程序生成的,圖一是在調節係數爲1的狀況下生成的,圖二是在調節係數爲25的狀況下生成的。爲了增長對比度,我故意把兩幅圖片的分辨率調節的差異很大。這個地形網格若是達到全分辨率的話將會是513像素*513像素。然而讀者看到的圖片並無達到全分辨率。爲何呢?你們想一下,在現實世界中,人眼的視角是有一個範圍的並不能看到360度範圍的場景;隨着視線的往遠處移動,看到的東西愈來愈模糊;你們在想一下另一個問題,把圖片貼到一個物體上,若是這個物體的表面是平的,那麼任務確定很是容易完成,若是物體表面凸凹不平,那麼你就不能把圖片直接貼在上面,你必須把圖片剪成小的片斷而後再艱難的貼在物體表面。同理層次細節算法就是模擬上述現實場景的技術,只不過咱們將換一套專有名詞來描述。上述地形網格在知足下面三個條件時候被繪製,一,不在照相機視景體內的網格部分將不會被繪製;二,距離相機視點遠的地方網格以低分辨率來繪製,近的地方以高分辨率來繪製;三,粗糙的部分以高分辨率繪製,平坦的部分以較低的分辨率來繪製。這三個條件合稱節點評價系統。 工具
地形高程圖 spa
在層次細節地形繪製的過程當中,每個頂點座標(x,y,z)被分爲兩部分處理,(x,z)與y兩個部分。至於爲何這樣作,當你看完本篇文章以後你就會明白。在OpenGL裏面Y軸是垂直向上的,所以頂點的y座標值表明頂點的高度。高程圖就是存儲y座標值的,其中一種方法就是使用raw格式的文件。raw格式文件是8位的,也就是說把raw格式的圖片當作width*hight大小的矩形的話,其中每一個元素表示一個8位的數據,範圍是0到256,而這個範圍在乘以相應的比例正好能夠表示高度值。在層次細節算法中要求地形的大小必須是正方形,並且必須知足邊長的像素數爲(2的n次方)+1;後文你們會明白爲何會有這兩個限制。本文咱們使用.raw格式的高程圖,其製做方法有不少,本文介紹一種最簡單的方法,用photoshop生成,若是要想生成本身想要的那種地形須要使用專業的方法來生成高程圖,本文使用的是隨機生成的高程圖。方法很簡單,打開PS快捷鍵crtl+N新建一個513*513像素大小的項目,肯定後從工具欄裏選擇濾鏡->渲染->分層雲彩,而後存儲爲選擇.raw格式,至此高程圖就完成了。下面給出加載高程圖的程序代碼: .net
01.void GLLod::loadRawFile(LPSTR strName, int nSize) 02.{ 03. FILE *pFile=NULL; 04. pFile=fopen(strName,"rb"); 05. if(pFile==NULL) 06. { 07. MessageBox(NULL,TEXT("不能打開高度圖文件"),TEXT("錯誤"),MB_OK); 08. return; 09. } 10. fread(pHeightMap,1,nSize,pFile); 11. int result=ferror(pFile); 12. if(result) 13. { 14. MessageBox(NULL,TEXT("讀取數據失敗"),TEXT("錯誤"),MB_OK); 15. } 16. fclose(pFile); 17.}
從高程圖中獲得座標(x,z)處的高度值的程序代碼: code
01.int GLLod::getRawHeight(int x, int z) 02.{ 03. int xx=x%(map_size+1); 04. int zz=z%(map_size+1); 05. if(!pHeightMap) return 0; 06. return pHeightMap[xx+(zz*(map_size+1))]; 07.}
層次細節算法(LOD算法)
如今要步入正題了,我將會向你們介紹層次細節算法的原理。LOD算法採用的是四叉樹的結構來處理的。 htm
咱們看上圖,網格一是最初始的沒有分割的正方形,其邊長是若是以像素表示邊長的話那麼邊上有個像素(假設每兩個最近的像素間的距離爲1)。那麼在這裏咱們能夠看出每個網格的邊長是5個像素,若每一個像素間的距離爲一的話,那麼每一個網格的邊長是4,下文中咱們默認每一個像素間的距離是1。將網格一均分爲4個小網格,那麼這四個小網格是原網格的四個兒子節點,繼續劃分,圖二中的每一個兒子網格繼續劃分爲四個兒子。咱們能夠有選擇的劃分某些網格,每一個網格一旦劃分的話,就必需劃分爲四個相等的小網格。這種劃分方法很明顯是符合四叉樹的,只不過這個四叉樹每一個節點要麼有四個兒子,要麼沒有兒子。上面描述的網格是在x-z平面上,至此尚未考慮y座標呢。等劃分到必定標準的時候就會在此基礎上考慮y座標,這個時候就能夠渲染了,不過距離這一步還有很遠的距離。這個劃分結束的標準就是前文中所說的節點評價系統(若是知足節點評價系統,可是網格已經達到最大分辨率的話也不能再劃分了)。下面咱們就來詳細講述節點評價系統。 blog
節點評價系統
相機裁剪 上面咱們劃分了網格一,直至網格三,可是那個劃分是盲目的,接下來咱們就必需按照必定的標準有目的的劃分,來看幾副圖片。
圖一圖二
圖三
圖中的紅色線表明照相機(也能夠理解爲人的眼睛)的視野範圍,即便只有一小部分在相機視野內,咱們就須要對其進行劃分,顯然圖一的正方形在視野內,對其劃分獲得圖二中的網格,這個時候能夠發現圖二中的最右邊的兩個兒子網格仍然在視野內,所以對其繼續劃分,最左邊兩個兒子網格不在視野內,所以沒必要劃分也即被裁剪掉了,因而獲得了圖三中的網格。如今呈現給讀者的是平面的,實際上考慮y軸座標的話,網格節點是一個三維的,爲了方便咱們以平面的方式來闡述,其實是三維的。那麼如何進行三維的裁剪呢?在筆者的前一篇文章《3D座標系、矩陣運算、視景體與裁剪》中對其算法進行了描述,這裏只給出代碼。
01.int GLFrustum::isAabbInFrustum( AABB &aabb) 02.{ 03. //aabb是一個AABB包圍盒 04. calculateFrustumPlanes();//計算平截頭體的六個面的方程 05. bool insect=false;//相機裁剪的標誌 06. for(int i=0;i<6;i++) 07. { 08. //接下來3個if語句是軸分離的方法調整aabb包圍盒 09. if(g_frustumPlanes[i][0]<0.0f) 10. { 11. int temp=aabb.min[0]; 12. aabb.min[0]=aabb.max[0]; 13. aabb.max[0]=temp; 14. } 15. if(g_frustumPlanes[i][1]<0.0f) 16. { 17. int temp=aabb.min[1]; 18. aabb.min[1]=aabb.max[1]; 19. aabb.max[1]=temp; 20. } 21. if(g_frustumPlanes[i][2]<0) 22. { 23. int temp=aabb.min[2]; 24. aabb.min[2]=aabb.max[2]; 25. aabb.max[2]=temp; 26. } 27. 28. if((g_frustumPlanes[i][0]*aabb.min[0]+g_frustumPlanes[i][1]*aabb.min[1]+g_frustumPlanes[i][2]*aabb.min[2]+g_frustumPlanes[i][3])>0.0f) 29. { 30. 31. return 0;//不可見 32. } 33. 34. if((g_frustumPlanes[i][0]*aabb.max[0]+g_frustumPlanes[i][1]*aabb.max[1]+g_frustumPlanes[i][2]*aabb.max[2]+g_frustumPlanes[i][3])>=0.0f) 35. { 36. insect=true;//裁剪 37. } 38. 39. } 40. if(insect) return 1;//裁剪 41. return 2;//徹底可見 42.}
這其中的calculateFrustumPlanes();是用來計算平截頭體的六個面的方程讀者能夠點擊這裏瞭解詳情。
視點距離上面的相機裁剪部分說在視野範圍內的繼續劃分,那麼視野內的那部分是否是一直繼續劃分呢?固然不是,過分的劃分並不會帶來視覺上的改觀而會增長GPU的處理負擔,試想若是有1000萬個頂點的話,那處理起來是很是消耗GPU的。根據常識,在現實世界中咱們看遠處的物體會感受到模糊,看近處的會感受比較清晰,一樣LOD算法裏也是採用這個理念,距離視點遠的網格節點就沒必要要繼續劃分,而近處的網格則要繼續劃分。
如上圖,一個小兔子的眼睛看着右上角的兒子網格,咱們定義距離L是眼睛到網格中心的距離,d是目標網格的邊長。知足條件的時候網格繼續劃分,不然不須要繼續劃分。C1是一個能夠根據實際渲染狀況調節的因子。圖一 圖二就是不一樣調節因子下所造成的兩幅地形網格。
粗糙程度
在物體粗糙的部分須要繼續劃分以更好的顯示,而平坦的部分則不須要作過多的劃分,好比一個平面直接貼紋理就好了,作過多的劃分是沒有意義的。
那麼怎麼定義網格的粗糙程度呢?如上圖所示,若是在xz平面考慮問題的話,那麼網格的每一個節點的邊都是在一個平面上的,若是考慮y座標的話原來是直線的邊會變成曲線,咱們以每一個網格四條邊的起伏程度和中心點的起伏程度的最大值來定義網格的粗糙度。舉個例子在上圖中dh4的值是那條邊的兩個頂點的y座標值相加再除以2之後減去邊的中點的y座標值獲得dh4,這個dh4就是所在邊的起伏度,一樣方法計算dh1 dh2 dh3。中心點須要計算兩次,由於中心點所在的邊有兩條,分別是對應的兩條對角線。這6個值計算出來後選擇其中最大的做爲粗糙度。設粗糙度爲DHmax,那麼知足條件時繼續劃分,不然不劃分。其中C2是能夠調節的因子。這個條件能夠和上一個條件合併獲得。
總結來講if(相機裁剪經過&&視點-粗糙值合適&&沒有達到最大分辨率) then 劃分節點,不然不劃分。
消除裂縫若是僅僅作到上面所說的是否是就能夠渲染地形了呢?答案是否認的,由於這樣會產生裂縫。什麼是裂縫呢?咱們來看兩幅圖片就能夠知道了。
地形一
地形二
爲了方便你們觀察,每一副圖片部分網格以線框的模式渲染另外一部分以填充的方式渲染。圖一是正常的,圖二卻有許多列縫。這是什麼緣由呢?爲了弄明白這一點咱們先放一放,看另一個問題,網格是怎麼渲染的呢?
如圖是一個即將送往3D API渲染的網格節點。共有九個頂點,中心點是0點,另外還有8個點。在OpenGL裏這個網格將會以三角形扇的方式進行渲染,好比032是一個三角形,021又是一個三角形。可是這樣渲染是有問題的,看接下來的圖片:
在上面這個圖中左右兩個網格的劃分層次相差爲1,也就是右側的網格比左側的多劃分一次。這時候問題出現了,當以頂點1 2 3渲染三角形和以頂點2 4 5渲染三角形以及以頂點5 4 3渲染三角形時候就出現了裂縫。圖上面是在xz平面上看不出問題,可是當給頂點賦予y座標值的時候問題就出現了,由於點4可能和點2 點3不在一個高度,因此點2 點4點3組成的多是一條折線。假如點2 點3的高度相同,點4比點2 點3 高,那麼點2 點3 點4便組成了一個三角形,這個三角形就是地形二上面的裂縫。你們能夠看下比較大的裂縫會發現正好是個三角形,這就是不少諸如點2 點3 點4組成的三角形裂縫。那麼怎麼解決這個問題呢?能夠在點1 點4之間增長一條線段,這個處理起來比較麻煩,本文采起的是將點4點5組成的線段取消,也即刪除點4,這個時候裂縫便會消失,如地形一那樣。可是咱們忽略了一個問題,這個仍是比較棘手的問題,咱們再看一張圖片:
在這幅圖裏面,右側的網格比左側的網格多劃分了2次,這個時候頂點3 頂點6 頂點5 頂點4組成的邊比先前那個例子更加複雜了。即便刪除點5,點3 點6 點4仍然可能組成一個三角形裂縫。點6不能刪除,由於點6是正方形網格的頂點,刪除它就等於刪除網格了,因此只能刪除邊的中點。這下怎麼辦呢?若是有一種方法保證左右兩側的兩個正方形網格劃分層次相差小於等於1,那麼就能夠按照前文所說的那樣經過刪除點來消除三角形裂縫。能作到這一點嗎?答案是確定的。假設左側的網格劃分值爲f2,右側的網格的父親節點劃分值爲f1,當f1網格須要劃分的時候必須保證f2也劃分,如何作到這一點呢?經過觀察若是知足表達式;f2<f1的狀況下f2會在f1劃分時也被劃分,由於f1若是被劃分的話說明f1<1,而f2<f1從而f2<1也即f1知足節點評價系統,因此也會被劃分。也即
由於d2=2d1因此化簡之後獲得
如今問題集中在L2和L1的比值。看圖:
從視點作垂直於xz平面的直線交予xz面與O點。此時有代入後獲得
也就是說DHmax2>DHmax1的話就能夠知足上式,從而消除三角形裂縫。如何使得DHmax2>DHmax1呢?能夠這樣作,對於左邊的正方形網格求緊貼其四條邊的邊長爲其一半的8個小正方形的DHmax值,再與其自身的DHmax比較,並將最大值當初該網格的DHmax值。這樣就能夠保證右側的小正方形父親被劃分時候左側的大的正方形網格也被劃分,這樣就能夠保證他們的劃分層次相差小於等於1。如此便不會出現三角形裂縫。調整DHmax的函數代碼是:
01.void GLLod::modifyDHMatrix() 02.{ 03. int edgeLength=2; 04. while(edgeLength<=map_size) 05. { 06. int halfEdgeLength=edgeLength>>1; 07. int halfChildEdgeLength=edgeLength>>2; 08. for(int z=halfEdgeLength;z<map_size;z+=edgeLength) 09. 10. { 11. for(int x=halfEdgeLength;x<map_size;x+=edgeLength) 12. 13. if(edgeLength==2) 14. { 15. int DH6[6]; 16. DH6[0]=abs(((getRawHeight(x-halfEdgeLength,z+halfEdgeLength)+getRawHeight(x+halfEdgeLength,z+halfEdgeLength))>>1)-getRawHeight(x,z+halfEdgeLength)); 17. DH6[1]=abs(((getRawHeight(x+halfEdgeLength,z+halfEdgeLength)+getRawHeight(x+halfEdgeLength,z-halfEdgeLength))>>1)-getRawHeight(x+halfEdgeLength,z)); 18. DH6[2]=abs(((getRawHeight(x-halfEdgeLength,z-halfEdgeLength)+getRawHeight(x+halfEdgeLength,z-halfEdgeLength))>>1)-getRawHeight(x,z-halfEdgeLength)); 19. DH6[3]=abs(((getRawHeight(x-halfEdgeLength,z+halfEdgeLength)+getRawHeight(x-halfEdgeLength,z-halfEdgeLength))>>1)-getRawHeight(x-halfEdgeLength,z)); 20. DH6[4]=abs(((getRawHeight(x-halfEdgeLength,z-halfEdgeLength)+getRawHeight(x+halfEdgeLength,z+halfEdgeLength))>>1)-getRawHeight(x,z)); 21. DH6[5]=abs(((getRawHeight(x+halfEdgeLength,z-halfEdgeLength)+getRawHeight(x-halfEdgeLength,z+halfEdgeLength))>>1)-getRawHeight(x,z)); 22. int DHMax=DH6[0]; 23. for(int i=1;i<6;i++) 24. { 25. if(DHMax<DH6[i]) 26. DHMax=DH6[i]; 27. 28. } 29. 30. setDHMatrix(x,z,DHMax); 31. } 32. else 33. { 34. int DH14[14]; 35. int numDH=0; 36. 37. int neighborX; 38. int neighborZ; 39. neighborX=x-edgeLength; 40. neighborZ=z; 41. if(neighborX>0) 42. { 43. DH14[numDH]=getDHMatrix(neighborX+halfChildEdgeLength,neighborZ-halfChildEdgeLength); 44. numDH++; 45. DH14[numDH]=getDHMatrix(neighborX+halfChildEdgeLength,neighborZ+halfChildEdgeLength); 46. numDH++; 47. } 48. neighborX=x; 49. neighborZ=z-edgeLength; 50. if(neighborZ>0) 51. { 52. DH14[numDH]=getDHMatrix(neighborX-halfChildEdgeLength,neighborZ+halfChildEdgeLength); 53. numDH++; 54. DH14[numDH]=getDHMatrix(neighborX+halfChildEdgeLength,neighborZ+halfChildEdgeLength); 55. numDH++; 56. } 57. neighborX=x+edgeLength; 58. neighborZ=z; 59. if(neighborX<map_size) 60. { 61. DH14[numDH]=getDHMatrix(neighborX-halfChildEdgeLength,neighborZ-halfChildEdgeLength); 62. numDH++; 63. DH14[numDH]=getDHMatrix(neighborX-halfChildEdgeLength,neighborZ+halfChildEdgeLength); 64. numDH++; 65. } 66. neighborX=x; 67. neighborZ=z+edgeLength; 68. if(neighborZ<map_size) 69. { 70. DH14[numDH]=getDHMatrix(neighborX-halfChildEdgeLength,neighborZ-halfChildEdgeLength); 71. numDH++; 72. DH14[numDH]=getDHMatrix(neighborX+halfChildEdgeLength,neighborZ-halfChildEdgeLength); 73. numDH++; 74. } 75. DH14[numDH]=abs(((getRawHeight(x-halfEdgeLength,z+halfEdgeLength)+getRawHeight(x+halfEdgeLength,z+halfEdgeLength))>>1)-getRawHeight(x,z+halfEdgeLength)); 76. numDH++; 77. DH14[numDH]=abs(((getRawHeight(x+halfEdgeLength,z+halfEdgeLength)+getRawHeight(x+halfEdgeLength,z-halfEdgeLength))>>1)-getRawHeight(x+halfEdgeLength,z)); 78. numDH++; 79. DH14[numDH]=abs(((getRawHeight(x-halfEdgeLength,z-halfEdgeLength)+getRawHeight(x+halfEdgeLength,z-halfEdgeLength))>>1)-getRawHeight(x,z-halfEdgeLength)); 80. numDH++; 81. DH14[numDH]=abs(((getRawHeight(x-halfEdgeLength,z+halfEdgeLength)+getRawHeight(x-halfEdgeLength,z-halfEdgeLength))>>1)-getRawHeight(x-halfEdgeLength,z)); 82. numDH++; 83. DH14[numDH]=abs(((getRawHeight(x-halfEdgeLength,z-halfEdgeLength)+getRawHeight(x+halfEdgeLength,z+halfEdgeLength))>>1)-getRawHeight(x,z)); 84. numDH++; 85. DH14[numDH]=abs(((getRawHeight(x+halfEdgeLength,z-halfEdgeLength)+getRawHeight(x-halfEdgeLength,z+halfEdgeLength))>>1)-getRawHeight(x,z)); 86. numDH++; 87. int DHMax=DH14[0]; 88. for(int i=1;i<14;i++) 89. { 90. if(DHMax<DH14[i]) 91. DHMax=DH14[i]; 92. } 93. setDHMatrix(x,z,DHMax); 94. 95. 96. } 97. } 98. edgeLength=edgeLength<<1; 99. 100. } 101.}
生成地形網格的代碼以下:
01.void GLLod::updateQuadTreeNode(int centerX, int centerZ,int edgeLength,int child) 02.{ 03. 04. if(edgeLength>2) 05. { 06. 07. if(isObjectCulled(centerX,centerZ,edgeLength)) 08. { 09. 10. quadNode[centerX+centerZ*(map_size+1)].blend=2; 11. 12. } 13. else 14. { 15. 16. 17. 18. float fViewDistance,f; 19. int halfChildEdgeLength; 20. int childEdgeLength; 21. int blend; 22. int centerQuad[3]; 23. centerQuad[0]=centerX; 24. centerQuad[2]=centerZ; 25. centerQuad[1]=getRawHeight(centerX,centerZ); 26. fViewDistance=frustum.distanceOfTwoPoints(centerQuad); 27. 28. f=fViewDistance/(edgeLength*mfMinResolution*(max(mfDetailLevel*getDHMatrix(centerX,centerZ),1.0f))); 29. 30. if(f<1.0f) 31. blend=1; 32. else 33. blend=0; 34. 35. int temp=centerX+centerZ*(map_size+1); 36. quadNode[temp].blend=blend; 37. quadNode[temp].centerX=centerX; 38. quadNode[temp].centerY=centerQuad[1]; 39. quadNode[temp].centerZ=centerZ; 40. quadNode[temp].child=child; 41. quadNode[temp].edgeLength=edgeLength; 42. if(blend==1) 43. { 44. int halfChildEdgeLength=edgeLength>>2; 45. int childEdgeLength=edgeLength>>1; 46. 47. updateQuadTreeNode(centerX-halfChildEdgeLength,centerZ-halfChildEdgeLength,childEdgeLength,1); 48. updateQuadTreeNode(centerX+halfChildEdgeLength,centerZ-halfChildEdgeLength,childEdgeLength,2); 49. updateQuadTreeNode(centerX-halfChildEdgeLength,centerZ+halfChildEdgeLength,childEdgeLength,3); 50. updateQuadTreeNode(centerX+halfChildEdgeLength,centerZ+halfChildEdgeLength,childEdgeLength,4); 51. } 52. 53. } 54. } 55.}
如今具體渲染的時候如何選擇頂點還沒講,如今已經很晚了,之後再寫吧。。
郵箱:microland@126.com歡迎指出文中的錯誤之處。