Axiom3D:Ogre地形代码解析

Axiom3D:Ogre地形代码解析

作者: 天天不在  发布日期:2014-09-01 20:52:05
Tag标签: 地形   代码  
  • 大致流程.

     这里简单介绍下,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的结果,而到33后,TerrainQuadTreeNode不会生成子节点了,而是生成log2(maxBatch - 1)-log2(maxBatch - 1)=1个LodLevel.请看下图.(节点生成的顺序是LOD4-LOD0,不要搞反了.)

    data-cke-saved-src=http://www.it165.net/uploadfile/files/2014/0901/2014090119281418.png

      其中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 0: 2049 vertices, 32 x 65(总1024块) vertex tiles (tree depth 5) vdata 0-15  [129x16](总256块)

                LOD 1: 1025 vertices, 32 x 33(总1024块) vertex tiles (tree depth 5) vdata 0-15  [129x16](总256块)

                LOD 2: 513  vertices, 16 x 33(总256块) vertex tiles (tree depth 4) vdata 0-15  [129x16](总256块)

                LOD 3: 257  vertices, 8  x 33(总64块) vertex tiles (tree depth 3) vdata 16-17 [129x2](总4块)

                LOD 4: 129  vertices, 4  x 33(总16块) vertex tiles (tree depth 2) vdata 16-17 [129x2](总4块)

                LOD 5: 65   vertices, 2  x 33(总4块) vertex tiles (tree depth 1) vdata 16-17 [129x2](总4块)

                LOD 6: 33   vertices, 1  x 33(总1块) vertex tiles (tree depth 0) vdata 18    [33](总一块) 

      这个和前面差不多,唯一就是最后多出来的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有二处,大家有兴趣可以先不看我下面的修改,自己来动手修改下.      

    修改的源码部分.

      为了运行这个例子,源代码几乎不有运行,我想可能是Axiom这个项目关注的人不多,并且这只是一个组件功能,不影响主要的功能有关.我修改的地方总结一下有: 

      1.AxiomCoreDisposableObject.cs 255行屏掉,因为再把对应高度图片里的数据转换成对应的高度值时,在Terrain类里Prepare里调用PixelConverter.BulkPixelConversion时花费大量时间。 

      2.AxiomMediaDDSCodec.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是值类型.

    001. 1     public class DefaultGpuBufferAllocator : GpuBufferAllocator
    002. 2     {
    003. 3         protected List<HardwareVertexBuffer> FreePosBufList = new List<HardwareVertexBuffer>();
    004. 4         protected List<HardwareVertexBuffer> FreeDeltaBufList = new List<HardwareVertexBuffer>();
    005. 5         //protected Dictionary<int, HardwareIndexBuffer> SharedIBufMap = new Dictionary<int, HardwareIndexBuffer>();
    006. 6         protected Dictionary<Tuple<ushort, ushort, int, ushort, ushort, ushort, ushort>, HardwareIndexBuffer> SharedIBufMap = new Dictionary<Tuple<ushort, ushort, int, ushort, ushort, ushort, ushort>, HardwareIndexBuffer>();
    007. 7         [OgreVersion(172)]
    008. 8         public override void AllocateVertexBuffers(Terrain forTerrain, int numVertices, out HardwareVertexBuffer destPos,
    009. 9                                                     out HardwareVertexBuffer destDelta)
    010. 10         {
    011. 11             //destPos = this.GetVertexBuffer( ref FreePosBufList, forTerrain.PositionBufVertexSize, numVertices );
    012. 12             //destDelta = this.GetVertexBuffer( ref FreeDeltaBufList, forTerrain.DeltaBufVertexSize, numVertices );
    013. 13
    014. 14             destPos = GetVertexBuffer(ref this.FreePosBufList, forTerrain.PositionVertexDecl, numVertices);
    015. 15             destDelta = GetVertexBuffer(ref this.FreeDeltaBufList, forTerrain.DeltaVertexDecl, numVertices);
    016. 16         }
    017. 17
    018. 18         [OgreVersion(172)]
    019. 19         public override void FreeVertexBuffers(HardwareVertexBuffer posbuf, HardwareVertexBuffer deltabuf)
    020. 20         {
    021. 21             this.FreePosBufList.Add(posbuf);
    022. 22             this.FreeDeltaBufList.Add(deltabuf);
    023. 23         }
    024. 24
    025. 25         [OgreVersion(172)]
    026. 26         public override HardwareIndexBuffer GetSharedIndexBuffer(ushort batchSize, ushort vdatasize, int vertexIncrement,
    027. 27                                                                   ushort xoffset, ushort yoffset, ushort numSkirtRowsCols,
    028. 28                                                                   ushort skirtRowColSkip)
    029. 29         {
    030. 30             //int hsh = HashIndexBuffer(batchSize, vdatasize, vertexIncrement, xoffset, yoffset, numSkirtRowsCols, skirtRowColSkip);
    031. 31             var hsh = Tuple.Create(batchSize, vdatasize, vertexIncrement, xoffset, yoffset, numSkirtRowsCols, skirtRowColSkip);
    032. 32             if (!this.SharedIBufMap.ContainsKey(hsh))
    033. 33             {
    034. 34                 // create new
    035. 35                 int indexCount = Terrain.GetNumIndexesForBatchSize(batchSize);
    036. 36                 HardwareIndexBuffer ret = HardwareBufferManager.Instance.CreateIndexBuffer(IndexType.Size16, indexCount,
    037. 37                                                                                             BufferUsage.StaticWriteOnly);
    038. 38                 var pI = ret.Lock(BufferLocking.Discard);
    039. 39                 Terrain.PopulateIndexBuffer(pI, batchSize, vdatasize, vertexIncrement, xoffset, yoffset, numSkirtRowsCols,
    040. 40                                              skirtRowColSkip);
    041. 41                 ret.Unlock();
    042. 42
    043. 43                 this.SharedIBufMap.Add(hsh, ret);
    044. 44                 return ret;
    045. 45             }
    046. 46             else
    047. 47             {
    048. 48                 return this.SharedIBufMap[hsh];
    049. 49             }
    050. 50         }
    051. 51
    052. 52         [OgreVersion(172)]
    053. 53         public override void FreeAllBuffers()
    054. 54         {
    055. 55             this.FreePosBufList.Clear();
    056. 56             this.FreeDeltaBufList.Clear();
    057. 57             this.SharedIBufMap.Clear();
    058. 58         }
    059. 59
    060. 60         /// <summary>
    061. 61         /// 'Warm start' the allocator based on needing x instances of
    062. 62         /// terrain with the given configuration.
    063. 63         /// </summary>
    064. 64         [OgreVersion(172)]
    065. 65         public void WarmStart(int numInstances, ushort terrainSize, ushort maxBatchSize, ushort minBatchSize)
    066. 66         {
    067. 67             // TODO
    068. 68         }
    069. 69
    070. 70         [OgreVersion(172'~DefaultGpuBufferAllocator')]
    071. 71         protected override void dispose(bool disposeManagedResources)
    072. 72         {
    073. 73             if (!IsDisposed)
    074. 74             {
    075. 75                 if (disposeManagedResources)
    076. 76                 {
    077. 77                     FreeAllBuffers();
    078. 78                 }
    079. 79             }
    080. 80
    081. 81             base.dispose(disposeManagedResources);
    082. 82         }
    083. 83
    084. 84         [OgreVersion(172)]
    085. 85         protected int HashIndexBuffer(ushort batchSize, ushort vdatasize, int vertexIncrement, ushort xoffset, ushort yoffset,
    086. 86                                        ushort numSkirtRowsCols, ushort skirtRowColSkip)
    087. 87         {
    088. 88             int ret = batchSize.GetHashCode();
    089. 89             ret ^= vdatasize.GetHashCode();
    090. 90             ret ^= vertexIncrement.GetHashCode();
    091. 91             ret ^= xoffset.GetHashCode();
    092. 92             ret ^= yoffset.GetHashCode();
    093. 93             ret ^= numSkirtRowsCols.GetHashCode();
    094. 94             ret ^= skirtRowColSkip.GetHashCode();
    095. 95             return ret;
    096. 96         }
    097. 97
    098. 98         [OgreVersion(172)]
    099. 99         //protected HardwareVertexBuffer GetVertexBuffer( ref List<HardwareVertexBuffer> list, int vertexSize, int numVertices )
    100. 100         protected HardwareVertexBuffer GetVertexBuffer(ref List<HardwareVertexBuffer> list, VertexDeclaration decl,
    101. 101                                                         int numVertices)
    102. 102         {
    103. 103             int sz = decl.GetVertexSize() * numVertices; // vertexSize* numVertices;
    104. 104             foreach (var i in list)
    105. 105             {
    106. 106                 if (i.Size == sz)
    107. 107                 {
    108. 108                     HardwareVertexBuffer ret = i;
    109. 109                     list.Remove(i);
    110. 110                     return ret;
    111. 111                 }
    112. 112             }
    113. 113
    114. 114             // Didn't find one?
    115. 115             return HardwareBufferManager.Instance.CreateVertexBuffer(decl, numVertices, BufferUsage.StaticWriteOnly);
    116. 116
    117. 117             //TODO It should looks like this
    118. 118             //return HardwareBufferManager.Instance.CreateVertexBuffer( vertexSize, numVertices, BufferUsage.StaticWriteOnly );
    119. 119         }
    120. 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);
            }

      使地形的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也同样这样修改. 

    效果图

      上面这些修改后,我们就可以看到比较完美的显示效果了.下面是效果图.

    data-cke-saved-src=http://www.it165.net/uploadfile/files/2014/0901/2014090119281421.png

    data-cke-saved-src=http://www.it165.net/uploadfile/files/2014/0901/2014090119281422.png

    data-cke-saved-src=http://www.it165.net/uploadfile/files/2014/0901/2014090119281636.png

    data-cke-saved-src=http://www.it165.net/uploadfile/files/2014/0901/2014090119281637.png     

      PS:非常感谢Axiom里的这些还没修正的BUG,因为这些BUG,我想了更多,得到了更多.后面如果有时间,我会针对地形组件的TerrainMaterialGeneratorA来说明如何生成material及对应的着色器方法,以及在Axiom中如何来使用着色器的一些流程.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值