2021-02-25

转载于https://zhuanlan.zhihu.com/p/74537236
UE4 Navmesh寻路(一)Recast基础

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步:创建体素模型,把模型分割成简单的区域,把这些区域再分割成简单多边形(凸的)

  1. 通过把输入的三角形mesh进行光栅化,形成一个多层的高度场,就能得到体素模型。之后可以对体素模型做一些简单的过滤,去掉玩家不可达的位置
  2. 体素模型描述的可行走区域被划分为重叠的2D区域,这些区域只有一个未重叠的等高线,这可以大大简化最后一步处理步骤
  3. 首先沿着边界划这些区域并进行化简可以剥离出导航多边形。然后把这些导航多边形处理为凸多边形,凸多边形可以更好的用于寻路和对场景进行空间推理。

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生成的过程适合离线处理。

 

  1. 填写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步:创建体素模型,把模型分割成简单的区域,把这些区域再分割成简单多边形(凸的)

  1. 通过把输入的三角形mesh进行光栅化,形成一个多层的高度场,就能得到体素模型。之后可以对体素模型做一些简单的过滤,去掉玩家不可达的位置
  2. 体素模型描述的可行走区域被划分为重叠的2D区域,这些区域只有一个未重叠的等高线,这可以大大简化最后一步处理步骤
  3. 首先沿着边界划这些区域并进行化简可以剥离出导航多边形。然后把这些导航多边形处理为凸多边形,凸多边形可以更好的用于寻路和对场景进行空间推理。

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生成的过程适合离线处理。

 

  1. 填写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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值