3.4 默认屏 vs 离屏帧缓冲 本节摘要:默认帧缓冲连着 canvas,画完由浏览器合成到页面。用户创建的帧缓冲对象把颜色、深度绑到纹理或渲染缓冲,绘制结果不直接上屏,而作为后续采样源。后处理、阴影贴图、动态反射、GPU 拾取,都是“先画到离屏,再当纹理用”。只加片元公式却仍绑着默认 FBO,读不到“刚才那张画面”。切换绑定发生在 CPU,采样发生在 GPU,两端合同再次分家。 阅读收获 阅读完本节,你应当能够: 对照默认 FBO 与用户 FBO 的绑定、附件、呈现路径 说明颜色附件用纹理还是渲染缓冲的差别 列出阴影、Bloom、延迟着色各自离屏的原因 解释 回到画布的含义 指出读回 与继续当纹理采样在性能上的对立 屏幕只是附件的一种 帧缓冲是一组附件槽:颜色、深度、模板。
本节摘要:默认帧缓冲连着 canvas,画完由浏览器合成到页面。用户创建的帧缓冲对象把颜色、深度绑到纹理或渲染缓冲,绘制结果不直接上屏,而作为后续采样源。后处理、阴影贴图、动态反射、GPU 拾取,都是“先画到离屏,再当纹理用”。只加片元公式却仍绑着默认 FBO,读不到“刚才那张画面”。切换绑定发生在 CPU,采样发生在 GPU,两端合同再次分家。
阅读完本节,你应当能够:
bindFramebuffer(null) 回到画布的含义readPixels 与继续当纹理采样在性能上的对立帧缓冲是一组附件槽:颜色、深度、模板。默认对象由浏览器提供,颜色通向 canvas。你 createFramebuffer,再 framebufferTexture2D 把一张纹理塞进颜色槽,绘制就改这张纹理。GPU 并不觉得这更“高级”,它只是换了写目标。CPU 必须在 draw 前 bind 对对象,并保证附件尺寸、格式完整,否则状态是 incomplete,绘制被丢弃。
暗房对照:默认 FBO 像拍立得,出片就是给观众看的。用户 FBO 像底片,还要进放大机(另一次绘制加采样)才成为照片。Bloom 不是一种灯光,是底片做两次模糊再叠回。阴影贴图是从灯的相机拍的深度底片。没有底片,只有拍立得,这些效果没有存放处。
bind 用户FBO → 画场景 → 颜色在纹理里 bind 默认FBO → 画全屏三角,FS 采刚才那张纹理 → 出现在 canvas
| 附件形式 | 能当纹理采吗 | 适合 | 不适合 |
|---|---|---|---|
| 颜色纹理 | 能 | 后处理、拾取、反射 | 纯深度阴影还用它浪费 |
| 深度纹理 | 能 | 阴影、后期雾 | WebGL1 需扩展 |
| 渲染缓冲 | 一般不能采 | 只要深度测试不要采深度 | 后处理链 |
| 多颜色附件 MRT | 能 | 延迟着色 G-buffer | WebGL1 需扩展且槽有限 |
WebGL1 单次绘制通常一个颜色附件,除非 WEBGL_draw_buffers。WebGL2 核心 MRT。延迟着色在 1.0 上很别扭,不是公式写不出,是一次 FS 不能合法写出多张图,只好多 pass 重复几何。版本轴再次变成架构,而不是画质滤镜。
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo); gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, colorTex, 0); gl.viewport(0, 0, w, h); drawScene(); gl.bindFramebuffer(gl.FRAMEBUFFER, null); gl.viewport(0, 0, canvas.width, canvas.height); drawFullscreen(colorTex);
viewport 必须跟着附件尺寸走。离屏 512、画布 1920,忘了改 viewport,不是拉伸那么简单,可能只更新一角或采样错。这是 CPU 状态,与着色器无关。

