Unity的导航网格拥有32种不同的AreaType类型,我发现了这么一个情况,对于两个重叠在一起的不同AreaType类型的物体,最终的导航网格到底是按照哪一个?
如下图所示的例子,展示了第一种情况,较高的Cube为Walkable类型,较矮的为自定义的类型,设置完成后Bake导航网格,可以看到地面表面是什么物体,出现的就是什么类型的导航网格。
再来看看第二种情况,如果再把矮的Cube拉高一点,让二者高度更接近,如下图所示:
再去Build导航网格,会发现出现的都是较矮的Cube的导航网格类型,如下图所示:
至于为什么会这样,这涉及到了CellHeight的精度问题与底层体素合并的机制,由于Unity的导航网格是基于RecastDemo生成的,所以就拿RecastDemo的代码进行举例。
RecastDemo里是在每一列的空间里,对每一个三角形面进行体素化的,这里有两个Cube,每一个Cube有12个三角形面,一共是36个面,但是实际参与导航过程的只有一个Cube的上下面,如果取Cube的中间竖列来计算,则一共能生成4个体素(每个Cube上下面各对应一个体素),如下图所示,四个圈圈代表在中间列得到的四个体素:
拿第一种情况举例,得到的4个体素形成了一个链表,数据如下:
(0,2,0) -> (3,4,0) -> (80,81,69) -> (83,84,63)
解释一下,体素链表是空间上从往上生成的,第一个体素数据为(0,2,0),代表其下表面高度为0,上表面高度为2,AreaType为0,0代表着NULL_Area,因为该体素是正方体的底部,法向量判断该地方视作不存在体素
其他的AreaType,上面的63对应的Walkable,69对应的63 + 3*2 = 69,这里的3对应下面的序号差,每一个AreaType之间差为2,如下图所示:
所以在第一阶段体素化后,得到了链表体素数据(0,2,0) -> (3,4,0) -> (80,81,69) -> (83,84,63),由于四个体素之间没有交集
在体素化完成之后,RecastDemo里会有这么一个函数,代码如下所示:
//如果某Span的上方Span与其间隔小于walkableHeight,则标记为span->area = RC_NULL_AREA
if (m_filterWalkableLowHeightSpans)
rcFilterWalkableLowHeightSpans(m_ctx, m_cfg.walkableHeight, *m_solid);
函数代码如下:
void rcFilterWalkableLowHeightSpans(rcContext* ctx, int walkableHeight, rcHeightfield& solid)
{
rcAssert(ctx);
rcScopedTimer timer(ctx, RC_TIMER_FILTER_WALKABLE);
const int w = solid.width;
const int h = solid.height;
const int MAX_HEIGHT = 0xffff;
// Remove walkable flag from spans which do not have enough
// space above them for the agent to stand there.
for (int y = 0; y < h; ++y)
{
for (int x = 0; x < w; ++x)
{
for (rcSpan* s = solid.spans[x + y*w]; s; s = s->next)
{
const int bot = (int)(s->smax);
const int top = s->next ? (int)(s->next->smin) : MAX_HEIGHT;
if ((top - bot) <= walkableHeight)
s->area = RC_NULL_AREA;
}
}
}
}
可以看到,由于(80,81,69) -> (83,84,63)两个体素的距离为2,小于角色的高度,所以体素会被标记为NULL_AREA,最后的序列变为:
0,2,0) -> (3,4,0) -> (80,81,0) -> (83,84,63)
对于情况1,所以最后得到的颜色为正常Walkable的Layer
而第二种情况,两个正方体上表面距离更接近,其体素化的序列变为了:
(0,2,0) -> (1,2,0) -> (80,81,69) -> (81,82,63)
对于后面两个体素,明显二者是可以合并的,合并的代码如下:
static bool addSpan(rcHeightfield& hf, const int x, const int y,
const unsigned short smin, const unsigned short smax,
const unsigned char area, const int flagMergeThr)
{
int idx = x + y*hf.width;
rcSpan* s = allocSpan(hf);
if (!s)
return false;
s->smin = smin;
s->smax = smax;
s->area = area;
s->next = 0;
// Empty cell, add the first span.
if (!hf.spans[idx])
{
hf.spans[idx] = s;
return true;
}
rcSpan* prev = 0;
rcSpan* cur = hf.spans[idx];
// Insert and merge spans.
while (cur)
{
if (cur->smin > s->smax)
{
// Current span is further than the new span, break.
break;
}
else if (cur->smax < s->smin)
{
// Current span is before the new span advance.
prev = cur;
cur = cur->next;
}
else
{
// Merge spans.
if (cur->smin < s->smin)
s->smin = cur->smin;
if (cur->smax > s->smax)
s->smax = cur->smax;
// Merge flags.二者合并后会取更大的Area类型
if (rcAbs((int)s->smax - (int)cur->smax) <= flagMergeThr)
s->area = rcMax(s->area, cur->area);
// Remove current span.
rcSpan* next = cur->next;
freeSpan(hf, cur);
if (prev)
prev->next = next;
else
hf.spans[idx] = next;
cur = next;
}
}
// Insert new span.
if (prev)
{
s->next = prev->next;
prev->next = s;
}
else
{
s->next = hf.spans[idx];
hf.spans[idx] = s;
}
return true;
}
代码中可以看到,合并时会取更大的Area的值,所以合并后链表变为:
(0,2,0) -> (1,2,0) -> (80,82,69)
所以中间部分会显示红色,显示了自定义名为3的Layer.
结论
(1) 对于同一位置不同高度的表面,当高度差距较大时,谁在更上方,就选取谁作为最终的网格类型,不过当二者高度比较接近的时候,这个时候就要看导航网格的精度了。默认导航网格的CellHeight是CellSize的一半,当CellSize比较大的时候,两个高度接近的表面更容易发生Layer的合并。
也就是说,导航网格是否合并的高度差与CellHeight正相关,当VoxelSize设为2m时,高度差很大的情况下仍然合并了导航Layer,如下图所示,里面的小Cube边长为1m
(2)Unity一共提供了32个Layer(如果算上代码里的NULL_AREA则是33个Layer),如下图所示,其中UnWakable的Area的优先级是最大的,因为Unity对Unwalkable类型的区域做了导航网格剔除操作,其它的就按照正常顺序排列,其他31个类型里,Walkable类型的Area优先级最低,User31的Area优先级最高