帧缓冲
需要预先学习:
1. 帧缓冲(Framebuffer)
着色与渲染管线 的片元处理结束时,通过所有测试的片元会被写入帧缓冲(framebuffer)——渲染管线的最终目的地。默认情况下这个目的地是屏幕:窗口系统在创建窗口时自动配置好一块帧缓冲,渲染结果直接显示。帧缓冲对象(FBO,Framebuffer Object)允许你把渲染目标从屏幕换成自定义的纹理或缓冲,这是后处理、阴影贴图、环境光预计算等一切"渲染到纹理"技术的基础。这篇笔记介绍 FBO 的三种附件(颜色、深度、模板),并重点讲清片元何时写入附件。
2. 默认帧缓冲
深度缓冲 中讲过,帧缓冲不是一个单独的缓冲,而是多块逐像素并列的存储的集合:
| 缓冲 | 存储内容 | 每个像素存什么 |
|---|---|---|
| 颜色缓冲(color buffer) | 片元最终颜色 | 一个 RGBA 颜色值 |
| 深度缓冲(depth buffer) | 当前最近的深度 | 一个深度值 |
| 模板缓冲(stencil buffer) | 模板测试用的标记 | 一个整数 |
默认帧缓冲由窗口系统(Win32、X11 等)创建,尺寸与窗口客户区一致。片元处理时:
- 深度测试:与深度缓冲比较,不通过则丢弃片元(见 深度缓冲);
- 模板测试:与模板缓冲比较,不通过则丢弃(见 着色与渲染管线);
- 混合:与颜色缓冲中已有的颜色混合;
- 全部通过后,颜色写入颜色缓冲、深度写入深度缓冲。
默认帧缓冲还使用双缓冲(double buffering):渲染写入后台缓冲,显示读取前台缓冲,一帧画完调用 glfwSwapBuffers 之类的函数交换二者,避免画面撕裂(tearing,见 着色与渲染管线)。
注:深度测试与模板测试默认是关闭的,需要
glEnable(GL_DEPTH_TEST)、glEnable(GL_STENCIL_TEST)显式开启;颜色写入默认开启。
3. 帧缓冲对象(FBO)
窗口绑定到默认帧缓冲,但很多效果需要先画到别处再处理。帧缓冲对象(FBO)就是自定义的渲染目标:把颜色、深度、模板缓冲换成你自己创建的纹理或缓冲,之后所有绘制命令都写到那里,而不是屏幕。
3.1 为什么需要离屏渲染
离屏渲染(offscreen rendering)指把场景渲染到 FBO 而不是屏幕。典型用途:
| 用途 | 做法 | 例子 |
|---|---|---|
| 后处理 | 场景先渲染到颜色纹理,再把它当普通纹理画满屏幕 | 反色、模糊、色调映射、FXAA(见 光栅化) |
| 阴影贴图 | 深度附件挂纹理,从光源渲染后采样比较 | ShadowMap与PCF、PCSS、CSM级联阴影 |
| 环境光预计算 | 把积分结果渲染进纹理,运行时直接采样 | PBR 的 IBL |
| 多 pass 渲染 | 第一遍产出中间结果,后续 pass 消费 | 延迟着色、描边、泛光 |
共同点:渲染结果不再是像素,而是数据——纹理可以被采样、被保存、被下一遍绘制使用。
3.2 FBO 是附件的集合
FBO 本身不存储任何数据,它只是附件(attachment)的挂载点。附件是实际存放数据的对象,通过 glFramebuffer* 系列函数挂到 FBO 上:
| 附件类型 | 枚举值 | 数据来源 | 参与哪个阶段 |
|---|---|---|---|
| 颜色附件 | GL_COLOR_ATTACHMENT0 ~ GL_COLOR_ATTACHMENT15 |
纹理或 RBO | 片元着色器颜色输出 |
| 深度附件 | GL_DEPTH_ATTACHMENT |
纹理或 RBO | 深度测试 |
| 模板附件 | GL_STENCIL_ATTACHMENT |
纹理或 RBO | 模板测试 |
| 深度+模板 | GL_DEPTH_STENCIL_ATTACHMENT |
纹理或 RBO(GL_DEPTH24_STENCIL8 格式) |
深度与模板测试 |
颜色附件可以有多个(多重渲染目标,MRT),深度、模板附件各一个。每个附件都是一个纹理(texture)或渲染缓冲对象(RBO,renderbuffer object,见后文)——纹理可以被后续 pass 采样,RBO 不能。
注:FBO 是 OpenGL 对象(object),遵循 缓冲对象与顶点 中讲过的通用模式:
glGenFramebuffers创建、glBindFramebuffer绑定、用完glDeleteFramebuffers删除。绑定目标GL_FRAMEBUFFER同时作用于读写,也可分别用GL_DRAW_FRAMEBUFFER、GL_READ_FRAMEBUFFER指定。
3.3 创建 FBO
创建 FBO 分四步:生成对象、挂附件、检查完整性、解绑。
// 1. 生成并绑定 FBO
GLuint fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// 2a. 颜色附件:一张纹理(渲染后供后处理采样)
GLuint texColor;
glGenTextures(1, &texColor);
glBindTexture(GL_TEXTURE_2D, texColor);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, W, H, 0, // W、H 与窗口同尺寸
GL_RGB, GL_UNSIGNED_BYTE, nullptr); // 不填数据,只分配存储
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0,
GL_TEXTURE_2D, texColor, 0);
// 2b. 深度+模板附件:用 RBO(只需要参与测试,不需要采样)
GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, W, H);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, rbo);
// 3. 完整性检查:附件缺失或组合不合法时渲染会静默失败
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
// 配置有误:例如颜色附件与深度附件尺寸不一致
}
// 4. 解绑回默认帧缓冲
glBindFramebuffer(GL_FRAMEBUFFER, 0);
注意:完整性检查不能省。FBO 配置错误时 OpenGL 不会报错,而是所有绘制命令被静默忽略——画面什么都不出,最难排查。常见的
GL_FRAMEBUFFER_INCOMPLETE_ATTACHMENT(附件存储未分配)与GL_FRAMEBUFFER_INCOMPLETE_MISSING_ATTACHMENT(没有挂任何附件)都可以靠它抓出来。
4. 附件的写入时机
FBO 不会改变片元处理的流程,只改变片元写入的目的地。但"什么时候写、写到哪里"有几个容易踩坑的点,这一节专门讲清。
4.1 渲染目标的切换
绑定 FBO 之后,所有绘制命令(包括 glClear)都作用于该 FBO 的附件:
// 渲染到 FBO
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, W, H); // 视口要匹配附件尺寸
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawScene(); // 写入 fbo 的附件
// 切回默认帧缓冲(屏幕)
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, windowW, windowH); // 视口也要切回来
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawPostProcess(); // 写入屏幕
注意:视口不会随帧缓冲自动切换。
glViewport只受显式调用影响,FBO 附件尺寸与窗口尺寸不一致时,忘记调glViewport会导致渲染范围错位——画面只画出一部分,或贴图坐标越界。
4.2 片元写入附件的时机
片元写入附件发生在片元处理阶段的末尾,与渲染目标是默认帧缓冲还是 FBO 无关(见 着色与渲染管线):
片元着色器输出颜色
↓
剪裁测试(scissor test) 矩形裁剪区域外的片元被丢弃
↓
模板测试(stencil test) 失败 -> 丢弃,颜色与深度都不写
↓
深度测试(depth test) 失败 -> 丢弃,颜色与深度都不写
↓
混合(blending) 与颜色附件中已有的颜色混合
↓
写入附件 颜色写颜色附件,深度写深度附件
三个关键结论:
- 测试失败 = 什么都不写。深度测试失败的片元不会更新颜色附件,也不会更新深度附件——深度缓冲里"更近"的记录被保留,这正是深度测试能工作的原因(见 深度缓冲);
- 深度测试通过时,颜色与深度同步写入。一个片元通过深度测试,颜色与深度在同一次更新中写入各自附件;
- 颜色与深度可以分开控制。
glDepthMask(GL_FALSE)关闭深度写入(只测试不写),glColorMask(GL_FALSE, ...)关闭颜色写入——例如只想用深度测试剔除物体、不画颜色时(ShadowMap 的 Pass 1 就是典型的"只写深度")。
// 伪代码:一次深度测试通过的片元更新了什么
// 深度测试通过
frame_buffer.depth[x][y] = fragment.depth; // 深度附件(若 glDepthMask 开启)
frame_buffer.color[x][y] = blended_color; // 颜色附件(若 glColorMask 开启)
注:混合发生在写入之前,所以混合读到的是颜色附件中已有的旧值;深度不参与混合。半透明物体需要先画不透明物体再画自身,原因就在这里(见 着色与渲染管线)。
4.3 多颜色附件与 glDrawBuffers
FBO 可以挂多个颜色附件(MRT,Multiple Render Targets),片元着色器的多个输出各写一个附件:
#version 330 core
layout (location = 0) out vec4 gColor; // 写 GL_COLOR_ATTACHMENT0
layout (location = 1) out vec4 gNormal; // 写 GL_COLOR_ATTACHMENT1
C++ 侧用 glDrawBuffers 声明哪些附件被写入:
GLuint attachments[2] = { GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1 };
glDrawBuffers(2, attachments);
注意:默认情况下 FBO 只写
GL_COLOR_ATTACHMENT0。挂上了第二个颜色附件但没调glDrawBuffers,第二个附件永远是空的——这是 MRT 最常见的"为什么没数据"原因。glDrawBuffers(GL_NONE)表示不写任何颜色附件,只跑深度测试(ShadowMap 的 Pass 1 正是这样配的,见 ShadowMap与PCF、PCSS)。
4.4 附件的读取
纹理附件在渲染完成后就是一张普通纹理,可以直接绑定给下一遍绘制采样:
// 后处理 pass:把上一遍渲染的颜色纹理当输入
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texColor);
glUniform1i(glGetUniformLocation(postShader, "screenTexture"), 0);
glReadPixels 则把附件内容读回 CPU 内存(glReadBuffer 可先选择读哪个颜色附件)——用于截图、拾取等低频操作。注意纹理附件不能边渲染边采样:把正在写入的纹理绑定到同一遍绘制的采样器上,行为未定义(OpenGL 4.5 之前的限制),需要采样时先解绑 FBO。
5. 渲染缓冲对象(RBO)
5.1 为什么需要 RBO
渲染缓冲对象(renderbuffer,RBO)是另一种附件载体,与纹理的关键区别:RBO 不能被采样,只能作为渲染目标。既然不能采样,为什么还要它?
- RBO 的数据布局针对"作为渲染目标"做了优化,读写效率更高,开销更小;
- 深度、模板缓冲在渲染中只参与测试,从不被采样(阴影贴图要采样深度比较,那是特例,用纹理);
- 不需要为用不到的功能付代价:纹理的过滤、环绕、mipmap 等参数对 RBO 毫无意义。
| 维度 | 纹理附件 | RBO 附件 |
|---|---|---|
| 能否采样 | 能(后处理、阴影贴图) | 不能 |
| 性能 | 一般 | 针对渲染目标优化,更快 |
| 存储格式 | 纹理格式(GL_RGB 等) |
渲染格式(GL_DEPTH24_STENCIL8 等) |
| 适用场景 | 需要把渲染结果当数据用的附件 | 只参与测试的深度、模板附件 |
注:深度附件到底用纹理还是 RBO,取决于后续要不要采样它的值——ShadowMap与PCF、PCSS 的深度贴图用纹理(Pass 2 要采样比较),本篇示例的深度附件用 RBO(只需要测试)。同一张 FBO 里两者可以混用。
5.2 创建 RBO
GLuint rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, W, H);
glBindRenderbuffer(GL_RENDERBUFFER, 0); // 解绑
// 挂到 FBO 的深度+模板附件上
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT,
GL_RENDERBUFFER, rbo);
glBindFramebuffer(GL_FRAMEBUFFER, 0);
GL_DEPTH24_STENCIL8 是常用的组合格式:24 位深度 + 8 位模板,一张缓冲同时支撑两个测试。若只要深度,用 GL_DEPTH_COMPONENT24 即可。
6. 完整示例:渲染到纹理与后处理
把上面的内容串起来:第一遍把场景渲染到 FBO 的颜色纹理,第二遍用全屏四边形做后处理(这里用反色演示),把结果画到屏幕。
6.1 Pass 1:场景渲染到 FBO
// 初始化:创建 FBO(代码见"创建 FBO"一节,texColor + rbo + fbo)
// 渲染循环
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, W, H);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
drawScene(); // 普通场景绘制,写入附件
glBindFramebuffer(GL_FRAMEBUFFER, 0);
场景绘制与平时完全相同——深度测试、混合照常工作,只是目的地变成了附件。
6.2 Pass 2:全屏后处理
后处理 pass 不需要任何模型变换,画一个覆盖全屏的四边形(顶点直接用 NDC 坐标,不再经过 model/view/projection 变换):
// 后处理顶点着色器:NDC 顶点直达裁剪空间
#version 330 core
layout (location = 0) in vec2 aPos; // NDC 坐标,范围 [-1, 1]
layout (location = 1) in vec2 aUV; // 纹理坐标,范围 [0, 1]
out vec2 UV;
void main()
{
UV = aUV;
gl_Position = vec4(aPos, 0.0, 1.0); // 不再乘 model/view/projection
}
// 后处理片段着色器:反色
#version 330 core
in vec2 UV;
uniform sampler2D screenTexture; // Pass 1 渲染出的颜色纹理
out vec4 FragColor;
void main()
{
vec3 color = texture(screenTexture, UV).rgb;
FragColor = vec4(1.0 - color, 1.0); // 反色:1 - 原色
}
C++ 侧:绑定后处理着色器,画一个全屏四边形(两个三角形),把 texColor 绑定给 screenTexture。灰度、模糊、色调映射都是同一框架——只改片段着色器里的几行。
glUseProgram(postShader);
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, texColor); // Pass 1 的产物
glUniform1i(glGetUniformLocation(postShader, "screenTexture"), 0);
drawFullscreenQuad(); // 覆盖全屏的四边形
注:后处理 pass 的四边形要关掉深度测试(
glDisable(GL_DEPTH_TEST))——它永远在最上层,不需要也不应该参与深度比较。
7. 应用回顾
帧缓冲贯穿了本系列的多篇笔记,每篇都只用了 FBO 的一个侧面:
| 笔记 | 用到的附件 | 用途 |
|---|---|---|
| ShadowMap与PCF、PCSS | 深度附件挂纹理(GL_DEPTH_ATTACHMENT) |
阴影贴图,Pass 2 采样比较 |
| CSM级联阴影 | 深度附件挂纹理数组的每一层(glFramebufferTextureLayer) |
多级联深度贴图 |
| PBR | 颜色附件挂纹理(IBL 预计算) | 环境贴图预滤波、BRDF 查找表 |
| 本篇 | 颜色附件 + 深度 RBO 附件 | 渲染到纹理与后处理 |
共同的模式:把渲染结果当作数据存进附件,再被下一遍绘制消费。理解 FBO 的附件机制与写入时机,这些技术的代码就只剩"换附件"的区别了。



