navmesh 生成網格資訊 總 (更新中 )

只要你在發表於2019-02-24

The general process is as follows:

Voxelization - Create a solid heightfield from the source geometry.

體素化,根據幾何體建立solid heightfield(recast裡面的rcHeightField)

 

Generate Regions - Detect the top surface area of the solid heightfield and divide it up into regions of contiguous spans.

生成區域,決定哪一部分表面是可通過的。然後把這些可通過區域劃分為鄰近的 regions of spans,最終可以用它們形成凸多邊形。

 

第一步,把solid heightfiled轉成open heightfield(對應recast裡面的rcHeightField轉成rcCompactHeightField),open heightfield 也就是表示上表面開闊區域的heightfiled

建立Neighbor Links

比如上面這些,桌子下面這些,在生成solid heightfiled的過程就已經被排除了,因為一個體素中有多邊形的面穿過而被認為是solid的。

然而在這個時刻桌子上面還是被認為是可行走的。

第二步,進一步排除 un-walkable spans ,排除條件:1 距離阻擋夠遠 2 上面能走人而不會碰撞到物體

 

為所有的open spans會產生鄰居資訊,演算法:

所有鄰居columns 為候選物件,只考慮四個方向的,而不考慮diagonal方向的,滿足以下兩條的span會被認為是neibour span:

1 兩個spanfloor的高度差夠小

2 agent通過的時候不會撞到頭

如上圖,我們每一個獨立的span是能放下一個人的,但是在鄰居間移動的過程中沒辦法保證gap足夠大。

 

生成distance field

distance field是什麼:包含每個span距離最近的border span的距離。在生成regions的過程中這個演算法會用到。

Border span:沒有8neighborspan。是用來表示原始geometry的可行走表面與阻擋或者empty space之間的boundary

 

 

生成regions

Regions的很重要一點是他們在對映到xz平面的時候會是簡單多邊形。

初始的生成region的演算法:分水嶺演算法。

分水嶺演算法會從最低點的span開始慢慢地開始“floods”,進行迭代,每次迭代有一個當前的水平線,當前水平線以下的spans會被增加到現有的regions或者建立出新的regions。在region擴張的過程中如果一個被flood到的span臨接一個已經存在的region,那麼它會被加入這個region

然後引入一些演算法:

去掉非常小的regions。(以及 CleanNullRegionBorders 

非常小的region要合併。

有時候分水嶺演算法會導致一個region圍繞著一個null region進行flow,這個時候這個region會在這個null region的地方分開。

 

在這個過程正還會出現的問題如圖:

Null region的邊上的span可能會出問題:比如上面的一個region只圍繞著一個span,那麼在後續的生成輪廓的過程中可能會生成自交的polygon。演算法會將其變成深藍色的。

 

Generate Contours - Detect the contours of the regions and form them into simple polygons.

生成輪廓,contours of the regions “走”出來的。這是從體素空間到向量空間的第一步,但是它仍然是以voxel space的單位來表示的。

這個部分的輸入是open heightfield,是從關注span的表面到關注span的邊的一個轉換。

兩種edges: region and internal. 

Region edges:邊的兩側是不同的region。

區分這兩種edges的演算法:

遍歷所有spans,檢查每一個的軸上的鄰居,如果一個軸的鄰居與當前span的region不一樣,則被標記為region edge。

 

接下來“走”出輪廓來~

Start by facing the known region edge. Add it to the contour.

首先面向一個已知的region edge,加入contour。

Large

Rotate in 90 degree increments, adding region edges to the contour, until we find an internal edge. Step forward into the neighbor span.

順時針旋轉90度,直到找到internal edge,進入下一個span。

Large

Rotate 90 degrees counter-clockwise and re-perform the previous step. Continue until we return to the starting span, facing the starting direction.

逆時針旋轉90度並重復前一個step。直到回到了第一個span,並且面向開始的方向。

 

接下來把邊資訊轉成頂點資訊。

首先確認x,z值,對於每一個邊,我們沿著從span內部來看,順時針的防線,獲取邊上的點的x,z值,如下:

確認y軸的值比較難,這也正是為什麼我們返回3D空間。

上圖中,不知道選哪個點的y軸的值,我們用的是最高的!!

 

Note that there are two types of contour sections. Sections that represent the portal between two adjoining regions, and sections that border on "empty" space. In the code documentation empty space is referred to as the "null region". I will use the same term here.

 

有兩種輪廓的片段,一種是連線兩個regions,稱作portal,另一種是連線null region。

真的需要下圖這麼多的頂點嗎?不是的,真正必要的頂點是:region發生變化的地方,比如region portals的頂點。

所以可以進行如下簡化:

但是這樣切掉的部分就太大了,與原來的輪廓不一致,所以會定義一個MaxDeviation來確保誤差不會太大,Douglas-Peucker演算法用來做到這一點:

It starts off with the mandatory vertices, then adds vertices back such that none of the original vertices are farther than edgeMaxDeviation distance from the simplified edges.

從一個必須的頂點開始增加頂點直到所有的頂點到簡化後的邊的距離都不超過MaxDeviation。

但是這個演算法會生成很長的線段,最終會在生成網格的過程中生成非常長而細的三角形。

所以會使用第二個演算法,設定最長的邊長,將超過長度的邊切成兩半,直到沒有超過最長邊長的邊。

到了這一步雖然還是在體素空間,但是已經為返回vector space做好了準備。

 

 

regions生成細節多邊形(highly detailed polygons

其次,用一些演算法來實現:

1 簡化兩個多邊形之間的邊(regions之間的portals

2 確保border 符合可行走表面的border???

Boarder就是連線空的或者阻擋區域的輪廓的邊

3 優化border邊的長度

 

到了這個階段,可行走表面被表示為簡單多邊形

 

簡單多邊形可定義為:

1.周界不自交的多邊形。

https://i.iter01.com/images/3cbe2a9575fc102960f3761f6d0e88b35eb0a107426060c16b60f608cdcd8770.png


  2.滿足條件:
  1)頂點與頂點不重合。
  2)頂點不在邊上。
  3)邊與邊不相交的多邊形。

 

 

