需要预先学习:


1. 帧缓冲(Framebuffer)

着色与渲染管线 的片元处理结束时,通过所有测试的片元会被写入帧缓冲(framebuffer)——渲染管线的最终目的地。默认情况下这个目的地是屏幕:窗口系统在创建窗口时自动配置好一块帧缓冲,渲染结果直接显示。帧缓冲对象(FBO,Framebuffer Object)允许你把渲染目标从屏幕换成自定义的纹理或缓冲,这是后处理、阴影贴图、环境光预计算等一切"渲染到纹理"技术的基础。这篇笔记介绍 FBO 的三种附件(颜色、深度、模板),并重点讲清片元何时写入附件。


2. 默认帧缓冲

深度缓冲 中讲过,帧缓冲不是一个单独的缓冲,而是多块逐像素并列的存储的集合:

缓冲 存储内容 每个像素存什么
颜色缓冲(color buffer) 片元最终颜色 一个 RGBA 颜色值
深度缓冲(depth buffer) 当前最近的深度 一个深度值
模板缓冲(stencil buffer) 模板测试用的标记 一个整数

默认帧缓冲由窗口系统(Win32、X11 等)创建,尺寸与窗口客户区一致。片元处理时:

  1. 深度测试:与深度缓冲比较,不通过则丢弃片元(见 深度缓冲);
  2. 模板测试:与模板缓冲比较,不通过则丢弃(见 着色与渲染管线);
  3. 混合:与颜色缓冲中已有的颜色混合;
  4. 全部通过后,颜色写入颜色缓冲、深度写入深度缓冲。

默认帧缓冲还使用双缓冲(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)           与颜色附件中已有的颜色混合
  ↓
写入附件                  颜色写颜色附件,深度写深度附件

三个关键结论:

  1. 测试失败 = 什么都不写。深度测试失败的片元不会更新颜色附件,也不会更新深度附件——深度缓冲里"更近"的记录被保留,这正是深度测试能工作的原因(见 深度缓冲);
  2. 深度测试通过时,颜色与深度同步写入。一个片元通过深度测试,颜色与深度在同一次更新中写入各自附件;
  3. 颜色与深度可以分开控制。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 的附件机制与写入时机,这些技术的代码就只剩"换附件"的区别了。