Axiom3D:Ogre地形組件代碼解析


大致流程.

  這里簡單介紹下,Axiom中采用的Ogre的地形組件的一些概念與如何生成地形. 

  先說下大致流程,然后大家再往下看.(只說如何生成地形與LOD,除高度紋理圖外別的紋理暫時不管.)  

  1.生成TerrainGroup,增加Request與Response處理,設置大小,高度圖.

        比較重要的屬性是DefaultImportSettings(ImportData),包含地形的大小,分塊最大與最小值, 

  2.TerrainGroup生成與設置各地形Terrain塊的大小,高度圖,ImportData. 

  3.TerrainGroup調用LoadAllTerrains,各地形放入Request隊列. 

  4.TerrainGroup的Request事件調用Terrain的Prepare. 

  5.Terrain的Prepare獲取高度紋理里的數據,生成TerrainQuadTreeNode類型的QuadTree,在生成時,會根據ImportData里的數據來生成子節點(TerrainQuadTreeNode)與對應子節點的LodLevels,Movable,Rend.(比較重要,下面詳細說) 

  6.Terrain的Prepare調用CalculateHeightDeltas生成QuadTree與子節點里LodLevel的高度差. 

  7.Terrain的Prepare調用FinalizeHeightDeltas生成QuadTree與子節點里AABB包圍盒子. 

  8.Terrain的Prepare調用DistributeVertexData,這個方法主要生成子節點的數據頂點信息mVertexDataRecord,只包含位置,不包含索引.注意mVertexDataRecord會由多個樹層次共用(比較重要,下面詳細說).在這里Prepare調用就完成. 

  9.返回到TerrainGroup的Response事件發生,主要調用對應的Terrain的Load方法. 

  10.Terrain里的Load主要是對QuadTree.Load調用. 

  11.QuadTree的Load調用,主要是針對子節點的TerrainQuadTreeNode的LodLevel生成頂點索引,對應Terrain->Prepare->DistributeVertexData生成的mVertexDataRecord生成的頂點.(比較重要,下面詳細說) 

  12.渲染時LOD動態計算,Camera調用RenderScene,引發對應Terrain里的CalculateCurrentLod計算QuadTree以及對應各子節點的Lodlevel.對應第11步里的QuadTree的Load生成的各LodLevel. 

TerrainQuadTreeNode生成子塊.

  在第五步中,Terrain有三個屬性(TerrainGroup里的DefaultImportSettings設置)來決定如何分區分塊,分別Size,MaxBatchSize,MinBatchSize.意思分別是這個Terrain的大小,Terrain里的單個塊里的最大值與最小值,注意這三個值是同一量程,並且他們的長度都滿足 2^n+1,我們都可以看做是地形的虛擬單位,他的實際大小是worldSize,比如worldSize是10240,而Size是257,那么Size,MaxBatchSize,MinBatchSize里的一個單位1可以看做是實際長度40.而MaxBatchSize與Size影響Terrain的如何分塊,而MaxBatchSize與MinBatchSize影響Terrain精度最高的塊的再細分.綜合MinBatchSize與size就是地形的LOD層數.如下公式

       LODlevels = log2(size - 1) - log2(minBatch - 1) + 1

       TreeDepth = log2(size - 1) - log2(maxBatch - 1) + 1 

  Terrain在創建時,會生成一個TerrainQuadTreeNode節點,TerrainQuadTreeNode根據地形的Size,MaxBatchSize,MinBatchSize生成四叉樹,這里我們先假設Size等於257,MaxBatchSize等33,而MinBatchSize等於17,根據公式LODlevels=5,TreeDepth=4這個TerrainQuadTreeNode的Size就是Terrain的Size,然后生成四個子節點,想象一下把一個正方形中間橫豎各一條線分隔成四個正方形.TerrainQuadTreeNode生成的子節點也是這個過程,那么子節點的邊長為父節點邊長的1/2,也就是(257-1)/2+1=129.這個過程會遞歸下去,一直到MaxBatchSize的長度,也就是257,129,65,33一共是四層(這四層的頂點密度都是MinBatchSize),也就是上面TreeDepth的結果,而到1/8*257=33(下圖中的8*17,LOD1)后,TerrainQuadTreeNode不會生成子節點了,而是生成log2(maxBatch - 1)-log2(minBatch - 1)+1=2個LodLevel(LOD0,LOD1),請看下圖.(節點生成的順序是LOD4-LOD0,不要搞反了.)

  其中Lod越高,Terr depth越低,地度的精度越低.我們可以看到深度對應着地形的分塊大小,代碼同TerrainQuadTreeNode生成子節點的過程,一共四層.而在LOD1后,沒有生成子節點,而是再生成一個LodLevel,其中BatchSize為33(對應面積每邊取點),之前每個TerrainQuadTreeNode對應的BatchSize都為17.而MaxBatchSize與MinBatchSize是針對圖上的每1/256的Terrain的細分.TreeDepth加上最后細分的LodLevel就等於LODlevels.總結如下:根據Size與MaxBatchSize得到最小塊 (256/32)^2 =8*8,8對應四層,分別是8*8,4*4,2*2,1*1.而4*4,2*2,1*1這三塊的精度都為MinBatchSize就17,而8*8精度從MaxBatchSize到MinBatchSize. 

