泛光、亮区提取与乒乓高斯
泛光、亮区提取与乒乓高斯:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能解释 泛光 怎么把场景里超亮的区域(亮度
>阈值)单独提取出来、糊成柔和光晕、再叠加回原图,让强光源透出像眩光一样的辉光 - 能改出 泛光 效果:在 demo 里开关泛光看辉光从无到有、调 亮度阈值 让发光的范围变多变少、调 泛光强度 看辉光变浓变淡;并能写出「MRT 出两张图 → 两遍可分离高斯 → 叠加合成」这条多遍流程
- 能回答:为什么用两遍可分离高斯,而不是直接拿一个 2D N×N 大核糊一遍?两者结果一样吗,差在哪?
为什么夜里看路灯:那圈化不开的柔光
夜里盯着一盏路灯看,灯泡本身那一点是刺眼的亮,可它周围总像晕开一圈化不开的柔光,越靠近灯越亮、越往外越淡,把灯衬得格外耀眼。拿手机拍强光也一样:对着太阳或一束车灯,照片里强光处会溢出一片光雾,糊住周边。
这圈柔光不是灯真的那么大,而是我们的眼睛和相机镜头在面对特别强的光时,光线会向四周散开、漫出来。正是这一圈溢出的辉光,让画面里的强光看起来「真的很亮、很刺眼」。
游戏渲染里,光源和高光算出来的亮度其实也分外悬殊,但如果只是把它们硬画成几个亮点,画面会显得「平、假」——亮是亮,却不刺眼、不出彩。这一章要解决的就是:怎么让场景里超亮的地方自己透出一圈柔和的辉光。没有它,强光只是几个生硬的亮斑;有了它,灯和高光才真的「发光」。
泛光是什么:让超亮区自己溢出光晕
先一句话说清。↡Bloom,泛光。一种后处理效果:把画面里特别亮(亮度超过某个阈值)的区域单独提取出来,模糊成柔和的光晕,再叠加回原画面。效果是强光源和高光周围透出一圈化不开的辉光,模拟肉眼/相机面对强光时光线向四周溢出的现象,让画面里的亮处看起来更刺眼、更真实。(Bloom)是一种后处理效果:把画面里特别亮的区域挑出来,糊成一片柔和的光晕,再叠加回原画面。结果就是强光源和高光周围透出一圈辉光——正是夜里路灯那圈化不开的柔光。
本章推荐把它建在 HDR 之上:颜色能真的 >1(浮点帧缓冲保住的高动态范围值)时,阈值可以设在 1 以上,能更干脆地把强光源和普通浅色表面区分开。LDR 缓冲也能做泛光,但阈值只能落在 [0,1] 内,普通浅色更容易被误选为亮区。因此 HDR 与泛光不是同一件事;本章的泛光位置是 HDR 场景渲完、还没 tonemap 上屏之前,插进「提亮 → 模糊 → 合成」的多遍处理。
整条流水线分四步,下面这个分步图每步都配了张缩略图示意数据长什么样,先扫一眼建立全局印象,后面 Demo 里我们再走一遍:
第一步:只把「够亮」的挑出来当泛光源
泛光绝不能糊整个画面——那只会让全图发虚变脏。关键的第一刀是 ↡按亮度把画面里超过某个阈值的像素单独挑出来的操作。具体做法是算每个像素的感知亮度(常用 dot(color, vec3(0.2126,0.7152,0.0722)) 这个加权和),只有亮度大于阈值(如 1.0)的像素才被保留进『亮区图』当泛光源,其余的全部置黑、不参与后续模糊。阈值调高 → 只有最亮的才发光晕;调低 → 更多区域参与泛光。:算出每个像素的感知亮度(用 dot(color, vec3(0.2126, 0.7152, 0.0722)) 这个加权和,对人眼更敏感的绿色权重最大),只有亮度超过阈值(比如 1.0)的像素才被留下来当「泛光源」,其余一律置黑、不参与发光。
那这张「只剩亮区」的图怎么来?最高效的做法是 ↡Multiple Render Targets,多渲染目标。让一次几何渲染(一个片段着色器)同时往多个颜色附件输出不同的结果,靠 layout(location=N) 指定写到第几个附件。泛光里用它一次性输出两张图:location 0 写正常场景色、location 1 写按亮度阈值筛出的亮区色,省得为了拿亮区图再把整个场景重渲一遍。(Multiple Render Targets):让几何 pass 的片段着色器一次性输出两张图——location 0 写正常的场景色,location 1 写按阈值筛出的亮区色。这样不必为拿亮区图再把场景重渲一遍。
第二、三步:用可分离高斯把亮区糊成光晕
挑出亮区后,要把这些孤立的亮点「糊开」成柔和光晕,靠的是 ↡一种模糊算法:用一个钟形(高斯函数形状)的权重核,把每个像素替换成它和周围邻居的加权平均,中心权重最大、越往外越小。权重之和为 1,所以不改变整体亮暗、只是把亮的地方向四周平滑铺开。泛光用它把提取出来的亮区点糊成柔和的光晕。(Gaussian Blur):用一个钟形权重核把每个像素替换成它和周围邻居的加权平均,中心权重最大、越往外越小,权重之和为 1(不改变整体亮暗),亮的地方就被平滑地向四周铺开。
但如果直接拿一个 N×N 的二维核糊,一个像素要采它周围整片 N² 个邻居,采样次数随核变大平方级暴涨。这里有个关键技巧——↡把一个二维高斯核拆成『先做一遍水平方向的一维核、再做一遍垂直方向的一维核』两步来算。因为高斯函数数学上可以分离,这样做出来的模糊结果和直接用二维核完全等价,但每像素采样次数从 N×N(N²)降到 N+N(2N),N 越大省得越多。泛光的模糊就靠它来压低开销。:二维高斯核在数学上可以拆成「先横一遍 1×N、再纵一遍 N×1」两步,结果完全等价,但每像素采样次数从 N² 降到 2N:
而且一遍横纵往往糊得不够柔,得来回多糊几遍。这就用上 ↡ping-pong framebuffer,乒乓帧缓冲。准备两个内容一样的帧缓冲(FBO),让它们交替充当『输入』和『输出』:第一遍从 A 读、写进 B(横向模糊),第二遍从 B 读、写回 A(纵向模糊),如此来回(像打乒乓)反复若干次。这样只用两个 FBO 就能把横、纵模糊交替做很多遍,把亮区越糊越柔,又不必为每一遍都新建一个帧缓冲。(ping-pong):准备两个帧缓冲交替当输入/输出,第一遍从 A 读、写进 B(横向),第二遍从 B 读、写回 A(纵向),像打乒乓一样来回若干次,只用两个 FBO 就能把横、纵模糊反复做很多遍,越糊越柔:
第四步:把光晕加回原图
最后一步最简单:把糊好的光晕图直接加回原始场景色——scene + bloom,亮点周围就透出了那圈辉光。加完之后,整张图还是 HDR(>1 的值),所以照例走 HDR 那章的出口:先 色调映射(tonemap)压回 [0,1],再 gamma 校正 编码回 sRGB 上屏。整条流水线到这里闭环:
scene + bloom),再 tonemap + gamma 上屏 —— 亮点周围透出柔和辉光。动手:开关泛光、调阈值和强度,把辉光玩出来
猜一猜:下面是一片暗背景上的几个超亮光点(HDR 值远
>1)。先把「泛光开关」关掉——你会看到几个边缘生硬的亮点,干干净净没有光晕。把开关打开,你觉得这些亮点周围会发生什么?再想想:如果把亮度阈值调得很高、只剩最亮的那个超过阈值,会怎样?先动手试,再读下面。
这块画布里,片段着色器程序化地摆了几个亮度远超 1 的光点。三个控件:泛光开关(关=只见硬光点、无辉光,开=亮点透出柔和光晕);亮度阈值(只有亮度超过它的光点才发光晕,调高→只剩最亮的泛光、调低→更多点参与);泛光强度(光晕叠加的浓淡)。盯着光点,逐个拨一拨:
实时演示加载中…
谜底:关泛光时,只剩几个边缘生硬的小亮盘,干净但「不刺眼」;开泛光,每个超过阈值的光点周围都晕开一圈柔光,画面立刻「发光」了。把亮度阈值往上推,发光的光点越来越少——推到最高(约 3.5)时只剩最亮的那盏蓝灯还在泛光,其余光点光晕一个个熄灭,画面回归冷清。这里有个要点:阈值比的是颜色的感知亮度(luma 加权和),不是 HDR 峰值——那盏蓝灯峰值虽是 6.5,它的 luma 只有约 3.6,所以阈值推到 3.5 左右它就处在『最后一个还亮着』的临界。把泛光强度拖大,光晕越叠越浓、向外铺得越远;拖到 0 就等于关了泛光。
流程走一遍:从 MRT 到合成
上面是单遍近似,真实泛光是多遍的。用下面这个分步走一遍正经流程,每一步都配了缩略图示意那一刻的数据长什么样:
① 几何 pass + MRT:一次出 场景色 + 亮区色
几何 pass 的片段着色器靠 MRT 一次输出两张图:
location 0 是正常场景色,
location 1 是按亮度阈值筛出的
亮区色——只有亮度 >阈值
的像素留下当泛光源,暗的全黑、不发光晕。
左右擦一擦:关泛光 vs 开泛光
下面把「关泛光」和「开泛光」定格成两态——左边只有几个硬光点(几何 pass 直出、没经泛光),右边是经过完整多遍流程后亮点透出柔和辉光的样子:
scene + bloom),再 tonemap + gamma 上屏 —— 亮点周围透出柔和辉光。左侧几何 pass 直出的亮区只是几个生硬的点;右侧走完「提亮 → 横纵高斯 → 合成」后,那几个点周围晕开了柔和的光晕。一句话:泛光做的就是把左边的硬亮点,变成右边那圈化得开的辉光。
代码逐段拆解
泛光的代码就四块,全建立在 HDR 浮点帧缓冲和帧缓冲多遍渲染之上:MRT 出两张图、可分离高斯、乒乓循环、叠加合成。
第一块:几何 pass 用 MRT 一次输出两张图
几何 pass 的片段着色器声明两个输出,靠 layout(location=N) 指定写到第几个颜色附件:location 0 写场景色、location 1 写亮区色。亮区色怎么定?算出片元亮度,超过阈值才原样写、否则写黑:
#version 300 es
precision highp float;
in vec3 LightingColor; // 上游算好的 HDR 光照结果(可能 >1)
layout(location = 0) out vec4 FragColor; // 附件 0:正常场景色
layout(location = 1) out vec4 BrightColor; // 附件 1:仅亮区色
void main() {
vec3 color = LightingColor;
FragColor = vec4(color, 1.0);
// 亮度阈值提取:算感知亮度,>1.0 才当泛光源,否则写黑
float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722));
if (brightness > 1.0) BrightColor = vec4(color, 1.0);
else BrightColor = vec4(0.0, 0.0, 0.0, 1.0);
}// HDR FBO 挂两张 RGBA16F 浮点纹理(colorBuffers[0] 场景色 / [1] 亮区色)
unsigned int colorBuffers[2];
glGenTextures(2, colorBuffers);
for (unsigned int i = 0; i < 2; i++) {
glBindTexture(GL_TEXTURE_2D, colorBuffers[i]);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, W, H, 0, GL_RGBA, GL_FLOAT, NULL);
// (过滤/环绕设置从略,同 HDR 那章)
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0 + i,
GL_TEXTURE_2D, colorBuffers[i], 0);
}
// 关键:告诉这次渲染要往「两个」附件写,否则只有附件 0 生效
unsigned int attachments[2] = {GL_COLOR_ATTACHMENT0, GL_COLOR_ATTACHMENT1};
glDrawBuffers(2, attachments);着色器声明了两个输出还不够——还得告诉这次渲染「确实要往两个附件写」,这就是 glDrawBuffers。这一步在 WebGL2 里 API 名字不同:
第二块:可分离高斯——一个 weight[] + horizontal 切横纵
高斯模糊的着色器只写一遍就够:核心是一个钟形权重数组 weight[](中心权重最大、向外递减、和为 1),再用一个 horizontal uniform 切「这遍是横还是纵」——横向就只在 x 方向取邻居,纵向就只在 y 方向。同一段代码,横纵复用:
#version 300 es
precision highp float;
in vec2 TexCoords;
uniform sampler2D image; // 上一遍的结果(亮区图 / 半成品)
uniform bool horizontal; // true=横向这遍, false=纵向这遍
out vec4 FragColor;
// 钟形权重:中心最大、向外递减、和≈1(只列正半边,对称使用)
const float weight[5] = float[](0.227027, 0.194594, 0.121621, 0.054054, 0.016216);
void main() {
vec2 texel = 1.0 / vec2(textureSize(image, 0)); // 一个纹素的步距
vec3 result = texture(image, TexCoords).rgb * weight[0]; // 中心
for (int i = 1; i < 5; i++) {
// 横向遍只在 x 取邻居、纵向遍只在 y 取——这就是『可分离』的关键
vec2 off = horizontal ? vec2(texel.x * float(i), 0.0)
: vec2(0.0, texel.y * float(i));
result += texture(image, TexCoords + off).rgb * weight[i]; // 正方向邻居
result += texture(image, TexCoords - off).rgb * weight[i]; // 负方向邻居
}
FragColor = vec4(result, 1.0);
}注意循环里只在一个轴上取邻居:横向遍走 x、纵向遍走 y。一遍只采 2N 个邻居(这里 N=5,含中心共 9 个),而不是 N×N 的整片——这正是上面 SeparableGaussianDiagram 画的 N²→2N。
第三块:乒乓循环——两个 FBO 交替糊 N 次
光有「一遍模糊」着色器还不够柔,得横纵交替反复糊。CPU 侧用两个 FBO 打乒乓:每次绑其中一个当输出、采另一个当输入,horizontal 在 true/false 间翻转,循环若干次:
bool horizontal = true, first = true;
int amount = 10; // 模糊总遍数(5 横 + 5 纵)
blurShader.use();
for (int i = 0; i < amount; i++) {
glBindFramebuffer(GL_FRAMEBUFFER, pingpongFBO[horizontal]); // 输出到一个
blurShader.setBool("horizontal", horizontal);
// 第一遍采亮区图,之后采另一个 pingpong 纹理(交替读写=乒乓)
glBindTexture(GL_TEXTURE_2D,
first ? brightTexture : pingpongTex[!horizontal]);
RenderQuad(); // 画全屏四边形,跑一遍模糊
horizontal = !horizontal; // 翻转方向:横→纵→横…
if (first) first = false;
}let horizontal = true,
first = true;
const amount = 10; // 模糊总遍数(5 横 + 5 纵)
blurProgram.use();
for (let i = 0; i < amount; i++) {
gl.bindFramebuffer(gl.FRAMEBUFFER, pingpongFBO[horizontal ? 1 : 0]);
gl.uniform1i(uHorizontal, horizontal ? 1 : 0);
// 第一遍采亮区图,之后采另一个 pingpong 纹理(交替读写=乒乓)
gl.bindTexture(
gl.TEXTURE_2D,
first ? brightTexture : pingpongTex[horizontal ? 0 : 1],
);
renderQuad(); // 画全屏四边形,跑一遍模糊
horizontal = !horizontal; // 翻转方向:横→纵→横…
first = false;
}两端逻辑一一对应:amount=10 就是 5 次横 + 5 次纵交替。方向 horizontal 每遍翻转,输入/输出 FBO 也跟着换——这就是「乒乓」。糊完后,最后一个 pingpong 纹理里就是那张柔和的光晕图。
第四块:叠加合成——scene + bloom 再 tonemap + gamma
最后绑回默认帧缓冲,画一个全屏四边形,采两张纹理:原始场景色 + 糊好的光晕,相加后照 HDR 那章的出口先 tonemap、再 gamma:
#version 300 es
precision highp float;
in vec2 TexCoords;
uniform sampler2D scene; // 原始场景色(附件 0)
uniform sampler2D bloomBlur; // 糊好的光晕(乒乓最终结果)
uniform float intensity; // 泛光强度(光晕浓淡)
out vec4 FragColor;
void main() {
vec3 hdrColor = texture(scene, TexCoords).rgb;
vec3 bloom = texture(bloomBlur, TexCoords).rgb;
hdrColor += bloom * intensity; // 叠加合成:scene + bloom
vec3 mapped = hdrColor / (hdrColor + vec3(1.0)); // tonemap(Reinhard)
FragColor = vec4(pow(mapped, vec3(1.0 / 2.2)), 1.0); // 再 gamma 校正上屏
}hdrColor += bloom * intensity 就是那圈辉光的来源,intensity 正对应上面 demo 的「泛光强度」滑块。加完仍是 HDR,所以末尾两行雷打不动:先 tonemap、再 gamma(顺序反了颜色全乱,这是 HDR 那章的硬规矩)。整条出口流水线:MRT 出两图 → 乒乓横纵高斯糊亮区 → scene + bloom 合成 → tonemap → gamma → 上屏。
容易踩的坑
小结
- 泛光 把画面里超亮的区域提取出来、糊成柔和光晕、再加回原图,让强光源透出像眩光一样的辉光——夜里路灯那圈柔光
- 第一刀是 亮度阈值提取:只把亮度
>阈值的像素当泛光源(暗的全黑、不发光),通常靠 MRT 在几何 pass 一次同时输出 场景色 + 亮区色 - 把亮区糊成光晕用 可分离高斯:二维核拆成「先横后纵」两遍,结果等价但采样从
N²降到2N;靠 乒乓帧缓冲 两个 FBO 交替反复横纵糊多次 - 最后 叠加合成
scene + bloom,仍是 HDR,照例先色调映射、再 gamma 校正上屏 - HDR 与泛光不是同一件事;HDR 浮点帧缓冲(
RGBA16F)让颜色能真的>1,可把阈值设在 1 以上、比 LDR 泛光更容易区分真正强光
练习
问题 1(改 Demo 代码型) 在上面那个光点 demo 里,把「亮度阈值提取」这件事亲手拨明白:① 找到 frag 里决定「这个光点发不发光晕」的那行代码,说出它在做什么;② 如果想让所有光点都泛光(无论亮度),把阈值控件拖到什么位置就行?为什么?
问题 2(独立实现 / 选型题 · C 型) 你要给一个 HDR 场景加泛光,目标效果:几个强光源周围有又大又柔的圆形光晕,且要在中端机上流畅跑。下面两个实现方案,选哪个?错的那个会怎样?
- 方案 A:一遍渲染,片段着色器里对每像素采它周围
25×25的二维核做高斯模糊。 - 方案 B:先 MRT 出亮区图,再用两个 FBO 乒乓、横纵各糊 5 遍的可分离高斯,最后
scene + bloom。
问题 3(问答型) 有人做泛光:颜色附件用了普通 RGBA8 帧缓冲,几何 pass 里写 if (brightness > 1.0) BrightColor = ...。结果亮区图全黑,泛光完全没效果。问题出在哪?怎么改?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 泛光
Bloom,泛光。一种后处理效果:把画面里特别亮(亮度超过某个阈值)的区域单独提取出来、模糊成柔和的光晕,再叠加回原画面。效果是强光源和高光周围透出一圈化不开的辉光,模拟肉眼/相机面对强光时光线向四周溢出的现象——就像夜里看路灯那圈柔光。详见本章「泛光是什么」一节。
- 亮度阈值提取
按亮度把画面里超过阈值的像素单独挑出来。算每个像素的感知亮度(常用
dot(color, vec3(0.2126,0.7152,0.0722))这个加权和,绿色权重最大),只有亮度>阈值(如1.0)的像素才留进「亮区图」当泛光源,其余全置黑、不参与模糊。阈值调高 → 只有最亮的才发光晕;调低 → 更多区域参与。详见本章「第一步」一节。- MRT 多渲染目标
Multiple Render Targets,多渲染目标。让一次几何渲染(一个片段着色器)同时往多个颜色附件输出不同结果,靠
layout(location=N)指定写到第几个附件。泛光用它一次输出两张图:location 0写正常场景色、location 1写按亮度阈值筛出的亮区色,省得为拿亮区图把场景重渲一遍。详见本章「第一步」一节。- 高斯模糊
一种模糊算法:用一个钟形(高斯函数形状)的权重核,把每个像素替换成它和周围邻居的加权平均,中心权重最大、越往外越小,权重之和为 1(不改变整体亮暗)。亮的地方就被平滑地向四周铺开。泛光用它把提取出的亮区点糊成柔和光晕。详见本章「第二、三步」一节。
- 可分离高斯
把一个二维高斯核拆成「先做一遍水平方向的一维核、再做一遍垂直方向的一维核」两步。因为高斯函数可分离,做出来的结果和直接用二维核完全等价,但每像素采样次数从
N×N(N²)降到N+N(2N),N越大省得越多。泛光的模糊就靠它压低开销。详见本章「第二、三步」一节。- 乒乓帧缓冲
ping-pong framebuffer,乒乓帧缓冲。准备两个内容相同的帧缓冲(FBO),让它们交替充当「输入」和「输出」:第一遍从 A 读、写进 B(横向模糊),第二遍从 B 读、写回 A(纵向模糊),像打乒乓一样来回若干次。只用两个 FBO 就能把横、纵模糊交替做很多遍,把亮区越糊越柔。详见本章「第二、三步」一节。
版本、来源与运行边界
本章以 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 结果的验证。
正式概念与状态责任
- bloom:在“泛光、亮区提取与乒乓高斯”中由HDR MRT framebuffer、两张 ping-pong textures 与 composite pass负责解释其输入、受控状态和可观察结果;运行时以draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素定位它的第一处变化。
- bright pass:在“泛光、亮区提取与乒乓高斯”中由HDR MRT framebuffer、两张 ping-pong textures 与 composite pass负责解释其输入、受控状态和可观察结果;运行时以draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素定位它的第一处变化。
- gaussian blur:在“泛光、亮区提取与乒乓高斯”中由HDR MRT framebuffer、两张 ping-pong textures 与 composite pass负责解释其输入、受控状态和可观察结果;运行时以draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素定位它的第一处变化。
- ping-pong:在“泛光、亮区提取与乒乓高斯”中由HDR MRT framebuffer、两张 ping-pong textures 与 composite pass负责解释其输入、受控状态和可观察结果;运行时以draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素定位它的第一处变化。
章专属 OpenGL 状态实验
先预测“一次写 scene+bright,多轮交替水平/垂直模糊,最后相加并 tone map”发生后,HDR MRT framebuffer、两张 ping-pong textures 与 composite pass应怎样改变scene/bright attachments、threshold、blur direction、read/write texture 和合成值;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。
实验一:Context—资源—结果合同
选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。
Context · resource · observable result
泛光、亮区提取与乒乓高斯:状态合同
用 MRT 提取亮区,在两个纹理间 ping-pong 高斯模糊,再与原 HDR scene 合成
验证场景
官方教程正式概念
logl-36 · 基线帧
bloom:固定 context、资源内容与输入事件,执行“一次写 scene+bright,多轮交替水平/垂直模糊,最后相加并 tone map”
冻结输入:bloom
HDR MRT framebuffer、两张 ping-pong textures 与 composite pass记录scene/bright attachments、threshold、blur direction、read/write texture 和合成值
结果:得到可重复的初始 GL 状态与资源身份
观测:draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素中的初始快照
预期:HDR MRT framebuffer、两张 ping-pong textures 与 composite pass得到可复查结果,并持续满足“每轮读取与写入不是同一纹理;最终选择最后一次实际写入的 ping-pong 目标”
实验二:CPU 命令到 GPU 结果的五段轨迹
逐段执行“一次写 scene+bright,多轮交替水平/垂直模糊,最后相加并 tone map”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“每轮读取与写入不是同一纹理;最终选择最后一次实际写入的 ping-pong 目标”。
CPU command · GL state · GPU result
泛光、亮区提取与乒乓高斯:五段轨迹
当前观测:draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素中的初始快照
不变量:每轮读取与写入不是同一纹理;最终选择最后一次实际写入的 ping-pong 目标
实验三:单故障与同输入恢复
注入“模糊 pass 同时从当前 color attachment 采样并写回,形成未定义 feedback loop”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素一起恢复才算修复。
Single fault · first divergence · replay
泛光、亮区提取与乒乓高斯:反例与恢复
故障:模糊 pass 同时从当前 color attachment 采样并写回,形成未定义 feedback loop
第 1 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“模糊 pass 同时从当前 color attachment 采样并写回,形成未定义 feedback loop”
每轮读取与写入不是同一纹理;最终选择最后一次实际写入的 ping-pong 目标
draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素
最小可重放检查
unit: logl-36
owner: HDR MRT framebuffer、两张 ping-pong textures 与 composite pass
state_or_resource: scene/bright attachments、threshold、blur direction、read/write texture 和合成值
command: 一次写 scene+bright,多轮交替水平/垂直模糊,最后相加并 tone map
pass_invariant: 每轮读取与写入不是同一纹理;最终选择最后一次实际写入的 ping-pong 目标
single_fault: 模糊 pass 同时从当前 color attachment 采样并写回,形成未定义 feedback loop
required_evidence: draw buffers、bright texel、每轮方向/输入/输出、最终纹理索引与合成像素复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。