天天看点

用Ogre实现无缝地图

用Ogre实现无缝地图

         1.7版本之前,如果想用Ogre内建的地形系统实现一个像样的无缝地图,恐怕要闹到抓狂。所幸sinbad在1.7为Ogre加入了全新的地形组件,它囊括了一个地形系统所需要的基本功能,并且具备扩展出丰富特性的能力,使开发者得以避免重复造轮子的麻烦。

         以《魔兽世界》为标杆,我们尝试使用Ogre的新地形系统,构建一个类似的无缝地图,它理论上支持无限扩展、支持挖洞、支持缤纷易变的地貌、采用分层纹理混合的方式进行渲染。

分页

         无缝地图的精髓在于分块,动态地加载视野所及的地形块,动态地卸载不再活跃的地形块,这是实现无限连续地形的基本原理。

         Ogre使用分页模块(OgrePaging)和地形模块(OgreTerrain)配合实现地形的动态加载。

         分页模块的类构架如下:

         PageManager

                  PagedWorld

                            PagedWorldSection

                                     Page        

                                               ContentCollection

                                                        Content

         PageManger管理整个分页系统,这一层保存着下层对象的工厂列表,同时实现了一些

实例化下层对象的方法。

         PagedWorld类用于声明一个分页的世界,扮演的角色相对较弱。

         PagedWorldSection按照字面意思是世界中一块区域,这一层是分页功能主体所在。本层通过当前的分页策略PageStrategy类对象,根据Page和相机的距离,调用loadPage(),驱动Page发送WorkQueueRequest以完成加载,或者调用unloadPage()完成Page卸载,这是分页的关键。

         下来的一层是受PagedWorldSection管理的Page,即分页系统中的基本单元——页,再下来是内容集类ContentCollection及其下的内容类Content,它们代表了一个页中包含的内容。

         配合分页模块,地形模块代码结构如下:

         TerrainPaging

                  PageManager

         TerrainPagedWorldSection

                   TerrainGroup

         可以看到,地形模块的类和分页模块的类结构并非一一对应。

         TerrainPaging类包含了一个PageManager的对象,仅此而已,可以将其视为等同于PageManager。PagedWorld类直接被略过。

         继承自PagedWorldSection 的TerrainPagedWorldSection类则比较关键,其中包含有一个TerrainGroup对象。在TerrainPagedWorldSection中,地形Terrain被视为和Page处于对等地位的块,这些地形被按照位置映射存储在TerrainGroup里,重载的loadPage()和unloadPage()在加载、卸载Page的同时,驱动TerrainGroup加载、卸载对应位置槽里的Terrain实例。

         Ogre一贯秉承“结构重于特性”的理念,所以代码读起来难免有些拗口。总得来说,Ogre借助Paging分页系统,管理一个TerrainGroup下的多个地形,完成了地形的分块加载。

       实际上,如果不需求分页动态加载的话,你还可以选择不使用Ogre的Paging系统,而使用自己的方法来管理TerrainGroup。

         地形上的洞要求地形的一部分镂空或不予显示,一般用来制作深入地形的洞穴或地堡。

         Ogre地形挖洞的问题在Ogre社区里有不少讨论,但似乎没有公认的靠谱办法。在开洞处使用透明贴图过于笨拙且浪费资源,而更改地形的顶点索引留出开洞部分也由于略显复杂而少有人问津。

         我们使用隐藏挂接于地形四叉树下的场景节点的方法。Ogre的地形使用四叉树管理LOD,整个地形被细分为一颗四叉树,不同深度的树节点代表LOD系统中不同的地形细节,可以想象根节点代表着LOD最粗糙的一级,往下类推。每个四叉树节点下包含一个场景节点,用于显示该节点对应的顶点列表。这些场景节点被挂接在地形实例所创建的场景根节点上,组成一棵深度为2的场景树。地形四叉树节点和场景节点的关系如下图所示。

用Ogre实现无缝地图

 Ogre中显示或隐藏一个场景节点是很方便的,我们可以找到开洞部分对应的场景节点,将其隐藏,从而达到地形挖洞的效果。

         Ogre地形使用Skirt技术解决LOD带来的T裂缝问题,即每个LOD地形块由四周往外延伸出一格并垂直折下,宛如为自己围上一条裙子,用来遮挡当相邻地形块处于不同LOD层级时可能会出现的裂缝,这样的地形块看起来就像一个没有底面的方形盒子。如果在地形上开洞,留意把SkirtSize设小一点,以免透过洞口看到这些盒子的侧边。

LOD结构

         有三个参数对Ogre地形的网格结架比较重要:terrainSize、minBatch及maxBatch。

terrainSize决定地形的顶点数目,minBatch决定四叉树非叶节点所包含的顶点数目,maxBatch决定四叉树叶节点所包含的顶点数目。

LOD的数目也并非等同于四叉树的层数,在非叶节点,一层四叉树对应一个LOD,顶点数目为minBatch;但是在叶节点,则包含了从minBatch到maxBatch的一套LOD列表。

         按照理论,Ogre地形LOD层数及四叉树深度计算公式如下:

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

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