地形頂點計算.

  那么我們如何根據上面的LOD來給出對應的頂點與索引數據.這個在Terrain的方法DistributeVertexData里的注釋詳細講解了如何根據Terrain的Size,MaxBatchSize,MinBatchSize來創建頂點元素,在這里設Size為2049,MaxBatchSize為65,MinBatchSize為33. 

  因為要支持16位的索引,故最大為2^8*2^8也就是256*256的值,考慮這個值太大,TERRAIN_MAX_BATCH_SIZE為128+1.就是說支持最大的正方形每邊的頂點為129個,總索引為129*129.根據前面一節我們來分成如下段:

            LODlevels = log2(2049 - 1) - log2(33 - 1) + 1 = 11 - 5 + 1 = 7
            TreeDepth = log2((2049 - 1) / (65 - 1)) + 1 = 6
            Number of vertex data splits at most detailed level:
            (size - 1) / (TERRAIN_MAX_BATCH_SIZE - 1) = 2048 / 128 = 16    

  四叉對節點深度:每邊頂點數/(每邊塊數*每邊頂點)地形總塊數/LOD/頂點索引/數據內存划分:[每邊頂點*每邊塊數](總塊數=每邊塊平方)

  • tree depth 0: 33   vertices, 1  x 33(總1塊) vertex tiles (LOD 6) vdata 18    [33](總一塊) 
  • tree depth 1: 65   vertices, 2  x 33(總4塊) vertex tiles (LOD 5) vdata 16-17 [2x129](總4塊)
  • tree depth 2: 129  vertices, 4  x 33(總16塊) vertex tiles (LOD 4) vdata 16-17 [2x129](總4塊)
  • tree depth 3: 257  vertices, 8  x 33(總64塊) vertex tiles (LOD 3) vdata 16-17 [2x129](總4塊)
  • tree depth 4: 513  vertices, 16 x 33(總256塊) vertex tiles (LOD 2) vdata 0-15  [16x129](總256塊)
  • tree depth 5: 1025 vertices, 32 x 33(總1024塊) vertex tiles (LOD 1) vdata 0-15  [16x129](總256塊)
  • tree depth 5: 2049 vertices, 32 x 65(總1024塊) vertex tiles (LOD 0) vdata 0-15  [16x129](總256塊)

  這個和前面差不多,唯一就是最后多出來的129*16,129*2,33這三段,前面的LODLevels可以說是數據索引分區,而這129*16,129*2,33是數據分區.直接來看,32*65,32*33,16*33如何分進129*16這個塊,而為什么8*33以及如下又不能分進這塊.我們看到的32*65,12*33,129*16都是正方形一邊的點數,而129是每小塊最大點數(TERRAIN_MAX_BATCH_SIZE),那么邊為2049點的正方形在每塊最大占129點的情況下,可以分為16*16個129點的小正方形塊.在LOD 0 32*(65點)的情況下,就是一個16*16*(129點)的塊分成四個32*(65點)的塊.而LOD1和LOD0一樣也是一個16*16*129的塊分成了四個,但是每塊的點數只有33點,就是四個32*(33點)的塊.而LOD2的一塊也是16*16*(33點),也就是說,LOD2的一塊大小和16*16*(129點)大小一樣,只是原始的每塊每邊有129個基點,現在LOD2的每塊只有33個頂點,但是他們的大小是一樣.那么在LOD3開始,他是分成8*8*(33點),這一塊有16*16*(129點)四個大,這樣LOD3里的索引匹配不到16*16*(129點)里的數據,所以重新開始分割,然后LOD3,4,5和前面的分塊一樣,都能匹配到2*2*(129)點塊上.最后一塊是直接分成33*33,這里對應注釋告訴我們有二個選擇,為了減少渲染次數而分成33*33.而不是分成四塊17*17. 

  大家可能要說,為什么不為每LOD直接生成對應每層數據.這樣完全沒必要,因為就和上面分析一樣,LOD0,1,2完全可以共用頂點,只需要選擇好相應索引就能正確的渲染,而每層生成頂點數據,直接造成內存緊張.這樣分三層的第一層是2049*2049=4198401,第二層是513*513=263169,第三層是33*33=1089.第二層和第三層只占第一層的3%左右,在合適的情況下,可以只要第二層的數據,大大縮減內存使用. 

