帧缓冲、附件与可靠的两遍渲染

帧缓冲、附件与可靠的两遍渲染:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。

学习目标

  • 能实现颜色纹理 + 深度 renderbuffer 的 FBO,并根据完整性状态定位附件配置错误
  • 能写出两遍渲染的目标、viewport、clear 与深度状态切换,避免把结果写回正在采样的附件
  • 能设计窗口像素尺寸变化后的附件重建,使 FBO、viewport 与采样步长保持一致
  • 能修改后处理得到反相、灰度、模糊和边缘检测,并用 textureSize 计算单 texel 偏移
  • 能分析黑屏、深度穿插、边缘接缝和缩放后画面拉伸四类故障

为什么要另开一块离屏画板

前面几章,你画的每一笔最后都落在同一块画布上——也就是你屏幕里看到的那块。测试、混合、剔除,全是在「往这块画布上画的过程中」做手脚。可有些效果,光在画的过程里做不到:比如想把整幅画变成黑白、整体反相、或者沿着轮廓描一圈边。

这些是对整张完成图动手的「后期」,得先有一张完整的图才行。办法是:另外开一块画板,先把整个场景画到这块离屏的画板上、当成一张图存起来;再回到屏幕这块画布上,把那张图贴满整个屏幕,贴的时候顺手做后期——反相、模糊、描边随你。

这一章要解决的就是:怎么把整个场景先画进一张「图」、再对这张图整体做加工。没有它,你只能一笔一笔地画,永远摸不到「先画完、再后期」这种工序,做不出全屏的滤镜效果。

帧缓冲:自己开的那块离屏画板

你一直在画的那块屏幕,其实是系统替你准备好的一块画板,叫 ——桌面 OpenGL 里它的编号是 0,WebGL 里用 null 表示。平时你什么都不绑,画的内容就落在它上面、显示到屏幕。

本章要做的,是自己再开一块这样的画板:(framebuffer,常简称 FBO)。它和默认帧缓冲长得一样、用法一样,唯一区别是画进去的东西默认不显示到屏幕,而是先存着,供你后续取用。在工厂流水线的比喻里:默认帧缓冲是「成品出货口」,自建帧缓冲是车间里一块「中转画板」,先把半成品画上去,下一道工序再加工。

它在管线里的位置在最末端——所有测试、混合都过完、颜色要落地时,落在「当前绑定的帧缓冲」上。绑默认帧缓冲就落到屏幕,绑自建帧缓冲就落到那块离屏画板。怎么往这块画板上挂「能存东西的地方」,是下一节。

附件:往画板上挂存储槽

帧缓冲本身不存像素——它只是个「框」。真正存数据的,是往这个框上挂的 (attachment)。本章的后处理 FBO 需要一份可采样颜色和一份深度存储,因此使用下面两类附件:

第一类是 ——它是一张纹理。渲染时,画面的颜色写进这张纹理;渲完之后,它就能像任何普通纹理一样被采样。这正是「把场景渲成一张图、再贴出来」的命门:颜色落在纹理里,下一遍才取得出来。它挂在帧缓冲的 COLOR_ATTACHMENT0(颜色附件 0 号位)上。

第二类是 ——本章用来存深度(和模板)。离屏渲染立方体时,前后面仍靠深度测试判遮挡,因此 FBO 要有深度存储。这里后续不在 shader 采样深度,选 renderbuffer 能准确表达“只作为渲染目标”的用途;它是否比纹理更快取决于实现,不能当成普遍保证。下面这张图把「FBO 这个框 + 挂上去的两个附件」画清楚:

帧缓冲完整性:挂齐了才能用

附件挂好不等于能用。帧缓冲必须至少有一个有效附件,每个 attachment image 都要存在、格式可渲染,多重采样附件的 sample 数还要一致;启用的 draw/read buffer 也必须指向存在的颜色附件。这个状态叫 。本章颜色 + 深度组合还应保持相同渲染尺寸,避免 viewport 超出较小附件的有效区域。挂完附件后,一定要glCheckFramebufferStatus 查一下它是不是 GL_FRAMEBUFFER_COMPLETE

