转载于https://zhuanlan.zhihu.com/p/74537236
UE4 Navmesh寻路(一)Recast基础
网易游戏码农
59 人赞同了该文章
相关文章:
南京周润发:UE4 Navmesh寻路(二)Detour寻路zhuanlan.zhihu.com
寻路是游戏人工智能中必不可少的部分,它赋予了智能体生命力,使智能体可以和玩家交互,追逐,也可以为玩家自动完成向目标点的移动,比如MMO中常见的“自动跑图”功能。
如今主流的寻路方案为Navmesh,可以实现复杂的寻路功能,而UE4也正式使用了这套寻路方案。
本系列文章将会介绍UE4中的Navmesh寻路部分,包括导航数据构建,寻路等等。
第一部分将介绍Navmesh的基石,也是重中之重:Recast。它可以把地形数据进行抽象和简化,生成人工智能体可理解的导航数据。
其实,目前生成Navmesh数据主要有两种方式:多边形裁剪和体素化。
- 多边形裁剪是直接对地形的多边形网格数据进行裁剪及合并,从而生成导航网格。方法比较直观,但难度更高,目前havok引擎使用了此方法。
- 体素化是对地形多边形网格进行栅格化,然后用这些“格子”重新生成导航网格,方法更复杂,但难度更低,Recast使用了此方案,而UE4使用了Recast。
简介
Recast是一种优秀的navmesh生成套件
- 自动化,可以处理任意地形数据,输出导航网格
- 快速高效
- 能处理动态碰撞
- 开源,可根据自己游戏内容进行定制
- 自带可视化工具
Recast:将场景网格模型生成用于寻路的导航网格
Detour:利用导航网格进行寻路
Recast在运行时,首先会从关卡地形中构建一个体素模型,之后使用体素创建导航网格。
处理过程包括3步:创建体素模型,把模型分割成简单的区域,把这些区域再分割成简单多边形(凸的)
- 通过把输入的三角形mesh进行光栅化,形成一个多层的高度场,就能得到体素模型。之后可以对体素模型做一些简单的过滤,去掉玩家不可达的位置
- 体素模型描述的可行走区域被划分为重叠的2D区域,这些区域只有一个未重叠的等高线,这可以大大简化最后一步处理步骤
- 首先沿着边界划这些区域并进行化简可以剥离出导航多边形。然后把这些导航多边形处理为凸多边形,凸多边形可以更好的用于寻路和对场景进行空间推理。
Detour
Recast可以与Detour共同工作,Detour是寻路和空间推理组件。Detour可以用于任何navmesh数据,但是recast生成的数据效果更好。
Detour提供了简单静态navmesh,以及tiled navmesh,前者已适合很多简单情况,后者允许对mesh进行动态的增减操作。Tiled mesh可以在游戏运行时动态更新,当场景数据改变时能重新生成tile,因此可以支持动态阻挡。
整合Recast进自己的工程
推荐把DebugUtils,Detour,DetourCrowd,DetourTileCache,Recast文件夹加入到工程中,每个文件夹下文件的功能从命名里可以比较直观了解。
SoleMesh生成Navmesh的流程
生成过程的时间复杂度是相当高的,尤其是第四步到第七步中的region生成、多边形轮廓形成,多边形网格生成及优化,都非常耗时,因此以下Navmesh生成的过程适合离线处理。
- 填写build的配置结构体rcConfig
初始化参数,有
cs:xz平面下体素的大小(所以是正方形)
ch:y轴下体素的高度
walkableSlopeAngle:可行走倾斜角度
walkableHeight:寻路agent高度
walkableClimb:寻路agent爬坡高度(楼梯等场景)
walkableRadius:寻路agent半径(过滤太靠地图边缘地区)
等等
2. 将原网格光栅化
这里所说的光栅化就是体素化,是将三角网格转换为体素格子的过程。其中体素格子的大小由上面的cs和ch定义。
首先要创建空的高度域rcHeightfield。之后初始化高度域,为spans二维数组分配内存空间。
执行rcMarkWalkableTriangles,标记源地形数据中可行走的三角形,把结果存储在m_triareas数组中。具体做法为计算三角形法向量,与walkableSlopeAngle属性相比较,小于这个倾斜角标记为可行走。
接下来就是光栅化操作,执行rcRasterizeTriangles。先计算三角形AABB包围盒在xz平面下的投影,然后遍历被投影覆盖的高度域的格子列,每个格子列与三角形相交的部分取最低点和最高点,中间部分就是新增加的span。新增span时,若与原有区间有重叠,则需要合并span,合并span时已经会考虑walkableClimb来设置area了。span的area属性取值就源于之前计算的m_triareas数组。
3. 过滤可行走表面
首先执行rcFilterLowHangingWalkableObstacles,过滤低垂的可行走障碍,若一个span标记为可行走,那么位于它上方且高度相差小于walkableClimb的span也应该标记为可行走。典型场景包括:楼梯台阶、路沿,这一步会扩大可行走区域判定。
执行rcFilterLedgeSpans,过滤突起span,如果从span顶端到相邻区间下降距离超过了WalkableClimb,那么这个span被认为是个突起,不可行走。这里的相邻区域定义为前后左右四个方向。
执行rcFilterWalkableLowHeightSpans,过滤可行走的低高度span。当span上方有距离小于walkableHeight的障碍物时,它的顶端表面也不可行走,因为容纳的高度不够。
4. 分割可走面为简单多边形
执行rcBuildCompactHeightfield根据之前的高度域创建紧缩开放高度域。方法为先遍历每个可行走的span,它与上方span之间的部分就是一个开放区间。得到所有开放空间后,再计算每个开放区间相邻4个区间的连通关系,这里是基于walkableHeight和walkableClimb判断是否合法,计算出的连通关系以编码的形式存于con属性中。
关于邻居的连通性需要注意,rcGetCon函数返回某个方向的连通邻居。如果A在B的左边,且都连通,则rcGetCon(A,2)和rcGetCon(B,0)并不一定分别返回B和A,因为一个span在一个方向上的连通邻居可能不止一个,但所有span都计算一下连通邻居的做法肯定不会漏掉任何邻居关系。
执行rcErodeWalkableArea,基于walkableRadius裁剪可行走区域。
这里用dist数组去存每个rcCompactSpan与可行走区域边缘的最近距离,得到dist结果后,如果距离小于walkableRadius*2就将chf.areas[i]标记为不可行走。
具体分为3步:
1. 初始化,span不可行走或者四周有一个不可行走的点,则dist标记为0
2. 从左下开始扫描一遍,做一次dist紧缩
3. 从右上开始扫描一遍,再做一次dist紧缩
这样每个span的dist就肯定是离最近边界的最近距离了
接下来的过程比较复杂,执行区域划分算法,把离散的Span整合为大的Region。
有三种方法可选:
1. Watershed 最经典,最常用,效果最好,慢,一般用于离线处理,适合大地图
2. Monotone 最快且保证生成的是不重叠、没有洞的Region,但生成的Region可能又细又厂,不过速度最快
3. Layer:速度、效果介于Watershed和Monotone之间,适用于tiled navmesh,且tile大小偏小
在此先介绍Watershed算法。
5. 生成region轮廓
这个步骤,将会对region边界上一个个span点,进行适当选取和增减,使span间隔均匀,从而组成一个轮廓。
对每个span,标记其四个方向中与其他region连通的方向,之后通过另一种行走算法找到轮廓点。规则为:左边有邻居region,上边为轮廓点;上边有邻居region,右上为轮廓点;右边有邻居region,右边有轮廓点;下边有邻居region,span自身坐标为轮廓点。把这些轮廓点都顺序添加到verts数组里面。
verts中的一个span如果和下一个span连通region或者area不同,则加入到简化列表中。之后顺序遍历简化列表,如果verts中离简化列表相邻两点距离最远的点距离大于maxError,则把该点也加入到简化列表中。 如果简化列表中相邻两点间的距离大于maxEdgeLen,就找这两点中间下标的点,插入到简化列表中,因为这些轮廓点都是有序排列的。然后设置简化后点的region和area信息。
简化列表中如果相邻两点的x或者z相同,则仅保留前一个点。
之后建立一个轮廓结构(rcContour),把这轮迭代得到的span点坐标都保存到轮廓中,再把这个轮廓加入到轮廓集合cset。
接下来为空洞合并过程,空洞会造成一个region逻辑上有多个闭合的轮廓边缘,影响mesh的生成。
对轮廓集合中的每个rcCounter,使用calcAreaOfPolygon2D来计算多边形面积,若面积为负则证明出现了空洞。需要进行空洞合并,使这两个轮廓变成一个闭合的轮廓。
6. 通过轮廓创建多边形网格
首先做一些检查,一个轮廓最多支持65533个顶点。
把一个轮廓组成的多边形划分为三角形,方法为一个个的找分离顶点,三角形结果存储于tris数组中,一个三角形用三个span的下标索引表示。此方法的专业名称为耳切法(Ear Clipping)。
使用tris中的三角形信息构建初始多边形,此时每个多边形就是一个三角形。
合并初始的多边形,把有共同边的多边形进行合并,而且要保证合并后的多边形是凸的,同时多边形的顶点数不能大于配置中设置的maxVertsPerPoly。合并过程不断持续,直到找不到能合并的多边形为止。
移除边界的顶点,建立网格邻居关系, 寻找可传送的边。这部分涉及到不少图形学内容。
7. 创建细节网格(允许访问每个多边形的近似高度)
创建细节网格rcPolyMeshDetail,高度细节使用rcHeightPatch表示,存储了网格的宽高,最低点高度等信息。
至此,navmesh数据已经可用了。
参考:
http://critterai.org/projects/nmgen_study/ 这篇文章研究的比较深入,推荐
https://blog.csdn.net/needmorecode/article/details/81416553
https://github.com/youlanhai/recastnavigation-learn
http://www.stevefsp.org/projects/rcndoc/prod/index.html
编辑于 2019-08-19
UE4 Navmesh寻路(一)Recast基础
网易游戏码农
59 人赞同了该文章
相关文章:
南京周润发:UE4 Navmesh寻路(二)Detour寻路zhuanlan.zhihu.com
寻路是游戏人工智能中必不可少的部分,它赋予了智能体生命力,使智能体可以和玩家交互,追逐,也可以为玩家自动完成向目标点的移动,比如MMO中常见的“自动跑图”功能。
如今主流的寻路方案为Navmesh,可以实现复杂的寻路功能,而UE4也正式使用了这套寻路方案。
本系列文章将会介绍UE4中的Navmesh寻路部分,包括导航数据构建,寻路等等。
第一部分将介绍Navmesh的基石,也是重中之重:Recast。它可以把地形数据进行抽象和简化,生成人工智能体可理解的导航数据。
其实,目前生成Navmesh数据主要有两种方式:多边形裁剪和体素化。
- 多边形裁剪是直接对地形的多边形网格数据进行裁剪及合并,从而生成导航网格。方法比较直观,但难度更高,目前havok引擎使用了此方法。
- 体素化是对地形多边形网格进行栅格化,然后用这些“格子”重新生成导航网格,方法更复杂,但难度更低,Recast使用了此方案,而UE4使用了Recast。
简介
Recast是一种优秀的navmesh生成套件
- 自动化,可以处理任意地形数据,输出导航网格
- 快速高效
- 能处理动态碰撞
- 开源,可根据自己游戏内容进行定制
- 自带可视化工具
Recast:将场景网格模型生成用于寻路的导航网格
Detour:利用导航网格进行寻路
Recast在运行时,首先会从关卡地形中构建一个体素模型,之后使用体素创建导航网格。
处理过程包括3步:创建体素模型,把模型分割成简单的区域,把这些区域再分割成简单多边形(凸的)
- 通过把输入的三角形mesh进行光栅化,形成一个多层的高度场,就能得到体素模型。之后可以对体素模型做一些简单的过滤,去掉玩家不可达的位置
- 体素模型描述的可行走区域被划分为重叠的2D区域,这些区域只有一个未重叠的等高线,这可以大大简化最后一步处理步骤
- 首先沿着边界划这些区域并进行化简可以剥离出导航多边形。然后把这些导航多边形处理为凸多边形,凸多边形可以更好的用于寻路和对场景进行空间推理。
Detour
Recast可以与Detour共同工作,Detour是寻路和空间推理组件。Detour可以用于任何navmesh数据,但是recast生成的数据效果更好。
Detour提供了简单静态navmesh,以及tiled navmesh,前者已适合很多简单情况,后者允许对mesh进行动态的增减操作。Tiled mesh可以在游戏运行时动态更新,当场景数据改变时能重新生成tile,因此可以支持动态阻挡。
整合Recast进自己的工程
推荐把DebugUtils,Detour,DetourCrowd,DetourTileCache,Recast文件夹加入到工程中,每个文件夹下文件的功能从命名里可以比较直观了解。
SoleMesh生成Navmesh的流程
生成过程的时间复杂度是相当高的,尤其是第四步到第七步中的region生成、多边形轮廓形成,多边形网格生成及优化,都非常耗时,因此以下Navmesh生成的过程适合离线处理。
- 填写build的配置结构体rcConfig
初始化参数,有
cs:xz平面下体素的大小(所以是正方形)
ch:y轴下体素的高度
walkableSlopeAngle:可行走倾斜角度
walkableHeight:寻路agent高度
walkableClimb:寻路agent爬坡高度(楼梯等场景)
walkableRadius:寻路agent半径(过滤太靠地图边缘地区)
等等
2. 将原网格光栅化
这里所说的光栅化就是体素化,是将三角网格转换为体素格子的过程。其中体素格子的大小由上面的cs和ch定义。
首先要创建空的高度域rcHeightfield。之后初始化高度域,为spans二维数组分配内存空间。
执行rcMarkWalkableTriangles,标记源地形数据中可行走的三角形,把结果存储在m_triareas数组中。具体做法为计算三角形法向量,与walkableSlopeAngle属性相比较,小于这个倾斜角标记为可行走。
接下来就是光栅化操作,执行rcRasterizeTriangles。先计算三角形AABB包围盒在xz平面下的投影,然后遍历被投影覆盖的高度域的格子列,每个格子列与三角形相交的部分取最低点和最高点,中间部分就是新增加的span。新增span时,若与原有区间有重叠,则需要合并span,合并span时已经会考虑walkableClimb来设置area了。span的area属性取值就源于之前计算的m_triareas数组。
3. 过滤可行走表面
首先执行rcFilterLowHangingWalkableObstacles,过滤低垂的可行走障碍,若一个span标记为可行走,那么位于它上方且高度相差小于walkableClimb的span也应该标记为可行走。典型场景包括:楼梯台阶、路沿,这一步会扩大可行走区域判定。
执行rcFilterLedgeSpans,过滤突起span,如果从span顶端到相邻区间下降距离超过了WalkableClimb,那么这个span被认为是个突起,不可行走。这里的相邻区域定义为前后左右四个方向。
执行rcFilterWalkableLowHeightSpans,过滤可行走的低高度span。当span上方有距离小于walkableHeight的障碍物时,它的顶端表面也不可行走,因为容纳的高度不够。
4. 分割可走面为简单多边形
执行rcBuildCompactHeightfield根据之前的高度域创建紧缩开放高度域。方法为先遍历每个可行走的span,它与上方span之间的部分就是一个开放区间。得到所有开放空间后,再计算每个开放区间相邻4个区间的连通关系,这里是基于walkableHeight和walkableClimb判断是否合法,计算出的连通关系以编码的形式存于con属性中。
关于邻居的连通性需要注意,rcGetCon函数返回某个方向的连通邻居。如果A在B的左边,且都连通,则rcGetCon(A,2)和rcGetCon(B,0)并不一定分别返回B和A,因为一个span在一个方向上的连通邻居可能不止一个,但所有span都计算一下连通邻居的做法肯定不会漏掉任何邻居关系。
执行rcErodeWalkableArea,基于walkableRadius裁剪可行走区域。
这里用dist数组去存每个rcCompactSpan与可行走区域边缘的最近距离,得到dist结果后,如果距离小于walkableRadius*2就将chf.areas[i]标记为不可行走。
具体分为3步:
1. 初始化,span不可行走或者四周有一个不可行走的点,则dist标记为0
2. 从左下开始扫描一遍,做一次dist紧缩
3. 从右上开始扫描一遍,再做一次dist紧缩
这样每个span的dist就肯定是离最近边界的最近距离了
接下来的过程比较复杂,执行区域划分算法,把离散的Span整合为大的Region。
有三种方法可选:
1. Watershed 最经典,最常用,效果最好,慢,一般用于离线处理,适合大地图
2. Monotone 最快且保证生成的是不重叠、没有洞的Region,但生成的Region可能又细又厂,不过速度最快
3. Layer:速度、效果介于Watershed和Monotone之间,适用于tiled navmesh,且tile大小偏小
在此先介绍Watershed算法。
5. 生成region轮廓
这个步骤,将会对region边界上一个个span点,进行适当选取和增减,使span间隔均匀,从而组成一个轮廓。
对每个span,标记其四个方向中与其他region连通的方向,之后通过另一种行走算法找到轮廓点。规则为:左边有邻居region,上边为轮廓点;上边有邻居region,右上为轮廓点;右边有邻居region,右边有轮廓点;下边有邻居region,span自身坐标为轮廓点。把这些轮廓点都顺序添加到verts数组里面。
verts中的一个span如果和下一个span连通region或者area不同,则加入到简化列表中。之后顺序遍历简化列表,如果verts中离简化列表相邻两点距离最远的点距离大于maxError,则把该点也加入到简化列表中。 如果简化列表中相邻两点间的距离大于maxEdgeLen,就找这两点中间下标的点,插入到简化列表中,因为这些轮廓点都是有序排列的。然后设置简化后点的region和area信息。
简化列表中如果相邻两点的x或者z相同,则仅保留前一个点。
之后建立一个轮廓结构(rcContour),把这轮迭代得到的span点坐标都保存到轮廓中,再把这个轮廓加入到轮廓集合cset。
接下来为空洞合并过程,空洞会造成一个region逻辑上有多个闭合的轮廓边缘,影响mesh的生成。
对轮廓集合中的每个rcCounter,使用calcAreaOfPolygon2D来计算多边形面积,若面积为负则证明出现了空洞。需要进行空洞合并,使这两个轮廓变成一个闭合的轮廓。
6. 通过轮廓创建多边形网格
首先做一些检查,一个轮廓最多支持65533个顶点。
把一个轮廓组成的多边形划分为三角形,方法为一个个的找分离顶点,三角形结果存储于tris数组中,一个三角形用三个span的下标索引表示。此方法的专业名称为耳切法(Ear Clipping)。
使用tris中的三角形信息构建初始多边形,此时每个多边形就是一个三角形。
合并初始的多边形,把有共同边的多边形进行合并,而且要保证合并后的多边形是凸的,同时多边形的顶点数不能大于配置中设置的maxVertsPerPoly。合并过程不断持续,直到找不到能合并的多边形为止。
移除边界的顶点,建立网格邻居关系, 寻找可传送的边。这部分涉及到不少图形学内容。
7. 创建细节网格(允许访问每个多边形的近似高度)
创建细节网格rcPolyMeshDetail,高度细节使用rcHeightPatch表示,存储了网格的宽高,最低点高度等信息。
至此,navmesh数据已经可用了。
参考:
http://critterai.org/projects/nmgen_study/ 这篇文章研究的比较深入,推荐
https://blog.csdn.net/needmorecode/article/details/81416553
https://github.com/youlanhai/recastnavigation-learn
http://www.stevefsp.org/projects/rcndoc/prod/index.html
编辑于 2019-08-19