值得一提的是,由于OgreTerrain的顶点采用16位索引,所以所能索引到最大顶点数目为TERRAIN_MAX_BATCH_SIZE,这样一来,不仅仅顶点索引会根据四叉树分层,顶点数据本身也会根据四叉树分块,理论上顶点数据被分为terrainSize / TERRAIN_MAX_BATCH_SIZE块,但由于上层LOD不能跨块进行索引,Ogre会再开辟一些新的块,所以总共用到的顶点数据会多于实际数据,另外层与层之间视情况还可能共享顶点,比较复杂。根据官方的解释,这种多建顶点的办法更易实现且数目不大,所以是可接受的;更重要的是在制作超大场景的时候,大量底层顶点缓存可以在低LOD时被释放,这在降低内存占用上很重要。TERRAIN_MAX_BATCH_SIZE这一特殊设定的存在,限定了地形的maxBatch不能大于TERRAIN_MAX_BATCH_SIZE,地形本身的size不能小于TERRAIN_MAX_BATCH_SIZE(terrainSize小于TERRAIN_MAX_BATCH_SIZE会导致只有Top节点有顶点数据)。TERRAIN_MAX_BATCH_SIZE = 129,在使用时需要谨记。

渲染

         魔兽世界渲染地形时使用最多4层地形贴图和一张混合贴图,地形贴图的Alpha通道用于高光贴图,混合贴图的Alpha通道用于阴影。

         尽管在资源压缩利用上可能没有魔兽这么严格,但这种时下标准的地形渲染方式在Ogre新地形系统中也都得到了较完善的支持。另外Ogre地形还内建有动态阴影、顶点色图、合成图生成、光照图生成等功能,不过如没有特别需求,可以尝试适当关闭其中几个,此举能在保持基本效果的同时,为最终生成的每个地形文件省去非常可观的大小。

定制

每个Terrain使用一个材质,为了保证地貌足够丰富,Terrain作为最基本的渲染单元,size不能太大,terrainSize取最小值129,worldSize取100/3;而在地形分块时,由于TerrainPaging以Terrain为单位进行分页,所以只能以Terrain同时作为动态加载单元,这和魔兽不同,魔兽相当于16x16个Terrain组成一个MapTile作为动态加载单元。

         地形上每一个洞需要隐藏一个TerrainQuadTreeNode叶节点来实现,minBatch决定了非叶节点的大小,而maxBatch决定了叶节点的大小,所以maxBatch必须不能很大,考虑到wow一个单元可以开4x4个洞,所以maxBatch取(terrainSize - 1)/4 + 1为33,minBatch可自行定夺,这里取5,这样得到的LOD层数为6,四叉树深度为3。

         这种定制方案看起来更加倚重Ogre地形的分页特性,而淡化了其LOD功能。

细节上,设置地形层混合图大小为64x64;

         mTerrainGlobals->setLayerBlendMapSize(64);

         每层贴图worldSize取8为佳;

         defaultimp.layerList[0].worldSize = 8;

         不在每层使用法线和高度贴图,所以省掉这张图,同时调用:

         matProfile->setLayerNormalMappingEnabled(false);

         matProfile->setLayerParallaxMappingEnabled(false);

         同时我们关闭了动态阴影、顶点色图、合成图、光照图:

         matProfile->setReceiveDynamicShadowsEnabled(false);

         matProfile->setGlobalColourMapEnabled(false);

         matProfile->setCompositeMapEnabled(false);

         matProfile->setLightmapEnabled(false);

         如果你使用LightMap,可能会发现得到了不跨越地形的光照图,即影子会在地形边界被截断,而不是延伸到相邻地形块。这一般是由于地形块计算光照图时没有考虑进相邻节点导致的,此时相继调用Terrain的dirty()、update()函数,手动让地形块重新计算一遍,即可解决问题。实际情况中,光照图需要扩展,烘焙的时候需要加入地面静态物体的影子计算。

Demo

工程源码及资源可以移步此处下载,开发环境为Ogre1.7.3+VS2008,部分资源取自《魔兽世界》。

         源码中默认注释掉了PAGING宏定义,此时地形不使用分页系统,即不会动态异步加载,所以程序启动时会创建所有地形,稍等片刻,这可能会多花一点时间。

         待地形加载、构建完毕,通过H键可以显示或隐藏地形上的洞。

用Ogre实现无缝地图

按下Ctrl+S,地形会在Media目录下保存为一个个形如Terrain_00000000.dat之类的文件,动态加载时使用的地形文件正是这些东西。

         接下来打开PAGING宏定义,重新编译,此时地形会在分页系统的管理下被动态加载,漫游场景时你会发现新入视野中的地形被动态加载,淡出视野的块则被动态删除,场景中添加了天空盒和雾,后者能较好地缓和远处地形的突兀变化。

用Ogre实现无缝地图

在分页模式下地形挖洞的效果没有实现,这是因为设置挖洞需要等到地形四叉树及场景树构建完毕之后,如果不改写Ogre的源码,很难在异步加载的情况下定位到这些时间点,Demo仅仅演示了一种思路,而实现这样的功能无疑需要在Ogre源码上进行更多更系统性的扩展。

         其实对Ogre稍微熟悉就不难发现,源码和Ogre本身提供的地形例子基本上没有二致,所以Demo更像是对Ogre地形的定制,唯一称得上扩展的部分是setHoleVisible()函数,它用于实现在地形上挖洞。

源码对Ogre有些许修改,增加了几个用于访问地形相关类成员的函数,对照着加上即可。

         最后,记得在编译时开启Ogre多线程。