Navmesh 的寻路依赖于导航网格的生成,本文将基于 Recast Navigation 源码进行分析。
简介
初始化生成配置
- 导航网格的生成配置结构
rcConfig
如下:
// Recast/Include/Recast.h
...
struct rcConfig
{
/// The width of the field along the x-axis. [Limit: >= 0] [Units: vx]
int width;
/// The height of the field along the z-axis. [Limit: >= 0] [Units: vx]
int height;
/// The width/height size of tile's on the xz-plane. [Limit: >= 0] [Units: vx]
int tileSize;
/// The size of the non-navigable border around the heightfield. [Limit: >=0] [Units: vx]
int borderSize;
/// The xz-plane cell size to use for fields. [Limit: > 0] [Units: wu]
float cs;
/// The y-axis cell size to use for fields. [Limit: > 0] [Units: wu]
float ch;
/// The minimum bounds of the field's AABB. [(x, y, z)] [Units: wu]
float bmin[3];
/// The maximum bounds of the field's AABB. [(x, y, z)] [Units: wu]
float bmax[3];
/// The maximum slope that is considered walkable. [Limits: 0 <= value < 90] [Units: Degrees]
float walkableSlopeAngle;
/// Minimum floor to 'ceiling' height that will still allow the floor area to
/// be considered walkable. [Limit: >= 3] [Units: vx]
int walkableHeight;
/// Maximum ledge height that is considered to still be traversable. [Limit: >=0] [Units: vx]
int walkableClimb;
/// The distance to erode/shrink the walkable area of the heightfield away from
/// obstructions. [Limit: >=0] [Units: vx]
int walkableRadius;
/// The maximum allowed length for contour edges along the border of the mesh. [Limit: >=0] [Units: vx]
int maxEdgeLen;
/// The maximum distance a simplified contour's border edges should deviate
/// the original raw contour. [Limit: >=0] [Units: vx]
float maxSimplificationError;
/// The minimum number of cells allowed to form isolated island areas. [Limit: >=0] [Units: vx]
int minRegionArea;
/// Any regions with a span count smaller than this value will, if possible,
/// be merged with larger regions. [Limit: >=0] [Units: vx]
int mergeRegionArea;
/// The maximum number of vertices allowed for polygons generated during the
/// contour to polygon conversion process. [Limit: >= 3]
int maxVertsPerPoly;
/// Sets the sampling distance to use when generating the detail mesh.
/// (For height detail only.) [Limits: 0 or >= 0.9] [Units: wu]
float detailSampleDist;
/// The maximum distance the detail mesh surface should deviate from heightfield
/// data. (For height detail only.) [Limit: >=0] [Units: wu]
float detailSampleMaxError;
};
...
rcConfig
参数的含义如下:width
- 场在 x 轴上的宽度,由网格包围盒的宽度换算成单元格表示。
height
- 场在 z 轴上的高度,由网格包围盒的高度换算成单元格表示。
tileSize
- xz 平面上的瓦片大小。
borderSize
- 高度场不可导航边界的大小。
cs
- 场在 xz 平面上的单元格大小。
ch
- 场在 y 轴上的单元格大小。
bmin[3]
- 场的 AABB 包围盒的最小边界。
bmax[3]
- 场的 AABB 包围盒的最大边界。
walkableSlopeAngle
- 可行走的最大斜坡角度(0° ~ 90°)。
walkableHeight
- 可行走的最小高度(地面到天花板的距离,>= 3)。
walkableClimb
- 可通行的最大高度。
walkableRadius
- 在生成可行走区域时,需要从障碍物边缘向内收缩(侵蚀)的距离。
maxEdgeLen
- 轮廓边缘在网格边界上的最大长度。
maxSimplificationError
- 简化后的轮廓边缘和原轮廓的最大允许偏差。。
minRegionArea
- 形成孤岛区域的最小单元格数量。
mergeRegionArea
- 区域包含的单元格数量少于该值时,尝试将此区域合并到更大的相邻区域。
maxVertsPerPoly
- 单个多边形的最大顶点数(>= 3)。
detailSampleDist
- 生成细节网格(高度细节)的采样间距(0 或 >= 0.9)。
detailSampleMaxError
- 细节网格(高度细节)表面与原始高度场数据的最大允许偏差。
光栅化输入的多边形汤
- 输入的原始 3D 模型数据,通常是一组无序的三角形(或其他多边形),没有明确的拓扑结构或层级关系,这些多边形可能重叠、重复或不封闭,统称为多边形汤。
标记可行走三角面
- 通过
rcMarkWalkableTriangles
方法,将坡度小于walkableSlopeAngle
的三角面标记为可行走区域,其实现如下:
// Recast/Source/Recast.cpp
...
void rcMarkWalkableTriangles(rcContext* context, const float walkableSlopeAngle,
const float* verts, const int numVerts,
const int* tris, const int numTris,
unsigned char* triAreaIDs)
{
rcIgnoreUnused(context);
rcIgnoreUnused(numVerts);
const float walkableThr = cosf(walkableSlopeAngle / 180.0f * RC_PI);
float norm[3];
for (int i = 0; i < numTris; ++i)
{
const int* tri = &tris[i * 3];
calcTriNormal(&verts[tri[0] * 3], &verts[tri[1] * 3], &verts[tri[2] * 3], norm);
// Check if the face is walkable.
if (norm[1] > walkableThr)
{
triAreaIDs[i] = RC_WALKABLE_AREA;
}
}
}
...
- 其中,
calcTriNormal
方法用于计算三角面的归一化法线。三角面和法线的示意图如下: - 三角面和 xz 平面的夹角即为坡度 θ,而法线和 y 轴的夹角也为 θ。由于法线为归一化,则法线在 y 轴上的投影即为
cosθ
。随着坡度 θ 增大,cosθ
逐渐减小,当坡度 θ 小于walkableSlopeAngle
时,该三角面则为可行走区域,将三角面的areaID
设置为RC_WALKABLE_AREA
。
光栅化三角面
- 光栅化三角面的过程,主要是对每个三角面,通过
rasterizeTri
方法进行处理,其实现如下:
// Recast/Source/RecastRasterization.cpp
static bool rasterizeTri(const float* v0, const float* v1, const float* v2,
const unsigned char areaID, rcHeightfield& heightfield,
const float* heightfieldBBMin, const float* heightfieldBBMax,
const float cellSize, const float inverseCellSize, const float inverseCellHeight,
const int flagMergeThreshold)
{
// Calculate the bounding box of the triangle.
float triBBMin[3];
rcVcopy(triBBMin, v0);
rcVmin(triBBMin, v1);
rcVmin(triBBMin, v2);
float triBBMax[3];
rcVcopy(triBBMax, v0);
rcVmax(triBBMax, v1);
rcVmax(triBBMax, v2);
// If the triangle does not touch the bounding box of the heightfield, skip the triangle.
if (!overlapBounds(triBBMin, triBBMax, heightfieldBBMin, heightfieldBBMax))
{
return true;
}
const int w = heightfield.width;
const int h = heightfield.height;
const float by = heightfieldBBMax[1] - heightfieldBBMin[1];
// Calculate the footprint of the triangle on the grid's z-axis
int z0 = (int)((triBBMin[2] - heightfieldBBMin[2]) * inverseCellSize);
int z1 = (int)((triBBMax[2] - heightfieldBBMin[2]) * inverseCellSize);
// use -1 rather than 0 to cut the polygon properly at the start of the tile
z0 = rcClamp(z0, -1, h - 1);
z1 = rcClamp(z1, 0, h - 1);
// Clip the triangle into all grid cells it touches.
float buf[7 * 3 * 4];
float* in = buf;
float* inRow = buf + 7 * 3;
float* p1 = inRow + 7 * 3;
float* p2 = p1 + 7 * 3;
rcVcopy(&in[0], v0);
rcVcopy(&in[1 * 3], v1);
rcVcopy(&in[2 * 3], v2);
int nvRow;
int nvIn = 3;
for (int z = z0; z <= z1; ++z)
{
// Clip polygon to row. Store the remaining polygon as well
const float cellZ = heightfieldBBMin[2] + (float)z * cellSize;
dividePoly(in, nvIn, inRow, &nvRow, p1, &nvIn, cellZ + cellSize, RC_AXIS_Z);
rcSwap(in, p1);
if (nvRow < 3)
{
continue;
}
if (z < 0)
{
continue;
}
// find X-axis bounds of the row
float minX = inRow[0];
float maxX = inRow[0];
for (int vert = 1; vert < nvRow; ++vert)
{
if (minX > inRow[vert * 3])
{
minX = inRow[vert * 3];
}
if (maxX < inRow[vert * 3])
{
maxX = inRow[vert * 3];
}
}
int x0 = (int)((minX - heightfieldBBMin[0]) * inverseCellSize);
int x1 = (int)((maxX - heightfieldBBMin[0]) * inverseCellSize);
if (x1 < 0 || x0 >= w)
{
continue;
}
x0 = rcClamp(x0, -1, w - 1);
x1 = rcClamp(x1, 0, w - 1);
int nv;
int nv2 = nvRow;
for (int x = x0; x <= x1; ++x)
{
// Clip polygon to column. store the remaining polygon as well
const float cx = heightfieldBBMin[0] + (float)x * cellSize;
dividePoly(inRow, nv2, p1, &nv, p2, &nv2, cx + cellSize, RC_AXIS_X);
rcSwap(inRow, p2);
if (nv < 3)
{
continue;
}
if (x < 0)
{
continue;
}
// Calculate min and max of the span.
float spanMin = p1[1];
float spanMax = p1[1];
for (int vert = 1; vert < nv; ++vert)
{
spanMin = rcMin(spanMin, p1[vert * 3 + 1]);
spanMax = rcMax(spanMax, p1[vert * 3 + 1]);
}
spanMin -= heightfieldBBMin[1];
spanMax -= heightfieldBBMin[1];
// Skip the span if it's completely outside the heightfield bounding box
if (spanMax < 0.0f)
{
continue;
}
if (spanMin > by)
{
continue;
}
// Clamp the span to the heightfield bounding box.
if (spanMin < 0.0f)
{
spanMin = 0;
}
if (spanMax > by)
{
spanMax = by;
}
// Snap the span to the heightfield height grid.
unsigned short spanMinCellIndex = (unsigned short)rcClamp((int)floorf(spanMin * inverseCellHeight), 0, RC_SPAN_MAX_HEIGHT);
unsigned short spanMaxCellIndex = (unsigned short)rcClamp((int)ceilf(spanMax * inverseCellHeight), (int)spanMinCellIndex + 1, RC_SPAN_MAX_HEIGHT);
if (!addSpan(heightfield, x, z, spanMinCellIndex, spanMaxCellIndex, areaID, flagMergeThreshold))
{
return false;
}
}
}
return true;
}
...
- 光栅化过程就是将三角面沿着 z 轴和 x 轴进行切分,再计算 y 高度,从而得到多个长方体,主要步骤如下:
- 计算三角面包围盒的最大最小值,检查包围盒是否和高度场的包围盒有交集,无交集则不需要处理。
- 将三角面包围盒的 z 坐标最大最小值,以高度场包围盒 z 坐标最小值为基准,转化为单元格 z' 表示。
- 将 z' 最小值限制为 [-1, heightfield.height - 1] ,最大值限制为 [0, heightfield.height - 1] 。
- 遍历 z' 坐标最小值到最大值区间,通过
dividePoly
方法,将三角面按单元格 z' 切分成多个多边形。 - 对每个切分后的多边形,计算包围盒的 x 坐标最大最小值,,以高度场包围盒 x 坐标最小值为基准,转化为单元格 x' 表示。
- 将 x' 最小值限制为 [-1, heightfield.width - 1] , 最大值限制为 [0, heightfield.width - 1] 。
- 遍历 x' 坐标最小值到最大值区间,通过
dividePoly
方法,将多边形按单元格 x' 坐标再切分成多个子多边形。 - 对每个子多边形,计算顶点的 y 坐标最大最小值,以高度场包围盒 y 坐标最小值为基准。
- 将顶点 y 坐标最大最小值,转换成单元格索引 y' 表示,最小值为 [0,
RC_SPAN_MAX_HEIGHT
] ,最大值为 [最小值 + 1,RC_SPAN_MAX_HEIGHT
] 。 - 调用
addSpan
方法,将当前 x' 、z' 对应的单元格的 y' 索引区间,加入到高度场中。
- 分割多边形方法
dividePoly
的实现如下:
// Recast/Source/RecastRasterization.cpp
static void dividePoly(const float* inVerts, int inVertsCount,
float* outVerts1, int* outVerts1Count,
float* outVerts2, int* outVerts2Count,
float axisOffset, rcAxis axis)
{
rcAssert(inVertsCount <= 12);
// How far positive or negative away from the separating axis is each vertex.
float inVertAxisDelta[12];
for (int inVert = 0; inVert < inVertsCount; ++inVert)
{
inVertAxisDelta[inVert] = axisOffset - inVerts[inVert * 3 + axis];
}
int poly1Vert = 0;
int poly2Vert = 0;
for (int inVertA = 0, inVertB = inVertsCount - 1; inVertA < inVertsCount; inVertB = inVertA, ++inVertA)
{
// If the two vertices are on the same side of the separating axis
bool sameSide = (inVertAxisDelta[inVertA] >= 0) == (inVertAxisDelta[inVertB] >= 0);
if (!sameSide)
{
float s = inVertAxisDelta[inVertB] / (inVertAxisDelta[inVertB] - inVertAxisDelta[inVertA]);
outVerts1[poly1Vert * 3 + 0] = inVerts[inVertB * 3 + 0] + (inVerts[inVertA * 3 + 0] - inVerts[inVertB * 3 + 0]) * s;
outVerts1[poly1Vert * 3 + 1] = inVerts[inVertB * 3 + 1] + (inVerts[inVertA * 3 + 1] - inVerts[inVertB * 3 + 1]) * s;
outVerts1[poly1Vert * 3 + 2] = inVerts[inVertB * 3 + 2] + (inVerts[inVertA * 3 + 2] - inVerts[inVertB * 3 + 2]) * s;
rcVcopy(&outVerts2[poly2Vert * 3], &outVerts1[poly1Vert * 3]);
poly1Vert++;
poly2Vert++;
// add the inVertA point to the right polygon. Do NOT add points that are on the dividing line
// since these were already added above
if (inVertAxisDelta[inVertA] > 0)
{
rcVcopy(&outVerts1[poly1Vert * 3], &inVerts[inVertA * 3]);
poly1Vert++;
}
else if (inVertAxisDelta[inVertA] < 0)
{
rcVcopy(&outVerts2[poly2Vert * 3], &inVerts[inVertA * 3]);
poly2Vert++;
}
}
else
{
// add the inVertA point to the right polygon. Addition is done even for points on the dividing line
if (inVertAxisDelta[inVertA] >= 0)
{
rcVcopy(&outVerts1[poly1Vert * 3], &inVerts[inVertA * 3]);
poly1Vert++;
if (inVertAxisDelta[inVertA] != 0)
{
continue;
}
}
rcVcopy(&outVerts2[poly2Vert * 3], &inVerts[inVertA * 3]);
poly2Vert++;
}
}
*outVerts1Count = poly1Vert;
*outVerts2Count = poly2Vert;
}
...
- 分割多边形的主要步骤为:
- 沿指定轴
axis
方向,计算分割面和每个顶点的的指定轴坐标差值,用于确定顶点是否在分割面以内。 - 遍历每一个顶点,根据当前顶点 A 和上一个顶点 B所处位置进行处理。
- 如果 A 、B 处在分割面两侧:
- 计算 B 点到分割面的距离占 AB 到分割面距离和的比例。
- 根据比例,通过 A 、B 的坐标插值得到 AB 和分割面的交点 C 坐标。
- 将 C 点加入到分割面内顶点列表
outVerts1
和分割面外顶点列表outVerts2
中。 - 根据 A 点和分割面的位置关系,将 A 点加入到对应顶点列表中。
- 如果 A 、B 处在分割面内侧,将 A 点加入到分割面内顶点列表
outVerts1
。 - 如果 A 、B 处在分割面外侧,将 A 点加入到分割面外顶点列表
outVerts2
。
- 如果 A 、B 处在分割面两侧:
- 沿指定轴
- 高度场添加跨度信息
addSpan
方法的实现如下:
// Recast/Source/RecastRasterization.cpp
const int x, const int z,
const unsigned short min, const unsigned short max,
const unsigned char areaID, const int flagMergeThreshold)
{
// Create the new span.
rcSpan* newSpan = allocSpan(heightfield);
if (newSpan == NULL)
{
return false;
}
newSpan->smin = min;
newSpan->smax = max;
newSpan->area = areaID;
newSpan->next = NULL;
const int columnIndex = x + z * heightfield.width;
rcSpan* previousSpan = NULL;
rcSpan* currentSpan = heightfield.spans[columnIndex];
// Insert the new span, possibly merging it with existing spans.
while (currentSpan != NULL)
{
if (currentSpan->smin > newSpan->smax)
{
// Current span is completely after the new span, break.
break;
}
if (currentSpan->smax < newSpan->smin)
{
// Current span is completely before the new span. Keep going.
previousSpan = currentSpan;
currentSpan = currentSpan->next;
}
else
{
// The new span overlaps with an existing span. Merge them.
if (currentSpan->smin < newSpan->smin)
{
newSpan->smin = currentSpan->smin;
}
if (currentSpan->smax > newSpan->smax)
{
newSpan->smax = currentSpan->smax;
}
// Merge flags.
if (rcAbs((int)newSpan->smax - (int)currentSpan->smax) <= flagMergeThreshold)
{
// Higher area ID numbers indicate higher resolution priority.
newSpan->area = rcMax(newSpan->area, currentSpan->area);
}
// Remove the current span since it's now merged with newSpan.
// Keep going because there might be other overlapping spans that also need to be merged.
rcSpan* next = currentSpan->next;
freeSpan(heightfield, currentSpan);
if (previousSpan)
{
previousSpan->next = next;
}
else
{
heightfield.spans[columnIndex] = next;
}
currentSpan = next;
}
}
// Insert new span after prev
if (previousSpan != NULL)
{
newSpan->next = previousSpan->next;
previousSpan->next = newSpan;
}
else
{
// This span should go before the others in the list
newSpan->next = heightfield.spans[columnIndex];
heightfield.spans[columnIndex] = newSpan;
}
}
- 跨度信息即多边形的 y' 格子坐标跨度,在高度场中,跨度信息通过链表存储,记录了每一个 x’z' 单元格的所有跨度区间,区间按坐标从小到大顺序存储。插入新区间时,同样需要保持从小到大的顺序,找到插入位置。如果新区间和原有区间有相交,则会将两个区间进行合并。