地形LOD是最近的一个难点,花了三天时间把它攻了下来,剪枝效率和效果都不错,很爽,特来与君分享。
揭秘:3D游戏是如何骗人的

设计实现方案时纠结了一段时间,先实现了一个不修补裂缝的版本,核心递归函数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上递归结果影响了本节点其他部分!为修补裂缝而来,却修出了更多裂缝……
Troll Face Here

为实现图中效果已是非常不易,代码已经迅速肿胀(各种if-else switch-case),而最终发现裂缝问题,的确不是一份愉快的经历 :(
最后认可了这是不可行的LOD方案,尽管它开始时看上去更像是直觉所认可的最佳方案。

参考资料

[1] 节点细分条件、高度差计算方法
[2] 扇形修补裂缝、Huffman编码
可执行文件下载
源码下载


周五去看了寻龙诀,此梗在脑中久久挥之不去,与君同乐:
"彼岸花触动了地宫的自动销毁装置,快逃啊![各种华丽崩塌特效]" ——论析构函数的可视化


YunHsiao
36 声望10 粉丝

Make the best out of what we already have.


引用和评论

0 条评论