日期:2025年7月3日标签:Computer Graphics

OpenGL:混合与面剔除 #

本文整理 LearnOpenGL Advanced OpenGL 中的 Blending(混合)Face Culling(面剔除) 两节内容,并补充一些便于理解和记忆的说明。(LearnOpenGL CN)

一、混合 Blending #

1. 什么是混合 #

OpenGL 中,混合(Blending)主要用于实现透明、半透明效果

一个半透明物体最终显示的颜色,并不完全来自物体本身,而是:

  • 一部分来自当前物体;
  • 一部分来自物体后面的背景;
  • 两种颜色按照一定比例进行混合。

例如一块半透明玻璃:

摄像机
   ↓
┌─────────┐
│ 半透明玻璃 │   ← Source
└─────────┘
   ↓
┌─────────┐
│ 后面的墙体 │   ← Destination
└─────────┘

通常通过颜色的第四个分量 Alpha 表示透明程度:

vec4 color = vec4(r, g, b, alpha);

例如:

alpha = 1.0   完全不透明
alpha = 0.5   半透明
alpha = 0.0   完全透明

需要注意:

Alpha 本身更准确地说表示当前颜色的“不透明程度”,而不是“透明度”。

例如 alpha=0.8alpha = 0.8 ,可以理解为当前物体颜色贡献约 80%。


2. 两种透明处理方式 #

透明纹理通常有两类情况:

透明纹理
│
├── 只有完全透明 / 完全不透明
│      ↓
│    discard
│
└── 存在半透明
       ↓
     Blending

例如:

  • 草、树叶:可以使用 discard
  • 玻璃、烟雾、半透明 UI:通常需要 Blending。(LearnOpenGL CN)

3. RGBA 纹理 #

如果纹理包含 Alpha 通道,那么纹理数据需要使用 GL_RGBA

glTexImage2D(
    GL_TEXTURE_2D,
    0,
    GL_RGBA,
    width,
    height,
    0,
    GL_RGBA,
    GL_UNSIGNED_BYTE,
    data
);

片段着色器也需要保留 Alpha:

#version 330 core

out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D texture1;

void main()
{
    FragColor = texture(texture1, TexCoords);
}

这里 texture() 返回:

vec4(R, G, B, A)

而不是只使用 RGB。


4. 完全透明:discard #

如果纹理只有:

alpha = 0

和:

alpha = 1

两种状态,例如草、树叶等,可以直接在片段着色器中丢弃透明片段:

void main()
{
    vec4 texColor = texture(texture1, TexCoords);

    if (texColor.a < 0.1)
        discard;

    FragColor = texColor;
}

discard 的作用是:

立即终止当前 Fragment 的处理,这个 Fragment 不会写入颜色缓冲。

可以把它理解成:

Fragment Shader
      │
      ├── alpha >= 0.1
      │       ↓
      │    正常输出
      │
      └── alpha < 0.1
              ↓
           discard
              ↓
           不产生像素

它不是把颜色设置为透明:

FragColor = vec4(0, 0, 0, 0);

而是这个 Fragment 直接不存在了。(LearnOpenGL CN)


5. Alpha 纹理边缘问题 #

透明纹理使用:

GL_REPEAT

时,边缘可能出现一圈奇怪的半透明颜色。

原因是纹理过滤会对边缘 Texel 进行插值,而 GL_REPEAT 会让纹理一侧和另一侧的数据参与插值。

因此对于草、树叶等 Alpha 纹理,通常设置:

glTexParameteri(
    GL_TEXTURE_2D,
    GL_TEXTURE_WRAP_S,
    GL_CLAMP_TO_EDGE
);

glTexParameteri(
    GL_TEXTURE_2D,
    GL_TEXTURE_WRAP_T,
    GL_CLAMP_TO_EDGE
);

这样纹理边界不会和另一侧重复区域进行插值。(LearnOpenGL CN)


6. 开启混合 #

对于真正的半透明,例如:

alpha = 0.2
alpha = 0.5
alpha = 0.8

不能简单使用 discard

需要开启 OpenGL 的混合:

glEnable(GL_BLEND);

然后设置混合方式:

glBlendFunc(
    GL_SRC_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA
);

这是最常见的 Alpha Blending 配置。(LearnOpenGL CN)


7. 混合发生在哪里 #

可以把简化后的 Fragment 处理过程理解成:

Fragment Shader
      │
      ↓
输出 Fragment Color
      │
      ↓
Stencil Test
      │
      ↓
