上文说到recast的寻路分为两部分:recast(建网格)和detour(寻路),这里就建网格这一步谈谈自己结合代码的理解。这里的navmesh指的是solo mesh,也就是静态网格,与能动态添加障碍的tile cache加以区别。后者另外单独撰文介绍。
这里推荐一个网站critterAI(网站地址),里面有两个很好的学习项目:CAINav是一个基于recast开发的可以在Unity运行的导航系统;NMGen包括Java版的recast静态网格生成代码,以及相关原理和算法的介绍文章。想要探究原理的话值得一看。
recast建网格这一部分涉及到较多的空间几何知识和算法,新人要能看懂着实不易。因涵盖内容较多,打算分两篇来写,主要的步骤包括:
体素化(Voxelization)
生成navmesh的函数入口在Sample_SoloMesh::handleBuild()中。第一步是初始化构造参数(initialize build config),重要的几个有:
- cs:xz平面下体素的大小。
- ch:y轴下体素的高度。
- walkableSlopeAngle:可行走倾斜角度。
- walkableHeight:寻路agent的高度。
- walkableClimb:寻路agent的爬坡高度。
- walkableRadius:寻路agent的半径。
这些参数都会在后面的体素化过程中用到,待用到时再细说。
第二步是将源网格光栅化(rasterize input polygon soup)。学过图形学的都知道,光栅化是渲染管线中的一个流程,作用是将图元数据转换为像素;而这里光栅化的作用类似,只不过转换成的是体素而不是像素。所以这里说的体素化也就是指光栅化,是将三角网格转换为体素格子的过程。其中体素格子的大小由上面提到的cs和ch来定义。
这里还引入了几个后面会用到的概念(不熟悉这些概念的可参见介绍文章):
- 高度域(height field):容纳所有体素格子的AABB包围盒。
- 区间(span):一列上空间连续的体素集合。
- 实心区间(solid span):被障碍物占据位置的区间。
- 开放区间(open span):没有被障碍物占据位置的区间。
为什么需要体素化这一步呢?这是为了方便使用前面定义的几个参数(如walkableHeight、walkableClimb)来对可行走区域做进一步的裁剪,否则单纯基于三角形网格是很难做到的。当然裁剪完成后,我们最终还是要把体素还原成navmesh。
下面具体看下执行代码。首先创建高度域(rcHeightfield),然后标记可行走三角形(rcMarkWalkableTriangles):方法是通过计算三角平面的法向量,与walkableSlopeAngle比较,小于这个倾斜角标记为可行走(RC_WALKABLE_AREA),存于 m_triareas数组中。
接下来是将三角形光栅化(rcRasterizeTriangles)。具体过程是:先计算三角形的AABB包围盒在xz平面下的投影。
然后遍历所有被投影覆盖的高度域格子列(grid column),每个格子列与三角形相交的部分取最低点和最高点,中间部分就是新增加的区间。新增区间时,若与老的区间有重叠,则还需要合并。最终我们得到一个spans数组,存在rcHeightField中。每个span中有属性area,取值源于前面计算出的m_triareas数组。
后面进入第三步:过滤可行走表面(Filter walkables surfaces)。这一步又细分为三步:
过滤低垂的可行走障碍(rcFilterLowHangingWalkableObstacles):若一个span标记为可行走,那么位于它上方且高度相差小于walkableClimb的span也应标记为可行走。典型的场景包括:楼梯台阶、路沿。这一步实际会扩大可行走区域判定。
过滤凸起区间(rcFilterLedgeSpans):如果从区间的顶端到相邻的区间下降的距离超过了walkableClimb,那么这个区间被认为是个凸起,不可行走。
过滤可行走的低高度区间(rcFilterWalkableLowHeightSpans):当区间上方有距离小于walkableHeight的障碍物时,它的顶端表面也不可行走,因为可容纳的高度不够。
区域生成
这是第四步:将可行走表面划分成简单的区域(Partition walkable surface to simple regions)。
首先是创建紧缩开放高度域(rcCompactHeightfield)。具体过程是:先遍历每个可行走的区间,它与上方的另一个区间之间的部分就是一个开放区间;得到所有开放空间后,再计算每个开放区间与相邻的4个区间之间的连通关系,这里是基于walkableHeight和walkableClimb判断是否合法,计算出的连通关系以编码的形式存于rcCompactSpan的con属性中。这一步结束后会完成对rcCompactHeightfield中spans、cells和areas数组的赋值。
其次基于walkableRadius来裁剪可行走区域。这里用一个dist数组去存每个rcCompactSpan与可行走区域边缘的最近距离,小于walkableRadius的将chf.areas[i]标记为不可行走。计算过程中用上了前面计算出的连通关系con。
最后是执行区域划分算法。可选算法分三种,这里只研究了第一种:分水岭算法。大意就是从地势的最低点开始灌水,水漫过的区间若与原区域相邻则认为是同一区域,否则认为是一个新的区域。分水岭算法通常用于图形处理领域,基于图像的灰度值来分割图像。这里唯一的不同点是用距离域来取代灰度值。距离域是指每个区间与可行走区域边缘的最近距离。距离域越大,等同于地势越低。先算出每个区间的距离域。
再执行分水岭算法,算出的区域id保存在rcCompactSpan.reg中。
至此区域分割完毕。