You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

仰视或俯视时Frustum Culling(视锥体剔除)功能失效求助

视锥体剔除(Frustum Culling)仰视/俯视失效问题

相机非上下朝向时,视锥体剔除功能正常(已通过控制台绘制调用数验证),但仰视或俯视时功能失效(疑似缩放异常,但无法确定)。

相关代码实现

Frustum.h

struct BoundingBox
{
    Vec center;
    Vec extents;
};

struct Plane_t
{
    Vec normal;
    float distance;
};

struct Frustum_t
{
    Plane_t topface;
    Plane_t bottomface;

    Plane_t rightface;
    Plane_t leftface;

    Plane_t farface;
    Plane_t nearface;
};

Frustum_t MakeFrustumFromView(const Vec& pos, const Vec& dir, float fov, float near, float far);

bool isBoundingBoxOnForwardPlane(const BoundingBox& boundingbox, const Plane_t& plane);
bool IsBoundingBoxInFrustum(const BoundingBox& boundingbox, const Frustum_t& frustum, const Mat4& transform);

Frustum.cpp

inline Plane_t MakeFrustumPlane(const Vec& point, const Vec& norm) 
{ 
    return { norm, norm.Dot(point) }; 
}

Frustum_t MakeFrustumFromView(const Vec& pos, const Vec& dir, float fov, float near, float far)
{
    Vec normdir = dir.Normalize();
    Vec upvec = { 0.0f, 1.0f, 0.0f };

    Vec right = normdir.Cross(upvec).Normalize();
    Vec up = right.Cross(normdir).Normalize();

    Frustum_t frustum;
    const float fHalfVSide = far * tanf(fov * DEG2RAD * 0.5f);
    const float fHalfHSide = fHalfVSide * ((float)g_pGlobals->m_iScreenWidth / g_pGlobals->m_iScreenHeight);
    const Vec fDirFar = normdir * far;
    
    frustum.nearface = MakeFrustumPlane(pos + normdir * near, normdir);
    frustum.farface = MakeFrustumPlane(pos + fDirFar, -normdir);

    frustum.rightface = MakeFrustumPlane(pos, (fDirFar - right * fHalfHSide).Cross(up));
    frustum.leftface = MakeFrustumPlane(pos, up.Cross(fDirFar + right * fHalfHSide));

    frustum.topface = MakeFrustumPlane(pos, right.Cross(fDirFar - up * fHalfVSide));
    frustum.bottomface = MakeFrustumPlane(pos, (fDirFar + up * fHalfVSide).Cross(right));

    return frustum;
}

bool isBoundingBoxOnForwardPlane(const BoundingBox& boundingbox, const Plane_t& plane)
{
    const float r = boundingbox.extents.x * fabsf(plane.normal.x) + boundingbox.extents.y * fabsf(plane.normal.y) + boundingbox.extents.z * fabsf(plane.normal.z);

    return -r <= (plane.normal.Dot(boundingbox.center) - plane.distance);
}

bool IsBoundingBoxInFrustum(const BoundingBox& boundingbox, const Frustum_t& frustum, const Mat4& transform)
{
    Vec4D globalCenter = transform * Vec4D(boundingbox.center, 1.0f);

    Vec bbRight = Vec(transform.m0, transform.m1, transform.m2) * boundingbox.extents.x;
    Vec bbUp = Vec(transform.m4, transform.m5, transform.m6) * boundingbox.extents.y;
    Vec bbForward = -Vec(transform.m8, transform.m9, transform.m10) * boundingbox.extents.z;

    const float newIi = fabsf(Vec(1.0f, 0.0f, 0.0f).Dot(bbRight)) + fabsf(Vec(1.0f, 0.0f, 0.0f).Dot(bbUp)) + fabsf(Vec(1.0f, 0.0f, 0.0f).Dot(bbForward));
    const float newIj = fabsf(Vec(0.0f, 1.0f, 0.0f).Dot(bbRight)) + fabsf(Vec(0.0f, 1.0f, 0.0f).Dot(bbUp)) + fabsf(Vec(0.0f, 1.0f, 0.0f).Dot(bbForward));
    const float newIk = fabsf(Vec(0.0f, 0.0f, 1.0f).Dot(bbRight)) + fabsf(Vec(0.0f, 0.0f, 1.0f).Dot(bbUp)) + fabsf(Vec(0.0f, 0.0f, 1.0f).Dot(bbForward));

    const BoundingBox globalBoundingBox = { globalCenter.ToVec(), {newIi, newIj, newIk} };

    return (isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.leftface) &&
        isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.rightface) &&
        isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.topface) &&
        isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.bottomface) &&
        isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.nearface) &&
        isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.farface));
}

问题根源与修复方案

1. 平面方程定义错误(核心问题)

