OpenGL中Face culling 面剔除的具体实现

《LearnOpenGL》这篇文章主要侧重于如何使用 OpenGL 的 API 来开启和配置面剔除,确实没有深入探讨底层数学和实现逻辑。

面剔除的核心原理其实并不复杂,它主要依赖于线性代数中的向量叉乘和点乘。

下面我将为你详细拆解面剔除的具体实现原理,并展示如何用代码(C++/数学库)手动实现这一过程。


核心原理:法向量与视线方向

面剔除的本质是判断一个三角形面是**"朝向摄像机"还是"背向摄像机"**。

1. 顶点环绕顺序

在 3D 图形学中,三角形的三个顶点定义顺序至关重要。

  • 逆时针:通常定义为"正面"。
  • 顺时针:通常定义为"背面"。

(注:OpenGL 默认是逆时针为正面,可以通过 glFrontFace 修改)

2. 计算三角形法向量

当我们有三角形的三个顶点 V1,V2,V3V_1, V_2, V_3V1,V2,V3 时,我们可以通过叉乘计算出该三角形的法向量 N⃗\vec{N}N 。

叉乘的顺序决定了法向量的方向:

  • U⃗=V2−V1\vec{U} = V_2 - V_1U =V2−V1
  • V⃗=V3−V1\vec{V} = V_3 - V_1V =V3−V1
  • N⃗=U⃗×V⃗\vec{N} = \vec{U} \times \vec{V}N =U ×V

如果顶点顺序是逆时针的,计算出的法向量会指向三角形的外部(朝向观察者);如果是顺时针的,法向量会指向内部。

3. 判断可见性(点乘)

有了法向量 N⃗\vec{N}N 和视线向量 L⃗\vec{L}L (从三角形指向摄像机),我们使用点乘来判断方向:

result=N⃗⋅L⃗ result = \vec{N} \cdot \vec{L} result=N ⋅L

  • 如果 result>0result > 0result>0:法向量与视线方向一致,说明面朝向摄像机(正面)。
  • 如果 result<0result < 0result<0:法向量与视线方向相反,说明面背向摄像机(背面)。
  • 如果 result=0result = 0result=0:面与视线垂直(侧视)。

实际实现方式:屏幕空间判定

虽然上面的原理在世界空间是正确的,但在 GPU 内部(光栅化阶段),面剔除通常发生在裁剪空间屏幕空间。这种方法效率更高,因为不需要计算真实的 3D 法向量,也不需要视线向量,只需要看投影后的三角形面积符号。

具体算法(有符号面积法):

当顶点被变换到 2D 屏幕坐标 (x,y)(x, y)(x,y) 后,我们计算三角形覆盖的面积。如果面积为正,代表逆时针;为负,代表顺时针。

公式如下(基于叉积的 2D 简化版):
S=(x2−x1)(y3−y1)−(y2−y1)(x3−x1) S = (x_2 - x_1)(y_3 - y_1) - (y_2 - y_1)(x_3 - x_1) S=(x2−x1)(y3−y1)−(y2−y1)(x3−x1)

  • S>0S > 0S>0:顶点顺序为逆时针。
  • S<0S < 0S<0:顶点顺序为顺时针。
  • S=0S = 0S=0:三角形退化成一条线。

GPU 会根据这个 SSS 的符号以及用户设置的剔除模式(GL_BACKGL_FRONT)决定是否丢弃该三角形。


代码实现演示

为了让你彻底理解,我将用 C++ 代码模拟这个过程。我们假设有一个简单的软件光栅化器流程。

第一步:定义数学工具

我们需要一个简单的向量结构体和点乘/叉乘运算。

cpp 复制代码
#include <iostream>
#include <vector>
#include <cmath>

struct Vec3 {
    float x, y, z;
};

// 向量相减
Vec3 sub(const Vec3& a, const Vec3& b) {
    return {a.x - b.x, a.y - b.y, a.z - b.z};
}

