HDR
前面的光照已经允许很亮的光源,例如光源颜色达到 200。问题不在光照公式,而在最终保存颜色的地方。
普通颜色缓冲通常只保存 0 到 1。假设 Fragment Shader 算出:
(4.0, 2.0, 0.5)
↓ 普通 LDR 颜色缓冲
(1.0, 1.0, 0.5)4.0 和 2.0 原本代表不同亮度,但过早截断后都变成了 1.0。
HDR 的关键不是让屏幕直接显示 4.0,而是:
在最终显示之前,先用浮点颜色缓冲把真实的高亮度差异保存下来。
两遍渲染
LearnOpenGL 的 HDR 流程:
Pass 1
Scene Lighting
↓
RGBA16F HDR Framebuffer
↓
保存超过 1.0 的线性颜色
Pass 2
HDR Color Buffer
↓
Tone Mapping
↓
Gamma Correction
↓
默认 Framebuffer第一遍不急着压缩亮度。第二遍才决定大范围亮度如何映射到屏幕。
浮点颜色缓冲
官方创建颜色附件:
glTexImage2D(
GL_TEXTURE_2D,
0,
GL_RGBA16F,
SCR_WIDTH,
SCR_HEIGHT,
0,
GL_RGBA,
GL_FLOAT,
NULL
);GL_RGBA16F 允许颜色附件保存 1.7、12.0 等超过 1.0 的中间结果,而不是写入时立刻截断。
这里的 HDR 是渲染管线内部的高动态范围,并不等于已经向 HDR 显示器输出 HDR 信号。
Pass 1:保留真实线性光照
官方隧道场景使用一个很强的白色光源:
lightColors.push_back(
glm::vec3(200.0f)
);并继续使用平方反比衰减:
float distance =
length(
fs_in.FragPos
- lights[i].Position
);
result *=
1.0 /
(distance * distance);靠近强光的位置可以得到 20、10、3 等值。这些结果先进入 HDR FBO。
Tone Mapping
第二遍读取 HDR Color Buffer:
vec3 hdrColor =
texture(
hdrBuffer,
TexCoords
).rgb;然后把高动态范围压到可显示范围。
Reinhard:
vec3 result =
hdrColor /
(hdrColor + vec3(1.0));例如:
0.5 → 0.33
1.0 → 0.50
2.0 → 0.67
10 → 0.91
100 → 0.99它不会把所有大于 1 的值都变成同一个白色,而是保留高亮之间的相对差异。
LearnOpenGL 最终使用 Exposure Tone Mapping:
vec3 result =
vec3(1.0)
- exp(
-hdrColor * exposure
);exposure 小,画面更暗、高光细节保留更多;exposure 大,暗部更亮、高光更快接近白色。
Exposure 改的是 HDR 到屏幕的映射,不会重新计算场景光照。
Gamma Correction 的位置
Tone Mapping 应处理线性颜色,因此顺序是:
Linear Lighting
↓
HDR Float Buffer
↓
Tone Mapping
↓
[0,1] Linear Color
↓
Gamma Encode
↓
Display官方随后执行:
result =
pow(
result,
vec3(1.0 / 2.2)
);这与前面的 Gamma Correction 章节直接连接。
官方 HDR Shader
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D hdrBuffer;
uniform bool hdr;
uniform float exposure;
void main()
{
const float gamma = 2.2;
vec3 hdrColor =
texture(
hdrBuffer,
TexCoords
).rgb;
if(hdr)
{
vec3 result =
vec3(1.0)
- exp(
-hdrColor * exposure
);
result =
pow(
result,
vec3(1.0 / gamma)
);
FragColor =
vec4(result, 1.0);
}
else
{
vec3 result =
pow(
hdrColor,
vec3(1.0 / gamma)
);
FragColor =
vec4(result, 1.0);
}
}关闭 HDR 时,官方不是重新用 LDR FBO 计算光照,而是跳过 Tone Mapping。超过 1.0 的值最后写入默认 framebuffer 时仍会被截断,因此高光容易变成一大片白。
FBO 数据流
CPU 创建:
hdrFBO
├─ Color Attachment: RGBA16F texture
└─ Depth Attachment: renderbuffer第一遍:
glBindFramebuffer(
GL_FRAMEBUFFER,
hdrFBO
);
// render scene第二遍:
glBindFramebuffer(
GL_FRAMEBUFFER,
0
);
glBindTexture(
GL_TEXTURE_2D,
colorBuffer
);
renderQuad();这与 Framebuffer 章节是同一机制:先离屏渲染,再把结果当纹理处理。HDR 只是把中间颜色附件升级成浮点格式,并在第二遍加入 Tone Mapping。
Clamp 和 Tone Mapping 的区别
假设两个高亮像素:
A = 2.0
B = 20.0普通 Clamp:
A → 1.0
B → 1.0差异完全丢失。
Tone Mapping:
A → 很亮
B → 更接近白高亮关系仍然存在。
所以 HDR 保存的不是“最终屏幕颜色”,而是 Tone Mapping 之前仍然有意义的亮度信息。
Light Intensity 和 Exposure
Light Intensity
→ 改变 Pass 1 算出来的真实线性光照值
Exposure
→ 改变 Pass 2 如何显示这些已经存在的 HDR 值例如 HDR Buffer 已经保存 (8,4,1),调 Exposure 不会改变这个缓冲里的值,只改变它映射到屏幕后的结果。
和前面章节的连接
Gamma Correction
→ 线性空间计算,最后再编码
Framebuffers
→ 离屏渲染结果可以成为纹理
HDR
→ 浮点颜色附件保存 >1 的线性亮度
→ Tone Mapping 压缩动态范围完整流程:
sRGB Diffuse
↓ decode
Linear Lighting
↓
RGBA16F HDR FBO
↓
Tone Mapping / Exposure
↓
Gamma Encode
↓
Default Framebuffer