6-HDR
今天来学学HDR,并在OpenGL上实现它。
HDR
背景
存储在帧缓冲中亮度和颜色的值被限制在\([0,1]\)之间,我们一直在遵循这句话。但是存在一种特殊区域,多个光源使这些数值总和超过了1.0,如果还要遵循这句话,就会使大片场景的亮度被约束为1.0,导致整个场景都是亮的:
解决这种问题的一种方法是,减少光源的强度,控制场景亮度总和不超过1,这种方法并不好,不真实;还有一种更好的方法是允许颜色值暂时超过1.0,然后在最后一步控制到\([0,1]\)之间,以避免细节丢失。
概念
没有HDR的显示器只能显示\([0,1]\)间的颜色,但在光照计算中却没有这个限制。通过允许片段的颜色超过1.0,我们就有了更大的颜色范围,这也被称为 高动态范围(High Dynamic Range,HDR)。有了HDR,亮的东西就是真亮了,暗的东西就是真暗了,并且充满细节。
HDR原本只用于摄影,摄影师用不同曝光等级拍多张照片,获取大范围的颜色值。这些图片被合成为HDR图片,它综合不同的曝光等级使得更大范围的细节可见。例如下图中,左图和右图合成中间的HDR图片,使得能被看到的细节更多。
这和人眼的工作方式相似,也是HDR渲染的基础。当光线很弱的时候,人眼会自己调节,以便能看到更暗的细节;光线很强的时候同理。
HDR渲染也和它类似。我们允许渲染更大的颜色值范围,通过收集场景中大范围的亮暗信息,最后将HDR值变换回位于\([0.0, 1.0]\)中的LDR(Low Dynamic Range)值。将HDR值转换为LDR值的过程被称作 色调映射(Tone Mapping),并且有很多色调映射的算法供我们选择。这些色调映射算法通常包含一个选择倾向于暗/亮区域的参数。
对于实时渲染领域,HDR不仅允许我们超过在颜色值上超越LDR以保留更多细节,而且允许我们根据设置光源 真正的 光强。例如,太阳比手电筒的光强大得多,而LDR无法实现这种大光强。
由于不支持HDR的显示器只显示\([0,1]\)间的颜色值,我们必须要将当前HDR值转换为LDR值。通过取均值简单地转换这些颜色值并不能解决这个问题,因为明亮的地方会变得更亮。我们应该做的是 用一个不同的方程/曲线来进行色调映射。
浮点数帧缓冲
为了实现HDR,我们需要找到在每个片段着色器运行后预防颜色值被约束的方法。当帧缓冲使用标准化的定点数格式(如GL_RGB
)作为其颜色缓冲的内部格式时,OpenGL会将这些值约束到LDR后才存入帧缓冲,除了浮点数格式。
常用的浮点数格式如下:GL_RGB16F
,GL_RGBA16F
,GL_RGB32F
,GL_RGBA32F
。这些浮点数格式可以存储HDR的值,非常适合实现HDR渲染。
创建一个浮点数帧缓冲的代码如下,只需更改内部格式即可:
glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGBA, GL_FLOAT, NULL);
OpenGL的默认帧缓冲中,每个颜色分量只有8位。而浮点数帧缓冲每个颜色分量有32位(使用GL_RGB32F
或GL_RGBA32F
)。由于这里对精度要求不高,这里使用GL_RGBA16F
就够了。
将这个colorBuffer
附加到一个FBO对象后,就能将场景渲染到HDR帧缓冲了。在这个教程demo中,首先渲染一个光照场景到HDRFBO中,然后展示这个FBO的colorBuffer
,大致结构如下:
glBindFramebuffer(GL_FRAMEBUFFER, hdfFBO);
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 渲染光照场景...
glBindFramebuffer(GL_FRAMEBUFFER, 0);
// 用色调映射着色器渲染这个HDRFBO的colorBuffer
hdrShader.use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture);
RenderQuad();
在demo场景中,有如下4个光强不同的点光源:
std::vector<glm::vec3> lightColors;
lightColors.push_back(glm::vec3(200.0f, 200.0f, 200.0f));
lightColors.push_back(glm::vec3(0.1f, 0.0f, 0.0f));
lightColors.push_back(glm::vec3(0.0f, 0.0f, 0.2f));
lightColors.push_back(glm::vec3(0.0f, 0.1f, 0.0f));
目前给HDR的片段着色器如下:
#version 330 core
out vec4 FragColor;
in vec2 TexCoords;
uniform sampler2D hdrBuffer;
void main()
{
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
FragColor = vec4(hdrColor, 1.0);
}
这里简单采样了HDRFBO中colorBuffer
的纹理值作为输出,然而最后仍然是LDR值,结果如下:
很明显,隧道尽头大片的光被被限制在1.0,导致几乎是白的,损失了大部分细节。接下来我们要实现色调映射,将HDR值无损映射到LDR值上。
色调映射
色调映射这一步骤将HDR范围的值尽量不失细节地转换到LDR范围内,通常是风格化的。
一种简单的色调映射算法是 Reinhard 色调映射,它将整个范围HDR值划分并映射到LDR中。这个算法将HDR值均匀分散到LDR上。我们将这个Reinhard色调映射实现到之前的片段着色器上。
void main()
{
vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
// reinhard tone mapping
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
FragColor = vec4(mapped, 1.0);
}
可以发现画面像蒙了一层灰,原理是让亮的更暗,让暗的更亮。我这里好像有些问题,唉。
还有一种色调映射的方法如下:
// exposure tone mapping
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
这个动图展示了exposure
参数对HDR带来的影响。
Ps:感觉这一节没学好,有时间抽空再看看吧。
参考资料
- HDR - LearnOpenGL CN (learnopengl-cn.github.io)
- LearnOpenGL - HDR