以前学习笔记的角度是从程序流程,今次学习OGRE地形试着换个角度,我觉得这样的总结更方便日后查阅。
OGRE的地形部分分两个Component,Terrain和Paging,各由一些类构成。只要结合demo程序的流程搞清楚每个类的作用及各个类之间的关系,就能更深入探索OGRE地形处理机制(chunk lod,texture splat,skirt)了。
仔细研究下保存的地形数据dat文件的内容,也对学习地形很有帮助:
跟terrain有关的类:
Terrain:
Terrain的实例化对象就是一个Terrain Instance.
getLayerBlendMap()方法,为地形进行纹理splat时调用。如下:
// sync load since we want everything in place when we start mTerrainGroup->loadAllTerrains( true); if (mTerrainsImported) { Ogre::TerrainGroup::TerrainIterator ti = mTerrainGroup->getTerrainIterator(); while(ti.hasMoreElements()) { Ogre::Terrain* t = ti.getNext()->instance; initBlendMaps(t); } }void BasicTutorial3::initBlendMaps(Ogre::Terrain* terrain) { Ogre::TerrainLayerBlendMap* blendMap0 = terrain->getLayerBlendMap( 1); Ogre::TerrainLayerBlendMap* blendMap1 = terrain->getLayerBlendMap( 2); Ogre::Real minHeight0 = 70; Ogre::Real fadeDist0 = 40; Ogre::Real minHeight1 = 70; Ogre::Real fadeDist1 = 15; float* pBlend1 = blendMap1->getBlendPointer(); for (Ogre::uint16 y = 0; y < terrain->getLayerBlendMapSize(); ++y) { for (Ogre::uint16 x = 0; x < terrain->getLayerBlendMapSize(); ++x) { Ogre::Real tx, ty; blendMap0->convertImageToTerrainSpace(x, y, &tx, &ty); Ogre::Real height = terrain->getHeightAtTerrainPosition(tx, ty); Ogre::Real val = (height - minHeight0) / fadeDist0; val = Ogre::Math::Clamp(val, (Ogre::Real) 0, (Ogre::Real) 1); val = (height - minHeight1) / fadeDist1; val = Ogre::Math::Clamp(val, (Ogre::Real) 0, (Ogre::Real) 1); *pBlend1++ = val; } } blendMap0->dirty(); blendMap1->dirty(); blendMap0->update(); blendMap1->update(); }
update()方法,在Demo中是以每秒20次对每个Terrain调用该方法。作用是更新dirty sections的如下数据:
- The terrain geometry
- The terrain error metrics which determine LOD transitions
- The terrain normal map, if present
- The terrain lighting map, if present
- The terrain composite map, if present
TerrainGlobalOptions:
TerrainGlobalOptions必须在使用其他地形类之前构造。该类用来配置地形的全局选项,如:setMaxPixelError(),setLightMapDirection(),setCompositeMapDiffuse()这些。
TerrainGroup:
TerrainGroup管理构成整个世界的若干Terrain Instance。
含有Protected成员变量:TerrainSlotMap mTerrainSlots;
typedef map<uint32, TerrainSlot*>::type Ogre::TerrainGroup::TerrainSlotMap
这个映射表即是根据TerrainSlot的16位x,y索引值打包成32位ID,来映射到具体的TerrainSlot。
而TerrainSlot有公共成员变量:
TerrainSlotDefinition def; //TerrainSlotDefinition结构体包含了该slot是由地形文件还是ImportData定义的情况
Terrain * instance;
这就将TerrainGroup,Terrain,TerrainSlot,TerrainSlotDefinition联系起来了:TerrainGroup是总管,管理所有的TerrainSlot,每个TerrainSlot对应一个Terrain实例和TerrainSlotDefinition。
getDefaultImportSettings()方法,在这里设置所有地形实例的公共属性(ImportData结构体) ,如terrainsize,worldsize,纹理layer等。
defineTerrain方法有多种调用形式,即我们能通过不同方法来定义slot。根据API文档说明,貌似加载地形实例与定义slot是不能混为一谈的,后者只是在Terrain Grid中定义一个槽,占个位置先而已,而并没有实际加载地形,这是为了support background preparation of this terrain instance。slot(0,0)表示Terrain Grid的中心,往右x增加,往上y增加,有正负值。
defineTerrain(x,y),如果从已保存地形数据的dat文件来加载地形实例,则调用该函数。
defineTerrain(x,y,constantHeight),用来定义一个平坦地形slot.
defineTerrain(x,y,img),从高度图来定义slot.这用于第一次运行程序时(无地形dat文件),以后就能直接调用第一种形式了。
loadAllTerrains(bool synchronous = false)方法,这才是真正开始加载地形了。参数表示是否同步加载,是则加载强制发生在主线程中。
加载完地形,如果这是第一次运行程序,如前所述,我们是通过defineTerrain(x,y,img)来定义地形高度数据的,也就是还没有给地形贴上纹理。所以要给TerrainGroup中的每个地形实例都完成这项工作。见Terrain类的分析。
saveAllTerrains(),第一次运行程序时调用,将地形数据写入地形dat文件中保存在硬盘上,以后运行程序时直接读入就行了。
freeTemporaryResources()方法,地形构造完成后调用该方法,释放临时资源,提高程序效率。
isDerivedDataUpdateInProgress(),询问是否Derived Data(Light Map,Normal Map,Composite Map)正在处理中或已经处理完了,可以用来做label提示用户。
TerrainLayerBlendMap :
该类管理每层纹理的Blend Map.即Terrain的该层纹理怎么与前几层的结果相Alpha混合。API文档说得很清楚,每层的Blend Map其实只是最终Blend Texture的一个RGBA channel。我们在mTerrainGroup->getDefaultImportSettings()设置地形的公共属性时,已经设置好了一共有几层Terrain layer,每层由什么纹理构成,如下:
// Configure default import settings for if we use imported image Ogre::Terrain::ImportData& defaultimp = mTerrainGroup->getDefaultImportSettings(); defaultimp.terrainSize = 513; defaultimp.worldSize = 12000.0f; defaultimp.inputScale = 600; defaultimp.minBatchSize = 33; defaultimp.maxBatchSize = 65; // textures defaultimp.layerList.resize(3); defaultimp.layerList[0].worldSize = 100; defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_diffusespecular.dds"); defaultimp.layerList[0].textureNames.push_back("dirt_grayrocky_normalheight.dds"); defaultimp.layerList[1].worldSize = 30; defaultimp.layerList[1].textureNames.push_back("grass_green-01_diffusespecular.dds"); defaultimp.layerList[1].textureNames.push_back("grass_green-01_normalheight.dds"); defaultimp.layerList[2].worldSize = 200; defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_diffusespecular.dds"); defaultimp.layerList[2].textureNames.push_back("growth_weirdfungus-03_normalheight.dds");
具体的纹理splat过程见前面对Terrian类的笔记。
TerrainMaterialGenerator:
地形材质生成的基类,根据渲染需要子类化它。
TerrainMaterialGeneratorA:
Demo中用到了它的SM2Profile这个内嵌类,而SM2Profile继承自TerrainMaterialGenerator::Profile。
通过TerrainGlobalOptions::getDefaultMaterialGenerator()获取默认材质生成器,然后Demo中用SM2Profile进行了一些控制材质接收阴影的相关设置。
这部分完全没看懂,关于这两个类和地形的材质部分有待深入了解。
TerrainQuadTreeNode:
每个Terrain对象都有一棵四叉树。
通过对OGRE wiki的基础教程三学习,基本了解了地形的构建过程。但是我知道OGRE的地形是采用的chunk LOD,渲染机制包括四叉树,裂缝修补,geomorphing这些都还没在代码中看到。不过了解过chunked LOD的同学都知道,该算法里面有一个四叉树,储存了计算好的每个细节层次的各个chunk对应的误差尺度。在渲染地形时,用其来进行LOD选择。
该类应该就是完成这些工作,还有skirt index的计算,各个细节层次chunk的顶点,索引缓存具体怎么由该类来进行分配计算和管理现在没搞清楚。
找到的资料:
Terrain::distributeVertexData按照OgreTerrain的LOD算法,找到拥有vertex的TerrainQuadTreeNode,然后调用assignVertexData,开始创建缓冲区VertexDataRecord。VertexDataRecord包含了vertex data和index data。
关于实时对地形的编辑,如高度值,各层纹理blend,可以参考demo的代码:
void doTerrainModify(Terrain* terrain, const Vector3& centrepos, Real timeElapsed) { Vector3 tsPos; terrain->getTerrainPosition(centrepos, &tsPos); //centrepos是中心编辑点,即鼠标停留在地形上的位置。这里把该点从世界空间转换到地形空间。 //对于地形,有三个坐标系:世界空间,顶点空间,地形空间。 //顶点空间即从左下的(0,0)到右上的(2^n,2^n) //地形空间即从左下的(0,0)到右上的(1,1) #if OGRE_PLATFORM != OGRE_PLATFORM_IPHONE if (mKeyboard->isKeyDown(OIS::KC_EQUALS) || mKeyboard->isKeyDown(OIS::KC_MINUS)) { switch(mMode) { case MODE_EDIT_HEIGHT: { // we need point coords Real terrainSize = (terrain->getSize() - 1); //mBrushSizeTerrain决定变化区域的大小 long startx = (tsPos.x - mBrushSizeTerrainSpace) * terrainSize; long starty = (tsPos.y - mBrushSizeTerrainSpace) * terrainSize; long endx = (tsPos.x + mBrushSizeTerrainSpace) * terrainSize; long endy= (tsPos.y + mBrushSizeTerrainSpace) * terrainSize; startx = std::max(startx, 0L); starty = std::max(starty, 0L); endx = std::min(endx, (long)terrainSize); endy = std::min(endy, (long)terrainSize); //有了这四个点,我们就得到了一个编辑区域矩形,接下来很容易想到可以采用权值 //来修改每个顶点的新高度:距离中心编辑点越近,权值越大,这样最后就能形成 //平滑的圆锥形的修改后地形。 for (long y = starty; y <= endy; ++y) { for (long x = startx; x <= endx; ++x) { Real tsXdist = (x / terrainSize) - tsPos.x; Real tsYdist = (y / terrainSize) - tsPos.y; Real weight = std::min((Real)1.0, Math::Sqrt(tsYdist * tsYdist + tsXdist * tsXdist) / Real(0.5 * mBrushSizeTerrainSpace)); //根据当前编辑顶点到中心编辑顶点的两点间距离算出权值,该步是距离越大,权值越大 weight = 1.0 - (weight * weight); //我们需要的是,距离越大,权值越小. float addedHeight = weight * 250.0 * timeElapsed; //根据权值计算该点的高度变化值 float newheight; if (mKeyboard->isKeyDown(OIS::KC_EQUALS)) newheight = terrain->getHeightAtPoint(x, y) + addedHeight; else newheight = terrain->getHeightAtPoint(x, y) - addedHeight; terrain->setHeightAtPoint(x, y, newheight); //修改该点高度值 } } if (mHeightUpdateCountDown == 0) mHeightUpdateCountDown = mHeightUpdateRate; } break;
实时编辑各层layer的blend值也很相似,要根据编辑层的blend map size在image sapce进行blend data的修改,然后update().
实在是不好学啊,内容太特么的多了,脑袋完全是晕的,还有Paging组件呢。 T T
TerrainGruop::LoadAllTerrain()读取完了地形数据后,构造整个QuadTree。一个单边513的Terrain会得到如图示的2个树: