基於高度差的地形LOD與平截頭體剪裁

地形LOD是最近的一個難點,花了三天時間把它攻了下來,剪枝效率和效果都不錯,很爽,特來與君分享。
揭祕:3D遊戲是如何騙人的node

設計實現方案時糾結了一段時間,先實現了一個不修補裂縫的版本,核心遞歸函數20行作了80%的工做,非常精簡,尤爲是基於平截頭體的場景剪裁算法,效果好到令我意外,真是作到了一片很少一片很多。
其中有幾個技術點能夠提一下:算法

  • 判斷並計算三角形與平截頭體的位置關係與距離:可將三角形的世界座標經過視圖矩陣和投影矩陣變換,換到齊次剪裁空間(HCS)下,在此空間內問題可轉化爲判斷點與立方體的位置關係。但雙方距離在此空間下與世界座標比例尺徹底不一樣(簡單觀察後發現與z座標絕對值正相關),因此對位於平截頭體外的點,我採用的距離計算是,找到在HCS下平截頭體與目標點距離垂足座標,轉換回世界座標計算兩點距離平方,如大於節點半徑平方則裁剪:數組

float CTerrain::DistanceToFrustumSq(D3DXVECTOR3* vWorld) {
    D3DXVECTOR3 vProj, vNearest; int i(0);
    D3DXVec3TransformCoord(&vProj, vWorld, &m_mat);
    if (vProj.x < -1.f) vNearest.x = -1.f;
    else if (vProj.x > 1.f) vNearest.x = 1.f;
    else { vNearest.x = vProj.x; i++; }
    if (vProj.y < -1.f) vNearest.y = -1.f;
    else if (vProj.y > 1.f) vNearest.y = 1.f;
    else { vNearest.y = vProj.y; i++; }
    if (vProj.z < 0.f) vNearest.z = 0.f;
    else if (vProj.z > 1.f) vNearest.z = 1.f;
    else { vNearest.z = vProj.z; i++; }
    if (i == 3) return 0.f;
    D3DXVec3TransformCoord(&vNearest, &vNearest, &m_matR);
    return D3DXVec3LengthSq(&(*vWorld - vNearest));
}
  • 關於四叉樹:建立與析構可封裝在構造函數中,使四叉樹的建立銷燬與普通的堆對象無異;我選擇的成員變量是當前結點四個頂點位於整個地形的行列數(而非索引值),並在Terrain類中保存頂點位置數組,使得四叉樹的建立與使用都變得異常簡潔;不爲面向對象而面向對象,此處的Node就是爲地形一個類專門服務,把核心遞歸函數寫在Terrain類中,把Node指針做爲參數而非相反地(核心遞歸寫在Node裏,來回傳地圖信息)去實現,要簡潔清晰許多,Node定義以下:函數

struct SNode {
    SNode *nw, *ne, *sw, *se;
    int l, r, t, b, W, H;
    SNode(int _l, int _r, int _t, int _b, int _W, int _H) : l(_l), r(_r), 
        t(_t), b(_b), W(_W), H(_H), nw(NULL), ne(NULL), sw(NULL), se(NULL) {
        _W >>= 1; _H >>= 1;
        if ( H || W ) nw = new SNode( l, l + W, t, t - H, _W, _H );
        if ( W )      ne = new SNode( r - W, r, t, t - H, _W, _H );
        if ( H )      sw = new SNode( l, l + W, b + H, b, _W, _H );
        if ( H && W ) se = new SNode( r - W, r, b + H, b, _W, _H );
    }
    ~SNode() { Safe_Delete(nw); Safe_Delete(ne); Safe_Delete(sw); Safe_Delete(se); }
};
  • 核心遞歸函數:工具

