转:Ogre TerrainGroup地形赏析
1.1 参考
http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Ogre+Terrain+System
http://www.ogre3d.org/tikiwiki/tiki-index.php?page=Ogre+Terrain+Component+FAQ
http://www.ogre3d.org/forums/viewtopic.php?f=11&t=50674
http://tulrich.com/geekstuff/sig-notes.pdf
ogre_src_v1-8-1\Components\Terrain
├─include
│ OgreTerrain.h
│ OgreTerrainGroup.h
│ OgreTerrainLayerBlendMap.h
│ OgreTerrainMaterialGenerator.h
│ OgreTerrainMaterialGeneratorA.h
│ OgreTerrainPagedWorldSection.h
│ OgreTerrainPaging.h
│ OgreTerrainPrerequisites.h
│ OgreTerrainQuadTreeNode.h
│
└─src
OgreTerrain.cpp
OgreTerrainGroup.cpp
OgreTerrainLayerBlendMap.cpp
OgreTerrainMaterialGenerator.cpp
OgreTerrainMaterialGeneratorA.cpp
OgreTerrainPagedWorldSection.cpp
OgreTerrainPaging.cpp
OgreTerrainQuadTreeNode.cpp
Sample
ogre_src_v1-8-1\Samples\Terrain
1.2 类图
<帖不了图图>
1.3 使用流程
1、首先需要创建terrain options
TerrainGroup* mTerrainGroup;
mTerrainGlobals = OGRE_NEW TerrainGlobalOptions();
mTerrainGlobals->setMaxPixelError(8);
mTerrainGlobals->setCompositeMapDistance(3000);
mTerrainGlobals->setLightMapDirection(l->getDerivedDirection());
mTerrainGlobals->setCompositeMapAmbient(mSceneMgr->getAmbientLight());
mTerrainGlobals->setCompositeMapDiffuse(l->getDiffuseColour());
2、其次要创建TerrainGroup对象
mTerrainGroup->setFilenameConvention(Ogre::String("BasicTutorial3Terrain"), Ogre::String("dat"));
mTerrainGroup->setOrigin(Ogre::Vector3::ZERO);
3、然后设置Terrain Group
Terrain::ImportData& defaultimp = mTerrainGroup->getDefaultImportSettings();
defaultimp.terrainSize = TERRAIN_SIZE;
defaultimp.worldSize = TERRAIN_WORLD_SIZE;
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");
4、最后执行加载
后续需要计算blendmaps
6、清理Terrain Group
Terrain Group是Terrain的集合,如此可以取到集合里的terrain:
TerrainGroup::TerrainIterator ti = mTerrainGroup->getTerrainIterator();
while(ti.hasMoreElements())
{
Terrain* t = ti.getNext()->instance;
}
至此完成了ogre最新的TerrainGroup的生命周期。
1.4 地形文件
1.4.1 Terrain文件格式
TerrainData (Identifier 'TERR')
[Version 1]
Name |
Type |
Description |
Terrain orientation |
uint8 |
The orientation of the terrain; XZ = 0, XY = 1, YZ = 2 |
Terrain size |
uint16 |
The number of vertices along one side of the terrain |
Terrain world size |
Real |
The world size of one side of the terrain |
Max batch size |
uint16 |
The maximum batch size in vertices along one side |
Min batch size |
uint16 |
The minimum batch size in vertices along one side |
Position |
Vector3 |
The location of the centre of the terrain |
Height data |
float[size*size] |
List of floating point heights |
LayerDeclaration |
LayerDeclaration* |
The layer declaration for this terrain (see below) |
Layer count |
uint8 |
The number of layers in this terrain |
LayerInstance list |
LayerInstance* |
A number of LayerInstance definitions based on layer count (see below) |
Layer blend map size |
uint16 |
The size of the layer blend maps as stored in this file |
Packed blend texture data |
uint8* |
layerCount-1 sets of blend texture data interleaved as either RGB or RGBA depending on layer count |
Optional derived map data |
TerrainDerivedMap list |
0 or more sets of map data derived from the original terrain |
Delta data |
float[size*size] |
At each vertex, delta information for the LOD at which this vertex disappears |
Quadtree delta data |
float[quadtrees*lods] |
At each quadtree node, for each lod a record of the max delta value in the region |
TerrainLayerDeclaration (Identifier 'TDCL')
[Version 1]
Name |
Type |
Description |
TerrainLayerSampler Count |
uint8 |
Number of samplers in this declaration |
TerrainLayerSampler List |
TerrainLayerSampler* |
List of TerrainLayerSampler structures |
Sampler Element Count |
uint8 |
Number of sampler elements in this declaration |
TerrainLayerSamplerElement* |
List of TerrainLayerSamplerElement structures |
TerrainLayerSampler (Identifier 'TSAM')
[Version 1]
Name |
Type |
Description |
Alias |
String |
Alias name of this sampler |
Format |
uint8 |
Desired pixel format |
TerrainLayerSamplerElement (Identifier 'TSEL')
[Version 1]
Name |
Type |
Description |
Source |
uint8 |
Sampler source index |
Semantic |
uint8 |
Semantic interpretation of this element |
Element start |
uint8 |
Start of this element in the sampler |
Element count |
uint8 |
Number of elements in the sampler used by this entry |
LayerInstance (Identifier 'TLIN')
[Version 1]
Name |
Type |
Description |
World size |
Real |
The world size of this layer (determines UV scaling) |
Texture list |
String* |
List of texture names corresponding to the number of samplers in the layer declaration |
TerrainDerivedData (Identifier 'TDDA')
[Version 1]
Name |
Type |
Description |
Derived data type name |
String |
Name of the derived data type ('normalmap', 'lightmap', 'colourmap', 'compositemap') |
Size |
uint16 |
Size of the data along one edge |
Data |
varies based on type |
The data |
1.4.2 加载地形文件
OgreTerrain_d.dll!Ogre::Terrain::prepare
OgreTerrain_d.dll!Ogre::Terrain::prepare
OgreTerrain_d.dll!Ogre::TerrainGroup::handleRequest
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::RequestHandlerHolder::handleRequest
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::processRequest
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::processRequestResponse
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::addRequest
OgreTerrain_d.dll!Ogre::TerrainGroup::loadTerrainImpl
OgreTerrain_d.dll!Ogre::TerrainGroup::loadAllTerrains
Sample_Terrain_d.dll!Sample_Terrain::setupContent
首先加载全局选项TerrainGlobalOptions
然后从本地terrain文件中读取(@Terrain::prepare)
18:12:36: DefaultWorkQueueBase('Root') - QUEUED(thread:main): ID=1 channel=1 requestType=1
18:12:36: DefaultWorkQueueBase('Root') - PROCESS_REQUEST_START(main): ID=1 channel=1 requestType=1
18:12:36: Terrain created; size=513 minBatch=33 maxBatch=65 treeDepth=4 lodLevels=5 leafLods=2
18:12:36: Terrain::distributeVertexData processing source terrain size of 513
18:12:36: Assigning vertex data, resolution=513 startDepth=2 endDepth=4 splits=4
18:12:36: Assigning vertex data, resolution=129 startDepth=0 endDepth=2 splits=1
18:12:36: Terrain::distributeVertexData finished
18:12:36: DefaultWorkQueueBase('Root') - PROCESS_REQUEST_END(main): ID=1 channel=1 requestType=1 processed=1
18:12:36: DefaultWorkQueueBase('Root') - PROCESS_RESPONSE_START(thread:main): ID=1 success=1 messages=[] channel=1 requestType=1
18:12:36: Font Default/Vera using texture size 512x256
18:12:36: Info: Freetype returned null for character 160 in font Default/Vera
18:12:36: Texture: Default/VeraTexture: Loading 1 faces(PF_BYTE_LA,512x256x1) with 0 generated mipmaps from Image. Internal format is PF_BYTE_LA,512x256x1.
18:12:36: Mesh: Loading axes.mesh.
18:12:36: WARNING: axes.mesh is an older format ([MeshSerializer_v1.30]); you should upgrade it as soon as possible using the OgreMeshUpgrade tool.
18:12:36: Texture: axes.png: Loading 1 faces(PF_R8G8B8,256x256x1) Internal format is PF_X8R8G8B8,256x256x1.
18:12:36: DefaultWorkQueueBase('Root') - PROCESS_RESPONSE_END(thread:main): ID=1 success=1 messages=[] channel=1 requestType=1
1.4.3 地形表面网格
已经不存在一个具体的地形表面网格的概念了,地形是“分层分批处理”的东西,地形对象不再拥有一个具体的地形顶点数据,这些数据是在LODs中的。
http://www.ogre3d.org/forums/viewtopic.php?f=11&t=50674&start=275#p365005
Actually, the Terrain object doesn't hold this information. The terrain is what I call "hierarchically batched" which means there is no set of vertex data at the highest LOD which covers the entire terrain - instead there are a series of hierarchical nodes which each store a specific range of LODs, each of which has a different coverage of the terrain. The only batch which has the whole terrain stored in one are the lowest LOD levels - used when the terrain is very far away. This allows us to efficiently render the entire terrain in one batch when far away, but closer up smaller (physically) batches are used for higher LODs but overall the vertex data for each batch is of the same size (or within a small range). This also allows us to deal with terrains that would be impossible to address with 16-bit indexes - any patch with more than 256 vertices on each side is actually impossible to address as one batch anyway without 32-bit indexes, which I avoid for compatibility. My hierarchical batch system allows very large terrain patches while still respecting 16-bit indexes and generally giving better performance. Unfortunately, it can never be as simple as a single top-level set of vertex data.
So, if I gave you access to what we use internally, I think you'd just be very confused You really do just need to extract the raw heights or just walk across the terrain using getPoint() if you want something 'raw'. I suppose I could provide an API which dumps unindexed full-LOD triangles into a buffer (or maybe with 32-bit indexing), but this will be really inefficient if you then have to re-process the buffer yourself anyway. It's much better just to hook out the points and plug those into your system directly.
1.5 四叉树结构
每个叶子节点的size都是允许划分的批次最大size,也即65。它有2个LodLevel,其size是33,这个LodLevel已经是不可划分的批次最小size了。而非叶子节点的size都比允许的批次最大size大,并且它只有一个LodLevel,其size也是批次最大size。
由此可见,允许的批次最大和最小size是划分树节点和LodLevel的直接依据,它们约束了节点和Lod划分的顶点尺寸。对于树节点,简单来说划分的方法是将其平均分割成4块,如果每块比允许的批次最大size还要大,则用同样的方式对它再次递归分割。对于Lod来说,如果其所有者树节点不是叶子,那这个Lod就让其大小设为批次的最大size,否则,就对其进行Lod细分,让每个Lod尽量的小,但是不能小于批次允许的最小值。
批次最大最小size约束存在的意义是,一方面让每个lod的顶点尽可能的少,这样在渲染的时候可以更准确的找出最少的空间分割块,以便选择尽可能少的顶点;另一方面每个Lod的顶点又不能太少,否则会增加显卡的渲染批次。
+[1]New Node,size:257,lod:3,depth:1,quadrant:0,batch range[33,65]
+[2]New Node,size:129,lod:2,depth:2,quadrant:0,batch range[33,65]
+[ 3]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[ 4]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[ 5]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[ 6]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[7]New Node,size:129,lod:2,depth:2,quadrant:1,batch range[33,65]
+[ 8]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[ 9]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[10]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[11]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[12]New Node,size:129,lod:2,depth:2,quadrant:2,batch range[33,65]
+[13]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[14]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[15]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[16]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[17]New Node,size:129,lod:2,depth:2,quadrant:3,batch range[33,65]
+[18]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[19]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[20]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[21]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[22]New Node,size:257,lod:3,depth:1,quadrant:1,batch range[33,65]
+[23]New Node,size:129,lod:2,depth:2,quadrant:0,batch range[33,65]
+[24]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[25]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[26]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[27]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[28]New Node,size:129,lod:2,depth:2,quadrant:1,batch range[33,65]
+[29]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[30]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[31]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[32]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[33]New Node,size:129,lod:2,depth:2,quadrant:2,batch range[33,65]
+[34]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[35]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[36]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[37]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[38]New Node,size:129,lod:2,depth:2,quadrant:3,batch range[33,65]
+[39]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[40]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[41]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[42]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[43]New Node,size:257,lod:3,depth:1,quadrant:2,batch range[33,65]
+[44]New Node,size:129,lod:2,depth:2,quadrant:0,batch range[33,65]
+[45]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[46]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[47]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[48]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[49]New Node,size:129,lod:2,depth:2,quadrant:1,batch range[33,65]
+[50]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[51]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[52]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[53]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[54]New Node,size:129,lod:2,depth:2,quadrant:2,batch range[33,65]
+[55]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[56]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[57]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[58]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[59]New Node,size:129,lod:2,depth:2,quadrant:3,batch range[33,65]
+[60]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[61]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[62]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[63]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[64]New Node,size:257,lod:3,depth:1,quadrant:3,batch range[33,65]
+[65]New Node,size:129,lod:2,depth:2,quadrant:0,batch range[33,65]
+[66]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[67]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[68]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[69]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[70]New Node,size:129,lod:2,depth:2,quadrant:1,batch range[33,65]
+[71]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[72]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[73]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[74]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[75]New Node,size:129,lod:2,depth:2,quadrant:2,batch range[33,65]
+[76]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[77]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[78]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[79]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
+[80]New Node,size:129,lod:2,depth:2,quadrant:3,batch range[33,65]
+[81]New Node,size: 65,lod:1,depth:3,quadrant:0,batch range[33,65]
+[82]New Node,size: 65,lod:1,depth:3,quadrant:1,batch range[33,65]
+[83]New Node,size: 65,lod:1,depth:3,quadrant:2,batch range[33,65]
+[84]New Node,size: 65,lod:1,depth:3,quadrant:3,batch range[33,65]
Depth |
Size |
LOD |
节点数 |
0 |
513 |
4 |
1 |
1 |
257 |
3 |
4 |
2 |
129 |
2 |
16 |
3 |
65 |
1 |
64 |
1.5.1 叶子节点
叶子节点的顺序如下
1 |
2 |
|
|
|
|
|
|
3 |
4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
每个节点记录了一个当前节点的偏移值,这个偏移值是相对于父节点的继承偏移方式计算出的数值,有点类似场景管理器中的Node._getDerivedPosition,也即是相对于当前地形的偏移值,准确来说是相对于当前地形左上角的偏移,而不是相对其父节点。这个偏移值需要注意2点:
u 偏移用+x向下,+y向左的二维坐标系计算
u 偏移值与顶点数有-1差,也即最小的格子(LOD=1)的宽度的整数倍,当前是64。
1.6 预计算基础数据
首先,预先计算好一些全局基础数据,例如树深度,最大Lod,叶子节点的Lod。见Terrain::determineLodLevels。例如,计算结果如下:
18:12:36: DefaultWorkQueueBase('Root') - QUEUED(thread:main): ID=1 channel=1 requestType=1
18:12:36: DefaultWorkQueueBase('Root') - PROCESS_REQUEST_START(main): ID=1 channel=1 requestType=1
18:12:36: Terrain created; size=513 minBatch=33 maxBatch=65 treeDepth=4 lodLevels=5 leafLods=2
18:12:36: Terrain::distributeVertexData processing source terrain size of 513
18:12:36: Assigning vertex data, resolution=513 startDepth=2 endDepth=4 splits=4
18:12:36: Assigning vertex data, resolution=129 startDepth=0 endDepth=2 splits=1
18:12:36: Terrain::distributeVertexData finished
18:12:36: DefaultWorkQueueBase('Root') - PROCESS_RESPONSE_END(thread:main): ID=1 success=1 messages=[] channel=1 requestType=1
由于我们预先定义好了批次中最大最小顶点数、地形的顶点尺寸(分别是65,33,513),通过这个预先定义好的值,可以生成一个基于四叉树的LOD关系数据结构。这个数据结构可以“映射”到任意面积尺寸的地形中,例如一个单边为12000的正方形的地形。
1.7 分配地形顶点
地形定义都取到后,就开始分配地形顶点数据了。
OgreTerrain_d.dll!Ogre::Terrain::prepare
OgreTerrain_d.dll!Ogre::Terrain::prepare
OgreTerrain_d.dll!Ogre::TerrainGroup::handleRequest
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::RequestHandlerHolder::handleRequest
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::processRequest
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::processRequestResponse
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::addRequest
OgreTerrain_d.dll!Ogre::TerrainGroup::loadTerrainImpl
OgreTerrain_d.dll!Ogre::TerrainGroup::loadAllTerrains
Sample_Terrain_d.d
(分配顶点的流程图...只能看pdf了)
1.7.1 算法思想
现在需要找出如何分配顶点数据。我们要兼容16位的索引,这意味着我们最多可以拼凑129x129个地形,即使是松散的拼凑低细节的LODs也如此,因为下一个可拼凑数是257x257,这个数字太大了,所以不用它。
因此,我们需要将顶点数据分割成129块。主要地砖上创建的的数目也即表明了它上面的点,如果不使用其他的顶点数据我们在树节点中就不能在更低的LODs中合并地砖了。例如,使用如上所述的257x257的输入,顶点数据为了适合129x129的范围将不得不被分为2个(在每个维度上)。这些数据可以被树深度从1开始的所有的树共享,不过Lods 3-1将会从129x129的稀疏数据中采样,而LOD0将会从所有顶点数据中采样。
然而,最低的LOD4将不能使用同样的顶点数据进行处理,因为它需要覆盖整个地形。这里有2个选择:在17x17上创建另一组仅用于LOD4的顶点数据的,或者在出现树深度为1的时候用LOD4(例如仍旧分拆),并且沿着每一边像2x9一样渲染。
由于渲染非常小的批次不理想,以及顶点总数本质上不会很大,所以创建一个单独的顶点集还是有可行性。在出现遥远的地形时也将会让顶点缓存机制更高效。
我们可能需要一个更大尺寸的例子,因为在这种情况下只有层级1(LOD0)需要使用这种单独的顶点数据。较高细节的地形将会需要多种层次,这里有一个65/33批次设置的2049x2049的例子:
LODlevels = log2(2049 - 1) - log2(33 - 1) + 1 = 11 - 5 + 1 = 7
TreeDepth = log2((2049 - 1) / (65 - 1)) + 1 = 6
在最多细节层次上拆分的顶点数
(size - 1) / (TERRAIN_MAX_BATCH_SIZE - 1) = 2048 / 128 = 16
|
|
|
|
|
|
|
|
|
|
|
LOD |
0: |
2049 |
vertices |
32 x 65 |
vertex |
tiles |
(tree depth 5) |
vdata |
0-15 |
[129x16] |
LOD |
1: |
1025 |
vertices |
32 x 33 |
vertex |
tiles |
(tree depth 5) |
vdata |
0-15 |
[129x16] |
LOD |
2: |
513 |
vertices |
16 x 33 |
vertex |
tiles |
(tree depth 4) |
vdata |
0-15 |
[129x16] |
LOD |
3: |
257 |
vertices |
8 x 33 |
vertex |
tiles |
(tree depth 3) |
vdata |
16-17 |
[129x2] |
LOD |
4: |
129 |
vertices |
4 x 33 |
vertex |
tiles |
(tree depth 2) |
vdata |
16-17 |
[129x2] |
LOD |
5: |
65 |
vertices |
2 x 33 |
vertex |
tiles |
(tree depth 1) |
vdata |
16-17 |
[129x2] |
LOD |
6: |
33 |
vertices |
1 x 33 |
vertex |
tiles |
(tree depth 0) |
vdata |
18 |
[33] |
所有的顶点总数都是一个平方数,它们正好是沿着一条边的情形。所以,你可以看到我们需要有3个级别的顶点数据来满足(诚然,相当极端)这个情况,并且一共有19个顶点数据集。完整的细节几何,129的16个子集(X16)这样的有完全细节的几何体被用作LODs0-2。 LOD3不能使用这个子集,因为它需要通过这些子集进行组合,而且它只有8块地砖,所以我们需要在每一个顶点数据段最大是129个顶点的时候构造出另外一个集合来满足这个情况。因为在这种情况下LOD3需要整个257(X257)个的顶点,所以我们仍然将129分割成2(X2)个集合。虽然这一套集合是好用了,也包括了LOD5,但LOD6需要一个单一且连续的顶点集,所以我们为它构造了一个33x33的顶点集。
在顶点的数据存储方面,这意味着当我们的主要数据是:
2049^ 2 =4198401个顶点
最终我们存储的顶点数据是
(16 *129)^ 2+(2* 129)^ 2+ 33^ 2 =4327749个顶点
这相当于有3%的顶点冗余,但是为了从分组中减少批次它既必要又值得。此外,在LODs3和6(或树深度为3和0)中将有机会释放被更多细节LODs使用的数据,这在有巨大的地形的时候很重要。例如,如果我们在中等距离时为LOD0-2释放(GPU)顶点数据,就会为地形节省平均有98%的内存开销。
1.7.2 顶点数据
LODs在当前4叉树节点构造时候被创建,其中有个字段指向顶点数据,这个顶点数据在读取地形文件时被创建。
J 创建顶点数据
void TerrainQuadTreeNode::createGpuIndexData()
{
for (size_t lod = 0; lod < mLodLevels.size(); ++lod)
{
LodLevel* ll = mLodLevels[lod];
if (!ll->gpuIndexData)
{
// clone, using default buffer manager ie hardware
ll->gpuIndexData = OGRE_NEW IndexData();
populateIndexData(ll->batchSize, ll->gpuIndexData);
}
}
}
J 取回顶点数据
//渲染数据
void TerrainQuadTreeNode::getRenderOperation(RenderOperation& op)
{
mNodeWithVertexData->updateGpuVertexData();
op.indexData = mLodLevels[mCurrentLod]->gpuIndexData;
op.operationType = RenderOperation::OT_TRIANGLE_STRIP;
op.useIndexes = true;
op.vertexData = getVertexDataRecord()->gpuVertexData;
}
1.7.3 同步-异步机制
异步加载机制是Steven Streeting离开前奉献的一个重量级模块Paging的核心功能,Paging的异步加载实现了一个通用的分页机制,目前只知道在地形中有应用。但是这个优秀的分页机制可以在所有时间、帧率、消息、事件、渲染命令出现瓶颈的时候使用其扩展的各种灵活策略进行异步和有区分度的分解处理从而降低单帧负载。其算法思想Steven Streeting在Ogre官网有详细阐述,见章节“Page系统设计思想”。
为了区别异步加载给地形带来的复杂度,关闭了异步线程和地形Paging 。
#define OGRE_THREAD_SUPPORT 0
//#define PAGING
Root维护一个默认的工作队列DefaultWorkQueue,执行类似压入执行命令的逻辑用这个工作队列完成。由于关闭了异步,压入请求后会立即执行响应请求的子程序。
OgreTerrain_d.dll!Ogre::TerrainQuadTreeNode::load
OgreTerrain_d.dll!Ogre::Terrain::load
OgreTerrain_d.dll!Ogre::TerrainGroup::handleResponse
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::processResponse
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::processRequestResponse
OgreMain_d.dll!Ogre::DefaultWorkQueueBase::addRequest
OgreTerrain_d.dll!Ogre::TerrainGroup::loadTerrainImpl
OgreTerrain_d.dll!Ogre::TerrainGroup::loadAllTerrains
Sample_Terrain_d.dll!Sample_Terrain::setupContent
Sample_Terrain_d.dll!OgreBites::SdkSample::_setup
1.8 动态LOD计算
动态LOD计算主要目的是通过相机与四叉树节点的相对关系计算出几个关键指标:
u 当前Lod,标记当前节点LodLevel中第N个被使用的Lod
u 当前节点是否渲染,标记当前节点带领的子树是否有节点需要被渲染
LodLevel定义
/// Number of vertices rendered down one side (not including skirts)
uint16 batchSize;
/// Index data on the gpu
IndexData* gpuIndexData;
/// Maximum delta height between this and the next lower lod
Real maxHeightDelta;
/// Temp calc area for max height delta
Real calcMaxHeightDelta;
/// The most recently calculated transition distance
Real lastTransitionDist;
/// The cFactor value used to calculate transitionDist
Real lastCFactor;
LodLevel() : gpuIndexData(0), maxHeightDelta(0), calcMaxHeightDelta(0),
lastTransitionDist(0), lastCFactor(0) {}
};
1.8.1 LodLevel数据细节
一个节点聚合了4个LOD相关属性
|
|
基础Lod(Base Lod) |
预先计算好,最深的叶子节点是0,然后由内向外递增,且兄弟节点一样 |
当前Lod(Current Lod) |
动态计算,@TerrainQuadTreeNode::calculateCurrentLod |
Lod层级(Lod Level) |
保存Lod对应的具体顶点信息,每个节点“挂”一个或多个LodLevel |
Lod层级列表(Lod Level List) |
|
所有节点的Lod相关属性大部分都是初始化时就计算好,只有当前LOD是动态计算的。非叶子节点只有一个LodLevel,其顶点数量(LodLevel.batchSize)是当前terrain的批次顶点最小值;非叶子节点的基础Lod是父节点的基础Lod-1。叶子节点有多个LodLevel,其数量是由当前terrain预先计算好(NumLodLevelsPerLeaf),每个LodLevel中的顶点数量(LodLevel.batchSize)是当前terrain的批次顶点最大值(MaxBatchSize)------与非叶子节点的情况正好相反;叶子节点的基础LOD总是0。
可以看到,节点的Lod值由内从0开始向外逐渐递增,且同一深度(depth)的节点Lod也一样。而每个节点都会“挂”上一个或多个LodLevel,非叶子节点只“挂”一个,叶子节点“挂”多个,这个数量是有terrain根据世界尺寸、最大、最小单批次顶点数等值预先计算好的。
非叶子节点“挂”的LodLevel所包含的顶点数是terrain的允许的单个批次顶点最小数量。叶子节点的情况则不同,“挂”的第一个LodLevel所包含的顶点数是单个批次顶点的最大数,然后第二个减少一半---实际情况稍微复杂,数量由公式(((sz - 1) * 0.5) + 1)给出,也即几何学上的四边形顶点减半。有点类似D3DX中的层级纹理。
一切都很天衣无缝,只等当前Lod动态计算时,按照特定的规则决定哪些顶点需要被渲染,也即哪些LodLevel参与渲染。
1.8.2 动态计算Lod
一般的,Lod表示的值从0开始,依次递增,越到后面对象的细节越少。典型的如层级纹理。在地形中也如此。首先,在整个四叉树节点中,每个节点有自己的Lod,或者叫基础Lod,这个值是固定的:叶子节点的Lod占用0~M;兄弟关系的Lod一样;非叶子节点占用1个Lod,从M+1开始,越往树根走Lod越大。这样Lod最大的节点就是树根了。
动态计算Lod的关键因素是相机到节点中心的距离与节点中LodLevel的过渡距离,后者好比是一把尺子,用于判断这个Lod是否可以被相机可见。计算方法见TerrainQuadTreeNode::calculateCurrentLod。
每帧地形的Lod计算过程是对四叉树递归遍历的过程。首先遍历所有子树,然后在进行自身的计算。将计算过错分解为第一类计算过程和第二类计算过程:
{
mSelfOrChildRendered = false;
///------------------------------------------------------------------------
//深度优先的遍历,首先检查第一个叶子节点,考察其可见性,然后是兄弟节点,软后是父节点
int childRenderedCount = 0;
if (!isLeaf())
{
for (int i = 0; i < 4; ++i)
{
if (mChildren[i]->calculateCurrentLod(cam, cFactor))
++childRenderedCount;
}
}
//需要渲染的子节点数是0,或者是叶子节点,或者是所有子节点都不参与渲染的子树
//所有叶子节点都不需要渲染,那就只有考察节点自身是否需要渲染了
if (childRenderedCount == 0)
{
/// 第一类计算过程 ///
}
//当前节点有子节点参与渲染,那自身就不需要参与渲染了,
//如果需要渲染的子节点数量大于或等于4,只需做个标记,不用再计算当前节点了
else
{
/// 第二类计算过程 ///
//跳过自身的渲染
mCurrentLod = -1;
//当前子树需要被渲染
mSelfOrChildRendered = true;
//只考虑需要渲染的子节点小于4的情形
if (childRenderedCount < 4)
{
// only *some* children decided to render on their own, but either
// none or all need to render, so set the others manually to their lowest
for (int i = 0; i < 4; ++i)
{
TerrainQuadTreeNode* child = mChildren[i];
if (!child->isSelfOrChildRenderedAtCurrentLod())
{
child->setCurrentLod(child->getLodCount()-1);
child->setLodTransition(1.0);
}
}
} // (childRenderedCount < 4)
} // (childRenderedCount == 0)
}
1.8.3 LOD第一类计算过程
Vector3 localPos = cam->getDerivedPosition() - mLocalCentre - mTerrain->getPosition();
//相机到当前节点中心的绝对距离
Real dist = localPos.length();
dist -= (mBoundingRadius * 0.5f);
// For each LOD, the distance at which the LOD will transition *downwards*
// is given by
// distTransition = maxDelta * cFactor;
uint lodLvl = 0;
mCurrentLod = -1;
for (LodLevelList::iterator i = mLodLevels.begin(); i != mLodLevels.end(); ++i, ++lodLvl)
{
// If we have no parent, and this is the lowest LOD, we always render
// this is the 'last resort' so to speak, we always enoucnter this last
if (lodLvl+1 == mLodLevels.size() && !mParent)
{
mCurrentLod = lodLvl;
mSelfOrChildRendered = true;
mLodTransition = 0;
}
else
{
// check the distance
///------------------------------------------------------------------------
//计算过渡距离distTransition
// Calculate or reuse transition distance
Real distTransition;
LodLevel* ll = *i;
if (Math::RealEqual(cFactor, ll->lastCFactor))
distTransition = ll->lastTransitionDist;
else
{
distTransition = ll->maxHeightDelta * cFactor;
ll->lastCFactor = cFactor;
ll->lastTransitionDist = distTransition;
}
///------------------------------------------------------------------------
//相机是否离Lod足够近
//相机到节点中心的距离小于过渡距离,则让其显示
//对于叶子节点,Lod0的过渡距离小于Lod1
if (dist < distTransition)
{
// we're within range of this LOD
mCurrentLod = lodLvl;
mSelfOrChildRendered = true;
// Lod在节点中的存储顺序是用最高细节Lod到最低细节Lod的顺序
// 碰到第一个Lod就结束了,因为这个Lod细节已经是最高了
// 一般是叶子的第一个Lod,也即Lod0
break;
}//~相机足够近
}
}//~foreach LodLevelList
1.8.4 LOD第二类计算过程
mCurrentLod = -1;
//当前子树需要被渲染
mSelfOrChildRendered = true;
//只考虑需要渲染的子节点小于4的情形
if (childRenderedCount < 4)
{
// only *some* children decided to render on their own, but either
// none or all need to render, so set the others manually to their lowest
for (int i = 0; i < 4; ++i)
{
TerrainQuadTreeNode* child = mChildren[i];
if (!child->isSelfOrChildRenderedAtCurrentLod())
{
child->setCurrentLod(child->getLodCount()-1);
child->setLodTransition(1.0);
}
}
} // (childRenderedCount < 4)
第二类计算步骤比较简单,当前节点有子节点参与渲染,那自身就不需要参与渲染了。首先标记下这个子树需要被渲染,然后考察其递归子节点需要被渲染的数量,如果数量在[1,3]这个区间,则需要处理这个节点的4个直接子节点。
需要处理当前节点的4个直接子节点的了,如果这个子节点包含自身的树都不需要渲染,则将这个节点的Lod数量-1。之前在预计算全局基础数据时已经知道,就当前这个实例而言,叶子节点的Lod数量是2,非叶子节点的Lod数量是0。这样,对于当前节点的4个子树,如果这个子树不参与渲染,则其当前Lod=0。
1.9 渲染
地形四叉树的渲染使用了2个小技巧。一是每个树节点Hook了一个内嵌类TerrainQuadTreeNode.Rend和Movable参与到场景的管理,而它们并不是一个真实的场景对象和渲染对象,可以将它们理解为很多人喜欢使用的虚拟对象,在真正需要渲染的时候,将逻辑还是传递给TerrainQuadTreeNode。二是地形监听了场景管理器预渲染方法,这个方法正好在场景管理器八叉树遍历场景对象前执行。好处显而易见:让复杂的程序结构清晰易读。
首先,地形四叉树只应用于到地形,而不干涉场景。地形在渲染方面主要做了三件事,一是构建了一个四叉树和对应的Lod;二是构建了与每个Lod关联的顶点数据;三是每个四叉树节点都构造一个影子render对象和movable对象。
影子moveable对象在地形初次load的时候构建完成,每个四叉树节点都会新建一个场景节点,并关节上这个引子movable对象,这样让每个地形四叉树节点参与到场景八叉树节点的可见性计算中(当前考虑的场景管理器是默认的八叉树场景管理器)。
而在最开始,地形对象监听了场景管理器的SceneManager.firePreFindVisibleObjects方法,而这个方法正好仅仅在计算场景可见渲染对象的前一步:
Class Terrain : public SceneManager::Listener
void SceneManager::_renderScene(Camera* camera, Viewport* vp, bool includeOverlays)
{
firePreFindVisibleObjects(vp);
findVisibleObjects(camera);
firePostFindVisibleObjects(vp);
// Begin the frame
mDestRenderSystem->_beginFrame();
// Set rasterisation mode
mDestRenderSystem->_setPolygonMode(camera->getPolygonMode());
// Set initial camera state
mDestRenderSystem->_setProjectionMatrix(mCameraInProgress->getProjectionMatrixRS());
// Render scene content
renderVisibleObjects();
// End frame
mDestRenderSystem->_endFrame();
......
于是在渲染场景的时候,每个地形的四叉树节点由2个紧邻的分计算完成。首先预计算地形Lod,然后的计算其实是一个通用的场景管理器计算过程,地形并未与场景中其他节点有所不同。
OgreTerrain_d.dll!Ogre::Terrain::calculateCurrentLod
OgreTerrain_d.dll!Ogre::Terrain::preFindVisibleObjects
OgreMain_d.dll!Ogre::SceneManager::firePreFindVisibleObjects
OgreMain_d.dll!Ogre::SceneManager::_renderScene
OgreMain_d.dll!Ogre::Camera::_renderScene
OgreMain_d.dll!Ogre::Viewport::update
OgreMain_d.dll!Ogre::RenderTarget::_updateViewport
RenderSystem_Direct3D9_d.dll!Ogre::D3D9RenderWindow::_updateViewport
OgreMain_d.dll!Ogre::RenderTarget::_updateAutoUpdatedViewports
OgreMain_d.dll!Ogre::RenderTarget::updateImpl
OgreMain_d.dll!Ogre::RenderTarget::update
OgreMain_d.dll!Ogre::RenderSystem::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::renderOneFrame
(地形预计算Lod )
OgreTerrain_d.dll!Ogre::TerrainQuadTreeNode::updateRenderQueue
OgreTerrain_d.dll!Ogre::TerrainQuadTreeNode::Movable::_updateRenderQueue
OgreMain_d.dll!Ogre::RenderQueue::processVisibleObject
Plugin_OctreeSceneManager_d.dll!Ogre::OctreeNode::_addToRenderQueue
Plugin_OctreeSceneManager_d.dll!Ogre::OctreeSceneManager::walkOctree
Plugin_OctreeSceneManager_d.dll!Ogre::OctreeSceneManager::walkOctree
Plugin_OctreeSceneManager_d.dll!Ogre::OctreeSceneManager::walkOctree
Plugin_OctreeSceneManager_d.dll!Ogre::OctreeSceneManager::walkOctree
Plugin_OctreeSceneManager_d.dll!Ogre::OctreeSceneManager::_findVisibleObjects
OgreMain_d.dll!Ogre::SceneManager::_renderScene
OgreMain_d.dll!Ogre::Camera::_renderScene
OgreMain_d.dll!Ogre::Viewport::update
OgreMain_d.dll!Ogre::RenderTarget::_updateViewport
RenderSystem_Direct3D9_d.dll!Ogre::D3D9RenderWindow::_updateViewport
OgreMain_d.dll!Ogre::RenderTarget::_updateAutoUpdatedViewports
OgreMain_d.dll!Ogre::RenderTarget::updateImpl
OgreMain_d.dll!Ogre::RenderTarget::update
OgreMain_d.dll!Ogre::RenderSystem::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::renderOneFrame
(场景管理器通用渲染过程)
实际计算场景中的可见对象时,如果当前地形四叉树的影子Movable对象在相机中可见,那么只要这个四叉树节点中的当前Lod不是-1,就将其加入到渲染队列。还记得当前Lod表示的时候这个四叉树节点LodLevel层级中需要被渲染的那个Lod,这样在这里,这个渲染对象还是间接的渲染的一个LodLevel,这个真实的渲染对象在于场景管理器打交道的时候,表现为它的影子渲染对象TerrainQuadTreeNode.Rend。
如此这般,场景中需要渲染的对象都组装好了,好好的躺在渲染队列中。万事俱备只欠东风,是时候渲染了,这个渲染方法SceneManager.renderVisibleObjects在场景对象的预计算、实际计算两个步骤之后。
OgreTerrain_d.dll!Ogre::TerrainQuadTreeNode::Rend::getRenderOperation
OgreMain_d.dll!Ogre::SceneManager::renderSingleObject
OgreMain_d.dll!Ogre::SceneManager::SceneMgrQueuedRenderableVisitor::visit
OgreMain_d.dll!Ogre::QueuedRenderableCollection::acceptVisitorGrouped
OgreMain_d.dll!Ogre::QueuedRenderableCollection::acceptVisitor
OgreMain_d.dll!Ogre::SceneManager::renderObjects
OgreMain_d.dll!Ogre::SceneManager::renderBasicQueueGroupObjects
OgreMain_d.dll!Ogre::SceneManager::_renderQueueGroupObjects
OgreMain_d.dll!Ogre::SceneManager::renderVisibleObjectsDefaultSequence
OgreMain_d.dll!Ogre::SceneManager::_renderVisibleObjects
OgreMain_d.dll!Ogre::SceneManager::_renderScene
OgreMain_d.dll!Ogre::Camera::_renderScene
OgreMain_d.dll!Ogre::Viewport::update
OgreMain_d.dll!Ogre::RenderTarget::_updateViewport
RenderSystem_Direct3D9_d.dll!Ogre::D3D9RenderWindow::_updateViewport
OgreMain_d.dll!Ogre::RenderTarget::_updateAutoUpdatedViewports
OgreMain_d.dll!Ogre::RenderTarget::updateImpl
OgreMain_d.dll!Ogre::RenderTarget::update
OgreMain_d.dll!Ogre::RenderSystem::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::_updateAllRenderTargets
OgreMain_d.dll!Ogre::Root::renderOneFrame
void TerrainQuadTreeNode::getRenderOperation(RenderOperation& op)
{
mNodeWithVertexData->updateGpuVertexData();
op.indexData = mLodLevels[mCurrentLod]->gpuIndexData;
op.operationType = RenderOperation::OT_TRIANGLE_STRIP;
op.useIndexes = true;
op.vertexData = getVertexDataRecord()->gpuVertexData;
}
之前说过,加入到渲染队列的是影子渲染对象,真实渲染对象是一个LodLevel,这是一个很好的桥接模式,避免了地形四叉树场景八叉树之间的耦合。其实实现原理也非常简单,不能直接交互的2个对象之间需要交互,就让可以变通的对象投其所好,构建一个让另外那个对象熟悉的影子对象给它使用,只是在这个引子对象被访问的时候,将访问权限还是还给它的“真身”。
这样在渲染具体的LodLevel的时候,立即通过当前Lod标记找到这个具体的LodLevel,装配好需要的数据交给GPU完成图形设备的渲染流程。