zoukankan      html  css  js  c++  java
  • 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.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是值类型.

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

  • 相关阅读:
    区别@ControllerAdvice 和@RestControllerAdvice
    Cannot determine embedded database driver class for database type NONE
    使用HttpClient 发送 GET、POST、PUT、Delete请求及文件上传
    Markdown语法笔记
    Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required
    Mysql 查看连接数,状态 最大并发数(赞)
    OncePerRequestFilter的作用
    java连接MySql数据库 zeroDateTimeBehavior
    Intellij IDEA 安装lombok及使用详解
    ps -ef |grep xxx 输出的具体含义
  • 原文地址:https://www.cnblogs.com/zhouxin/p/3949659.html
Copyright © 2011-2022 走看看