大致流程.
這里簡單介紹下,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 };
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);
}

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中如何來使用着色器的一些流程.
