Recast/Detour是Unity、Unreal都使用的导航中间件,不过不同引擎对它们的包装方式并不相同,所以使用上感觉还是有一些区别,部分项目服务器使用导航时甚至可能完全脱离Unreal、Unity引擎。
UE4中在生成导航后在范围内再标记特定的区域需要使用到NavModifier
它看起来就是可以帮你抠掉一个笔刷样子的导航区域使得这个区域不可以被导航
说抠掉其实不完全对,每个NavModifier可以选择一个AreaClass,每个AreaClass类型背后实际上对应一个8位的AreaID和16位的AreaFlag,它背后实际所做的是帮你标记一个区域,使得这个区域有对应的AreaId和AreaFlag,如果配合NavQueryFilter可以实现某个区域部分agent可以路过,而另一部分agent不可以。
https://www.zhihu.com/video/1575441313071038464
问题1.NavModifier Volume支持任意形状
UE的NavModifier只能用Convex Volume
这种限制的背后也是有原因的,判断两物体相交或者一个点在是否在一个空间内通常都需要使用到凸形状,非凸形状得通过Convex Decomposition算法分解为凸形状。在UE对应ModifyVolume生效的机制:判断体素化后的小方块(在Recast中称为span)是否在Convex Volume中
代码语言:javascript复制NAVMESH_API dtStatus dtMarkCylinderArea(dtTileCacheLayer& layer, const float* orig, const float cs, const float ch,
const float* pos, const float radius, const float height, const unsigned char areaId);
//@UE4 BEGIN: more shapes
NAVMESH_API dtStatus dtMarkBoxArea(dtTileCacheLayer& layer, const float* orig, const float cs, const float ch,
const float* pos, const float* extent, const unsigned char areaId);
NAVMESH_API dtStatus dtMarkConvexArea(dtTileCacheLayer& layer, const float* orig, const float cs, const float ch,
const float* verts, const int nverts, const float hmin, const float hmax, const unsigned char areaId);
NAVMESH_API dtStatus dtReplaceCylinderArea(dtTileCacheLayer& layer, const float* orig, const float cs, const float ch,
const float* pos, const float radius, const float height, const unsigned char areaId,
const unsigned char filterAreaId);
NAVMESH_API dtStatus dtReplaceBoxArea(dtTileCacheLayer& layer, const float* orig, const float cs, const float ch,
const float* pos, const float* extent, const unsigned char areaId, const unsigned char filterAreaId);
NAVMESH_API dtStatus dtReplaceConvexArea(dtTileCacheLayer& layer, const float* orig, const float cs, const float ch,
const float* verts, const int nverts, const float hmin, const float hmax, const unsigned char areaId,
const unsigned char filterAreaId);
可以看到,修改Area的函数只有这几个,UE背后也是这样来用的
代码语言:javascript复制 switch (Modifier.GetShapeType())
{
case ENavigationShapeType::Cylinder:
...
case ENavigationShapeType::Box:
...
case ENavigationShapeType::Convex:
...
case ENavigationShapeType::InstancedConvex:
...
default: break;
}
这里有个巧妙的思路,可以直接在代码中拿生成过Convex Collision的Mesh作为Volume
代码语言:javascript复制FAreaNavModifier::FAreaNavModifier(const FKConvexElem& Convex, const FTransform& LocalToWorld, const TSubclassOf<UNavAreaBase> InAreaClass)
{
TArray<FVector> Verts;
for (int32 VertexIndex = 0; VertexIndex < Convex.VertexData.Num(); VertexIndex )
{
Verts.AddUnique(Convex.VertexData[VertexIndex]);
}
Init(InAreaClass);
SetConvex(Verts.GetData(), 0, Verts.Num(), ENavigationCoordSystem::Unreal, LocalToWorld);
}
扩展一下Modifier,这样就可以拿任意物体作为Volume形状了
问题2.NavModify物体,而非Volume
有了根据任意Convex Volume标记导航Area的机制但实际上还是不太够,比如游戏中有大量程序化生成不规则形状的面片(比如水面),我们想要标记这些面片为某种区域,但我们不太可能在DDC软件中先拉伸再塞回来生成Convex Collision再塞到Modifier里面去吧?这样不仅流程麻烦而影响内存和性能(在大世界游戏中,客户端本地的导航网格可能是动态生成)
UE Modifier或许是从动态障碍物出发考虑的,Dynamic生成模式下可缓存heightfeild数据
代码语言:javascript复制 if (bRegenerateCompressedLayers)
{
CompressedLayers.Reset();
bSuccess = GenerateCompressedLayers(BuildContext);
if (bSuccess)
{
// Mark all layers as dirty
DirtyLayers.Init(true, CompressedLayers.Num());
}
}
if (bSuccess)
{
bSuccess = GenerateNavigationData(BuildContext);
}
在几何上具体生效的机制是遍历每个Modifier执行问题1中的dtMarkBoxArea等函数
代码语言:javascript复制 for (const FRecastAreaNavModifierElement& Element : Modifiers)
{
for (const FAreaNavModifier& Area : Element.Areas)
{
但是我们如果并不是想标记一个Volume内的Area,而是想标记某个物体的表面是某个Area,真的有必要这么麻烦么?如果场景中有大量自动生成的Modifier,可以想象到这个过程明显十分浪费。
似乎这从理论上来说并不是必须的,一个三角形产生的体素对应的是什么Area,我们由这个三角形本身的信息就可以知道。
Recast生成的大致流程:
rcMarkWalkableTriangles 根据坡度标记可行走(实际是标记Walkable的AreaID)
rcRasterizeTriangles 体素化三角形(这一步中会生成span,span重叠合并span,span可以理解为实心小长方体)
rcFilterLowHangingWalkableObstacles
rcFilterLedgeSpans
rcFilterWalkableLowHeightSpans
rcBuildCompactHeightfield(这里生成compactSpan,即Span之间的空心部分)
rcErodeWalkableArea(根据半径缩小可行走区域,首先会构建SDF距离场,AreaID为NULL的compactSpan为边界)
---区域划分
rcBuildDistanceField(也是构建距离场,这里会判断areaID是否相等)
代码语言:javascript复制 for (int dir = 0; dir < 4; dir)
{
if (rcGetCon(s, dir) != RC_NOT_CONNECTED)
{
const int ax = x rcGetDirOffsetX(dir);
const int ay = y rcGetDirOffsetY(dir);
const int ai = (int)chf.cells[ax ay*w].index rcGetCon(s, dir);
if (area == chf.areas[ai])
nc ;
}
}
if (nc != 4)
src[i] = 0;
src是距边界距离,后续就是取轮廓点了生成区域等步骤了。
思考到这里似乎就可以有了一个思路,其实我们可以考虑在体素化三角形的时候就将AreaID使用上,另外在后续过程中将AreaFlags对应上
代码语言:javascript复制 // fill flags, or else detour won't be able to find polygons
// Update poly flags from areas.
for (int32 i = 0; i < GenerationContext.PolyMesh->npolys; i )
{
auto areaId = GenerationContext.PolyMesh->areas[i];
auto areaFlag = AdditionalCachedData.FlagsPerArea[areaId];
if(NavSystem->PreDefineAreas.Contains(areaId))
{
areaFlag = NavSystem->PreDefineAreas[areaId].GetAreaFlags();
}
GenerationContext.PolyMesh->flags[i] = areaFlag;
}
实际证明这最终是可行的
配合一套周边修改,就可以在UE4中做到直接标记一个物体为某种Area了(这似乎在Unity中是很方便的),不过对Span进行Area修改或许还可以有一些更巧妙的操作,比如游戏中水面很多是面片,对于空旷的水域(水中没有漂浮物)可以很方便标记水面和水底,配合物理引擎将材质信息写入碰撞中或许可以进一步做到根据材质来标记Area等,也曾有人分享过通过修改Recast生成的其它阶段来进行NavLink的自动化生成。
其它.方便设置AreaFlag
AreaID和AreaFlag都可以做到导航筛选,对普通用户来说差别可能并不大,只有在动态修改的时候可能产生一些差别(PolyFlags と AreaFlags の違いについて),因为Flag和ID记录的位置不一样。
Unreal对Area的包装并不能在NavModifier里面方便地设置AreaFlag,因为在Modifier Volume中可以选择的几个类都是写死的
哪怕你创建蓝图继承自NavArea,也不能设置AreaFlag
可以考虑简单继承扩展一下NavArea
代码语言:javascript复制UNavArea_Flags::UNavArea_Flags(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer),
bNavFlag0(0), bNavFlag1(0), bNavFlag2(0), bNavFlag3(0),
bNavFlag4(0), bNavFlag5(0), bNavFlag6(0), bNavFlag7(0),
bNavFlag8(0), bNavFlag9(0), bNavFlag10(0), bNavFlag11(0),
bNavFlag12(0), bNavFlag13(0), bNavFlag14(0), bNavFlag15(0)
{
DrawColor = FColor(127, 127, 0); // brownish
}
uint16 UNavArea_Flags::GetAreaFlags() const
{
return bNavFlag0 | (bNavFlag1 << 1) | (bNavFlag2 << 2) | (bNavFlag3 << 3)
| (bNavFlag4 << 4) | (bNavFlag5 << 5) | (bNavFlag6 << 6) | (bNavFlag7 << 7)
| (bNavFlag8 << 8) | (bNavFlag9 << 9) | (bNavFlag10 << 10) | (bNavFlag11 << 11)
| (bNavFlag12 << 12) | (bNavFlag13 << 13) | (bNavFlag14 << 14) | (bNavFlag15 << 15);
}
这样我们就可以用新建蓝图的方式创建新的Area以及设置AreaFlag了