地形頂點索引計算.

  如上數據,在Terrain的方法DistributeVertexData里,分別是LOD2,LOD5,LOD6.在這三層分別會進入地形的根TerrainQuadTreeNode里的方法AssignVertexData.在LOD2時,根節點從tree dapth 0找到tree dapth 4,可以找到256個TerrainQuadTreeNode,在這每個TerrainQuadTreeNode調用CreateCPUVertexData首先生成數據頂點屬性所需的空間.然后調用UpdateVertexBuffer生成最終的頂點數據(里面有代碼用skiter skill來填LOD邊的點.),LOD0,LOD1會經UseAncsetorVertexData得到父節點的VertexDataRecord,就是LOD2的一塊對應LOD1的四塊,LOD0比較特殊,按前面所說,和LOD1是共用對應的TerrainQuadTreeNode的.同樣,會在LOD5再次進入AssignVertexData,給對應4塊地圖分配VertexDataRecord,而LOD4,LOD3會被分到LOD5的VertexDataRecord,LOD5的一塊對應LOD4的四塊,LOD3的16塊.最后LOD6再分一次,就一塊33*33.     

  TerrainQuadTreeNode的Load調用PopulateIndexData來生成對應LOD的頂點索引,這個過程主要交給對應Terrain里的GpuBufferAllocator,通過調用GetSharedIndexBuffer來生成IndexBuffer空間,而IndexBuffer里的數據又會回到Terrain里的PopulateIndexBuffer生成.在這里對PopulateIndexBuffer方法需要的一些參數說明下,我們以LOD5來說明,batchSize是33,指的是LOD5每塊每邊分到的頂點是33.vdatasize是指LOD5對應每邊分給VertexDataRecord的頂點,去掉LOD6,余下的LOD應該全是129.在各LOD索引頂點生成后,然后就是對Layel層的處理,這個先不細說.這樣整個Load過程就相當於完成了. 

  最后第十二步渲染時,如何根據攝像機的位置選擇正確的LOD渲染,大致由Camera調用RenderScene,到Terrain里的CalculateCurrentLod,這里面會對上面的各個TerrainQuadTreeNode里的LodLevel計算正確的值,不過Axiom里這個方法BUG有二處,大家有興趣可以先不看我下面的修改,自己來動手修改下.      

