高效八叉樹octree:基於hash函數的數據結構

1.基礎知識

八叉樹octree是一種遞歸、軸對齊且空間分隔的數據結構,常用於計算機幾何來優化碰撞檢測、最鄰近搜索等,且常用於3D數據的表達。

一個八叉樹結構,將有限的三維體數據等分爲8個octants。octants也被稱爲nodes(節點,樹結構概念中)或者cells(單元,空間概念中)。將空間分成等大小的立方體可以加速細分運算,且單元大小的存儲也節省空間。


一個八叉樹是通過遞歸的將空間細分成八個子單元,直到每個單元中剩餘空間的大小在預定權值下,或者達到了最大樹深度。每個單元都被單個軸對齊的平面細分的,類似於空間座標系,其原點通常放在父節點的中心位置。(此處可思考不放在中心會怎麼樣)

因此,每個節點都可以有0或8個子節點。因此,相較於正常的網格結構,便於存儲稀疏分佈的結構。

2.octree的節點表達形式

傳統基於指針的節點表達:

常用於octree需要頻繁更新且內存消耗可以忍受的情況。通常,該節點結構體(104 byte)爲:

// standard representation (104 byte)
struct OctreeNode
{
    OctreeNode * Children[8];//8個子節點指針
    OctreeNode * Parent; // optional 父節點指針
    Object *     Objects;//物體指針
    Vector3      Center;//中心位置vector
    Vector3      HalfSize;//octan大小vector
};

位於中間位置的節點以上都有,但葉節點沒有子節點。因此,還常常加一項bool Isleaf;

稍微節約一點的方式:不存儲每個節點的指針,而是將8個子節點連續存儲成一個塊,只需要存儲開頭的指針就好了:


其結構體(49 byte):

// block representation (49 bytes)
struct OctreeNode
{
    OctreeNode * Children;
    OctreeNode * Parent; // optional
    Object *     FirstObj;
    Vector3      Center;
    Vector3      HalfSize;
    bool         IsLeaf;
};

結合前兩者:兄妹表達sibling-child

每個節點有兩個指針,一個是NextSibling,指向父節點的下一個子節點的;一個是FirstChild,指向節點的第一個子節點。


// sibling-child representation (56 bytes)
struct OctreeNode
{
    OctreeNode * NextSibling;
    OctreeNode * FirstChild;
    OctreeNode * Parent; // optional
    Object *     FirstObj;
    Vector3      Center;
    Vector3      HalfSize;
};

簡介高效的基於index索引的表達:

現在常用的高效的octree,其節點都用基於index索引的表達,引入哈希函數的,大大節省了內存和遍歷消耗。

這種表達形式,不再存儲其父節點、子節點的指針,而是在每個節點存儲唯一的index索引,稱爲位置編碼。

而且,所有八叉樹節點都存在哈希映射中,允許根據位置編碼直接訪問任意節點。因爲,應用哈希映射,非常容易的根據任意節點的父節點和子節點來推導計算出他的位置編碼。爲避免不需要的哈希映射尋找不存在的子節點,節點結構西可以通過bit-mask(比特掩碼,即爲每個位置設置0-1編碼)來確定該子節點是否被分配了內存空間。其結構體(13 byte)爲:

struct OctreeNode // 13 bytes
{
    Object *    Objects;
    uint32_t    LocCode;     // or 64-bit, depends on max. required tree depth
    uint8_t     ChildExists; // optional
};

位置編碼:

每個octant都獲得了一個0-7的數字(3-bit),依賴於節點相對於其父節點中心的相對位置關係。

可能的編碼爲:左下前000,右下前001,左下後010,右下後011,左上前100,右上前101,左上後110,右上後111,如下圖:


所以,任意樹中子節點的位置編碼都能,通過自上而下遞歸的計算並連接,所有節點的octant位置編碼來表達其在octree中的位置。

爲了根據位置編碼來獲得該節點所處樹的深度,還需要設立一個標誌位來表示位置編碼的結束。因爲,沒有這種標誌位,很難區分001和000001.通過使用1bit來mark序列的結束,1001可以輕鬆的區別於1000001,等於設立octree的根爲1。1bit用於flag+每層3bit用於位置表示,所以32-bit的位置編碼可以表示最大深度是10的octree。給定位置編碼c,其處於樹的level,深度值爲


size_t Octree::GetNodeTreeDepth(const OctreeNode *node)
{
    assert(node->LocCode); // at least flag bit must be set
    // for (uint32_t lc=node->LocCode, depth=0; lc!=1; lc>>=3, depth++);
    // return depth;
 
#if defined(__GNUC__)
    return (31-__builtin_clz(node->LocCode))/3;
#elif defined(_MSC_VER)
    long msb;
    _BitScanReverse(&msb, node->LocCode);
    return msb/3;
#endif
}
按照位置編碼對節點進行排序,最終順序和cotree的遍歷順序一致,即000的會被先遍歷完其子節點。這等效於morton編碼(wikipedia上叫z-order curve),它可以對多維數據線性編碼,並在多level裏保留數據所在位置。可以從下圖中背景裏的Z,知道爲什麼這麼叫:

樹遍歷:

給定位置編碼,沿octree向上或者向下移動,兩步操作實現:1.獲得下一個節點的位置編碼;2.在哈希映射中尋找該節點。

爲了能向上遍歷octree。必須確定每個位置編碼的父節點:只要去掉當前節點位置編碼的最後3bit就行了:

class Octree
{
public:
    OctreeNode * GetParentNode(OctreeNode *node)
    {
        const uint32_t locCodeParent = node->LocCode>>3;
        return LookupNode(locCodeParent);
    }
 
private:
    OctreeNode * LookupNode(uint32_t locCode)
    {
        const auto iter = Nodes.find(locCode);
        return (iter == Nodes.end() ? nullptr : &(*iter));
    }
 
private:
    std::unordered_map<uint32_t, OctreeNode> Nodes;
};

爲了能向下遍歷octree。必須確定每個位置編碼的子節點:只要在當前節點位置編碼的最後3bit加上相應的子節點位置編碼就行了:

void Octree::VisitAll(OctreeNode *node)
{
    for (int i=0; i<8; i++)
    {
        if (node->ChildExists&(1<<i))
        {
            const uint32_t locCodeChild = (node->LocCode<<3)|i;
            const auto *child = LookupNode(locCodeChild);
            VisitAll(child);
        }
    }
}

完整的octree:

完整的八叉樹,每個中間節點都有8個子節點,所有葉節點都有相同的樹深度D,葉節點數 N_L=8^D,等於用 2^D\times 2^D\times 2^D的分辨率細化了一個三維網格。所有節點數量爲


但我們一般用octree,都不是完整的,完整的就等效於一般網格了,沒必要用了。一般都是使用其對稀疏數據表達的強大能力。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章