COMPLETE 才能往里渲染;不是的话,往这块帧缓冲画啥都白搭——轻则一片空白、重则报错。这步检查很容易被忘掉,忘了又恰好不完整,就会对着白屏一头雾水(§7 第一个坑就是它)。检查通过,这块离屏画板就备好了。

渲染到纹理 + 两遍渲染:先画成图,再贴出来

画板和附件都备齐,整套流程是两遍渲染。第一遍叫 (render-to-texture):绑上自建帧缓冲,把整个场景照常画一遍——但画面没上屏,而是写进了那张颜色纹理附件,存成了一张「图」。

第二遍才上屏:绑回默认帧缓冲,画一个铺满整个屏幕的 (screen quad)——它的顶点正好落在屏幕四角,由两个三角形拼成、占满整块屏幕。这个四边形的片段着色器采样第一遍那张纹理,把它贴满屏幕显示出来。两遍走完,你就把整个场景「先存成图、再贴回屏幕」了。这两遍的流程下面 Demo 会单步走一遍。

后处理与卷积核:贴的时候顺手做后期

光是「原样贴回去」没意思——精彩的在于:第二遍采样纹理时,可以对采到的颜色任意加工再输出。这种「对整张完成图做整体加工」就叫 (post-processing)。最简单的两种只看当前像素:反相就是 1 − 颜色,灰度就是按亮度权重把 RGB 压成一个灰度值。

更有意思的是要看周围像素的效果,靠 (kernel):拿一张 3×3 的小权重表,对当前像素和它周围 8 个邻居各采一次样,分别乘上表里对应的权重再加起来,就是新颜色。换不同的权重表,就是不同滤镜——每格都填 1/9模糊(取邻域平均、抹平细节),中心填大数、四周填负数是边缘检测(突出明暗突变、描出轮廓)。下面这张图把「核怎么对邻域加权求和」拆开:

动手:切换后处理核,看同一画面被加工

猜一猜:下面这块画布里有一个自转的彩色立方体——但它其实是先被画进一张离屏纹理、再贴回屏幕的。如果你把后处理核切到**「边缘检测」,画面会变成什么样?是整体变暗,还是只剩下立方体的轮廓线**、平坦的面都变黑?先切一下看看,再读下面的解释。

这块画布严格走了两遍渲染:第一遍把自转立方体渲进帧缓冲的颜色纹理附件(开了深度测试、挂了深度 renderbuffer,所以前后面遮挡正确),第二遍画一个全屏四边形采样这张纹理,按你选的核处理后上屏。切换下面的核,盯着同一个立方体怎么被加工:

切到反相,颜色全部翻转、像看底片;切到灰度,立方体变成黑白;切到模糊,每个像素被换成它和邻居的平均色、细节糊掉、边缘发虚;切到边缘检测最明显——平坦的面因为「邻域都一样、加权求和接近 0」变黑,只有颜色突变的棱边被留下,整个立方体被描成了线框轮廓。后三种都是 3×3 卷积核在起作用,区别只在那张权重表填了什么。

两遍渲染:单步走一遍从场景到屏幕

猜一猜:上面那个立方体明明显示在屏幕上,凭什么说它「先被画进了一张纹理」?这中间到底分了几遍画、每遍画在哪?想一想「离屏画板」那个比喻,再单步走一遍下面的流程。

下面把整套两遍渲染拆成三步。每一步都画出数据这一刻流到哪了——盯着画面是先进了帧缓冲、还是已经上了屏:

分步1 / 3

① 第一遍:把场景渲进帧缓冲的纹理

第一遍是离屏的:先绑定自己创建的 帧缓冲,再把场景里的彩色立方体照常画一遍(开 深度测试,靠挂上去的深度附件判前后遮挡)。但这一遍的画面 没有上屏,而是写进了帧缓冲的颜色纹理附件 ——整个场景被存成了一张「图」。

走完这遍就清楚了:屏幕上看到的,其实是「先画进纹理、再贴回屏幕」的第二遍结果。第一遍只在离屏画板上忙活、不上屏;正因为中间多了「存成一张图」这一道,才有机会对整张图做后处理。

