强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 8 章:高级 OpenGL

第 8 章:高级 OpenGL

本章讲解 OpenGL 中影响像素最终输出的高级特性:模板测试、混合、面剔除,以及帧缓冲对象(FBO)的离屏渲染和后处理技术。


8.1 逐片段操作流水线

在片段着色器输出颜色之后,像素还需要经过一系列测试才能写入帧缓冲:

片段着色器输出 FragColor
    │
    ▼
┌───────────────────┐
│  裁剪测试          │  在视口内?
│  (Scissor Test)    │
└───────┬───────────┘
        ▼
┌───────────────────┐
│  模板测试          │  模板缓冲匹配?
│  (Stencil Test)    │
└───────┬───────────┘
        ▼
┌───────────────────┐
│  深度测试          │  更近的物体?
│  (Depth Test)      │
└───────┬───────────┘
        ▼
┌───────────────────┐
│  混合              │  透明度混合
│  (Blending)        │
└───────┬───────────┘
        ▼
    帧缓冲写入

8.2 模板测试(Stencil Test)

8.2.1 概念

模板测试使用模板缓冲(Stencil Buffer,通常 8-bit,值 0-255)来决定片段是否被丢弃。常用于实现物体轮廓描边、镜子、裁剪区域等效果。

8.2.2 基本配置

// 启用模板测试
glEnable(GL_STENCIL_TEST);

// 配置模板测试
glStencilFunc(
    GL_ALWAYS,    // 测试函数:总是通过
    1,            // 参考值
    0xFF          // 掩码
);

// 配置模板操作
glStencilOp(
    GL_KEEP,      // 测试失败:保持原值
    GL_KEEP,      // 测试通过,深度失败:保持原值
    GL_REPLACE    // 都通过:写入参考值
);

8.2.3 模板函数对照

函数含义
GL_ALWAYS总是通过
GL_EQUAL缓冲值 == 参考值
GL_NOTEQUAL缓冲值 != 参考值
GL_LESS缓冲值 < 参考值
GL_GREATER缓冲值 > 参考值

8.2.4 实战:物体轮廓描边

// 渲染流程:
// 1. 正常绘制物体,写入模板缓冲
glEnable(GL_STENCIL_TEST);
glStencilFunc(GL_ALWAYS, 1, 0xFF);
glStencilOp(GL_KEEP, GL_KEEP, GL_REPLACE);
glStencilMask(0xFF);       // 允许写入模板缓冲
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT | GL_STENCIL_BUFFER_BIT);

drawObject();  // 正常绘制物体

// 2. 绘制放大版物体,只在模板值 != 1 的区域绘制(描边)
glStencilFunc(GL_NOTEQUAL, 1, 0xFF);
glStencilMask(0x00);       // 禁止写入模板缓冲
glDisable(GL_DEPTH_TEST);  // 禁用深度测试,确保描边可见

outlineShader.use();
float scale = 1.1f;  // 放大 10%
glm::mat4 model = glm::scale(originalModel, glm::vec3(scale));
outlineShader.setMat4("model", model);
drawObject();

glStencilMask(0xFF);
glStencilFunc(GL_ALWAYS, 0, 0xFF);
glEnable(GL_DEPTH_TEST);
glDisable(GL_STENCIL_TEST);
效果示意:
┌─────────────────┐
│    ┌───────┐    │
│    │ 绿色  │    │  ← 模板值 = 1 的区域:正常绘制
│    │ 物体  │    │
│    └───────┘    │  ← 蓝色描边:模板值 != 1 的放大版
│                 │
└─────────────────┘

8.3 混合(Blending)

8.3.1 Alpha 混合公式

混合实现半透明效果:

最终颜色 = 源颜色 × 源因子 + 目标颜色 × 目标因子

C_result = C_src × F_src  +  C_dst × F_dst
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);

8.3.2 混合因子

因子说明
GL_ZERO(0, 0, 0, 0)不使用
GL_ONE(1, 1, 1, 1)完全使用
GL_SRC_ALPHA(As, As, As, As)源透明度
GL_ONE_MINUS_SRC_ALPHA(1-As, 1-As, 1-As, 1-As)1 减去源透明度
GL_DST_ALPHA(Ad, Ad, Ad, Ad)目标透明度

8.3.3 排序问题

⚠️ 半透明物体必须从后往前渲染(画家算法)。否则深度测试会错误地丢弃被遮挡的半透明片段。

// 1. 先渲染所有不透明物体
glDisable(GL_BLEND);
for (auto& obj : opaqueObjects) {
    renderObject(obj);
}

// 2. 排序半透明物体(按距离相机远近)
std::sort(transparentObjects.begin(), transparentObjects.end(),
    [&](const auto& a, const auto& b) {
        return glm::length(cameraPos - a.pos) > glm::length(cameraPos - b.pos);
    });

// 3. 从后往前渲染
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
for (auto& obj : transparentObjects) {
    renderObject(obj);
}

8.3.4 加法混合(粒子效果)

// 加法混合:颜色叠加(火焰、光晕)
glBlendFunc(GL_SRC_ALPHA, GL_ONE);

8.4 面剔除(Face Culling)

8.4.1 原理

三角形的顶点顺序决定了其正面/背面朝向。剔除背面三角形可以减少约 50% 的片段处理。

glEnable(GL_CULL_FACE);
glCullFace(GL_BACK);      // 剔除背面(默认)
glFrontFace(GL_CCW);      // 逆时针为正面(默认)
设置说明
GL_BACK剔除背面
GL_FRONT剔除正面
GL_FRONT_AND_BACK剔除两面(只绘制线框)
GL_CCW逆时针(Counter-Clockwise)为正面
GL_CW顺时针(Clockwise)为正面

8.4.2 顶点缠绕顺序

逆时针 (CCW = 正面):     顺时针 (CW = 背面):
     v2                       v0
     ╱╲                       ╱╲
    ╱  ╲                     ╱  ╲
   ╱    ╲                   ╱    ╲
  v0────v1                 v1────v2

⚠️ 如果模型加载后面剔除看起来反了,试试 glFrontFace(GL_CW) 或检查模型的顶点顺序。


8.5 帧缓冲对象(Framebuffer Object, FBO)

8.5.1 什么是帧缓冲?

默认情况下,OpenGL 渲染到默认帧缓冲(屏幕)。FBO 允许渲染到离屏目标(纹理),用于后处理、阴影映射、反射等。

默认帧缓冲:              自定义 FBO:
┌──────────────┐         ┌─────────────────────┐
│ 颜色附件 0    │ → 屏幕  │ 颜色附件 0 (纹理)    │ → 后处理
│ 颜色附件 1    │         │ 颜色附件 1 (纹理)    │ → G-Buffer
│ 深度/模板附件 │         │ 深度/模板附件        │
└──────────────┘         └─────────────────────┘

8.5.2 创建 FBO

// ===== 1. 创建帧缓冲 =====
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);

// ===== 2. 创建颜色附件(纹理) =====
unsigned int colorTexture;
glGenTextures(1, &colorTexture);
glBindTexture(GL_TEXTURE_2D, colorTexture);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTexture, 0);

// ===== 3. 创建深度/模板附件(渲染缓冲对象) =====
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, 800, 600);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);

// ===== 4. 检查完整性 =====
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
    std::cerr << "Framebuffer not complete!" << std::endl;
}
glBindFramebuffer(GL_FRAMEBUFFER, 0);

8.5.3 渲染到纹理

// 第一遍:渲染到 FBO
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
glEnable(GL_DEPTH_TEST);

sceneShader.use();
renderScene(sceneShader);

// 第二遍:使用 FBO 的颜色纹理进行后处理
glBindFramebuffer(GL_FRAMEBUFFER, 0);  // 切回默认帧缓冲
glClearColor(1.0f, 1.0f, 1.0f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);

postProcessShader.use();
glBindTexture(GL_TEXTURE_2D, colorTexture);
renderFullscreenQuad();

8.6 后处理效果

8.6.1 全屏四边形

float quadVertices[] = {
    // 位置          // UV
    -1.0f,  1.0f,   0.0f, 1.0f,
    -1.0f, -1.0f,   0.0f, 0.0f,
     1.0f, -1.0f,   1.0f, 0.0f,

    -1.0f,  1.0f,   0.0f, 1.0f,
     1.0f, -1.0f,   1.0f, 0.0f,
     1.0f,  1.0f,   1.0f, 1.0f,
};

8.6.2 反色效果

#version 460 core
in vec2 vTexCoord;
out vec4 FragColor;
uniform sampler2D screenTexture;

void main() {
    FragColor = vec4(vec3(1.0 - texture(screenTexture, vTexCoord)), 1.0);
}

8.6.3 灰度效果

void main() {
    vec4 color = texture(screenTexture, vTexCoord);
    float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
    FragColor = vec4(vec3(gray), 1.0);
}

8.6.4 核效果(边缘检测、锐化、模糊)

void main() {
    float offset = 1.0 / 300.0;
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), vec2(0.0,  offset), vec2(offset,  offset),
        vec2(-offset,  0.0),    vec2(0.0,  0.0),    vec2(offset,  0.0),
        vec2(-offset, -offset), vec2(0.0, -offset), vec2(offset, -offset)
    );

    // 边缘检测核
    float kernel[9] = float[](
        -1, -1, -1,
        -1,  8, -1,
        -1, -1, -1
    );

    vec3 result = vec3(0.0);
    for (int i = 0; i < 9; i++) {
        result += texture(screenTexture, vTexCoord + offsets[i]).rgb * kernel[i];
    }
    FragColor = vec4(result, 1.0);
}
核类型效果
锐化[0,-1,0, -1,5,-1, 0,-1,0]
模糊均值 [1/9 × 9] 或高斯权重
边缘检测[-1,-1,-1, -1,8,-1, -1,-1,-1]
浮雕[-2,-1,0, -1,1,1, 0,1,2]

8.7 延迟渲染(Deferred Rendering)

8.7.1 前向 vs 延迟

特性前向渲染延迟渲染
光照计算每物体 × 每光源每像素 × 每光源
M 个物体 N 个光源O(M×N)O(M+N)
多光源场景性能差性能好
透明物体容易处理需要单独处理
带宽开销高(G-Buffer)

8.7.2 G-Buffer 布局

G-Buffer:
┌────────────────────────────────────┐
│ 颜色附件 0 (RGB16F): 世界空间法线   │
│ 颜色附件 1 (RGBA8):  漫反射颜色    │
│ 颜色附件 2 (RGB16F): 镜面反射颜色  │
│ 深度附件:            深度值         │
└────────────────────────────────────┘

第一遍(几何阶段):渲染所有物体,填充 G-Buffer
第二遍(光照阶段):使用 G-Buffer 数据,在全屏四边形上计算光照

8.8 注意事项

⚠️ FBO 完整性检查:所有附件的尺寸必须相同。缺少颜色附件或深度附件会导致 GL_FRAMEBUFFER_INCOMPLETE

⚠️ 深度测试与混合的交互:深度测试在混合之前执行。如果半透明物体需要被远处物体透过看到,必须关闭深度写入(glDepthMask(GL_FALSE))或正确排序。

⚠️ 面剔除与镜像:镜像变换会翻转缠绕顺序,导致面剔除错误。在渲染镜像物体时临时切换 glFrontFace

⚠️ 性能:后处理效果需要额外的全屏绘制。多个后处理效果需要多个 FBO(链式处理)。


8.9 业务场景

场景 1:游戏中的描边效果

模板测试实现:选中物体时显示蓝色轮廓高亮。

场景 2:粒子系统

加法混合 + 关闭深度写入实现火焰、烟雾、光晕效果。

场景 3:照片编辑器

FBO 离屏渲染 + 核效果实现滤镜(模糊、锐化、边缘检测)。

场景 4:大量光源场景

延迟渲染处理数十甚至上百个动态光源(如赛车游戏的车灯)。


8.10 扩展阅读

资源说明
Learn OpenGL - Advanced OpenGL高级特性教程
Learn OpenGL - FramebuffersFBO 详解
Deferred Shading延迟渲染
OIT (Order-Independent Transparency)无关顺序透明

本章小结

  • 逐片段操作流水线:裁剪 → 模板测试 → 深度测试 → 混合 → 写入
  • 模板测试使用模板缓冲实现描边、裁剪等效果
  • 混合实现半透明,关键在于渲染顺序(从后往前)
  • 面剔除通过顶点缠绕顺序减少约 50% 的绘制量
  • FBO 允许离屏渲染,是后处理、阴影映射、延迟渲染的基础
  • 延迟渲染将几何和光照分离,适合多光源场景

上一章第 7 章:光照与阴影 下一章第 9 章:实例化渲染