Depth Test
      │
      ↓
Blending
      │
      ↓
Color Buffer

Fragment Shader 产生的是一个新的颜色。

而当前 Framebuffer 对应像素位置通常已经有一个颜色。

所以出现两个颜色:

Fragment Shader 输出
        ↓
   Source Color
        │
        ├─────┐
        │     │
        │   Blending
        │     │
        └─────┤
              ↓
       Result Color
              ↑
              │
        Destination Color
              ↑
        Color Buffer

8. Source 与 Destination #

理解 OpenGL Blending 最重要的两个概念:

Source #

Source 是:

当前正在绘制的 Fragment 的颜色。

通常就是 Fragment Shader:

FragColor

的输出。

表示为:

CsourceC_{source}

Destination #

Destination 是:

当前 Color Buffer 对应像素位置已经存在的颜色。

表示为:

CdestinationC_{destination}

所以:

Source
    ↓
当前正在画的 Fragment

Destination
    ↓
之前已经画进 Framebuffer 的颜色

9. 混合公式 #

OpenGL 的基本混合公式可以写成:

Cresult=CsourceFsource+CdestinationFdestinationC_{result} = C_{source} \cdot F_{source} + C_{destination} \cdot F_{destination}

其中:

  • CsourceC_{source} :当前 Fragment 的颜色;
  • CdestinationC_{destination} :Color Buffer 中已有颜色;
  • FsourceF_{source} :Source 的混合因子;
  • FdestinationF_{destination} :Destination 的混合因子。

最终:

Result
=
Source × SourceFactor
+
Destination × DestinationFactor

结果重新写入 Color Buffer。(LearnOpenGL CN)


10. 最常用的 Alpha Blending #

最常见的配置:

glBlendFunc(
    GL_SRC_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA
);

因此:

Fsource=alphasourceF_{source} = alpha_{source}

Fdestination=1alphasourceF_{destination} = 1 - alpha_{source}

混合公式变成:

Cresult=Csourceαsource+Cdestination(1αsource)C_{result} = C_{source} \cdot \alpha_{source} + C_{destination} \cdot (1-\alpha_{source})

例如当前 Fragment 是绿色:

Source = Green
alpha  = 0.6

背景是红色:

Destination = Red

最终颜色:

Cresult=Green0.6+Red0.4C_{result} = Green \cdot 0.6 + Red \cdot 0.4

也就是:

60% 绿色
+
40% 红色

11. glBlendFunc #

函数定义:

glBlendFunc(
    GLenum sfactor,
    GLenum dfactor
);

其中:

sfactor
    ↓
Source Factor

dfactor
    ↓
Destination Factor

常见参数:

参数含义
GL_ZERO00
GL_ONE11
GL_SRC_COLORSource RGB
GL_ONE_MINUS_SRC_COLOR1SourceRGB1-SourceRGB
GL_DST_COLORDestination RGB
GL_ONE_MINUS_DST_COLOR1DestinationRGB1-DestinationRGB
GL_SRC_ALPHASource Alpha
GL_ONE_MINUS_SRC_ALPHA1SourceAlpha1-SourceAlpha
GL_DST_ALPHADestination Alpha
GL_ONE_MINUS_DST_ALPHA1DestinationAlpha1-DestinationAlpha

日常最重要的就是:

glBlendFunc(
    GL_SRC_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA
);

12. glBlendFuncSeparate #

通常 RGB 和 Alpha 使用相同的混合策略。

如果希望分别设置,可以使用:

glBlendFuncSeparate(
    srcRGB,
    dstRGB,
    srcAlpha,
    dstAlpha
);

例如:

glBlendFuncSeparate(
    GL_SRC_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA,
    GL_ONE,
    GL_ZERO
);

前两个参数控制:

RGB

后两个参数控制:

Alpha

13. glBlendEquation #

前面的公式默认执行的是:

Source+DestinationSource + Destination

实际上 OpenGL 可以修改两部分之间的运算方式:

glBlendEquation(GL_FUNC_ADD);

常见模式:

模式计算
GL_FUNC_ADDSrc+DstSrc + Dst
GL_FUNC_SUBTRACTSrcDstSrc-Dst
GL_FUNC_REVERSE_SUBTRACTDstSrcDst-Src
GL_MINmin(Src,Dst)min(Src,Dst)
GL_MAXmax(Src,Dst)max(Src,Dst)

默认:

GL_FUNC_ADD

所以一般情况下不需要手动调用:

glBlendEquation(...)

14. 为什么透明物体存在渲染顺序问题 #

混合和深度测试结合时,会产生一个非常重要的问题。

假设有两个半透明窗口:

Camera
  │
  ↓
Window A       近
  │
  ↓
Window B       远

如果先画 A:

Depth Buffer
    ↓
记录 A 的深度

之后再画 B:

B 比 A 更远
    ↓
Depth Test Failed
    ↓
B 被丢弃

但是:

A 是半透明的

理论上应该能够通过 A 看到 B。

问题在于:

Depth Buffer 只记录深度,并不知道一个 Fragment 是不是半透明。

即使:

alpha = 0.1

只要这个 Fragment 通过了深度测试,它仍然可能写入深度缓冲。

因此不能完全依赖 Depth Test 处理透明物体。(LearnOpenGL CN)


15. 透明物体正确绘制顺序 #

通常采用:

1. 绘制所有不透明物体

2. 将透明物体按照摄像机距离排序

3. 从远到近绘制透明物体

即:

Opaque
  ↓
Transparent Far
  ↓
Transparent
  ↓
Transparent Near

不透明物体仍然可以正常依靠 Depth Test。

透明物体则需要:

Back-to-Front,从远到近绘制。 (LearnOpenGL CN)


16. 透明物体排序示例 #

可以计算摄像机和透明物体之间的距离:

float distance =
    glm::length(camera.Position - windows[i]);

然后排序。

例如:

std::map<float, glm::vec3> sorted;

for (unsigned int i = 0; i < windows.size(); i++)
{
    float distance =
        glm::length(camera.Position - windows[i]);

    sorted[distance] = windows[i];
}

绘制时反向遍历:

for (
    auto it = sorted.rbegin();
    it != sorted.rend();
    ++it
)
{
    model = glm::mat4(1.0f);
    model = glm::translate(model, it->second);

    shader.setMat4("model", model);

    glDrawArrays(GL_TRIANGLES, 0, 6);
}

即:

distance 大
    ↓
先画

distance 小
    ↓
后画

不过这种按照“物体位置”排序只是一个简单方案,对于很大的物体、互相穿插的透明几何体等情况并不能完全解决问题。更复杂的场景可以使用 OIT(Order Independent Transparency,顺序无关透明度)。(LearnOpenGL CN)


17. discard 与 Blending 的区别 #

两者都可以用于透明纹理,但原理完全不同。

discardBlending
适用情况完全透明 / 不透明半透明
Alpha通常 0 或 1可以是 0~1
Fragment 是否存在直接丢弃仍然存在
是否参与混合
是否需要透明排序通常不需要通常需要
常见场景草、树叶、栅栏玻璃、烟雾、透明 UI

例如草:

if (texColor.a < 0.1)
    discard;

玻璃:

glEnable(GL_BLEND);

glBlendFunc(
    GL_SRC_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA
);

18. 混合总结 #

最需要记住的是:

Blending
│
├── Source
│     当前 Fragment Shader 输出颜色
│
├── Destination
│     Color Buffer 已有颜色
│
└── Result
      Source × SrcFactor
      +
      Destination × DstFactor

普通 Alpha Blending:

Cresult=Csourceα+Cdestination(1α)C_{result} = C_{source}\alpha + C_{destination}(1-\alpha)

对应:

glEnable(GL_BLEND);

glBlendFunc(
    GL_SRC_ALPHA,
    GL_ONE_MINUS_SRC_ALPHA
);

透明物体绘制原则:

先不透明
   ↓
透明物体排序
   ↓
透明物体从远到近

二、面剔除 Face Culling #

1. 为什么需要面剔除 #

一个封闭的 3D 物体通常有大量从当前观察方向完全看不到的面。

以立方体为例:

一个立方体
    ↓
6 个面

从外部观察
    ↓
最多只能看到 3 个面

背面的面最终不会出现在屏幕上。

如果仍然对它们进行完整的光栅化和 Fragment Shader 计算,就会浪费 GPU 资源。

因此可以:

在光栅化阶段直接丢弃背向摄像机的三角形。

这就是:

Face Culling

面剔除。(LearnOpenGL CN)


2. Front Face 与 Back Face #

一个三角形存在两个观察方向:

          Front
            ↑
            │
      ┌──────────┐
      │ Triangle │
      └──────────┘
            │
            ↓
           Back

对于封闭模型:

模型外侧
   ↓
Front Face

模型内部
   ↓
Back Face

通常我们只需要绘制:

Front Face

而:

Back Face

可以直接剔除。


3. OpenGL 怎么判断正面和背面 #

OpenGL 并不是简单根据法向量判断正反面。

它主要根据三角形顶点在屏幕上的:

Winding Order(环绕顺序)

判断 Front Face 和 Back Face。

三角形:

v0
│\
│ \
│  \
│   \
v1───v2

顶点可以按照两种方向排列:

Counter-Clockwise
逆时针

v0 → v1 → v2

或者:

Clockwise
顺时针

v0 → v2 → v1

4. OpenGL 默认的正面 #

OpenGL 默认:

Counter-Clockwise
       ↓
   Front Face

也就是:

glFrontFace(GL_CCW);

其中:

CCW = Counter Clockwise
      逆时针

CW  = Clockwise
      顺时针

因此模型顶点一般约定:

从模型外部观察时,三角形顶点按照逆时针排列。 (LearnOpenGL CN)


5. 为什么同一个三角形会变成顺时针 #

这里是理解 Face Culling 最重要的地方。

假设定义一个三角形时,从它的正面观察:

1 → 2 → 3

是逆时针:

      1
     / \
    /   \
   3 ←── 2

那么:

从正面观察
    ↓
1 → 2 → 3
    ↓
CCW
    ↓
Front Face

但是如果摄像机跑到它的背面:

从背面观察
    ↓
1 → 2 → 3
    ↓
CW
    ↓
Back Face

也就是说:

顶点 Buffer 中存储的顺序没有发生变化,发生变化的是从摄像机视角看到的屏幕空间环绕方向。

因此 OpenGL 能够根据最终投影后的三角形环绕顺序判断它到底面向还是背向摄像机。(LearnOpenGL CN)


6. 环绕顺序什么时候判断 #

一个很重要的点是:

Front Face / Back Face 的判断是在顶点着色器处理之后、光栅化阶段根据投影后的顶点方向进行判断。

可以理解成:

Vertex Data
    ↓
Vertex Shader
    ↓
Clip Space
    ↓
Perspective Divide
    ↓
Screen / Window Orientation
    ↓
判断 CW / CCW
    ↓
Face Culling
    ↓
Rasterization

因此并不是单纯检查模型空间中:

vertices[0]
vertices[1]
vertices[2]

的坐标顺序。

而是要看:

从当前 Camera 的观察方向来看,这三个顶点最终呈现出来是 CW 还是 CCW。(LearnOpenGL CN)


7. 开启面剔除 #

OpenGL 默认关闭面剔除。

开启:

glEnable(GL_CULL_FACE);

默认情况下会:

剔除 Back Face

等价于:

glEnable(GL_CULL_FACE);

glCullFace(GL_BACK);

glFrontFace(GL_CCW);

这是最常见的配置。(LearnOpenGL CN)


8. glCullFace #

通过:

glCullFace(...);

指定剔除哪一种面。

剔除背面 #

glCullFace(GL_BACK);

最常用。

表示:

Front → 保留
Back  → 丢弃

剔除正面 #

glCullFace(GL_FRONT);

表示:

Front → 丢弃
Back  → 保留

正反面全部剔除 #

glCullFace(GL_FRONT_AND_BACK);

此时三角形正反面都会被剔除。


9. glFrontFace #

可以通过:

glFrontFace(...)

指定哪一种环绕顺序代表 Front Face。

OpenGL 默认:

glFrontFace(GL_CCW);

即:

CCW → Front
CW  → Back

也可以改为:

glFrontFace(GL_CW);

此时:

CW  → Front
CCW → Back

10. glFrontFace 与 glCullFace 的区别 #

这两个 API 很容易混淆。

glFrontFace

定义什么叫 Front Face。

例如:

glFrontFace(GL_CCW);

表示:

CCW = Front
CW  = Back

而:

glCullFace(GL_BACK);

表示:

确定了 Front / Back 之后,要丢弃哪一种。

所以完整逻辑是:

三角形
   ↓
判断 CW / CCW
   ↓
glFrontFace()
   ↓
确定 Front / Back
   ↓
glCullFace()
   ↓
决定是否丢弃

例如:

glFrontFace(GL_CCW);
glCullFace(GL_BACK);

就是:

CCW
 ↓
Front
 ↓
保留


CW
 ↓
Back
 ↓
剔除

11. 常见配置 #

标准封闭模型一般:

glEnable(GL_CULL_FACE);

glFrontFace(GL_CCW);

glCullFace(GL_BACK);

也就是:

