地形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编码
可执行文件下载
源码下载
周五去看了寻龙诀,此梗在脑中久久挥之不去,与君同乐:"彼岸花触动了地宫的自动销毁装置,快逃啊![各类华丽崩塌特效]" ——论析构函数的可视化