两遍之间不只是换一次绑定。每一遍都要明确当前 render target、viewport、深度状态和资源读写方向:

最危险的是把 texColor 仍挂在当前绘制 FBO 上,同时又在 shader 中采样它。这样一次 draw 同时读写同一图像,形成 feedback loop,结果未定义。正确做法是第二遍先绑定默认帧缓冲或另一只 FBO,再把第一遍颜色纹理作为只读输入。

代码逐段拆解

帧缓冲的代码分三块:建帧缓冲 + 挂附件 + 验完整性两遍渲染的绑定切换后处理片段着色器。三块对照看,C++/OpenGL 与 WebGL2/TS 互为镜像。

第一块:建 FBO、挂颜色纹理附件 + 深度 renderbuffer、验完整

先建一个帧缓冲并绑定,往它身上挂一张颜色纹理附件(存画面、渲完可采样)和一个深度 renderbuffer 附件(存深度供测试),最后用 glCheckFramebufferStatus 确认它完整。

// 1) 建帧缓冲对象并绑定
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
// 2) 颜色纹理附件:建一张空纹理(data = NULL),挂到 COLOR_ATTACHMENT0
unsigned int texColor;
glGenTextures(1, &texColor);
glBindTexture(GL_TEXTURE_2D, texColor);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, NULL);
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);

颜色附件挂好了,还差深度。本例后续 shader 不采样深度,因此用 renderbuffer 作为深度渲染目标。建一个渲染缓冲、给它分配「深度(+模板)」格式的存储、挂到帧缓冲的深度附件点上,最后检查完整性:

// 3) 深度 renderbuffer:本例后续 shader 不采样深度
unsigned int rbo;
glGenRenderbuffers(1, &rbo);
glBindRenderbuffer(GL_RENDERBUFFER, rbo);
glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, rbo);
// 4) 验完整性:不是 COMPLETE,往这个帧缓冲渲染就白搭
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
  std::cout << "ERROR: framebuffer not complete!" << std::endl;
glBindFramebuffer(GL_FRAMEBUFFER, 0);   // 建完先解绑,回到默认帧缓冲

两端逻辑一一对应,但有几处 API 名称和取值差异要留神:

width / height 必须使用实际 framebuffer 的像素尺寸,高 DPI 窗口下不能直接拿 CSS 或逻辑窗口尺寸代替。窗口尺寸变化时,旧附件不会自动跟着变大;要为颜色纹理和深度 renderbuffer 重新分配存储,然后再次检查完整性:

void ResizeOffscreenTargets(int width, int height) {
  glBindTexture(GL_TEXTURE_2D, texColor);
  glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA8, width, height, 0,
               GL_RGBA, GL_UNSIGNED_BYTE, nullptr);
 
  glBindRenderbuffer(GL_RENDERBUFFER, rbo);
  glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height);
 
  glBindFramebuffer(GL_FRAMEBUFFER, fbo);
  assert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE);
}

重新分配可能让旧内容失效,所以应在下一帧第一遍先 clear 再绘制。若不同 pass 使用不同分辨率,例如半分辨率 Bloom,也要分别保存每只 FBO 的像素尺寸。

第二块:两遍渲染——绑定切换

每帧画两遍。第一遍绑自建帧缓冲、开深度测试、画场景(写进颜色纹理);第二遍绑回默认帧缓冲、关深度测试、画全屏四边形并把第一遍那张纹理绑上去采样。

// 第一遍:绑自建帧缓冲,开深度测试,正常画场景(写进颜色纹理)
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
glViewport(0, 0, offscreenWidth, offscreenHeight);
glEnable(GL_DEPTH_TEST);
glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
DrawScene();                       // 立方体等都画进离屏纹理
// 第二遍:绑回默认帧缓冲(0),关深度测试,画全屏四边形采样那张纹理
glBindFramebuffer(GL_FRAMEBUFFER, 0);
glViewport(0, 0, framebufferWidth, framebufferHeight);
glDisable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT);
screenShader.use();
glBindVertexArray(quadVAO);
glBindTexture(GL_TEXTURE_2D, texColor);  // 第一遍渲好的颜色纹理
glDrawArrays(GL_TRIANGLES, 0, 6);

