OpenGL核心技术之HDR

笔者介绍:姜雪伟。IT公司技术合伙人,IT高级讲师,CSDN社区专家。特邀编辑,畅销书作者。国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术具体解释》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

一般来说,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的。这个看起来无辜的语句使我们一直将亮度与颜色的值设置在这个范围内,尝试着与场景契合。这样是能够执行的。也能给出还不错的效果。可是假设我们遇上了一个特定的区域。当中有多个亮光源使这些数值总和超过了1.0,又会发生什么呢?答案是这些片段中超过1.0的亮度或者颜色值会被约束在1.0,从而导致场景混成一片,难以分辨:

这是由于大量片段的颜色值都非常接近1.0。在非常大一个区域内每个亮的片段都有同样的白色。这损失了非常多的细节,使场景看起来非常假。

解决问题的一个方案是减小光源的强度从而保证场景内没有一个片段亮于1.0。然而这并非一个好的方案,由于你须要使用不切实际的光照參数。一个更好的方案是让颜色临时超过1.0,然后将其转换至0.0到1.0的区间内。从而防止损失细节。

显示器被限制为仅仅能显示值为0.0到1.0间的颜色,可是在光照方程中却没有这个限制。通过使片段的颜色超过1.0,我们有了一个更大的颜色范围。这也被称作HDR(High Dynamic Range, 高动态范围)

有了HDR。亮的东西能够变得非常亮,暗的东西能够变得非常暗,并且充满细节。

HDR原本仅仅是被运用在摄影上。摄影师对同一个场景採取不同曝光拍多张照片,捕捉大范围的色彩值。这些图片被合成为HDR图片,从而综合不同的曝光等级使得大范围的细节可见。看以下这个样例,左边这张图片在被光照亮的区域充满细节,可是在黑暗的区域就什么都看不见了。可是右边这张图的高曝光却能够让之前看不出来的黑暗区域显现出来。


这与我们眼睛工作的原理非常类似,也是HDR渲染的基础。当光线非常弱的啥时候,人眼会自己主动调整从而使过暗和过亮的部分变得更清晰。就像人眼有一个能自己主动依据场景亮度调整的自己主动曝光滑块。

HDR渲染和其非常类似,我们同意用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节,最后将全部HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。

转换HDR值到LDR值得过程叫做色调映射(Tone Mapping),如今现存有非常多的色调映射算法,这些算法致力于在转换过程中保留尽可能多的HDR细节。这些色调映射算法常常会包括一个选择性倾向黑暗或者明亮区域的參数。

在实时渲染中,HDR不仅同意我们超过LDR的范围[0.0, 1.0]与保留很多其它的细节。同一时候还让我们能够依据光源的真实强度指定它的强度。比方太阳有比闪光灯之类的东西更高的强度。那么我们为什么不这样子设置呢?(比方说设置一个10.0的漫亮度) 这同意我们用更现实的光照參数恰当地配置一个场景的光照,而这在LDR渲染中是不能实现的,由于他们会被上限约束在1.0。

由于显示器仅仅能显示在0.0到1.0范围之内的颜色。我们肯定要做一些转换从而使得当前的HDR颜色值符合显示器的范围。

简单地取平均值又一次转换这些颜色值并不能非常好的解决问题。由于明亮的地方会显得更加显著。我们能做的是用一个不同的方程与/或曲线来转换这些HDR值到LDR值,从而给我们对于场景的亮度全然掌控,这就是之前说的色调变换,也是HDR渲染的终于步骤。

在实现HDR渲染之前,我们首先须要一些防止颜色值在每个片段着色器执行后被限制约束的方法。

当帧缓冲使用了一个标准化的定点格式(像GL_RGB)为其颜色缓冲的内部格式,OpenGL会在将这些值存入帧缓冲前自己主动将其约束到0.0到1.0之间。这一操作对大部分帧缓冲格式都是成立的,除了专门用来存放被拓展范围值的浮点格式。