Generate Polygon Mesh - Sub-divide the contours into convex polygons.

生成多邊形網格,把輪廓劃分成凸多邊形。

這個過程是先切分簡單多邊形為三角形,然後把三角形合成為儘可能打的凸多邊形。

 

這個大步驟要做的事情:

1 voxel space:座標是整數格式的並且代表的是到heightfield的原點的距離。

所以我們要把多邊形的頂點資訊轉成原始地圖的vector space的座標。

2 把所有輪廓資訊合成一個單個的mesh資訊

3 把多邊形切成凸多邊形

4 計算出多邊形的鄰居資訊(Polygon neighbor information.)

 

切分為凸多邊形的演算法:

  1. Triangulate each contour.給每個多邊形三角化
  2. Merge triangles to form the largest possible convex polygons.和成為最大的可能的凸多邊形

 

三角化:

沿著輪廓的邊走,對於每三個頂點,看是否可以組成內部的三角形。對於所有的候選者選擇最短的邊。新的邊被稱作切分邊(partitions)。一直進行這個過程知道三角化完成。三角化是在輪廓的xz平面投影上完成的。

 

Step-by-Step: Find potential partitions:

接下來找到最短的分割並且生成三角形,去掉無效分割(https://blog.csdn.net/icebergliu1234/article/details/87933454),生成新的potential partitions:

 

一直這樣直到三角形化完畢。

 

合併到凸多邊形:

只在一個輪廓所生成的多邊形合併。

流程:

  1. 找到可以合併的多邊形。
  2. 從這個列表中找到兩個多邊形,共同的邊是最長的,並且合併它們。
  3. 重複直到沒有可以再合併的了。

可以合併的多邊形滿足:

  • 貢獻一條邊。
  • 合併之後仍然是凸多邊形。
  • 合併之後的多邊形邊數不會大於最大頂點數(maxVertsPerPoly )。

 

It is best to use visualizations. The first example demonstrates a valid merge.

檢測合併之後的多邊形是否是凸的:關鍵點在於共享的頂點。如果兩個頂點都可以組成內部的銳角,則合成之後的多邊形就仍然是凸多邊形。我們如何判斷這一點呢:

1 建立一條從當前頂點的前一個頂點到後一個頂點的向量。

2 如果頂點A在向量的左邊則形成的是銳角。

 

最後一步就是遍歷整個mesh的所有多邊形並且建立連線資訊。

到了這一步我們終於回到了向量空間,並且擁有了一個凸多邊形網格。

 

 

Generate Detailed Mesh - Triangulate the polygon mesh and add height detail.

生成細節網格,把多邊形網格三角化,並且新增高度細節。

這個三角化的過程是 Delaunay triangulation

多邊形的邊的裡面會被新增頂點,以保證原始的geometry會被足夠好地近似。

如下圖:

 

在三維空間中,多邊形網格可能沒有足夠地近似原始mesh的輪廓(比如上圖樓梯在Y軸上面近似的高度與實際高度相差過大)。所以就有了這個步驟。這個步驟增加了高度細節,這樣細節網格就可以很好地在所有的軸上面匹配原始mesh的表面。

為了完成這一點,我們遍歷所有的多邊形並且當多邊形偏離原始mesh超過一定值的時候,沿著多邊形的邊增加頂點。

 

從尋路的角度來講,這一步是不必要的,有上一步建立的凸多邊形網格就夠了。這個步驟的必要性在於當需要一個agent被放置在源網格表面是用到物理或者ray-casting的時候。當然了體素化過程決定了精確的位置是不可能的。而且過於精確的高度資訊會大大增加查詢效能已經記憶體佔用。

 

生成detail mesh首先補充高度資訊:

為了增加高度資訊,我們需要檢查多邊形表面是否與open heightfield中span的位置相差太遠。

height patch包含了每一個open heightfield中的格子與多邊形相交的格子的位置。

 Basically, it is a simplified cutout of a section of the open heightfield with the following characteristics.

  • It only contains height information for the AABB of a single polygon.
  • It only contains the floor height for each grid location. (No spans.)
  • It only has a single height for each grid location. (No overlapping.)

 

The main steps in this stage are as follows. For each polygon:

  1. Sample the hull edges of the polygon. Add vertices to any edge that deviates by more than the value of contourMaxDeviation from the height patch data.
  2. Perform a Delaunay triangulation of the polygon.
  3. Sample the internal surface of the polygon. Add vertices if the surface deviates by more than the value of contourMaxDeviation from the height patch data. Update the triangulation for any new vertices.

生成detail mesh的步驟:對於每個多邊形:

1 對多邊形的外殼邊進行取樣。對於height patch資料中超過輪廓最大偏移的邊增加頂點。

2 對多邊形進行德勞內三角化。

3 對多邊形表面內部進行取樣。對於height patch資料中超過輪廓最大偏移的表面增加頂點。對於新的頂點進行三角化。

 

接下來詳細說這三個步驟:

1 給多邊形的邊增加頂點:

對於多邊形每一條邊:根據contourSampleDistance(輪廓的取樣距離值)把邊分成線段。這些都是潛在的可能會增加的頂點。

從height patch中的高度資訊獲取每個頂點的y值

計算每個取樣頂點與原始邊的距離,如果超過了,在距離原始邊最遠的地方增加取樣頂點。

 

 

重複這個過程直到新的多邊形生成完畢

 

2 德勞內三角化是在給edges加入了細節之後進行的。

這一步之後原始的多邊形不復存在,取而代之的是三角形網格。

從這個時候開始, 當有新的頂點加入網格的時候,可能要重新進行三角化。

 

3  給多邊形表面內部增加細節

 

所有的頂點還仍然在網格的邊上呢。這一步,我們會在網格表面的內部進行檢查,看是不是與height patch 資料偏差太大。

取樣點:

網格以外的取樣點會被丟掉。剩下的頂點,找到與三角形網格表面最遠的。如果比設定的輪廓最大偏移值(contourMaxDeviation)要遠,增加這個點到網格里面並且重新進行三角化。

重複這個過程知道沒有再超過輪廓最大偏移值(contourMaxDeviation)的取樣點。

 

最後的步驟:

每一個單獨的多邊形生成的細節三角形網格被合併到一個網格中並且載入到一個三角形網格的例項中(TriangleMesh)。

 

 

 

 

=============================================================================================

相關概念

下圖中幾何體被 保守體素化 抽象為heightfield 表示阻擋空間。

1 Heightfield:可以理解成是一個資料結構,由所有的solid的HeightSpan構成。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2 HeightSpan

 

這就是一個HeightSpan,簡稱Span

 

3 Open height spans

因為我們更關注solid space的上表面,我們用Open height span來表示這些上表面的開闊區域,圖中黃色部分:

 

solid heightfiled 轉成open heightfield的演算法(Recast裡面的rcHeightField轉成rcCompactHeightField):

遍歷所有的solid spans,如果這個span被標記為traversable,則看這個column上的下一個span的最小值和當前span的最大值之間的高度差就可以定義一個solid span

 

 

 

 

 

 

 

 

 

Neighbour的表示:

 

 

===========================================================================================

NMGen 與 Recast 函式&類的對應

 

rcCompactHeightfield versus OpenHeightfield

 

Recast rcCompactHeightfield 結構與NMGen OpenHeightField class功能是一樣的。但是他們內部結構非常不一樣。

Recast的兩個最高的優勢是效能和記憶體佔用。

但是NMGen的優勢是對於學習有用。從含義上來講可以不用急於把SolidHeightfield and OpenHeightfield 兩個類分開。

如果你要學習的是Recast最好花一些時間去學習rcCompactHeightfield, OpenHeightfield以及它們的區別。

 

Iterating Heightfields

 

NMGen 隱藏了遍歷heightfield的複雜性,使用標準的java iterators   E.g.:

    IHeightfieldIterator<HeightSpan> iter = heightField.dataIterator();
    while (iter.hasNext())
    {
        HeightSpan span = iter.next();
        // Do stuff...
    }

 

如果你從NMGen轉到Recast,需要花些時間去認識一下標準的迭代模式,有兩種模式:

 

For rcHeightfield structures:

    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)
            {
                // Do stuff... 三重迴圈,找到span
            }
        }
    }

For rcCompactHeightfield structures:

    for (int y = 0; y < h; ++y)
    {
        for (int x = 0; x < w; ++x)
        {
            const rcCompactCell& c = chf.cells[x+y*w];//先找到這一個xz面上的column
            for (int i = (int)c.index, ni = (int)(c.index+c.count); i < ni; ++i)
            {
                const rcCompactSpan& s = chf.spans[i];//遍歷整個column上面的compactSpan
                // Do stuff...
            }
        }
    }

相關文章