关键不只是两句对绑:每次更换目标后都设置与目标像素尺寸匹配的 viewport,每遍各自 clear(两块帧缓冲互不影响),并明确深度状态。第二遍这里关掉深度测试,因为全屏四边形不需要和旧场景深度比较;若后续 pass 仍需深度,则应按 pass 目标重新启用并配置。全屏四边形的顶点直接写在 NDC 四角,UV 写在 0~1 四角,顶点着色器原样透传。

第三块:后处理片段着色器(核采样)

后处理全在第二遍那个全屏四边形的片段着色器里。最简单的反相、灰度只看当前像素一个采样;核效果(模糊、边缘检测)要采当前像素 + 周围 8 格,乘权重再求和。

#version 300 es
precision highp float;
in vec2 TexCoords;
uniform sampler2D screenTexture;
out vec4 FragColor;
void main() {
  vec3 c = texture(screenTexture, TexCoords).rgb;
  // 反相:1 − 颜色
  // c = vec3(1.0) - c;
  // 灰度:按人眼亮度权重把 RGB 压成一个灰度值
  float lum = dot(c, vec3(0.2126, 0.7152, 0.0722));
  c = vec3(lum);
  FragColor = vec4(c, 1.0);
}

核效果的骨架是:用 textureSize 分别求 X/Y 方向一个 texel 的 UV 步长,再对 9 个位置采样、乘权重并累加。不能把偏移写死成 1/300:纹理不是 300×300 或窗口缩放后,采样点就不再是相邻像素。上面是带权模糊核;边缘检测核是中心 8、四周 -1(权重和 0,平坦区相加为黑、只剩突变处)。本站 Demo 的「模糊」用的是 3×3 均值核(每格 1/9),效果同理。

卷积还要处理纹理边界。颜色附件创建时使用 CLAMP_TO_EDGE,可避免邻域采样越界后从另一侧重复进来形成接缝;需要更严格控制时,也可以在 shader 中 clamp UV,或只在有效内区执行卷积。

容易踩的坑

小结

  • 帧缓冲 FBO 是自己开的一块「离屏画板」,区别于显示到屏幕的默认帧缓冲(0 / null)
  • FBO 本身不存像素;本章挂可采样的颜色纹理和只作渲染目标的深度 renderbuffer,使用前必须检查完整性
  • 两遍渲染:第一遍写离屏颜色/深度,第二遍绑定另一目标并采样颜色纹理;不能在一次 draw 中读写同一附件
  • 每次切换 render target 都要设置匹配的 viewport、clear 和深度状态;窗口像素尺寸变化后要重建附件
  • 后处理可做反相、灰度与 3×3 卷积;用 textureSize 求 texel 步长,并用 CLAMP_TO_EDGE 处理边界

练习

问题 1(改 Demo 代码 / 设计题) 在上面的 FramebufferDemo 里你玩过 5 个核。现在要给它新增一个「锐化」核(让画面更清晰、边缘更硬)。写出这个 3×3 锐化核的 9 个权重,并说清:① 它的权重和应该是几?为什么?② 在本章那段「3×3 卷积核」的片段着色器里,你要改哪个量来加这个核?

问题 2(独立实现题) 不看上面的代码,把「建一个能用的帧缓冲」这件事按顺序写出来(用伪代码或函数名即可):从建 FBO 开始,到挂颜色纹理附件、挂深度 renderbuffer 附件、验完整性、解绑。并说清:为什么颜色用纹理附件、深度却用 renderbuffer 附件?

问题 3(排错题) 某人写了帧缓冲后处理,渲出来一片纯黑、屏幕上什么都没有,但代码也不报错。他绑了 FBO、挂了颜色纹理、画了场景、绑回默认帧缓冲画了全屏四边形。请列出至少两个最可能的原因,并各给修法。

名词解释

本章出现的专业名词,用大白话再讲一遍。

默认帧缓冲

窗口系统替你建好的那块帧缓冲,渲染结果最终显示到屏幕的就是它。桌面 OpenGL 里编号是 0,WebGL 里用 null 表示。平时不绑别的帧缓冲时,画的内容都落在它上面。详见本章「帧缓冲」一节。

