以前学习笔记的角度是从程序流程,今次学习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的如下数据:
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个树: