OpenGL / OpenCL 编程指南 / 第 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 - Framebuffers | FBO 详解 |
| Deferred Shading | 延迟渲染 |
| OIT (Order-Independent Transparency) | 无关顺序透明 |
本章小结
- 逐片段操作流水线:裁剪 → 模板测试 → 深度测试 → 混合 → 写入
- 模板测试使用模板缓冲实现描边、裁剪等效果
- 混合实现半透明,关键在于渲染顺序(从后往前)
- 面剔除通过顶点缠绕顺序减少约 50% 的绘制量
- FBO 允许离屏渲染,是后处理、阴影映射、延迟渲染的基础
- 延迟渲染将几何和光照分离,适合多光源场景
上一章:第 7 章:光照与阴影 下一章:第 9 章:实例化渲染