帧缓冲

一块可以画进去、但不一定显示到屏幕的「画板」。你可以自己创建多块,把场景渲到某一块上当成离屏的图,再拿来做后处理、镜像、阴影贴图等。它本身不存像素,只是把若干「附件」挂在一起的容器,常简称 FBO。详见本章「帧缓冲」一节。

附件

挂到 framebuffer attachment point、真正提供图像存储的对象。来源可以是纹理的某个 mip 层,也可以是renderbuffer;挂载点决定它用于颜色、深度还是模板。FBO 至少需要一个有效附件,但深度专用 FBO 可以没有颜色附件。详见本章「附件」一节。

颜色纹理附件

一种帧缓冲附件,它是一张纹理:渲染时画面颜色写进它,渲染完之后它就能像普通纹理一样被采样。这正是「把场景渲成一张图」的关键——颜色落在纹理里,下一遍就能贴出来。挂在 COLOR_ATTACHMENT0 上。详见本章「附件」一节。

渲染缓冲附件

renderbuffer 提供存储的附件。它可以作为渲染目标,也支持多重采样存储,但不能像普通纹理那样在 shader 中直接采样。常用于后续无需 shader 读取的深度或模板;若要采样深度,应改用纹理附件。详见本章「附件」一节。

帧缓冲完整性

帧缓冲的附件组合是否满足渲染要求。至少要有一个有效附件,格式和多重采样配置要兼容,启用的 draw/read buffer 还要指向实际存在的颜色槽;深度专用 FBO 可关闭颜色读写而不挂颜色。挂好附件后必须用 glCheckFramebufferStatus 检查它返回 GL_FRAMEBUFFER_COMPLETE,否则往这个帧缓冲渲染会失败或得到一片空白。详见本章「帧缓冲完整性」一节。

渲染到纹理

不把场景画到屏幕,而是画进一张纹理(帧缓冲的颜色纹理附件)。渲完这张纹理就存着整个场景的画面,可以当普通纹理拿去贴、采样、做后处理。是后处理、镜像、阴影贴图等一大类效果的基础。详见本章「渲染到纹理」一节。

全屏四边形

一个顶点正好落在屏幕四角(标准化设备坐标 NDC 的 −1 到 1)的矩形,由两个三角形拼成,铺满整个屏幕。它的作用是「把一张纹理贴满屏幕」:片段着色器逐像素采样那张离屏纹理、顺便做后处理,结果就铺满了整个画面。详见本章「渲染到纹理」一节。

后处理

拿一张已经渲好的完整画面(离屏纹理),对它整体做加工再显示:反相、转灰度、模糊、描边等。因为是在「把图贴满屏幕」那一遍逐像素处理,所以能对全屏统一施加效果,是游戏里各种屏幕滤镜的基础。详见本章「后处理与卷积核」一节。

卷积核

一个 3×3(也可更大)的小权重表,对当前像素及其周围 8 个邻居采样,分别乘上表里对应位置的权重再相加,得到这个像素的新颜色。换不同权重表就得到不同效果:每格 1/9 是模糊(取邻域平均),中心大、四周负是边缘检测(突出明暗突变处)。详见本章「后处理与卷积核」一节。

版本、来源与运行边界

本章以 Joey de Vries 的 LearnOpenGL 原章 为授权改编依据,教学运行基线是 OpenGL 3.3 Core Profile。Khronos 当前发布的规范参照是 OpenGL 4.6 Core Profile;这里用 4.6 规范核查术语和状态合同,但不把 4.6 API 偷偷倒填为原教程内容。GLFW、GLAD、Assimp 与驱动版本都属于运行环境,不能拿“编译通过”替代对 context、资源和 framebuffer 结果的验证。

