
写入帧缓冲区时,颜色的亮度值会从0.0减少到1.0。 因此,乍一看无害的功能,我们总是必须选择适合此限制的照明和颜色值。 这种方法行之有效并获得了不错的结果,但是如果我们遇到一个有很多明亮光源的特别明亮的区域,并且总亮度超过1.0会发生什么? 结果,所有大于1.0的值都将转换为1.0,看起来不太好:

由于将大量片段的颜色值减小为1.0,因此图像的大面积区域被填充为相同的白色,从而丢失了大量图像细节,并且图像本身开始看起来不自然。
解决此问题的方法可能是降低光源的亮度,以使舞台上没有比1.0亮的片段:这不是最佳解决方案,这迫使使用不真实的照明值。 最好的方法是让亮度值暂时超过1.0的亮度,并在最后一步更改颜色,以使亮度恢复到0.0到1.0的范围,而又不损失图像细节。
计算机显示器能够显示亮度范围为0.0到1.0的颜色,但是在计算照明时我们没有这种限制。 通过允许片段的颜色比统一的明亮,我们为工作HDR (高动态范围)获得了更高的亮度范围。 使用hdr,明亮的东西看起来很明亮,黑暗的东西可能真的很黑,这样做,我们将看到细节。
最初,在摄影中使用了高动态范围:摄影师以不同的曝光率拍摄了几张相同的场景照片,捕获几乎任何亮度的色彩。 这些照片的组合形成了一个hdr图像,由于具有不同曝光损失的图像组合,大多数细节都变得可见。 例如,在左图的下方,清晰可见的图像片段清晰可见(看窗口),但是当使用高曝光时,这些细节会消失。 但是,高曝光会使图像的暗区上的细节以前不可见。

这类似于人眼的工作方式。 在光线不足的情况下,眼睛会适应,因此黑暗的细节变得清晰可见,明亮的区域也是如此。 可以说,人眼具有自动曝光控制,具体取决于场景的亮度。
HDR渲染的工作方式几乎相同。 渲染时,我们允许使用大范围的亮度值来收集有关场景的明暗细节的信息,最后我们将值从HDR范围转换回LDR(低动态范围,范围从0到1)。 这种转换称为色调映射 。在转换为LDR时,有大量算法旨在保留大多数图像细节。 这些算法通常具有曝光设置,使它们可以更好地显示图像的亮或暗区域。
在渲染时使用HDR不仅使我们可以将LDR范围从0扩展到1,并保存更多图像细节,而且还可以指示光源的真实亮度。 例如,太阳比手电筒具有更大的光亮度,那么为什么不将太阳设置为此(例如,给它设置10.0的亮度)呢? 这将使我们能够使用更逼真的亮度参数更好地调整场景的照明,这对于LDR渲染和0到1的亮度范围是不可能的。
由于显示屏仅显示从0到1的亮度,我们被迫将使用的HDR值范围转换回监视器的范围。 简单地缩放范围将不是一个好的解决方案,因为明亮的区域将开始主导图像。 但是,我们可以使用各种方程式或曲线将HDR值转换为LDR,这将使我们能够完全控制场景的亮度。 这种转换称为色调映射 ,是HDR渲染的最后一步。
浮点帧缓冲区
为了实现HDR渲染,我们需要一种方法来防止将值从片段着色器带到0到1的范围内。 如果帧缓冲区将归一化定点格式(GL_RGB)用于颜色缓冲区,则OpenGL在保存到帧缓冲区之前会自动限制这些值。 此限制适用于除浮点格式之外的大多数帧缓冲区格式。
要存储超出[0.0..1.0]范围的值,我们可以使用以下格式的颜色缓冲区: GL_RGB16F, GL_RGBA16F, GL_RGB32F or GL_RGBA32F
。 这对于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位来存储每种颜色。 在采用GL_RGB32F
或GL_RGBA32F
格式的浮点帧缓冲区中,使用32位存储每种颜色-多四倍。 如果不需要非常高的精度,则GL_RGBA16F
格式就足够了。
如果将浮点缓冲区附加到该颜色的帧缓冲区,则可以考虑到颜色值将不限于0到1的范围,从而将场景渲染到其中。在本文的代码中,我们首先将场景渲染到浮点帧缓冲区,然后显示内容半屏矩形上的彩色缓冲区。 看起来像这样:
glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); // [...] hdr glBindFramebuffer(GL_FRAMEBUFFER, 0); // hdr 2 hdrShader.use(); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture); RenderQuad();
在这里,颜色缓冲区中包含的颜色值可能大于1。对于本文而言,创建了一个带有大型细长立方体的场景,该立方体看起来像带有四个点光源的隧道,其中一个位于隧道的末端,并且亮度很高。
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); }
我们从颜色缓冲区的浮点获取输入,并将其用作着色器的输出值。 但是,由于默认情况下将2D矩形渲染到帧缓冲区中,因此着色器的输出值将被限制为0到1的间隔,尽管在某些情况下该值大于1。

