UE 5 NavMesh 烘培 逻辑流程

 
关于UE引擎层面的东西:
  1. 在向场景重拖入一个NavMeshBoundsVolume时(或者修改时). 会调用
void UNavigationSystemV1::PerformNavigationBoundsUpdate(const TArray& UpdateRequests)
然后会 创建/更新 一个NavigationData Actor对象到场景中, 名字默认是RecastNavMesh-Default
0
0
0
  1. 在增加NavMeshBoundsVolume后的调用堆栈如下. 可以看到最后是把请求放到了PendingDirtyTiles里了. 走异步烘培
void UNavigationSystemV1::PerformNavigationBoundsUpdate
void ANavigationData::OnNavigationBoundsChanged
NavDataGenerator->OnNavigationBoundsChanged();
void FRecastNavMeshGenerator::OnNavigationBoundsChanged()
void FRecastNavMeshGenerator::MarkDirtyTiles -> PendingDirtyTiles
  1. 异步烘培的驱动堆栈如下, 可以看到是从World的Tick到ProcessTileTasksAsyncAndGetUpdatedTiles函数里. 这个函数里取出来PendingDirtyTiles里记录的异步请求. 执行
TArray FRecastNavMeshGenerator::ProcessTileTasksAsyncAndGetUpdatedTiles
TArray FRecastNavMeshGenerator::ProcessTileTasksAndGetUpdatedTiles
void FRecastNavMeshGenerator::TickAsyncBuild(float DeltaSeconds)
void UNavigationSystemV1::Tick
void UWorld::Tick
  1. 在ProcessTileTasksAsyncAndGetUpdatedTiles函数里创建了一个TileTask(调用CreateTileGenerator函数构造一个Generator初始化了Task)
0
上面这一行很关键,一点点的解释下;
1) 查看FRecastTileGeneratorTask的定义, 可以看到FRecastTileGeneratorTask其实是一个FAsyncTask
0
2) MakeUnique 是个模板函数, 功能是首先调用CreateTileGenerator(PendingElement.Coord, PendingElement.DirtyAreas)这个函数, 用它的返回值作为参数构造FRecastTileGeneratorTask
也就是构造FAsyncTask
0
 
3) FRecastNavMeshGenerator::CreateTileGenerator这个函数接受了coord和dirtyAreas作为参数, 调用了另外一个模板函数ConstuctTileGeneratorImpl. 返回一个TSharedRef 类型
0
4) 接着看ConstuctTileGeneratorImpl模板函数, 使用this指针, 也就是FRecastNavMeshGenerator和Coord参数构造了一个FRecastTileGenerator, 并调用了FRecastTileGenerator的Setup函数, 传入了this和DirtyAreas参数.
然后把完成初始化的FRecastTileGenerator返回出去.
0
0
0
0
5) 然后回到2)步里创建Task的过程里. 使用CreateTileGenerator返回的TSharedRef作为参数构造了FAsyncTask
然后看FAsyncTask的构造函数, 可以看到FAsyncTask是一个模板类, 含有一个TTask Task成员, 就是模板的类型FRecastTileGeneratorWrapper, 构造函数里把接收到的参数传递给了Task的构造函数
也就是FRecastTileGeneratorWrapper的构造函数, FRecastTileGeneratorWrapper里的TileGenerator成员就被初始化成4)里构造并调用过Setup的FRecastTileGenerator对象了.
0
0
6) 综上, 就完成了一个FAsyncTask的构建, 其中持有了一个FRecastTileGenerator的实例. 也就是TileGenerator成员.
7) 构造完成后, 通过调用TileTask.Release方法, 从智能指针处获得FRecastTileGeneratorTask* 指针. 赋值给RunningElement的AsyncTask对象.
然后就可以拿AsyncTask来调用同步或者异步接口. 调用完成后把RunningElement塞到RunningDirtyTiles里缓存. 并从PendingDiryTiles里移除.
0
0
0
 
8) 然后就是FAsyncTask的异步Work调度逻辑了
 
0
0
异步的话
0
0
0
9) 由于Task是FRecastTileGeneratorWrapper 所以是调用了这里.
0
然后是FRecastTileGenerator的这里
0
然后是这里. 调用GenerateNavigationData
0
 
具体的烘培逻辑
  1. UE的NavMesh烘培代码入口在 bool FRecastTileGenerator::GenerateTile()
  2. 主要烘培代码:
bool FRecastTileGenerator::GenerateNavigationData(FNavMeshBuildContext& BuildContext)
bool FRecastTileGenerator::GenerateNavigationDataLayer(FNavMeshBuildContext& BuildContext, FTileCacheCompressor& TileCompressor, FTileCacheAllocator& GenNavAllocator, FTileGenerationContext& GenerationContext, int32 LayerIdx)
  1. 烘培完成后数据存储:
0
  1. 逻辑流程参考Recast是走的Sample_TempObstacles流程, 烘培区域选择的算法是RC_REGION_WATERSHED
 
 
 
 
 

2024.4.3 补充

 

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
RebuildAll 逻辑流程.

  1. 在NavigationSystemV1 Build函数里调用NavData的RebuildAll, 实际会转发调用到FRecastNavMeshGenerator::RebuildAll()
  2. z在Generator的RebuildAll里先根据Bounds 标记所有的DirtyArea: MarkNavBoundsDirty()
  3. 然后通过EnsureBuildCompletion函数(图1, 图3)确保收集每次要处理的Tile任务和确保这些Tile任务执行EnsureCompletion, 完成Build (图3)
  4. 然后在Task的EnsureCompletion里, 调用的就是Task的DoWork, Task可以参考前面的分析.

 

 

 
 
 
 

过一遍后续的烘培流程 (以Tile为单位, 整个NavMesh是由n*m个tile组成. 多线程烘培. 同时最多进行MaxTileGeneratorTasks个Tile烘培任务):

                               从这里开始 bool FRecastTileGenerator::DoWork()   →  bool FRecastTileGenerator::GenerateTile()

具体:

  1. 先调用GenerateCompressedLayers 生成CompressedLayers,  这里有完整的烘培流程的前半部分
    1. 构造高度场 CreateHeightField
    2. 栅格化并标记NavModify. ComputeRasterizationMasks
    3. 栅格化网格, RasterizeTriangles → RasterizeGeometry / RasterizeGeometryRecast → rcRasterizeTriangles
    4. 过滤一些无效的span. ApplyVoxelFilter + GenerateRecastFilter → rcFilterLowHangingWalkableObstacles + rcFilterLedgeSpans + rcFilterWalkableLowHeightSpans
    5. 构造Compact高度场 BuildCompactHeightField
    6. Erode 一下寻路Span. RecastErodeWalkable → rcErodeWalkableAndLowAreas + rcErodeWalkableArea
    7. 重头戏, 烘培nav Layer: RecastBuildLayers → RC_REGION_WATERSHED → rcBuildDistanceField + rcBuildHeightfieldLayers → rcGatherRegionsNoFilter
    8. 构造dtBuildTileCacheLayer, 压缩Layer: RecastBuildTileCache
  2. 在得到CompressedLayers之后,  我们只是完成了NavMesh烘培的前半部分, 即完成BuildRegion阶段.  后续还需要做区域优化, 形成轮廓,  简化轮廓,  构建PolyMesh, 生成DetailPolyMesh
  3. 上面提到的步骤,紧接着GenerateCompressedLayers之后调用GenerateNavigationData实现.
    1. GenerateNavigationData()
    2. GenerateNavigationDataLayer
      1. 解压缩 dtDecompressTileCacheLayer
      2. Rasterize obstacles 处理动态阻挡. MarkDynamicAreas(*GenerationContext.Layer);
      3. 重新烘培区域, 类似上面的步骤g: dtBuildTileCacheDistanceField + dtBuildTileCacheRegions → filterSmallRegions
      4. 构建轮廓 dtBuildTileCacheContours
      5. 构建PolyMesh, dtBuildTileCachePolyMesh
      6. 构建PolyMeshDetail, dtBuildTileCachePolyMeshDetail
      7. 处理offmeshLinks
      8. 最后完成NavMeshData的构建: dtCreateNavMeshData(&Params, &NavData, &NavDataSize))
      9. 构建UE的NavMesh对象FNavMeshTileData: GenerationContext.NavigationData.Add(FNavMeshTileData(NavData, NavDataSize, LayerIdx, CompressedData.LayerBBox));

 

从上面可以看出来, Recast的Sample_TempObstacles和Sample_TileMesh关键的区别就在于. TempObstacles会先把烘培的前半部分数据Compress一下, 会有CompressedLayers

这部分数据是场景Mesh得到的原始数据,再加上动态阻挡的部分之后. 再重新烘培区域, 简化区域, 烘培细节网格. 得到最终的NavMesh.

这样做的原因不难理解: 场景不变的数据, 先烘培好压缩保存起来, 如果这里出现了动态阻挡, 则把场景不变的数据解压出来, 叠加动态阻挡, 再重新烘培一下区域. 形成更新的Navmesh. 

所以这就是Sample_TempObstacles烘培的是支持动态阻挡的Dynamic NavMesh和Sample_TileMesh烘培的不支持动态阻挡的Static NavMesh 之间的区别. UE亦是如此.

 
 
 
 
 
 
 
posted @ 2023-08-16 17:57  wmalloc  阅读(468)  评论(0编辑  收藏  举报