正式概念与状态责任

  • framebuffer:在“帧缓冲、附件与可靠的两遍渲染”中由自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer负责解释其输入、受控状态和可观察结果;运行时以attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理定位它的第一处变化。
  • attachment:在“帧缓冲、附件与可靠的两遍渲染”中由自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer负责解释其输入、受控状态和可观察结果;运行时以attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理定位它的第一处变化。
  • renderbuffer:在“帧缓冲、附件与可靠的两遍渲染”中由自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer负责解释其输入、受控状态和可观察结果;运行时以attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理定位它的第一处变化。
  • post-processing:在“帧缓冲、附件与可靠的两遍渲染”中由自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer负责解释其输入、受控状态和可观察结果;运行时以attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理定位它的第一处变化。

章专属 OpenGL 状态实验

先预测“绑定 FBO 检查完整性并画场景,再绑定 0、恢复 viewport、采样 color texture”发生后,自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer应怎样改变附件对象/格式/尺寸、draw buffers、完整性、viewport 和 pass 边界;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。

实验一:Context—资源—结果合同

选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。

Context · resource · observable result

帧缓冲、附件与可靠的两遍渲染:状态合同

创建完整离屏 FBO,完成场景 pass 后切回默认 framebuffer 做屏幕空间处理

验证场景

官方教程正式概念

logl-22 · 基线帧

framebuffer固定 context、资源内容与输入事件,执行“绑定 FBO 检查完整性并画场景,再绑定 0、恢复 viewport、采样 color texture”

状态所有者自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer
受控状态/资源附件对象/格式/尺寸、draw buffers、完整性、viewport 和 pass 边界
触发命令绑定 FBO 检查完整性并画场景,再绑定 0、恢复 viewport、采样 color texture

冻结输入:framebuffer

自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer记录附件对象/格式/尺寸、draw buffers、完整性、viewport 和 pass 边界

结果:得到可重复的初始 GL 状态与资源身份

观测:attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理中的初始快照

预期:自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer得到可复查结果,并持续满足“所有附件尺寸/样本数兼容;绝不从当前正在写入的同一附件采样”

实验二:CPU 命令到 GPU 结果的五段轨迹

逐段执行“绑定 FBO 检查完整性并画场景,再绑定 0、恢复 viewport、采样 color texture”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“所有附件尺寸/样本数兼容;绝不从当前正在写入的同一附件采样”。

CPU command · GL state · GPU result

帧缓冲、附件与可靠的两遍渲染:五段轨迹

选择一段命令—资源—结果1 / 5

当前观测:attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理中的初始快照

不变量:所有附件尺寸/样本数兼容;绝不从当前正在写入的同一附件采样

实验三:单故障与同输入恢复

注入“color attachment 尺寸更新而 depth renderbuffer 仍是旧尺寸,FBO 不完整”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理一起恢复才算修复。

Single fault · first divergence · replay

帧缓冲、附件与可靠的两遍渲染:反例与恢复

故障:color attachment 尺寸更新而 depth renderbuffer 仍是旧尺寸,FBO 不完整

1. 冻结输入一致

第 1 次沿用同一 context、资源、uniform 与 draw 输入

2. 注入单故障一致

保持其余输入不变,仅注入“color attachment 尺寸更新而 depth renderbuffer 仍是旧尺寸,FBO 不完整”

3. 定位首差一致

所有附件尺寸/样本数兼容;绝不从当前正在写入的同一附件采样

4. 清理并重放一致

attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理

最小可重放检查

unit: logl-22
owner: 自建 framebuffer、color/depth-stencil attachments 与默认 framebuffer
state_or_resource: 附件对象/格式/尺寸、draw buffers、完整性、viewport 和 pass 边界
command: 绑定 FBO 检查完整性并画场景,再绑定 0、恢复 viewport、采样 color texture
pass_invariant: 所有附件尺寸/样本数兼容;绝不从当前正在写入的同一附件采样
single_fault: color attachment 尺寸更新而 depth renderbuffer 仍是旧尺寸,FBO 不完整
required_evidence: attachment 参数、glCheckFramebufferStatus、viewport、pass 顺序与输出纹理

复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。

出处声明

本文为改编重写,改编自 Joey de Vries 的 LearnOpenGL。原文:learnopengl.com

原作及译作以 CC BY-NC 4.0 协议授权,本改编版同样遵循该协议(署名—非商业性使用)。

讨论

评论区加载中…