逆时针 = 正面

背面 = 剔除

由于:

glFrontFace(GL_CCW);
glCullFace(GL_BACK);

本身就是默认状态,所以通常只需要:

glEnable(GL_CULL_FACE);

12. 哪些物体不适合面剔除 #

面剔除特别适合:

封闭实体

例如:

  • Cube;
  • Sphere;
  • CAD Solid;
  • 人物模型;
  • 建筑模型。

因为:

外表面需要显示
内表面通常不需要显示

但是一些模型必须同时看到正反面。

例如草:

     Camera
       ↓

     ┌───┐
     │草 │
     └───┘
       ↑
     一个 Quad

如果从 Quad 背面看它,而启用了:

glCullFace(GL_BACK);

整棵草可能直接消失。

因此:

Grass
Leaf
Paper
Cloth
Billboard
某些透明 Quad

往往需要:

glDisable(GL_CULL_FACE);

或者进行双面渲染。(LearnOpenGL CN)


13. Face Culling 与 Depth Test 的区别 #

两者都可以减少不可见 Fragment,但原理完全不同。

Face Culling #

根据:

Triangle Orientation

决定整个三角形是否需要渲染。

Back Face
    ↓
整个 Triangle 丢弃
    ↓
甚至不需要生成 Fragment

Depth Test #

三角形已经经过光栅化产生 Fragment:

Triangle
    ↓
Rasterization
    ↓
Fragments
    ↓
Depth Test
    ↓
判断是否被其它物体遮挡

所以:

Face Culling

发生得更早,可以避免背面三角形产生大量无用 Fragment。


14. Face Culling 不等于视锥剔除 #

需要区分两个容易混淆的概念:

Face Culling

和:

Frustum Culling

Face Culling 的对象是:

Triangle Face

解决:

这个三角形正面对着 Camera,
还是背面对着 Camera?

Frustum Culling 的对象通常是:

Object / Mesh / Bounding Box

解决:

整个物体是否在摄像机视锥体之外?

例如:

                Camera Frustum

                   /\
                  /  \
                 / A  \
                /      \
               /________\

        B

其中:

A
↓
进入渲染流程
↓
其中的 Triangle 再进行 Face Culling


B
↓
整个对象 Frustum Cull
↓
连 Draw Call 都可以省掉

因此两者通常可以同时使用。


15. Face Culling 的完整流程 #

可以将整个过程理解为:

Vertex Data
     ↓
Vertex Shader
     ↓
Primitive Assembly
     ↓
Triangle
     ↓
判断屏幕空间 Winding Order
     ↓
┌───────────────┐
│ CW 还是 CCW? │
└───────────────┘
     ↓
glFrontFace()
     ↓
┌────────────────┐
│ Front / Back? │
└────────────────┘
     ↓
glCullFace()
     ↓
是否 Cull
     │
 ┌───┴─────┐
 │         │
Cull     Keep
 │         │
结束    Rasterization
           ↓
        Fragment
           ↓
       Depth Test
           ↓
        Rendering

16. 面剔除总结 #

最常见配置:

glEnable(GL_CULL_FACE);

glFrontFace(GL_CCW);

glCullFace(GL_BACK);

记忆:

glFrontFace
    ↓
谁是正面?

glCullFace
    ↓
丢弃谁?

默认:

CCW = Front
CW  = Back

Cull Back

最终:

逆时针三角形
    ↓
Front
    ↓
保留


顺时针三角形
    ↓
Back
    ↓
剔除

三、混合、深度测试与面剔除之间的关系 #

这三个功能可以放在一起理解:

Triangle
    │
    ↓
Face Culling
    │
    │ 整个三角形是否应该存在?
    ↓
Rasterization
    │
    ↓
Fragments
    │
    ↓
Fragment Shader
    │
    ↓
Stencil / Depth Test
    │
    │ 当前 Fragment 是否被遮挡?
    ↓
Blending
    │
    │ 当前颜色怎样和 Color Buffer 混合?
    ↓
Color Buffer

它们分别解决不同问题:

功能解决的问题
Face Culling三角形背面对着摄像机,要不要画?
Depth TestFragment 是否被前面的物体遮挡?
BlendingFragment 与已有颜色如何组合?

可以简单记成:

Face Culling
    ↓
这个面要不要画?

Depth Test
    ↓
这个像素能不能看到?

Blending
    ↓
这个像素最终是什么颜色?

这三个问题分别对应 几何可见性、深度可见性和颜色合成

(完)

目录