地形LOD是最近的一个难点,花了三天时间把它攻了下来,剪枝效率和效果都不错,很爽,特来与君分享。
设计实现方案时纠结了一段时间,先实现了一个不修补裂缝的版本,核心递归函数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,效果出来后感觉简直不要再美妙^^
其中的几个技术问题:
四叉树定义更新:
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编码
可执行文件下载
源码下载
周五去看了寻龙诀,此梗在脑中久久挥之不去,与君同乐:
"彼岸花触动了地宫的自动销毁装置,快逃啊![各种华丽崩塌特效]" ——论析构函数的可视化
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。