// 3D 叉乘 (用于计算法向量)
Vec3 cross(const Vec3& a, const Vec3& b) {
    return {
        a.y * b.z - a.z * b.y,
        a.z * b.x - a.x * b.z,
        a.x * b.y - a.y * b.x
    };
}

// 点乘
float dot(const Vec3& a, const Vec3& b) {
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

// 2D 叉乘 (用于屏幕空间判定,实际上是计算有符号面积)
// 返回值相当于 z 轴分量
float cross_2d(float x1, float y1, float x2, float y2) {
    return x1 * y2 - y1 * x2;
}
第二步:实现面剔除逻辑

这里展示两种方法:

  1. 3D 空间法(直观,容易理解几何意义)。
  2. 屏幕空间法(高效,GPU 实际采用的方式)。
cpp 复制代码
// 定义顶点顺序模式
enum WindingOrder {
    CCW, // 逆时针
    CW   // 顺时针
};

// 定义剔除模式
enum CullMode {
    NONE,
    FRONT,
    BACK
};

// ==========================================
// 方法一:3D 空间法 (世界坐标系)
// ==========================================
bool ShouldCull_3D(const Vec3& v1, const Vec3& v2, const Vec3& v3, 
                   const Vec3& cameraPos, CullMode mode) {
    // 1. 计算三角形的两条边
    Vec3 edge1 = sub(v2, v1);
    Vec3 edge2 = sub(v3, v1);
    
    // 2. 叉乘得到法向量
    // 注意:如果顶点是逆时针排列,法向量指向前方
    Vec3 normal = cross(edge1, edge2);
    
    // 3. 计算视线向量 (从三角形中心指向摄像机)
    Vec3 center = {(v1.x + v2.x + v3.x) / 3.0f, 
                   (v1.y + v2.y + v3.y) / 3.0f, 
                   (v1.z + v2.z + v3.z) / 3.0f};
    Vec3 viewDir = sub(cameraPos, center);
    
    // 4. 点乘判断方向
    float dotResult = dot(normal, viewDir);
    
    // 如果点乘 < 0,说明法向量与视线方向相反,即面背向摄像机
    bool isBackFacing = (dotResult < 0);
    
    // 根据设定的剔除模式决定是否剔除
    if (mode == BACK && isBackFacing) return true;
    if (mode == FRONT && !isBackFacing) return true;
    
    return false;
}

// ==========================================
// 方法二:屏幕空间法 (投影后 - GPU 标准做法)
// ==========================================
// 假设顶点已经经过 MVP 变换并除以了 w,变成了 2D 坐标
bool ShouldCull_2D(float x1, float y1, float x2, float y2, float x3, float y3, 
                   CullMode mode, WindingOrder expectedFrontFace) {
    
    // 计算有符号面积 (叉积的 z 分量)
    // 公式: (v2 - v1) x (v3 - v1)
    float signedArea = cross_2d(x2 - x1, y2 - y1, x3 - x1, y3 - y1);
    
    bool isCCW = (signedArea > 0);
    
    // OpenGL 默认 CCW 为正面
    bool isFrontFace;
    if (expectedFrontFace == CCW) {
        isFrontFace = isCCW;
    } else {
        isFrontFace = !isCCW;
    }
    
    // 判定是否剔除
    if (mode == BACK && !isFrontFace) return true;
    if (mode == FRONT && isFrontFace) return true;
    
    return false;
}
第三步:测试

让我们用一个简单的例子来验证。

cpp 复制代码
int main() {
    // 定义一个三角形
    Vec3 v1 = {0.0f, 0.0f, 0.0f};
    Vec3 v2 = {1.0f, 0.0f, 0.0f};
    Vec3 v3 = {0.0f, 1.0f, 0.0f};
    
    // 摄像机位置
    Vec3 cameraPos = {0.0f, 0.0f, 5.0f}; // 在 Z 轴正方向看
    
    std::cout << "--- 测试 3D 空间法 ---" << std::endl;
    
    // 情况 A: v1 -> v2 -> v3 (逆时针 CCW)
    // 观察者看向 Z 轴负方向时,这个顺序是逆时针
    bool cull1 = ShouldCull_3D(v1, v2, v3, cameraPos, BACK);
    std::cout << "顶点顺序 v1-v2-v3 (CCW), 剔除背面: " 
              << (cull1 ? "已剔除" : "保留") << std::endl;

    // 情况 B: v1 -> v3 -> v2 (顺时针 CW)
    bool cull2 = ShouldCull_3D(v1, v3, v2, cameraPos, BACK);
    std::cout << "顶点顺序 v1-v3-v2 (CW), 剔除背面: " 
              << (cull2 ? "已剔除" : "保留") << std::endl;

    std::cout << "\n--- 测试屏幕空间法 (模拟 GPU) ---" << std::endl;
    // 模拟投影后的 2D 坐标
    // 注意:屏幕坐标系 Y 轴通常向下,但在数学计算中我们通常假设 Y 轴向上。
    // 这里假设标准笛卡尔坐标系
    
    // 逆时针三角形
    float s1 = cross_2d(1, 0, 0, 1); 
    std::cout << "2D 叉积结果 (CCW): " << s1 << " (正数)" << std::endl;
    
    // 顺时针三角形
    float s2 = cross_2d(0, 1, 1, 0);
    std::cout << "2D 叉积结果 (CW): " << s2 << " (负数)" << std::endl;

    return 0;
}

总结

OpenGL 内部实现面剔除的流程如下:

  1. 顶点处理:顶点着色器运行完毕,顶点变换到裁剪空间。
  2. 透视除法 :执行除以 www 操作,得到归一化设备坐标 (NDC)。
  3. 视口变换 :将 NDC 映射到屏幕窗口坐标 (x,y)(x, y)(x,y)。
  4. 面剔除计算
    • 计算三角形面积:S=(x2−x1)(y3−y1)−(y2−y1)(x3−x1)S = (x_2-x_1)(y_3-y_1) - (y_2-y_1)(x_3-x_1)S=(x2−x1)(y3−y1)−(y2−y1)(x3−x1)。
    • 根据 glFrontFace(CCW/CW) 设定,判断符号位是否代表正面。
    • 根据 glCullFace(BACK/FRONT) 设定,如果符合剔除条件,则直接丢弃该三角形,不进入片段着色器。

这就是为什么面剔除是一个极其高效的操作------它在片段着色器运行之前就丢弃了整个三角形,且计算量仅仅是一次简单的 2D 叉乘。

相关推荐
IT猿手2 小时前
光伏模型参数估计:基于山羊优化算法(GOA )的光伏模型参数辨识问题求解研究,免费提供完整MATLAB代码链接
开发语言·算法·matlab·群智能优化算法·智能优化算法·光伏模型参数估计·光伏模型参数辨识
麻雀飞吧2 小时前
期货量化策略讲解:天勤量化下的跨期价差均值回归策略实战
python·算法·均值算法·回归
sali-tec3 小时前
C# 基于OpenCv的视觉工作流-章62-线线距离
图像处理·人工智能·opencv·算法·计算机视觉
WolfGang0073213 小时前
代码随想录算法训练营 Day53 | 图论 part11
算法·图论
呃呃本3 小时前
算法题(图论)
算法·图论
一只数据集3 小时前
商超上货人形机器人全身运控数据集分析——Kuavo 5机器人5W型号夹爪末端执行器操作轨迹数据
人工智能·算法·机器人
谙弆悕博士4 小时前
【附Python源码】基于决策树的信用卡欺诈检测实战
python·学习·算法·决策树·机器学习·数据分析·scikit-learn
MATLAB代码顾问4 小时前
黏菌算法(SMA)原理详解与Python实现
开发语言·python·算法