修改的源碼部分.

  為了運行這個例子,我修改了一些地方.源代碼在Terrain的size超過512后幾乎不能運行,我想可能是Axiom這個項目關注的人不多,並且這只是一個組件功能,不影響主要的功能有關.我修改的地方總結一下有: 

  1.Axiom\Core\DisposableObject.cs 255行屏掉,因為再把對應高度圖片里的數據轉換成對應的高度值時,在Terrain類里Prepare里調用PixelConverter.BulkPixelConversion時花費大量時間。 

  2.Axiom\Media\DDSCodec.cs796-799,檢查DDSHeader的數據時,因為DDSHeader不能用GCHandle.Alloc來生成空間,因為包含非基元成員。 

  3.Terrain里的DistributeVertexData並沒有正確按注釋所分,需要調整currentresolution = (ushort)(((currentresolution - 1) >> 1) + 1);這句在if(splits == targetsplits)之前才會如注釋所說生成結果.改不改不影響正常渲染,只會影響地形的數據層級.(Ogre也是這樣,所以此處只是提出來,應該不用修改.) 

  4.GpuBufferAllocator里的HashIndexBuffer,這句會造成相當多的重復值,完全不能用.地圖小點還好,如設129,可以看到有一些塊是正常的,有些就黑塊不斷閃爍,如設大些如2049,就會發現有一些塊沒顯示出來(Ogre是用的boost::hash_combine,C#里替代方法很多,下面是修改的源碼.)如下簡單的一種,用Tuple替換,記注Tuple是值類型.

  1     public class DefaultGpuBufferAllocator : GpuBufferAllocator
  2     {
  3         protected List<HardwareVertexBuffer> FreePosBufList = new List<HardwareVertexBuffer>();
  4         protected List<HardwareVertexBuffer> FreeDeltaBufList = new List<HardwareVertexBuffer>();
  5         //protected Dictionary<int, HardwareIndexBuffer> SharedIBufMap = new Dictionary<int, HardwareIndexBuffer>();
  6         protected Dictionary<Tuple<ushort, ushort, int, ushort, ushort, ushort, ushort>, HardwareIndexBuffer> SharedIBufMap = new Dictionary<Tuple<ushort, ushort, int, ushort, ushort, ushort, ushort>, HardwareIndexBuffer>();
  7         [OgreVersion(1, 7, 2)]
  8         public override void AllocateVertexBuffers(Terrain forTerrain, int numVertices, out HardwareVertexBuffer destPos,
  9                                                     out HardwareVertexBuffer destDelta)
 10         {
 11             //destPos = this.GetVertexBuffer( ref FreePosBufList, forTerrain.PositionBufVertexSize, numVertices );
 12             //destDelta = this.GetVertexBuffer( ref FreeDeltaBufList, forTerrain.DeltaBufVertexSize, numVertices );
 13 
 14             destPos = GetVertexBuffer(ref this.FreePosBufList, forTerrain.PositionVertexDecl, numVertices);
 15             destDelta = GetVertexBuffer(ref this.FreeDeltaBufList, forTerrain.DeltaVertexDecl, numVertices);
 16         }
 17 
 18         [OgreVersion(1, 7, 2)]
 19         public override void FreeVertexBuffers(HardwareVertexBuffer posbuf, HardwareVertexBuffer deltabuf)
 20         {
 21             this.FreePosBufList.Add(posbuf);
 22             this.FreeDeltaBufList.Add(deltabuf);
 23         }
 24 
 25         [OgreVersion(1, 7, 2)]
 26         public override HardwareIndexBuffer GetSharedIndexBuffer(ushort batchSize, ushort vdatasize, int vertexIncrement,
 27                                                                   ushort xoffset, ushort yoffset, ushort numSkirtRowsCols,
 28                                                                   ushort skirtRowColSkip)
 29         {
 30             //int hsh = HashIndexBuffer(batchSize, vdatasize, vertexIncrement, xoffset, yoffset, numSkirtRowsCols, skirtRowColSkip);
 31             var hsh = Tuple.Create(batchSize, vdatasize, vertexIncrement, xoffset, yoffset, numSkirtRowsCols, skirtRowColSkip);
 32             if (!this.SharedIBufMap.ContainsKey(hsh))
 33             {
 34                 // create new
 35                 int indexCount = Terrain.GetNumIndexesForBatchSize(batchSize);
 36                 HardwareIndexBuffer ret = HardwareBufferManager.Instance.CreateIndexBuffer(IndexType.Size16, indexCount,
 37                                                                                             BufferUsage.StaticWriteOnly);
 38                 var pI = ret.Lock(BufferLocking.Discard);
 39                 Terrain.PopulateIndexBuffer(pI, batchSize, vdatasize, vertexIncrement, xoffset, yoffset, numSkirtRowsCols,
 40                                              skirtRowColSkip);
 41                 ret.Unlock();
 42 
 43                 this.SharedIBufMap.Add(hsh, ret);
 44                 return ret;
 45             }
 46             else
 47             {
 48                 return this.SharedIBufMap[hsh];
 49             }
 50         }
 51 
 52         [OgreVersion(1, 7, 2)]
 53         public override void FreeAllBuffers()
 54         {
 55             this.FreePosBufList.Clear();
 56             this.FreeDeltaBufList.Clear();
 57             this.SharedIBufMap.Clear();
 58         }
 59 
 60         /// <summary>
 61         /// 'Warm start' the allocator based on needing x instances of 
 62         /// terrain with the given configuration.
 63         /// </summary>
 64         [OgreVersion(1, 7, 2)]
 65         public void WarmStart(int numInstances, ushort terrainSize, ushort maxBatchSize, ushort minBatchSize)
 66         {
 67             // TODO
 68         }
 69 
 70         [OgreVersion(1, 7, 2, "~DefaultGpuBufferAllocator")]
 71         protected override void dispose(bool disposeManagedResources)
 72         {
 73             if (!IsDisposed)
 74             {
 75                 if (disposeManagedResources)
 76                 {
 77                     FreeAllBuffers();
 78                 }
 79             }
 80 
 81             base.dispose(disposeManagedResources);
 82         }
 83 
 84         [OgreVersion(1, 7, 2)]
 85         protected int HashIndexBuffer(ushort batchSize, ushort vdatasize, int vertexIncrement, ushort xoffset, ushort yoffset,
 86                                        ushort numSkirtRowsCols, ushort skirtRowColSkip)
 87         {
 88             int ret = batchSize.GetHashCode();
 89             ret ^= vdatasize.GetHashCode();
 90             ret ^= vertexIncrement.GetHashCode();
 91             ret ^= xoffset.GetHashCode();
 92             ret ^= yoffset.GetHashCode();
 93             ret ^= numSkirtRowsCols.GetHashCode();
 94             ret ^= skirtRowColSkip.GetHashCode();
 95             return ret;
 96         }
 97 
 98         [OgreVersion(1, 7, 2)]
 99         //protected HardwareVertexBuffer GetVertexBuffer( ref List<HardwareVertexBuffer> list, int vertexSize, int numVertices )
100         protected HardwareVertexBuffer GetVertexBuffer(ref List<HardwareVertexBuffer> list, VertexDeclaration decl,
101                                                         int numVertices)
102         {
103             int sz = decl.GetVertexSize() * numVertices; // vertexSize* numVertices;
104             foreach (var i in list)
105             {
106                 if (i.Size == sz)
107                 {
108                     HardwareVertexBuffer ret = i;
109                     list.Remove(i);
110                     return ret;
111                 }
112             }
113 
114             // Didn't find one?
115             return HardwareBufferManager.Instance.CreateVertexBuffer(decl, numVertices, BufferUsage.StaticWriteOnly);
116 
117             //TODO It should looks like this
118             //return HardwareBufferManager.Instance.CreateVertexBuffer( vertexSize, numVertices, BufferUsage.StaticWriteOnly );
119         }
120     };
View Code

  5.地形的LOD沒跟攝像機變化,經調試查找主要是Root.cs里的NextFrameNumber,這個值一直為0,導致Terrain里的_preFindVisialeObjects里不會執行CalculateCurrentLod,也就是所有地形的LOD值是你一進去就定好了,后面你移動位置不會改變LOD值,改變方法,對比Ogre,NextFrameNumber應該是CurrentFrameCount,這個值在每楨時會加1,而NextFrameNumber都沒被賦值過,故各個LOD一直是初始的值. 

  6.當地形的wordsize與地形圖片的長寬不同時,對應圖片的縮放方法resize有問題,這個先不管,我把地形的wordsize與圖片長寬都調整為2049,然后渲染的窗口是一團亂,因為TerrainQuadTreeNode里的UpdateVertexBuffer里計算高度時,相關高度的偏移沒有正常計算,修改如下三處后面加上*sizeof(float).

     pBaseHeight += rowskip * sizeof(float);

     pBaseDelta += rowskip * sizeof(float);

     pBaseHeight += this.mTerrain.Size * skirtSpacing * sizeof(float);

     然后能看到生成的正確高度.此處可參見 Axiom3D:Buffer漫談

  7.還是LOD的計算,我們可以發現,改變以上幾點后,地形的LOD顯示有些問題,比如我腳下的這塊LOD有時還比不上前面一塊LOD的精度,經過調試查找發現主要是因為LodLevel是Struct類型,故如下:

     LodLevel tmp = this.mLodLevels[0];
     tmp.CalcMaxHeightDelta = System.Math.Max(tmp.CalcMaxHeightDelta, maxChildDelta * (Real)1.05);
     this.mChildWithMaxHeightDelta = childWithMaxHeightDelta;
  這段代碼並不能改變 this.mLodLevels[0]里的值(這處代碼我查找了下,共有差不多十處左右),有二種方法,一是在如上十處改變對應LodLevel 后,調用this.mLodLevels[0] = tmp,注意下CalculateCurrentLod這個里面有段foreach this.mLodLevels,這里的foreach首先要改變成for,在C#中foreach中不能對值類型賦值.二是直接把LodLevel 由Struch改為class. 

  8.上面的全部修改后,會發現一排的邊有些高度對應不上,就是有突起,我們修改如下代碼:

        public Real GetSquaredViewDepth(Camera cam)
        {
            if (this.mRend.Technique != null && this.mRend.Technique.PassCount > 0)
                this.mRend.Technique.GetPass(0).PolygonMode = cam.PolygonMode;
            return this.mMovable.ParentSceneNode.GetSquaredViewDepth(cam);
        }

  使地形的PolygonMode和攝像機的一起變,查看Wireframe模式下,發現是裙子節點不同,查看TerrainQuadTreeNode里的UpdateVertexBuffer里的x-y裙子點,這個位置取高度值不同前面y-x下指針移位,是直接定位索引,根進發現this.mTerrain.GetHeightAtPoint(x, y)里如下代碼: 

     public float GetHeightAtPoint(long x, long y)

        {
            //clamp
            x = Utility.Min(x, (long)this.mSize - 1L);
            x = Utility.Max(x, 0L);
            y = Utility.Min(y, (long)this.mSize - 1L);
            y = Utility.Max(y, 0L);
            return this.mHeightData[y + this.mSize * x];
        }

  this.mHeightData[y + this.mSize * x]修改成this.mHeightData[x + this.mSize * y].

  同樣的,在Terrain里的GetPointAlign也同樣這樣修改. 

效果圖

  上面這些修改后,我們就可以看到比較完美的顯示效果了.下面是效果圖.

     

  PS:非常感謝Axiom里的這些還沒修正的BUG,因為這些BUG,我想了更多,得到了更多.后面如果有時間,我會針對地形組件的TerrainMaterialGeneratorA來說明如何生成material及對應的着色器方法,以及在Axiom中如何來使用着色器的一些流程.


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM