RecastNavigation------体素化和高度场生成解析
本文参考链接:http://critterai.org/projects/nmgen_study/voxelization.html,如有错误,欢迎批评指正。
像素化是将平面上的2D图像转化为一个个小正方形,与此类似,RecastNavigation的体素化过程是把空间几何体转换为一个个小长方体的组合(与游戏:我的世界相似)
体素化过程如下:
一. 将整个场景体素化
对于任何一个在欧几里得坐标系里的场景,都可以找到一个完全包含场景的三边与xyz轴平行的最小长方体,组成长方体的体素的长和宽相同,均为cellSize,对应的高为cellHeight(以下简称cs和ch),可将场景对应的长方体进行体素化,如下图所示:
二.对组成场景内每一个物体的三角形面,进行体素化。
三维空间的每一个物体,由面组成,对于一个用三角形面组成的模型。模型的体素化过程,可以转换成对众多三角形面的体素化过程,三角形的体素化如图所示:
下面具体分析将三角形面体素化的过程。
2.1 对于任意三角形面,找到其在xz平面上的投影,基于此投影三角形,找出其对应的xz平面上的column的集合(一个体素的竖直列叫做一个column)
2.2 遍历每一个蓝色格点对应的column,检测与三角形是否相交,若相交则找出与三角形相交的部分,如图所示,利用立方体与面的相交算法,找出其相交的多边形,取得多边形在高度上的最大值和最小值,根据这两个值可以决定出column上应该产生几个固体体素。
三.基于体素化后的场景,生成对应的高度场
介绍下什么是Span,Span由column对应的一竖列的体素组成的,下图所示的两个连续的体素,可以组成一个Span,如下图所示:
联系上述体素化过程,生成高度场的基于以下三个条件:
- 高度场的每一个Span会在三角形面投影下的volumn中生成,即,Span的生成是在三角面的遍历上产生的
- 生成的Span的最小值为该volumn与三角形相交多边形的最低点,最大值为多边形的最高点
- 对三角形截面的坡度与角色设置的爬坡高度进行比较,标记span是否为traversable
由于场景的面可能会很多,势必会产生很多有交叉重叠的Span,为了减少重复, Span的生成和合并策略如下:
- 若Span在竖直方向上没有任何与其相接触的其他Span,则创建该Span
- 若Span在竖直方向上有与其相接触的其他Span,则合并两个Span,具体合并Span很简单,只要把两个Span空间结合起来就可以了
对于Span,角色只可能在其上表面行走,所以每一个Span均有一个判断参数,用来判断Span是否为上表面可通行的(Traversable),比如角色爬坡角度为45度,若Span的上表面对应的多边形的竖直方向的倾斜度小于45度,则该Span是可通行的。
关于其traversable参数的合并规则如下,假设原有Span为A,新的Span为B,:
1.若A与B的上表面高度相同,则Span上高度不变,只要A和B有一个为traversable,则该Span为traversable。
2.若A上表面高度大于B上表面高度,新的Span的traversable参数与A保持一致。
3.若A上表面高度小于B上表面高度,新的Span的traversable参数与B保持一致。
如图所示:
四. 额外的体素筛选操作
对于如下图所示的两个Span,虽然两个Span没有相接触,但是如果两个Span的距离太小,小于角色高度,角色实际上是不可能进入该区间的。。
对于这种情况:RecastNavigation中将下面蓝色的Span的traversable参数设置为false。
另一种可选择的边缘体素筛除操作:
An optional filter involves ledge detection. If stepping from the top of the span down to an axis-neighbor exceeds a configurable value, then the span is considered a ledge and not traversable.
整个过程的大致代码如下:
//get all trunks node which interract with the chukyMesh
float tbmin[2], tbmax[2];
tbmin[0] = m_cfg.bmin[0];
tbmin[1] = m_cfg.bmin[2];
tbmax[0] = m_cfg.bmax[0];
tbmax[1] = m_cfg.bmax[2];
int cid[512];// TODO: Make grow when returning too many items.
//获取所有与整个NavMesh场景对应的长方体相接触的所有体素长方体,取其id记录在cid中
const int ncid = rcGetChunksOverlappingRect(chunkyMesh, tbmin, tbmax, cid, 512); //chunkyMesh是一个BVH树,里面的叶子节点对应着每一个空间上单独的obj
if (!ncid)
return 0;
m_tileTriCount = 0;
//traverse node
for (int i = 0; i < ncid; ++i)
{
//获取节点
const rcChunkyTriMeshNode& node = chunkyMesh->nodes[cid[i]];
//获取节点对应的三角形
const int* ctris = &chunkyMesh->tris[node.i*3];
//获取节点包含的三角形的个数
const int nctris = node.n;
m_tileTriCount += nctris;
memset(m_triareas, 0, nctris*sizeof(unsigned char));//建立一个三角形数组,用来标记里面的walkable三角形
///遍历节点下所有的三角形,根据三角形对应面的斜率,与角色的爬坡角度进行对比
///若大于该角度,不作处理,否则改变该三角形的areaId, 标记为walkable area,存储在m_triareas里
rcMarkWalkableTriangles(m_ctx, m_cfg.walkableSlopeAngle,
verts, nverts, ctris, nctris, m_triareas);
//将所有的三角形体素化,并且生成Span加到heightFiled里
if (!rcRasterizeTriangles(m_ctx, verts, nverts, ctris, m_triareas, nctris, *m_solid, m_cfg.walkableClimb))
return 0;
}
if (!m_keepInterResults)
{
delete [] m_triareas;
m_triareas = 0;
}
// Once all geometry is rasterized, we do initial pass of filtering to
// remove unwanted overhangs caused by the conservative rasterization
// as well as filter spans where the character cannot possibly stand.
if (m_filterLowHangingObstacles)
rcFilterLowHangingWalkableObstacles(m_ctx, m_cfg.walkableClimb, *m_solid);
if (m_filterLedgeSpans)
rcFilterLedgeSpans(m_ctx, m_cfg.walkableHeight, m_cfg.walkableClimb, *m_solid);
if (m_filterWalkableLowHeightSpans)
rcFilterWalkableLowHeightSpans(m_ctx, m_cfg.walkableHeight, *m_solid);