当一个帧缓冲的颜色缓冲的内部格式被设定成了GL_RGB16FGL_RGBA16FGL_RGB32F 或者GL_RGBA32F时,这些帧缓冲被叫做浮点帧缓冲(Floating Point Framebuffer),浮点帧缓冲能够存储超过0.0到1.0范围的浮点值,所以非常适合HDR渲染。

想要创建一个浮点帧缓冲,我们仅仅须要改变颜色缓冲的内部格式參数即可了(注意GL_FLOAT參数):

glBindTexture(GL_TEXTURE_2D, colorBuffer);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB16F, SCR_WIDTH, SCR_HEIGHT, 0, GL_RGB, GL_FLOAT, NULL);  

默认的帧缓冲默认一个颜色分量仅仅占用8位(bits)。当使用一个使用32位每颜色分量的浮点帧缓冲时(使用GL_RGB32F 或者GL_RGBA32F)。我们须要四倍的内存来存储这些颜色。所以除非你须要一个非常高的准确度。32位不是必须的,使用GLRGB16F就足够了。

有了一个带有浮点颜色缓冲的帧缓冲。我们能够放心渲染场景到这个帧缓冲中。

在这个教程的样例当中,我们先渲染一个光照的场景到浮点帧缓冲中。之后再在一个铺屏四边形(Screen-filling Quad)上应用这个帧缓冲的颜色缓冲。代码会是这样子:

glBindFramebuffer(GL_FRAMEBUFFER, hdrFBO);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);  
    // [...] 渲染(光照的)场景
glBindFramebuffer(GL_FRAMEBUFFER, 0);

// 如今使用一个不同的着色器将HDR颜色缓冲渲染至2D铺屏四边形上
hdrShader.Use();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, hdrColorBufferTexture);
RenderQuad();

这里场景的颜色值存在一个能够包括随意颜色值的浮点颜色缓冲中,值可能是超过1.0的。这个简单的演示中,场景被创建为一个被拉伸的立方体通道和四个点光源,当中一个非常亮的在隧道的尽头:

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));  
渲染至浮点帧缓冲和渲染至一个普通的帧缓冲是一样的。新的东西就是这个的hdrShader的片段着色器。用来渲染终于拥有浮点颜色缓冲纹理的2D四边形。我们来定义一个简单的直通片段着色器(Pass-through Fragment Shader):

#version 330 core
out vec4 color;
in vec2 TexCoords;

uniform sampler2D hdrBuffer;

void main()
{             
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;
    color = vec4(hdrColor, 1.0);
}  

这里我们直接採样了浮点颜色缓冲并将其作为片段着色器的输出。

然而。这个2D四边形的输出是被直接渲染到默认的帧缓冲中,导致全部片段着色器的输出值被约束在0.0到1.0间,虽然我们已经有了一些存在浮点颜色纹理的值超过了1.0。


非常明显,在隧道尽头的强光的值被约束在1.0,由于一大块区域都是白色的,过程中超过1.0的地方损失了全部细节。由于我们直接转换HDR值到LDR值,这就像我们根本就没有应用HDR一样。

为了修复这个问题我们须要做的是无损转化全部浮点颜色值回0.0-1.0范围中。我们须要应用到色调映射。

色调映射(Tone Mapping)是一个损失非常小的转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程,一般会伴有特定的风格的色平衡(Stylistic Color Balance)。

最简单的色调映射算法是Reinhard色调映射,它涉及到分散整个HDR颜色值到LDR颜色值上,全部的值都有相应。

Reinhard色调映射算法平均得将全部亮度值分散到LDR上。

我们将Reinhard色调映射应用到之前的片段着色器上,并且为了更好的測量加上一个Gamma校正过滤(包括SRGB纹理的使用):

