溫馨提示:
由于沒有系統嚴謹的學習過相關理論,所以我在描述一些概念的時候可能會自己造一些名詞,也不保證我的理解就是正確的。希望各位帶哥海涵。
另外整個項目的代碼量很大,算法細節也很多,全部展開分析是一個浩大的工程,我應該沒有足夠的時間精力來做這件事,只能記錄下主流程以及我覺得需要拆解的點。
再有,為了能夠順暢的理解項目的思路和代碼,可能需要學習一些基礎的計算幾何和圖形學的知識,我在這篇文章中會有相應的整理
///正文開始
當一次尋路發生的時候,我們一般需要知道兩個信息:
1.地圖上哪些區域是可行走的
2.這些可行走區域之間的聯通關系是什么樣的
從這個角度來看,導航網格算是一種數據結構,用某種方式來描述地圖的上述兩種信息
在實際應用中,導航網格是以鄰接的凸多邊形集合來表示的
在獨立的凸多邊形內部,保證任意兩點直線可達
而尋路關鍵是通過算法找到一組多邊形,這組多邊形滿足這樣的條件:
第一個和最后一個多邊形包含了尋路的起始點和終點
中間的多邊形負責所有多邊形的連通性
因此導航網格尋路可以粗略的分成兩大部分:
1.將3D場景轉化為鄰接的凸多邊形集合
2.在凸多邊形集合上尋路
在RecastNavigation項目中,
Recast工程對應第一部分
Detour工程對應第二部分
而RecastDemo是一個GUI程序,可以直觀的驗證導航網格的生成和尋路的過程
配置參數 struct rcConfig
如何輸入一個3D場景 class InputGeom
生成導航網格可以從bool Sample_SoloMesh::handleBuild()這個函數開始看起(也就是在demo程序的界面上點擊“build"之后的回調函數)
Step 1. Initialize build config.
根據配置初始化參數,并計算出網格的大小
其中最重要的參數是這幾個:
場景尺寸(包圍盒)
所有的的頂點(頂點個數&&頂點坐標集合)
所有的三角形面(三角形個數&&三角形集合)
另外這里的半徑已經被轉換為以體素大小作為單位
m_cfg.walkableRadius = (int)ceilf(m_agentRadius / m_cfg.cs);
Step 2. Rasterize input polygon soup.
體素化
體素化的概念在第一篇中已經有所講述,這里再提一下,可以用2D圖形的光柵化最類比理解,其實就是為了找到3維空間中的三角形面與那些體素盒子相交
具體步驟:
a.標記所有三角形面是否可行走
rcMarkWalkableTriangles計算所有三角形的法線,與最大可行走角度做比較,并將結果存入m_triareas[]這個標記數組中
b.創建高度場
c.光柵化三角形面
rcRasterizeTriangles這個函數是重點,它主要做了這些事:
遍歷所有三角形,調用rasterizeTri,將每個初始的三角形面轉換成空間內的體素集
注意這里每個三角形之間是相互獨立的,也許某一組三角形可以圍成了一個多面體,但這里已經不再有"多面體"的概念,而是將地圖的信息全部拆解到三角形面,再進一步拆解到體素盒子
1.找到三角形的AABB(注意是3維空間內的三角形)
2.計算三角形的高度(y軸)范圍
3.按xz坐標網格切割三角形
俯視圖如下(注意只是俯視圖,實際上應該是3維空間的切割)
在分割三角形時代碼中有這樣一個數組
float buf[7*3*4];
float *in = buf, *inrow = buf+7*3, *p1 = inrow+7*3, *p2 = p1+7*3;
解釋一下7 3 4這三個數字
4:有4份對應的數據,用來存放輸入的圖形和分割后的圖形
3:對應一個頂點的x y z 坐標值
7:一個三角形在分割時會被切4刀(即一個正方形格子的4條邊),所以切完最多可能會變成一個7邊形,也就是最多需要存7個頂點
這里有一個重要的函數:
用一條直線將一個凸多邊形分成兩個凸多邊形
static void dividePoly(const float* in, int nin,
float* out1, int* nout1,
float* out2, int* nout2,
float x, int axis)
參數axis取值[0,2],對應表示x,y,z軸
枚舉所有邊,判斷邊的頂點坐標是在軸的同側還是不同側,
若是同側則將兩個頂點都加入對應的新多邊形
若是在不同側,則新生成一個分割點,將分割點(復制成兩份)加入兩個新多邊形
4.找到切割后的多邊形對應的span,根據對應的三角形面的可行走情況設置span的可行走屬性,并把這個span根據xz的投影坐標添加入到高度場中
這里要特別說明兩點
1.span在合并的時候,如果同一個span,有的面是可行走,有的是不可行走,在合并之后會變成可行走
// Merge flags.
if (rcAbs((int)s->smax - (int)cur->smax) <= flagMergeThr)
s->area = rcMax(s->area, cur->area);
2.如果一個面是完全垂直于xz平面且與x軸或者z軸平行,在切割時會怎么樣
不妨假設Xmax=Xmin,這時在x軸方向的切割只有一次即x=Xmax,并且dividePoly會判定所有三角形的邊都在分割線的同一側,分割的結果是一個原三角形和一個空集合