void CTerrain::GenerateIB(SNode *node, DWORD *pIndices) {
    D3DXVECTOR3 *vCenter = &m_pVertices[node->b+node->H][node->l+node->W];
    float fRadiusSq(node->H * m_fSegZ + node->W * m_fSegX);
    fRadiusSq *= fRadiusSq;
    float fThreshold(DIST * 1e3f * (node->H + node->W) / (m_iX + m_iZ));
    if (DistanceToFrustumSq(vCenter) > fRadiusSq &&
        DistanceToFrustumSq(&m_pVertices[node->b][node->l]) > fRadiusSq &&
        DistanceToFrustumSq(&m_pVertices[node->b][node->r]) > fRadiusSq &&
        DistanceToFrustumSq(&m_pVertices[node->t][node->l]) > fRadiusSq &&
        DistanceToFrustumSq(&m_pVertices[node->t][node->r]) > fRadiusSq) 
        return; // Cull
    if (!node->H || D3DXVec3LengthSq(&(*m_pPos - *vCenter)) > fThreshold) { // Draw
        pIndices[m_iTriangles*3]   = node->b * m_iVX + node->l;
        pIndices[m_iTriangles*3+1] = node->t * m_iVX + node->l;
        pIndices[m_iTriangles*3+2] = node->b * m_iVX + node->r;
        pIndices[m_iTriangles*3+3] = node->b * m_iVX + node->r;
        pIndices[m_iTriangles*3+4] = node->t * m_iVX + node->l;
        pIndices[m_iTriangles*3+5] = node->t * m_iVX + node->r;
        m_iTriangles += 2;
    } else { // Recurse
        GenerateIB(node->nw, pIndices);
        GenerateIB(node->ne, pIndices);
        GenerateIB(node->sw, pIndices);
        GenerateIB(node->se, pIndices);
    }
}
  • 平截頭體的渲染可直接給單位立方體的頂點、索引緩衝,每幀加視圖投影矩陣的逆變換便可。編碼

但接着修補裂縫是個大問題,在參考了一些解決方案後肯定沒有一種很是簡潔有效的方法,因而只好犧牲第一個版本的簡潔性,開始switch-case,好在編寫謹慎,最終完整cpp用400+行實現了所有功能,並加入了高度差的影響係數和平截頭體的互動觀察模式如圖1,效果出來後感受簡直不要再美妙^^
其中的幾個技術問題:spa

  • 四叉樹定義更新:.net

enum ERenderStatus {
    ERS_PRUNED,
    ERS_VISIBLE,
    ERS_RECURSED
};
struct SNode {
    SNode *n, *e, *w, *s; // neighbors
    SNode *nw, *ne, *sw, *se; // subnodes
    int l, r, t, b, W, H, C, D; // huffman Code in octonary, Depth
    float diff; // max height Difference
    int status;
    SNode(int _l, int _r, int _t, int _b, int _W, int _H, int _C, int _D, int d) : l(_l), r(_r),
        t(_t), b(_b), W(_W), H(_H), nw(NULL), ne(NULL), sw(NULL), se(NULL),
        n(NULL), e(NULL), w(NULL), s(NULL), diff(0.f), C(_C), D(_D), status(ERS_PRUNED) {
        C <<= 3; C += d; _W >>= 1; _H >>= 1;
        if (H || W) nw = new SNode(l, l + W, t, t - H, _W, _H, C, D + 1, 1);
        if (W)        ne = new SNode(r - W, r, t, t - H, _W, _H, C, D + 1, 2);
        if (H)        sw = new SNode(l, l + W, b + H, b, _W, _H, C, D + 1, 3);
        if (H && W) se = new SNode(r - W, r, b + H, b, _W, _H, C, D + 1, 4);
    }
    ~SNode() { Safe_Delete(nw); Safe_Delete(ne); Safe_Delete(sw); Safe_Delete(se); }
};
  • 基於高度差的節點細分條件與計算方法參考了[1]。設計

void CTerrain::InitQuadTreeDiff(SNode* node) {
    if (!node->H && !node->W) return;
    float diff(0.f), temp(0.f);
    if (node->nw) { InitQuadTreeDiff(node->nw); temp = node->nw->diff; if (temp > diff) diff = temp; }
    if (node->ne) { InitQuadTreeDiff(node->ne); temp = node->ne->diff; if (temp > diff) diff = temp; }
    if (node->sw) { InitQuadTreeDiff(node->sw); temp = node->sw->diff; if (temp > diff) diff = temp; }
    if (node->se) { InitQuadTreeDiff(node->se); temp = node->se->diff; if (temp > diff) diff = temp; }
    float l(m_pVertices[node->b + node->H][node->l].y),
        r(m_pVertices[node->b + node->H][node->r].y),
        t(m_pVertices[node->t][node->l + node->W].y),
        b(m_pVertices[node->b][node->l + node->W].y),
        c(m_pVertices[node->b + node->H][node->l + node->W].y),
        nw(m_pVertices[node->t][node->l].y),
        ne(m_pVertices[node->t][node->r].y),
        sw(m_pVertices[node->b][node->l].y),
        se(m_pVertices[node->b][node->r].y);
    temp = abs((nw + ne + sw + se) / 4 - c); if (temp > diff) diff = temp;
    temp = abs((nw + ne) / 2 - t); if (temp > diff) diff = temp;
    temp = abs((nw + sw) / 2 - l); if (temp > diff) diff = temp;
    temp = abs((sw + se) / 2 - b); if (temp > diff) diff = temp;
    temp = abs((ne + se) / 2 - r); if (temp > diff) diff = temp;
    node->diff = diff;
}
  • 使用Huffman編碼尋找四周臨近節點的思路參考了[2]。指針