显而易见的是,通道末端的太大的颜色值仅限于统一,因为图像的很大一部分是完全白色的,并且我们丢失了比统一更明亮的图像细节。 由于我们直接将HDR值用作LDR,因此这相当于没有HDR。 要解决此问题,我们必须在0到1的范围内显示不同的颜色值,而不会丢失图像中的任何细节。 为此,请应用色调压缩。
音调压缩
色调压缩是颜色值的转换,以使其适合0到1的范围而不会丢失图像细节,通常与赋予图像所需的白平衡相结合。
最简单的色调映射算法称为Reinhard色调映射算法。 它显示LDR范围内的任何HDR值。 将此算法添加到以前的片段着色器中,并应用伽玛校正(以及使用SRGB纹理)。
void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // vec3 mapped = hdrColor / (hdrColor + vec3(1.0)); // - mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); }
注意事项 反式 -对于x的较小值,函数x /(1 + x)的行为类似于x,对于x较大的函数趋向于统一。 功能图:

使用Reinhardt色调压缩后,我们不再会丢失图像明亮区域的细节。 该算法更喜欢明亮的区域,从而减少了黑暗的区域。

在这里,您可以再次看到图像末尾的细节,例如木材纹理。 使用这种相对简单的算法,我们可以清楚地看到HDR范围内的任何颜色,并且可以控制场景的照明而不会丢失图像细节。
值得注意的是,我们可以直接在着色器的末尾使用色调压缩来计算光照,然后完全不需要浮点帧缓冲区。 但是,在更复杂的场景中,您经常会遇到将中间HDR值存储在浮点缓冲区中的需求,因此这会派上用场。
音调压缩的另一个有趣功能是使用曝光参数。 您可能还记得,在本文开头的图像中,在不同的曝光值下可以看到各种细节。 如果我们有一个昼夜变化的场景,那么在白天使用低曝光而在晚上使用高曝光是有意义的,这类似于人眼的适应。 使用此曝光参数,我们可以配置将在不同照明条件下昼夜工作的照明参数。
具有曝光的相对简单的色调压缩算法如下所示:
uniform float exposure; void main() { const float gamma = 2.2; vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb; // vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure); // - mapped = pow(mapped, vec3(1.0 / gamma)); FragColor = vec4(mapped, 1.0); }
注意事项 per:为此功能添加一张图表,展示次数1和2:

在这里,我们为曝光定义了一个变量,默认情况下为1,使我们可以更准确地选择图像暗区和亮区的显示质量之间的平衡。 例如,在大曝光下,我们会在图像的暗区看到更多细节。 相反,低曝光使暗区无法区分,但可以使您更好地看到图像的亮区。 下面是具有不同暴露水平的隧道图像。

这些图像清楚地显示了hdr渲染的好处。 随着曝光水平的变化,我们会看到在正常渲染中会丢失的场景的更多细节。 以隧道的末端为例-在正常曝光下,几乎看不到树的纹理,但是在低曝光下,则完美可见。 同样,在高曝光下,黑暗区域的细节也非常清晰可见。
演示的源代码在这里。
更多HDR
已经显示的那两种音调压缩算法只是大量更高级算法中的一小部分,每种算法都有自己的优缺点。 有些算法可以更好地强调某些颜色/亮度,有些算法可以同时显示黑暗和明亮的区域,从而提供更多彩色和细节的图像。 还有许多方法称为自动曝光调整或眼睛适应 。 它们确定前一帧中场景的亮度,并(缓慢地)更改曝光参数,以使黑暗场景逐渐变亮,而明亮-变暗:类似于人眼的习惯。
HDR的真正好处最好是在大型和复杂的场景中使用严格的照明算法。 出于培训目的,本文使用了最简单的场景,因为创建大型场景可能很困难。 尽管场景简单,但仍可以看到hdr渲染的一些优势:在图像的暗区和亮区中,细节不会丢失,因为它们是使用色调压缩保存的,添加多个光源不会导致出现白色区域,并且这些值不必适合LDR范围。
此外,HDR渲染还使一些有趣的效果更加真实可信。 这些影响之一就是绽放,我们将在以后的文章中对其进行讨论。
其他资源:
附言:我们有一个电报会议,负责协调转让。 如果您有强烈的帮助翻译的愿望,欢迎您!