HDR、浮点中间缓冲与曝光出口
HDR、浮点中间缓冲与曝光出口:保留 LearnOpenGL 3.3 Core 正文机制,以 context—资源—结果合同、GPU 轨迹和章专属单故障完成可重放验收。
学习目标
- 能解释普通帧缓冲为什么把颜色截在
[0,1]会让强光区死白丢细节,以及浮点帧缓冲(RGBA16F)凭什么能先把算出来>1的颜色完整存下来 - 能改出 HDR 上屏效果:在 demo 里切 色调映射 模式(直接截断 / Reinhard / 曝光)把死白救成有层次,拖曝光值看暗处提亮、亮处压住的此消彼长;并能写出「第一遍渲进浮点 FBO、第二遍 tonemap 再 gamma 校正上屏」的两遍流程
- 能回答:为什么色调映射(tonemap)必须放在 gamma 校正之前?顺序反了会怎样?
为什么逆光拍照:要么窗外白成一片,要么屋里黑成一团
你一定拍过这种照片:人站在窗前,想把人和窗外的风景一起拍清楚。可手机一拍——要么窗外亮得白成一片、什么都看不见,要么为了看清窗外把曝光压低,结果屋里的人黑成一团。亮处和暗处,相机好像只能照顾一头。
渲染里也是同一个坎。一个场景里常常既有一扇极亮的窗(亮到刺眼)、又有墙角的暗部细节。问题在于:屏幕能显示的亮度就那么一个范围,最亮也就是「纯白」。窗户那块算出来远比纯白还亮,可一上屏就被强行摁成纯白——它和旁边稍暗一点的高光全糊成同一片死白,层次全没了。
这一章要解决的就是这件事:怎么让一张图同时照顾住极亮的窗和昏暗的墙角。办法分两步——先用一块「能装下超亮颜色」的画板把场景原样存住,再像调相机曝光一样把它柔和地压回屏幕能显示的范围。没有它,你的强光区永远是一坨没细节的死白;有了它,亮处暗处都能看清。
死白的根源:普通帧缓冲只装得下 [0,1]
先说清死白是怎么来的。你之前画到的帧缓冲,每个颜色通道用 8 位整数存(RGBA8),能表示的范围被锁死在 ↡Low Dynamic Range,低动态范围。指颜色每个通道被限制在 [0,1](或 0~255 整数)的普通表示方式,最亮就是纯白 1。亮度超过 1 的部分无法存储,会被截断(clamp)成 1,所以强光区会糊成一片没有层次的死白。普通的 RGBA8 帧缓冲就是 LDR 的。(低动态范围)的 0 到 1 之间——0 是黑、1 是能显示的最亮(纯白),再没有「比纯白还亮」这回事。
可光照算出来的颜色根本不守这个规矩。一扇正对太阳的窗、一盏贴脸的强灯,按公式(多盏光叠加、强光源)算出来的值轻松就 3.0、6.0——远比 1.0 大。这些「亮过头」的值一旦写进 LDR 帧缓冲,就被无情地 截断(clamp)成 1:窗户中心算出 6.0、窗框边上算出 2.5,写进去全成了 1.0,本该有的明暗层次在这一刻就永久丢了。这就是一个场景能容纳的明暗跨度——它的 ↡一个场景里最亮和最暗之间的亮度跨度。真实世界跨度极大(正午太阳能比阴影亮上万倍),渲染时强光源、多光叠加也会让某些区域远亮于 1。普通 LDR 帧缓冲只能表示 [0,1] 这一小段动态范围,跨度大的场景里超过 1 的高光就被压成死白、丢失层次。——被这块小画板硬生生砍掉了一截。下面这张图把「截断 vs 保留」上下排开:
[0,1],光强超过 1 的整段全被压成纯白——高光层次在写入时就丢了;下排HDR 浮点帧缓冲把 >1 的范围完整存下,高光仍有层次,留给色调映射再压回来。上排普通帧缓冲,强度过了 1 就全是纯白;下排我们想要的,是把 >1 那一截完整存下来。
解法第一步:用浮点帧缓冲先把 >1 存住
既然 8 位整数装不下 >1 的颜色,那就换一块能装下的画板。这就是 ↡一种颜色附件用 16 位(或 32 位)浮点数存储的帧缓冲,WebGL2 里用 RGBA16F 这类内部格式。浮点数能表示远大于 1 的值(也能表示很小的小数),所以光照算出来 >1 的高动态范围颜色可以原封不动地存进去、不被截断。HDR 的第一遍渲染就是把整个场景渲进这块浮点帧缓冲。:把帧缓冲的颜色附件从普通 8 位纹理换成 16 位浮点纹理(RGBA16F)。浮点数能轻松表示 6.0、100.0 这种远超 1 的值,于是窗户的 6.0、窗框的 2.5 都被原封不动地存进去,一个细节都不丢。
这一步就是整套 ↡High Dynamic Range,高动态范围。这里指一套渲染做法:先把场景渲进能存 >1 的浮点帧缓冲(保住超亮高光的全部层次),再用色调映射把这个大范围压回屏幕能显示的 [0,1]。好处是强光区不再死白一片,亮处暗处都能保留细节——就像相机的 HDR 模式同时照顾逆光下的亮处和暗处。(高动态范围)做法的开端:跟帧缓冲那章一样另开一块离屏画板,只不过这次它的颜色附件是浮点纹理。第一遍把整个场景照常渲进这块浮点帧缓冲——所有 >1 的高光都安然存住。但它还不能直接上屏:屏幕只认 [0,1],你把 6.0 直接丢给屏幕,照样被截成纯白。还差关键的第二步。
解法第二步:色调映射把大范围压回 [0,1]
存住了大范围,怎么把它「塞进」屏幕只认的 [0,1]、又不丢层次?答案是 ↡Tone Mapping,把 [0,∞) 的高动态范围颜色用一个函数压回屏幕能显示的 [0,1] 的过程。关键是它不是硬截断,而是平滑地压缩:越亮的部分压得越狠,但仍保留相对层次,于是窗户中心和窗框不再糊成同一片白。常见做法有 Reinhard(c/(c+1))和曝光映射(1-exp(-c*曝光))。是 HDR 第二遍上屏前必做的一步。(tone mapping)。它不是简单地一刀切(那就退回死白了),而是用一个平滑压缩的函数:越亮的地方压得越狠,但亮处之间的相对层次保留下来。于是 6.0 和 2.5 被压成两个不同的、都在 [0,1] 内的值——窗户中心和窗框终于分得开了。
本章用两种最常见的映射,下面这张曲线图把它们和「直接截断」画在一起对照:
clamp 到 1 就水平封顶——>1 的高光全压成死白、毫无层次;紫线 Reinhard 与灰虚线 exposure 都平滑趋近 1,把任意大的 HDR 输入压回可显示范围、高光区仍有层次。第一种是 Reinhard:c / (c + 1)。它把任意大的 c 都平滑地压进 [0,1)——c=1 压成 0.5、c=6 压成约 0.86、c 再大也只是越来越逼近 1 但永远到不了,所以高光区始终有层次、绝不死白。简单、均匀,是最常用的入门映射。
第二种是 ↡一种色调映射方式,模拟相机的曝光:tonemap 时乘上一个可调的曝光值,公式常用 1 - exp(-c * 曝光)。曝光值调大,暗处被快速提亮(适合看清昏暗细节),但亮处更容易压到接近 1;曝光值调小,亮处保留更多层次(适合强光场景),但暗处偏黑。本质是让你像调相机曝光一样,在『照顾亮处』和『照顾暗处』之间权衡。(exposure)映射:1 - exp(-c * 曝光)。它多了一个曝光值让你像调相机一样权衡——曝光调大,暗处被快速提亮、看清墙角细节,但亮处更容易顶到接近 1;曝光调小,亮处保留更多层次、看清窗外,但暗处偏黑。亮处暗处此消彼长,正是逆光拍照里那个取舍,现在交到你手里。
别忘了承接上一章:先 tonemap,再 gamma 校正
色调映射把颜色压回了 [0,1],但还没上屏——别忘了上一章的 gamma 校正:所有光照计算(包括 tonemap)都在线性空间做,最后一步才 pow(color, 1/2.2) 编码回 sRGB 给屏幕。所以完整的出口顺序是:先色调映射、再 gamma 校正。
顺序为什么不能反?tonemap 是在线性空间里把亮度压缩,它假设输入是线性的;要是先做了 gamma 校正(颜色已被提亮、不再线性),再拿去 tonemap,压缩曲线就作用在错误的数值上,颜色全乱。记牢这条出口流水线:浮点 FBO 存住 HDR → 色调映射压回 [0,1] → gamma 校正编码回 sRGB → 上屏。这一步的坑后面误区还会细说。
单步确认:从线性亮度到显示器
实现 HDR 时,先确认数据在哪个空间、什么格式里,再关心公式。下面三步分别锁住「高光不会过早丢失」和「出口顺序不会反」两个不变量。
线性空间允许亮度大于 1
多盏光相加、发光材质和反射都可能令线性颜色分量超过 1。这不是错误;错误是过早把它写进归一化的 RGBA8 附件。
动手:切色调映射模式,把死白的窗救回来
猜一猜:下面是一条走廊,尽头有一扇极亮的窗(算出来的颜色值约是
6.0——比纸白还亮 6 倍),两侧墙面有暗部纹理。先看「直接截断(clamp)」模式:你觉得那扇窗会显出窗框的层次,还是糊成一整块死白?而两侧昏暗的墙角,你能看清纹理吗?先切一下,再读下面。
这块画布里,片段着色器直接算出了一个高动态范围的场景:窗户中心亮到 6.0、墙面只有零点几。三种 色调映射模式 在「直接截断 / Reinhard / 曝光」之间切;曝光值 滑块只在曝光模式下生效,拖它看亮处暗处此消彼长。盯着窗户和墙角,对比三种模式:
实时演示加载中…
谜底:直接截断模式下,窗户中心算出的 6.0 和窗框的 4.5 全被摁成 1.0——整扇窗糊成一块死白,窗框层次荡然无存;同时为了「不过曝」你又没法提亮,两侧墙角漆黑一片看不清纹理。切到 Reinhard,6.0 被压成约 0.86、4.5 压成约 0.82,窗框层次透出来了,墙角细节也回来了。切到 曝光模式拖滑块:曝光调大,墙角越来越亮、但窗户更接近全白;曝光调小,窗户层次更足、但墙角转暗——这就是逆光取舍的此消彼长,全凭你一只手调。
左右擦一擦:截断 vs 色调映射
下面这个对比把「截断丢层次」和「色调映射保层次」定格成两态——左边是普通帧缓冲把 >1 全压成死白(高光层次丢失),右边是浮点帧缓冲把 >1 完整存下、再由色调映射压回带层次:
clamp 到 1 就水平封顶——>1 的高光全压成死白、毫无层次;紫线 Reinhard 与灰虚线 exposure 都平滑趋近 1,把任意大的 HDR 输入压回可显示范围、高光区仍有层次。[0,1],光强超过 1 的整段全被压成纯白——高光层次在写入时就丢了;下排HDR 浮点帧缓冲把 >1 的范围完整存下,高光仍有层次,留给色调映射再压回来。左侧那条强度轴超过 1 的整段全是纯白——这正是 clamp 模式下窗户的样子;右侧三条曲线里,红线 clamp 到 1 就封顶(还是死白),而紫线 Reinhard、灰虚线 exposure 平滑趋近 1,把同样 >1 的高光压成一段段不同的灰、层次保住了。一句话:截断是「砍掉超出的」,色调映射是「柔和地压缩进来」。
代码逐段拆解
HDR 的代码就三块,和帧缓冲那章的两遍渲染同构,只是颜色附件换成了浮点纹理:建浮点 FBO、第一遍渲进它、第二遍 tonemap + gamma 上屏。
第一块:建一个 RGBA16F 浮点帧缓冲
和普通帧缓冲唯一的区别在颜色纹理的内部格式——从 RGB/RGBA8 换成 RGBA16F(16 位浮点),这样才存得下 >1。WebGL2 里要先启用 EXT_color_buffer_float 扩展,否则浮点纹理不能当可渲染的颜色附件:
// 浮点颜色纹理:内部格式 GL_RGBA16F(16 位浮点,存得下 >1)
unsigned int hdrFBO;
glGenFramebuffers(1, &hdrFBO);
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
unsigned int colorBuffer;
glGenTextures(1, &colorBuffer);
glBindTexture(GL_TEXTURE_2D, colorBuffer);
// 关键:GL_RGBA16F 内部格式,数据类型 GL_FLOAT
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, W, H, 0, GL_RGBA, GL_FLOAT, NULL);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorBuffer, 0);
// (深度 renderbuffer 附件 + checkFramebufferStatus 同帧缓冲那章,从略)// WebGL2 必须先启用浮点可渲染扩展,否则 RGBA16F 当颜色附件会不完整
const ext = gl.getExtension("EXT_color_buffer_float");
if (!ext) console.error("EXT_color_buffer_float 不可用,无法用 RGBA16F 渲染");
const hdrFBO = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, hdrFBO);
const colorBuffer = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, colorBuffer);
// 关键:内部格式 gl.RGBA16F,数据类型 gl.FLOAT
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA16F, W, H, 0, gl.RGBA, gl.FLOAT, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.framebufferTexture2D(
gl.FRAMEBUFFER,
gl.COLOR_ATTACHMENT0,
gl.TEXTURE_2D,
colorBuffer,
0,
);
// (深度 renderbuffer 附件 + checkFramebufferStatus 同帧缓冲那章,从略)两端逻辑一一对应,差异都集中在「浮点纹理能不能渲」这件事上:
第二块:第一遍——把场景渲进浮点 FBO
绑上浮点 FBO,开深度测试,把整个场景照常渲一遍。光照算出来 >1 的高光,这一遍原封不动地写进了 RGBA16F 颜色纹理——一个细节都不丢,这一遍不做任何 tonemap、也不 gamma 校正:
// 第一遍:绑浮点 FBO,正常渲场景(>1 的高光原样写进 RGBA16F 纹理)
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
glEnable(GL_DEPTH_TEST);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
RenderScene(); // 强光源、多盏灯叠加,算出来 >1 的颜色照样存下
glBindFramebuffer(GL_FRAMEBUFFER, 0); // 解绑,准备第二遍上屏// 第一遍:绑浮点 FBO,正常渲场景(>1 的高光原样写进 RGBA16F 纹理)
gl.bindFramebuffer(gl.FRAMEBUFFER, hdrFBO);
gl.enable(gl.DEPTH_TEST);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
renderScene(); // 强光源、多盏灯叠加,算出来 >1 的颜色照样存下
gl.bindFramebuffer(gl.FRAMEBUFFER, null); // 解绑,准备第二遍上屏第二遍才是 HDR 的关键:绑回默认帧缓冲、画一个全屏四边形采样这张浮点纹理,在它的片段着色器里做 色调映射 + gamma 校正。下面这段就是那个 tonemap 着色器。
第三块:第二遍——tonemap + gamma 上屏(核心)
采样浮点纹理拿到原始 HDR 颜色,按所选模式做 Reinhard 或曝光映射压回 [0,1],最后一步再 gamma 校正编码回 sRGB。注意这三行的先后:tonemap 在前、gamma 在后:
#version 300 es
precision highp float;
in vec2 TexCoords;
uniform sampler2D hdrBuffer;
out vec4 FragColor;
void main() {
// 1) 采样浮点纹理,拿到原始 HDR 颜色(可能 >1)
vec3 hdr = texture(hdrBuffer, TexCoords).rgb;
// 2) 色调映射:Reinhard c/(c+1),平滑压回 [0,1)、保住高光层次
vec3 mapped = hdr / (hdr + vec3(1.0));
// 3) 再 gamma 校正(顺序:先 tonemap 后 gamma!)编码回 sRGB 上屏
FragColor = vec4(pow(mapped, vec3(1.0 / 2.2)), 1.0);
}#version 300 es
precision highp float;
in vec2 TexCoords;
uniform sampler2D hdrBuffer;
uniform float exposure; // 像相机一样可调的曝光值
out vec4 FragColor;
void main() {
// 1) 采样浮点纹理,拿到原始 HDR 颜色(可能 >1)
vec3 hdr = texture(hdrBuffer, TexCoords).rgb;
// 2) 曝光映射:1 - exp(-c * 曝光),曝光大→暗处提亮、亮处易顶白
vec3 mapped = vec3(1.0) - exp(-hdr * exposure);
// 3) 再 gamma 校正(先 tonemap 后 gamma)编码回 sRGB 上屏
FragColor = vec4(pow(mapped, vec3(1.0 / 2.2)), 1.0);
}两段就第 2 行不同——Reinhard 是固定的 c/(c+1),曝光多了个 exposure uniform 让你实时调。第 1 行采样、第 3 行 gamma 校正完全一样。整套出口顺序固定:采样 HDR → tonemap 压回 [0,1] → gamma 校正 → 上屏,三步谁也不能换位置(尤其 tonemap 和 gamma,反了颜色全乱,下面误区细说)。
容易踩的坑
小结
- 普通帧缓冲(
RGBA8)每通道只存[0,1],光照算出>1的高光在写入时就被截成死白、层次永久丢失 - 解法第一步:用 浮点帧缓冲(
RGBA16F)当颜色附件,第一遍把场景渲进它,>1的高 动态范围 颜色原样存住 - 解法第二步:第二遍做 色调映射——Reinhard
c/(c+1)平滑均匀压缩,或 曝光1-exp(-c*曝光)模拟相机、亮暗可调 - 出口顺序固定:先 tonemap 压回
[0,1],再 gamma 校正 编码回 sRGB 上屏,顺序反了颜色全乱 - WebGL2 建
RGBA16F可渲染 FBO 前必须启用EXT_color_buffer_float扩展
练习
问题 1(改 Demo 代码型) 在上面那个走廊 demo 里,亲手把「直接截断」的死白翻车现场写出来,再改成 Reinhard 救回层次。说出:① clamp 模式那行代码是什么、为什么会死白?② 改成 Reinhard 要把它换成哪一行?
问题 2(选型题 · C 型) 三个场景,色调映射该用 Reinhard 还是 曝光,各给一句理由:① 一个室内夜景,想让玩家能手动「眯眼/睁眼」适应明暗(像相机曝光);② 一个不需要交互、只要把任意亮度都稳妥压进 [0,1] 的离屏缩略图渲染;③ 一个逆光场景,美术希望调到「既看清窗外、墙角也不全黑」的那个平衡点。
问题 3(问答型) 有人做了 HDR:第一遍渲进 RGBA16F 浮点 FBO,第二遍先 pow(hdr, 1/2.2) 做 gamma 校正、再 c/(c+1) 做 Reinhard。画面颜色不对。问题出在哪?怎么改?
名词解释
本章出现的专业名词,用大白话再讲一遍。
- LDR
Low Dynamic Range,低动态范围。颜色每个通道被锁在
[0,1](或 0~255 整数)的普通表示,最亮就是纯白1,存不下「比纯白还亮」的值。亮度超过 1 的部分会被截断成 1,所以强光区糊成一片没层次的死白。普通的RGBA8帧缓冲就是 LDR 的。详见本章「死白的根源」一节。- 动态范围
一个场景里最亮和最暗之间的亮度跨度。真实世界跨度极大(正午太阳能比阴影亮上万倍),渲染时强光源、多光叠加也会让某些区域远亮于 1。普通 LDR 帧缓冲只能表示
[0,1]这一小段,跨度大的场景里超过 1 的高光就被压成死白、丢了层次。详见本章「死白的根源」一节。- HDR
High Dynamic Range,高动态范围。一套渲染做法:先把场景渲进能存
>1的浮点帧缓冲(保住超亮高光的全部层次),再用色调映射把这个大范围压回屏幕能显示的[0,1]。好处是强光区不再死白,亮处暗处都保留细节——就像相机的 HDR 模式同时照顾逆光下的亮处和暗处。详见本章「解法第一步」一节。- 浮点帧缓冲
颜色附件用 16 位(或 32 位)浮点数存储的帧缓冲(WebGL2 里用
RGBA16F这类内部格式)。浮点数能表示远大于 1 的值,所以光照算出来>1的高动态范围颜色可以原封不动存进去、不被截断。HDR 的第一遍渲染就是把场景渲进它。详见本章「解法第一步」一节。- 色调映射
Tone Mapping,把
[0,∞)的高动态范围颜色用一个函数平滑压回屏幕能显示的[0,1]。关键是它不硬截断,而是越亮压得越狠、但保留相对层次,于是窗户中心和窗框不再糊成同一片白。常见做法有 Reinhard(c/(c+1))和曝光映射(1-exp(-c*曝光))。详见本章「解法第二步」一节。- 曝光
一种色调映射方式,模拟相机的曝光:tonemap 时乘上一个可调的曝光值,公式常用
1 - exp(-c * 曝光)。曝光调大,暗处被快速提亮(看清昏暗细节),但亮处更易顶到接近 1;曝光调小,亮处保留更多层次(看清强光),但暗处偏黑。本质是让你像调相机一样在「照顾亮处」和「照顾暗处」之间权衡。详见本章「解法第二步」一节。
版本、来源与运行边界
本章以 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 结果的验证。
正式概念与状态责任
- hdr:在“HDR、浮点中间缓冲与曝光出口”中由floating-point scene FBO 与最终 tone-mapping pass负责解释其输入、受控状态和可观察结果;运行时以attachment format、HDR texel、exposure、tone-map 前后值与显示像素定位它的第一处变化。
- floating point framebuffer:在“HDR、浮点中间缓冲与曝光出口”中由floating-point scene FBO 与最终 tone-mapping pass负责解释其输入、受控状态和可观察结果;运行时以attachment format、HDR texel、exposure、tone-map 前后值与显示像素定位它的第一处变化。
- tone mapping:在“HDR、浮点中间缓冲与曝光出口”中由floating-point scene FBO 与最终 tone-mapping pass负责解释其输入、受控状态和可观察结果;运行时以attachment format、HDR texel、exposure、tone-map 前后值与显示像素定位它的第一处变化。
- exposure:在“HDR、浮点中间缓冲与曝光出口”中由floating-point scene FBO 与最终 tone-mapping pass负责解释其输入、受控状态和可观察结果;运行时以attachment format、HDR texel、exposure、tone-map 前后值与显示像素定位它的第一处变化。
章专属 OpenGL 状态实验
先预测“在线性浮点目标累积光照,屏幕 pass 执行 tone map 后编码显示”发生后,floating-point scene FBO 与最终 tone-mapping pass应怎样改变HDR scene color、attachment format、exposure、tone-map 输出和 gamma 编码;再操作三个实验。实验不生成变化率或正确率等虚构总分,只显示真实 GL 状态、资源、命令和可观察结果。
实验一:Context—资源—结果合同
选择任一正式概念与基线/单故障场景,核对它是否进入本章状态合同。正式概念只有同时出现在解释、可视状态和交付证据中才算覆盖。
Context · resource · observable result
HDR、浮点中间缓冲与曝光出口:状态合同
让亮度先保存在浮点 framebuffer,再通过曝光或其他 tone map 压缩到显示范围
验证场景
官方教程正式概念
logl-35 · 基线帧
hdr:固定 context、资源内容与输入事件,执行“在线性浮点目标累积光照,屏幕 pass 执行 tone map 后编码显示”
冻结输入:hdr
floating-point scene FBO 与最终 tone-mapping pass记录HDR scene color、attachment format、exposure、tone-map 输出和 gamma 编码
结果:得到可重复的初始 GL 状态与资源身份
观测:attachment format、HDR texel、exposure、tone-map 前后值与显示像素中的初始快照
预期:floating-point scene FBO 与最终 tone-mapping pass得到可复查结果,并持续满足“tone map 前不 clamp 高亮;曝光改变显示映射而不改原 HDR attachment”
实验二:CPU 命令到 GPU 结果的五段轨迹
逐段执行“在线性浮点目标累积光照,屏幕 pass 执行 tone map 后编码显示”,在每一步记录资源身份、状态变化与第一个可观察结果,并持续核对“tone map 前不 clamp 高亮;曝光改变显示映射而不改原 HDR attachment”。
CPU command · GL state · GPU result
HDR、浮点中间缓冲与曝光出口:五段轨迹
当前观测:attachment format、HDR texel、exposure、tone-map 前后值与显示像素中的初始快照
不变量:tone map 前不 clamp 高亮;曝光改变显示映射而不改原 HDR attachment
实验三:单故障与同输入恢复
注入“场景先写入 GL_RGBA8,超过 1 的亮度已截断,后续曝光无法恢复层次”,保存首个分岔;撤销后沿用完全相同的 context、资源内容、uniform 和 draw 输入重放。只有attachment format、HDR texel、exposure、tone-map 前后值与显示像素一起恢复才算修复。
Single fault · first divergence · replay
HDR、浮点中间缓冲与曝光出口:反例与恢复
故障:场景先写入 GL_RGBA8,超过 1 的亮度已截断,后续曝光无法恢复层次
第 1 次沿用同一 context、资源、uniform 与 draw 输入
保持其余输入不变,仅注入“场景先写入 GL_RGBA8,超过 1 的亮度已截断,后续曝光无法恢复层次”
tone map 前不 clamp 高亮;曝光改变显示映射而不改原 HDR attachment
attachment format、HDR texel、exposure、tone-map 前后值与显示像素
最小可重放检查
unit: logl-35
owner: floating-point scene FBO 与最终 tone-mapping pass
state_or_resource: HDR scene color、attachment format、exposure、tone-map 输出和 gamma 编码
command: 在线性浮点目标累积光照,屏幕 pass 执行 tone map 后编码显示
pass_invariant: tone map 前不 clamp 高亮;曝光改变显示映射而不改原 HDR attachment
single_fault: 场景先写入 GL_RGBA8,超过 1 的亮度已截断,后续曝光无法恢复层次
required_evidence: attachment format、HDR texel、exposure、tone-map 前后值与显示像素复核者先仅依据以上合同写出预期,再运行基线、单故障和清理后重放。若两次基线的资源身份、首个状态变化或 framebuffer 结果不同,必须保留差异,不能用最终截图相似掩盖中间状态错误。