离屏尺寸可以低于画布:一半分辨率做模糊更便宜。阴影贴图尺寸跟灯光覆盖有关,与窗口无关。随便用画布分辨率做所有 FBO,是把 GPU 填像素的账单复制了好几份。对照:最终呈现用全分辨率;中间缓冲按效果需要缩小。
精度:颜色常用 8 位,HDR 后处理要半浮点或浮点纹理,WebGL1 靠扩展且过滤支持参差。深度 16 位在大场景不够。选格式是 CPU 创建时的合同,运行时 FS 写不出更多比特。
ping-pong:两张纹理来回采,做迭代模糊。CPU 每步切换绑定与采样器。引擎后处理栈就是自动 ping-pong。自己写时切错,会采到正在写的对象——有的驱动能、有的出未定义。不要让同一纹理同时当附件和采样源。
readPixels 把 GPU 结果拉回 CPU,拾取颜色 ID 可以,每帧全屏读不行。能把拾取画到 1x1 或小 FBO 再读。与“继续当纹理在 GPU 里用”相比,方向相反。能不回读就不回读。
⚠️ 常见坑:FBO incomplete 仍继续 draw,画面停在上一帧或全黑,日志要自己
checkFramebufferStatus。WebGL 不会弹窗说附件缺深度。💡 关键直觉:想采样“刚才的画面”,必须当时就画在纹理上。默认屏不是纹理。后处理的本质是换绑定,不是换滤镜名。
抗锯齿:默认画布可在 getContext 时要 antialias。用户 FBO 的多重采样在 WebGL2 更完整,resolve 到纹理再采。把“开了 canvas 抗锯齿”当成离屏也有,会发现后处理边缘锯齿回来了。两条路径的 MSAA 合同不同。
不是。它是 CPU 上的 pass 列表:每个 pass bind 自己的 FBO、用自己的 program 画全屏三角。你写自定义 pass,就是提供那张全屏 FS 和输入纹理名。看懂本节,就能看懂作曲器;看不懂绑定,作曲器只是名词。
第一个离屏实验:把场景画到纹理,再画到画布上的一张四边形,不做任何滤镜。确认 UV 与 Y 翻转。能镜像出场景,再加灰度 FS。先通绑定,再通效果。很多人第一步就上双通道 Bloom,然后分不清是 FBO 坏了还是模糊核坏了。
后处理链一长,卫生比公式重要。每个中间缓冲的尺寸、格式、是否需要深度,写成表。模糊链可以四分之一分辨率、RGBA8;HDR 合成要更高精度;法线缓冲区要线性。清屏:有的 pass 全屏覆盖可不清颜色,但深度 pass 必须清深度。不清深度的阴影 FBO 会留下上一帧的鬼影。鬼影像着色器 bug,其实是附件里的旧货。
调试视图:把任意中间纹理画到屏幕四角。能看见深度是否合理、法线是否彩色正确、亮度是否爆。没有调试视图就调 Bloom 阈值,是盲调。引擎作曲器常有 pass 开关;裸写给每个纹理一个名字和一次按键切换。CPU 侧的可观测性再次比新公式值钱。
窗口 resize 必须重建附件。只改 canvas 宽高却不重建 FBO,采样拉伸或写越界。重建时删除旧纹理,防止泄漏。移动端旋转屏幕会触发这条路径。把它写进与上下文恢复同一份清单。
延迟着色的 G-buffer 是多附件的用户 FBO。带宽随附件数涨。移动端带宽紧,延迟不一定赢前向。选型用 profiler,不要用课表。WebGL1 上 MRT 还要扩展,失败则整条延迟路径不存在。能力表在 4.5,绑定卫生在本节。两节一起读,才不会在 incomplete 的 FBO 上写延迟论文。
WebGL2 可用多重采样渲染缓冲,再 blit 到纹理。抗锯齿离屏这条路比默认 canvas 抗锯齿更可控。blit 是 GPU 内部拷贝,仍有带宽。移动上 MSAA 4x 加全屏后处理,带宽可能比锯齿更不可接受。档位上:低档无 MSAA,中档默认 canvas AA 且无后处理,高档离屏 MSAA 再后处理。不要三档共用一条链。
incomplete 的常见原因:尺寸 0、格式不可做颜色附件、深度与颜色尺寸不一致、缺附件。检查函数返回枚举,对照表格,不要只看黑屏。黑屏还有可能是没清屏、相机看反、程序没链。FBO 调试先 check 再画全屏纯色。纯色出来说明绑定与视口对了,再接场景。分层验证,与着色器二分同一纪律。
没绑回默认 FBO,或 viewport 仍是离屏小尺寸,或采样器没指到那张纹理。先画全屏纯色证明默认 FBO 可写。再采纹理输出。Y 翻转会导致倒像,倒像至少证明绑定通了。倒像再翻 UV,不要重做整条链。
深度或颜色附件没 clear。覆盖不满全屏时旧像素留下。深度 pass 必须清深度。模糊 ping-pong 若偶发绑错成同一张,也像残影。禁止同时读写。每 pass 列清屏标记。
附件尺寸还是旧的。resize 重建 FBO 与纹理,删旧对象。视口跟随新 drawingBuffer。移动旋转屏幕必走这条。与上下文恢复共用清单。花了还当着色器精度问题,会浪费一天。
所有用户 FBO 进注册表:名字、尺寸、格式、是否要随 resize 重建、每帧是否 clear。resize 与丢失只扫表。表外的 FBO 必漏。调试键把表里每一张画到屏幕。没有键,后处理调参靠猜。中间缓冲默认四分之一,需要锐利才全分辨率。默认全分辨率是账单预支。MSAA 离屏只给高档。注册表加一列档位。低档根本不创建那张缓冲,比创建了不用更省。incomplete 时抛带名字的错,不要静默。名字来自表。
FBO 验收:纯色离屏再上屏、灰度后处理、阴影深度可视化。三步不过不准加 Bloom。Bloom 会掩盖绑定错误,绑错时也能“有光”。resize 自动化测试:连续改窗口尺寸五十次,看显存是否涨。涨就是没删旧附件。丢失演练同样扫注册表。中间缓冲默认四分之一,注释写明为什么。有人一看糊就改全分辨率,账单翻四倍。糊应先查 UV 与过滤,再允许升尺寸。MRT 延迟只在 2.0 包里创建。1.0 构建根本不编译这条路径,避免 else 里误调用。incomplete 抛错带名字,测试捕获名字。没有名字的黑屏会变成全员猜。
后处理链的每一张中间图都要能单独上屏。不能单独上屏的链不准加新 pass。新 pass 在绑错时仍可能“有画面”,因为上一张碰巧还能看。碰巧会让错误活过发布。发布后只在某机糊,才发现尺寸与视口不一致。不一致在注册表里本该一眼看见。表是为了不靠碰巧。
现场对照时只改一个变量。同时改版本、封装、通道,归因会消失。消失之后只能靠感觉,感觉正是本书要拆掉的东西。把改动写成一条,测完再写下一条。一条一条,四组对照才用得上。用不上的对照表只是装饰。装饰填不满格子,格子空着就不要标完工。离屏这一格改完,用纯色上屏验收,不要直接上 Bloom。纯色倒了只翻 UV,不要重做绑定。绑定通了再加滤镜。滤镜掩盖绑定错误,是后处理链最常见的自欺。注册表里写清尺寸与是否 clear,resize 只扫表,不靠记性。表外的 FBO 必漏,漏了会在旋转屏幕时花屏,花屏不像着色器,像附件尺寸还停在旧值。丢失恢复与 resize 共用同一张注册表,表是房间钥匙串。没有钥匙串就会在后处理链上猜绑定,猜绑定会把 Bloom 当遮羞布。
下一章把光照、阴影、纹理进阶、动画放进“路线对照”,并回到扩展对内置、引擎对裸 API、CPU 瓶颈对 GPU 瓶颈。