void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

    // Reinhard色调映射
    vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
    // Gamma校正
    mapped = pow(mapped, vec3(1.0 / gamma));

    color = vec4(mapped, 1.0);
}   
有了Reinhard色调映射的应用,我们不再会在场景明亮的地方损失细节。当然,这个算法是倾向明亮的区域的。暗的区域会不那么精细也不那么有区分度。



如今你能够看到在隧道的尽头木头纹理变得可见了。

用了这个非常easy地色调映射算法,我们能够合适的看到存在浮点帧缓冲中整个范围的HDR值。给我们对于无损场景光照精确的控制。

还有一个有趣的色调映射应用是曝光(Exposure)參数的使用。你可能还记得之前我们在介绍里讲到的,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);
    // Gamma校正 
    mapped = pow(mapped, vec3(1.0 / gamma));

    color = vec4(mapped, 1.0);
}  

在这里我们将exposure定义为默觉得1.0的uniform,从而同意我们更加精确设定我们是要注重黑暗还是明亮的区域的HDR颜色值。举例来说,高曝光值会使隧道的黑暗部分显示很多其它的细节。然而低曝光值会显著降低黑暗区域的细节,但同意我们看到很多其它明亮区域的细节。

以下这组图片展示了在不同曝光值下的通道:


这个图片清晰地展示了HDR渲染的长处。通过改变曝光等级,我们能够看见场景的非常多细节,而这些细节可能在LDR渲染中都被丢失了。比方说隧道尽头,在正常曝光下木头结构隐约可见,但用低曝光木头的花纹就能够清晰看见了。对于近处的木头花纹来说,在高曝光下会能更好的看见。

最后把实现的源码给读者展演示样例如以下,首先展示的是顶点着色器代码:

#version 330 core
layout (location = 0) in vec3 position;
layout (location = 1) in vec2 texCoords;

out vec2 TexCoords;

void main()
{
    gl_Position = vec4(position, 1.0f);
    TexCoords = texCoords;
}

片段着色器代码例如以下所看到的:

#version 330 core
out vec4 color;
in vec2 TexCoords;

uniform sampler2D hdrBuffer;
uniform float exposure;
uniform bool hdr;

void main()
{             
    const float gamma = 2.2;
    vec3 hdrColor = texture(hdrBuffer, TexCoords).rgb;

    // reinhard
    // vec3 result = hdrColor / (hdrColor + vec3(1.0));
    // exposure
    vec3 result = vec3(1.0) - exp(-hdrColor * exposure);
    // also gamma correct while we're at it       
    result = pow(result, vec3(1.0 / gamma));
    color = vec4(result, 1.0f);
}

在这里展示的两个色调映射算法仅仅是大量(更先进)的色调映射算法中的一小部分,这些算法各有长短.一些色调映射算法倾向于特定的某种颜色/强度,也有一些算法同一时候显示低于高曝光颜色从而能够显示更加多彩和精细的图像。

也有一些技巧被称作自己主动曝光调整(Automatic Exposure Adjustment)或者叫人眼适应(Eye Adaptation)技术,它能够检測前一帧场景的亮度并且缓慢调整曝光參数模仿人眼使得场景在黑暗区域逐渐变亮或者在明亮区域逐渐变暗。

HDR渲染的真正长处在庞大和复杂的场景中应用复杂光照算法会被显示出来,可是出于教学目的创建这样复杂的演示场景是非常困难的,这个教程用的场景是非常小的,并且缺乏细节。可是如此简单的演示也是能够显示出HDR渲染的一些长处:在明亮和黑暗区域无细节损失,由于它们能够由色调映射又一次获取;多个光照的叠加不会导致亮度被约束的区域。光照能够被设定为他们原来的亮度而不是被LDR值限定。并且,HDR渲染也使一些有趣的效果更加可行和真实; 当中一个效果叫做泛光(Bloom)。我们将在下篇博客讨论。


posted @ 2017-08-20 13:52  clnchanpin  阅读(1648)  评论(0编辑  收藏  举报