地形LOD是最近的一個難點,花了三天時間把它攻了下來,剪枝效率和效果都不錯,很爽,特來與君分享。
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上遞歸結果影響了本節點其餘部分!爲修補裂縫而來,卻修出了更多裂縫……
爲實現圖中效果已經是很是不易,代碼已經迅速腫脹(各類if-else switch-case),而最終發現裂縫問題,的確不是一份愉快的經歷 :(
最後承認了這是不可行的LOD方案,儘管它開始時看上去更像是直覺所承認的最佳方案。
[1] 節點細分條件、高度差計算方法
[2] 扇形修補裂縫、Huffman編碼
可執行文件下載
源碼下載
週五去看了尋龍訣,此梗在腦中久久揮之不去,與君同樂:"彼岸花觸動了地宮的自動銷燬裝置,快逃啊![各類華麗崩塌特效]" ——論析構函數的可視化