一般3D游戏的地形都是使用 LOD地形,Cocos2dx的Terrain也不例外, 原因是大的地形绘制需要在短时间内处理上亿个三角形,使用LOD技术能极大的提高地形的绘制效率。
多分辨率网格简化技术/细节层次模型技术(LOD技术),引入“分而治之”的思想,根据地形的不同复杂程度和人眼观察地形的特点,对地形的不同区域采取不同细节的描述和绘制。采用LOD技术绘制地形,在不降低表现效果的前提下,可以尽量减少三角形的数量,以提高图形绘制效率,实现地形的实时交互可视化,通俗具体讲就是将一块地形划分为很多块小地形(Chunk),对于离视点越近的区域,或者该区域地形越复杂(起伏大,如山区),绘制的三角形数目越多,地形描述精度越高;对于离视点越远的区域,或者该区域越平坦,绘制的三角形数目越少,地形描述精度越低。
在具体的实现算法中,需要用到四叉树来描述地形。
如下图所示,我们将一块地形的高程数据用一个四叉树结构组织起来,其中每个正方形代表一个节点,每个节点包含9个高程点,其中有一个中心点、4个角点和4个位于4条边上的中点。一个大正方形里面包含4个小正方形(叶子节点除外),这四个小正方形所代表的节点就是大正方形节点的子节点。
|
|
|
|
|
|
|
|
|
|
||
|
|
|
|
|
|
||
|
|
图(1)
地形的四叉树结构
首先按照地形的大小把整个地形分割成一棵满四叉树,满四叉树的概念是从满二叉树引申过来的,它必须满足以下的条件:
1) 除了叶子节点,每个节点都有4个子节点,并且其所有子节点都位于同一层上;
2) 地形越大,四叉树的深度越大
图(2)
满四叉树
这样我们就可以看出来,每个节点表示一块地形,节点的层次越低(根节点的层次最低,为0),所代表的正方形的面积越大,该节点的细节程度(Level)也越低。显然,叶子节点的层次最高,它的Level也最高。
【突起现象】
当视点移动的时候,地形网格不断更新,当节点Level发生变化的时候,地形中的某点会由一个高度突然跳变到另一个高度,这种称之为“突起”现象(Poping-up)。在地形漫游系统中,“突起”现象给人 一种不真实的感觉。为消除这种不自然的行为,引入了一种”形变“算法。形变的含义是,当节点从一个Level过渡到另一个Level的时候,其中某点平稳缓慢地从一个高度过渡到另一个高度。形变虽然没有本质上消除地形高度变化,但这种“缓变”往往肉眼难以察觉。
Cocos2dx实现地形的原理:
一个Terrain是由 n *m 个Chunk组成的,Terrain和Chunk的大小必须是2的幂次方,只有这样才可以方便的根据Chunk的LOD等级来绘制粗糙或者细致的网格。LOD的等级是根据摄像机所在位置与Chunk的距离来决定的,距离越小,LOD等级越小,绘制越精细。Terrain中定义了高度图,细节贴图,alpha贴图,Chunk的集合,原始数据等,以下是Terrain主要的成员变量:
//地形数据,包括顶点,法线,贴图坐标
struct TerrainVertexData
{
cocos2d::Vec3 _position;
cocos2d::Tex2F_texcoord;
cocos2d::Vec3 _normal;
};
//当T裂缝为INCREASE_LOWER: 缓存当前chunk的lod等级和它的四个邻居的lod等级,以及索引标志缓存, 在生成一个新的索引缓存后,并不是使用完就把它删掉,而是把它的索引标志缓存起来,下次还遇到一样的索引缓存,就直接使用即可,提高了效率。
std::vector <ChunkLODIndices>_chunkLodIndicesSet;
//当T裂缝为SKIRT:缓存当前chunk的lod等级和索引标志缓存
std::vector<ChunkLODIndicesSkirt>_chunkLodIndicesSkirtSet;
TerrainData _terrainData;
unsignedchar* _data; //高度图的数据
cocos2d::Image * _heightMapImage; //高度图
float_lodDistance[3]; //每个lod层次的距离基本值,用来和Chunk与相机的距离做比较以确定每个chunk的lod等级
Texture2D * _detailMapTextures[4]; //支持4层地表贴图
Texture2D * _alphaMap; //alpha贴图,决定4层贴图以什么方式进行显示,在shader中体现
Texture2D * _lightMap; //光照贴图
Vec3 _lightDir; //光照方向向量
QuadTree * _quadRoot; // 四叉树根节点
Chunk * _chunkesArray[MAX_CHUNKES][MAX_CHUNKES]; //chunk集合
std::vector<TerrainVertexData>_vertices; //地形数据,包括顶点,法线,贴图坐标
std::vector<unsigned int>_indices; //地形顶点索引
int _imageWidth; //高度图的宽,以字节为单位
int _imageHeight;
Size _chunkSize; //地图块的大小
bool _isEnableFrustumCull; //是否允许截视体剪裁
float _maxHeight; //地型的最高点和最低点
float _minHeight;
CrackFixedType_crackFixedType; //填补裂缝类型
enum classCrackFixedType{
SKIRT,
INCREASE_LOWER,
};
SKIRT方式: 用Chunk最外层的所有顶点都生成一个副本,并只改变副本顶点的高度值(根据什么规则来改变我没明白),把这些顶点先保存下来。之后渲染的时候先正常绘制一遍,即把整个chunk的三角形都绘制一遍,接下来就是用之前的副本顶点再一次渲染最外层的三角形。
INCREASE_LOWER方式:属于边插入方式,先绘制除了最外层的三角形,之后绘制最外层的三角形。要添加边以保证最外层的三角形和邻居Chunk的最外层三角形共用的那条边都一样长。
Chunk是一个地形块, 主要负责存储对于的顶点数据,根据当前的LOD等级生成索引缓存,以及渲染这个地形块。这里的难度在于填补T型裂缝,所要做的工作是根据crackFixedType类型来生成对于的索引缓存以及适当修改顶点数据的高度来达到无裂缝效果。以下是它的重要成员变量:
struct Chunk
{
std::vector<TerrainVertexData>_originalVertices; //原始的顶点数据
/*LODindices*/
structLOD{
std::vector<GLushort>_indices;
};
LOD _lod[4]; //四个LOD等级下的索引缓存
GLuint _vbo;
ChunkIndices_chunkIndices; //正在使用的顶点索引标志,如果相机位置改变了,每一帧都要重新计算索引缓存
struct ChunkIndices
{
GLuint _indices; //OpenGL索引标志
unsignedshort _size; //索引缓存的大小
};
//生成chunk数据
voidgenerate(int map_width, intmap_height, int m, intn, const unsignedchar* data);
//真正的绘制函数,每个chunk都会增加一个渲染批次,似乎可以合并渲染
voidbindAndDraw();
//平缓更新顶点高度值,解决突起问题
voidupdateVerticesForLOD();