void CTerrain::InitQuadTreeNeighbors(SNode* node) { // mind-bending
    static int v[5] = { 0, 3, 4, 1, 2 }, h[5] = { 0, 2, 1, 4, 3 };
    if (!node) return;
    // North
    int iTarget(node->C), C(node->C);
    if (node->t < m_iZ) {
        for (int i(0); i <= node->D; i++) {
            int d(C & 7), offset(3 * i); C >>= 3;
            iTarget += (v[d] << offset) - (d << offset);
            if (d > 2) break;
        } node->n = FindNode(m_root, iTarget, node->D);
    }
    // East
    if (node->r < m_iX) {
        iTarget = node->C; C = node->C;
        for (int i(0); i <= node->D; i++) {
            int d(C & 7), offset(3 * i); C >>= 3;
            iTarget += (h[d] << offset) - (d << offset);
            if (d % 2) break;
        } node->e = FindNode(m_root, iTarget, node->D);
    }
    // West
    if (node->l) {
        iTarget = node->C; C = node->C;
        for (int i(0); i <= node->D; i++) {
            int d(C & 7), offset(3 * i); C >>= 3;
            iTarget += (h[d] << offset) - (d << offset);
            if (!(d % 2)) break;
        } node->w = FindNode(m_root, iTarget, node->D);
    }
    // South
    if (node->b) {
        iTarget = node->C; C = node->C;
        for (int i(0); i <= node->D; i++) {
            int d(C & 7), offset(3 * i); C >>= 3;
            iTarget += (v[d] << offset) - (d << offset);
            if (d <= 2) break;
        } node->s = FindNode(m_root, iTarget, node->D);
    }
    InitQuadTreeNeighbors(node->nw); InitQuadTreeNeighbors(node->ne);
    InitQuadTreeNeighbors(node->sw); InitQuadTreeNeighbors(node->se);
}
SNode* CTerrain::FindNode(SNode* node, int C, int D) {
    if (C == 0) return node;
    if (!node) return NULL;
    int offset(3 * D), d(C >> offset); C -= (d << offset);
    if (d == 1) return FindNode(node->nw, C, D - 1);
    else if (d == 2) return FindNode(node->ne, C, D - 1);
    else if (d == 3) return FindNode(node->sw, C, D - 1);
    else return FindNode(node->se, C, D - 1);
}
  • 仍是關於四叉樹:Huffman編碼部分我用了八進制而非四進制,由於子節點取值爲1-4而非0-3(由於int類型沒法區分0與00),仍是有必定浪費;尋找臨近節點的過程十分有趣,最終實現也較爲優雅,主遞歸函數40(4*10)行左右,僅額外調用一個根據編碼返回Node指針的小工具函數。(如上所示)

有時switch-case是最直接便利的手段,不要在全部問題上都過於糾結於更優雅的實現。
開始時並不太但願使用這種看似很笨的三角形扇式的修補裂縫設計,並提出了一種看似完美的遞歸式解決方案,結果事實證實,不深刻思考就盲目相信"看似"的結論簡直是一場災難:
「看似正確」的修補裂縫遞歸思路示例

如圖,三角形ABE爲當前遍歷到的須要修補裂縫的節點的上1/4,矩形ABCD爲其上方相鄰節點,因ABCD被細分,因此將ABE分爲藍與紫三部分,直接將藍色部分信息壓入索引緩衝區,此時問題變爲對兩個紫色區域的遞歸問題:對左紫區,無再細分,直接繪製左紫色三角形;對右紫區有細分,依次類推,繪製小紫區,再遞歸兩紅色區域。
這個角度看,彷佛是理想的輕鬆解決方案,卻隱藏着很大的問題:在邊AE, BE上遞歸結果影響了本節點其餘部分!爲修補裂縫而來,卻修出了更多裂縫……
Troll Face Here

爲實現圖中效果已經是很是不易,代碼已經迅速腫脹(各類if-else switch-case),而最終發現裂縫問題,的確不是一份愉快的經歷 :(
最後承認了這是不可行的LOD方案,儘管它開始時看上去更像是直覺所承認的最佳方案。

參考資料

[1] 節點細分條件、高度差計算方法
[2] 扇形修補裂縫、Huffman編碼
可執行文件下載
源碼下載


週五去看了尋龍訣,此梗在腦中久久揮之不去,與君同樂:"彼岸花觸動了地宮的自動銷燬裝置,快逃啊![各類華麗崩塌特效]" ——論析構函數的可視化

相關文章
相關標籤/搜索