原代码中MakeFrustumPlane生成的平面距离参数不符合标准平面方程(ax + by + cz + d = 0),导致后续包围盒平面判断逻辑完全反向。

修复代码:

// 修正平面生成函数,确保符合标准平面方程
inline Plane_t MakeFrustumPlane(const Vec& point, const Vec& norm) 
{ 
    Vec normalizedNorm = norm.Normalize();
    // 标准平面方程中d = -normal·point
    return { normalizedNorm, -normalizedNorm.Dot(point) }; 
}

// 修正平面判断逻辑,确保包围盒在视锥体内部时返回true
bool isBoundingBoxOnForwardPlane(const BoundingBox& boundingbox, const Plane_t& plane)
{
    const float r = boundingbox.extents.x * fabsf(plane.normal.x) + 
                    boundingbox.extents.y * fabsf(plane.normal.y) + 
                    boundingbox.extents.z * fabsf(plane.normal.z);
    return (plane.normal.Dot(boundingbox.center) + plane.distance) >= -r;
}

2. 相机垂直朝向时up向量计算异常

当相机朝向接近(0,1,0)或(0,-1,0)时,固定使用(0,1,0)作为初始up向量,会导致normdir.Cross(upvec)结果趋近于零向量,后续正交化得到的right/up向量完全错误,进而生成错误的视锥体平面。

修复代码:

Frustum_t MakeFrustumFromView(const Vec& pos, const Vec& dir, float fov, float near, float far)
{
    Vec normdir = dir.Normalize();
    Vec upvec = { 0.0f, 1.0f, 0.0f };

    // 当相机朝向与初始up向量几乎平行时,切换初始up向量为侧方向,避免叉乘结果为零
    if (fabsf(normdir.Dot(upvec)) > 0.99f)
    {
        upvec = { 1.0f, 0.0f, 0.0f };
    }

    Vec right = normdir.Cross(upvec).Normalize();
    Vec up = right.Cross(normdir).Normalize();

    // 剩余逻辑保持不变...
}

3. 包围盒变换逻辑错误

原代码计算变换后包围盒extents的方式错误,使用世界空间轴点乘变换后的局部轴,会导致旋转后的包围盒extents计算偏差,在相机垂直朝向时误差被放大。推荐直接生成局部包围盒的8个顶点,变换后重新计算世界空间AABB:

修复代码:

bool IsBoundingBoxInFrustum(const BoundingBox& boundingbox, const Frustum_t& frustum, const Mat4& transform)
{
    // 生成局部包围盒的8个顶点
    Vec localVertices[] = {
        boundingbox.center + Vec(boundingbox.extents.x, boundingbox.extents.y, boundingbox.extents.z),
        boundingbox.center + Vec(boundingbox.extents.x, boundingbox.extents.y, -boundingbox.extents.z),
        boundingbox.center + Vec(boundingbox.extents.x, -boundingbox.extents.y, boundingbox.extents.z),
        boundingbox.center + Vec(boundingbox.extents.x, -boundingbox.extents.y, -boundingbox.extents.z),
        boundingbox.center + Vec(-boundingbox.extents.x, boundingbox.extents.y, boundingbox.extents.z),
        boundingbox.center + Vec(-boundingbox.extents.x, boundingbox.extents.y, -boundingbox.extents.z),
        boundingbox.center + Vec(-boundingbox.extents.x, -boundingbox.extents.y, boundingbox.extents.z),
        boundingbox.center + Vec(-boundingbox.extents.x, -boundingbox.extents.y, -boundingbox.extents.z)
    };

    // 变换到世界空间并计算AABB
    Vec vertexWS = (transform * Vec4D(localVertices[0], 1.0f)).ToVec();
    Vec minWS = vertexWS;
    Vec maxWS = vertexWS;

    for (int i = 1; i < 8; ++i)
    {
        vertexWS = (transform * Vec4D(localVertices[i], 1.0f)).ToVec();
        minWS.x = min(minWS.x, vertexWS.x);
        minWS.y = min(minWS.y, vertexWS.y);
        minWS.z = min(minWS.z, vertexWS.z);

        maxWS.x = max(maxWS.x, vertexWS.x);
        maxWS.y = max(maxWS.y, vertexWS.y);
        maxWS.z = max(maxWS.z, vertexWS.z);
    }

    BoundingBox globalBoundingBox;
    globalBoundingBox.center = (minWS + maxWS) * 0.5f;
    globalBoundingBox.extents = (maxWS - minWS) * 0.5f;

    return (isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.leftface) &&
            isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.rightface) &&
            isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.topface) &&
            isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.bottomface) &&
            isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.nearface) &&
            isBoundingBoxOnForwardPlane(globalBoundingBox, frustum.farface));
}

内容的提问来源于stack exchange,提问作者Alphenex

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.24 05:00:54