本部分主要来看一下HDR(High Dynamic Range, 高动态范围)主要参照:LearnOpenGL中的HDR章节
一、HDR原理
HDR(High Dynamic Range, 高动态范围)指的是:显示器被限制为只能显示值为0.0到1.0间的颜色,但是在光照方程中却没有这个限制。通过使片段的颜色超过1.0,我们有了一个更大的颜色范围。有了HDR,亮的东西可以变得非常亮,暗的东西可以变得非常暗,而且充满细节。
一般,当存储在帧缓冲(Framebuffer)中时,亮度和颜色的值是默认被限制在0.0到1.0之间的。由于大量片段的颜色值都非常接近1.0,在很大一个区域内每一个亮的片段都有相同的白色。解决这个问题的一个方案是减小光源的强度从而保证场景内没有一个片段亮于1.0。然而这并不是一个好的方案,因为你需要使用不切实际的光照参数。一个更好的方案是让颜色暂时超过1.0,然后将其转换至0.0到1.0的区间内,从而防止损失细节。
HDR渲染允许我们用更大范围的颜色值渲染从而获取大范围的黑暗与明亮的场景细节,最后将所有HDR值转换成在[0.0, 1.0]范围的LDR(Low Dynamic Range,低动态范围)。转换HDR值到LDR值得过程叫做色调映射(Tone Mapping),现在现存有很多的色调映射算法,这些算法致力于在转换过程中保留尽可能多的HDR细节。这些色调映射算法经常会包含一个选择性倾向黑暗或者明亮区域的参数。
二、色调映射原理
色调映射(Tone Mapping)是一个损失很小的转换浮点颜色值至我们所需的LDR[0.0, 1.0]范围内的过程,通常会伴有特定的风格的色平衡(Stylistic Color Balance)。
以下是无色调映射图片:
2.1 Reinhard色调映射
最简单的色调映射算法是Reinhard色调映射,它涉及到分散整个HDR颜色值到LDR颜色值上,所有的值都有对应。Reinhard色调映射算法平均地将所有亮度值分散到LDR上。我们将Reinhard色调映射应用到之前的片段着色器上,并且为了更好的测量加上一个Gamma校正过滤(包括SRGB纹理的使用):
// Reinhard色调映射
vec3 mapped = hdrColor / (hdrColor + vec3(1.0));
有了Reinhard色调映射的应用,我们不再会在场景明亮的地方损失细节。当然,这个算法是倾向明亮的区域的,暗的区域会不那么精细也不那么有区分度。
现在你可以看到在隧道的尽头木头纹理变得可见了。用了这个非常简单地色调映射算法,我们可以合适的看到存在浮点帧缓冲中整个范围的HDR值,使我们能在不丢失细节的前提下,对场景光照有精确的控制。
2.2 Exposure曝光映射
HDR图片包含在不同曝光等级的细节。如果我们有一个场景要展现日夜交替,我们当然会在白天使用低曝光,在夜间使用高曝光,就像人眼调节方式一样。有了这个曝光参数,我们可以去设置可以同时在白天和夜晚不同光照条件工作的光照参数,我们只需要调整曝光参数就行了。
一个简单的曝光色调映射算法会像这样:
// 曝光色调映射
vec3 mapped = vec3(1.0) - exp(-hdrColor * exposure);
在这里我们将exposure定义为默认为1.0的uniform,从而允许我们更加精确设定我们是要注重黑暗还是明亮的区域的HDR颜色值。举例来说,高曝光值会使隧道的黑暗部分显示更多的细节,然而低曝光值会显著减少黑暗区域的细节,但允许我们看到更多明亮区域的细节。下面这组图片展示了在不同曝光值下的通道:
这个图片清晰地展示了HDR渲染的优点。通过改变曝光等级,我们可以看见场景的很多细节,而这些细节可能在LDR渲染中都被丢失了。比如说隧道尽头,在正常曝光下木头结构隐约可见,但用低曝光木头的花纹就可以清晰看见了。对于近处的木头花纹来说,在高曝光下会能更好的看见。
三、开发步骤
本次场景与上几次类似,但开发步骤却大不相同,本文场景主要是处理场景帧缓冲(G-Buffer)中的数据,对其存储的场景颜色进行HDR处理,具体创建步骤可见之前文章,不再赘述,除此之外本文还采样天空盒与模型反射场景映射,主要的处理数据是在G-Buffer上的片元着色器中进行,之后对场景的渲染另用管线。
接下来我们来看下主要的G-Buffer片元着色器:
#version 450
layout (binding = 1) uniform samplerCube samplerEnvMap;
layout (location = 0) in vec3 inUVW;
layout (location = 1) in vec3 inPos;
layout (location = 2) in vec3 inNormal;
layout (location = 3) in vec3 inViewVec;
layout (location = 4) in vec3 inLightVec;
layout (location = 5) in mat4 inInvModelView;
layout (location = 0) out vec4 outColor0;//附件0:HDR
layout (constant_id = 0) const int type = 0; //VkSpecializationInfo 传入
layout (binding = 2) uniform UBO {
float exposure; //曝光参数
} ubo;
void main()
{
vec4 color;
vec3 wcNormal;
switch (type) {
case 0: // 天空盒采样
{
vec3 normal = normalize(inUVW);
color = texture(samplerEnvMap, normal);
}
break;
case 1: // 反射效果
{
vec3 wViewVec = mat3(inInvModelView) * normalize(inViewVec);
vec3 normal = normalize(inNormal);
vec3 wNormal = mat3(inInvModelView) * normal;
float NdotL = max(dot(normal, inLightVec), 0.0);
vec3 eyeDir = normalize(inViewVec);
vec3 halfVec = normalize(inLightVec + eyeDir);
float NdotH = max(dot(normal, halfVec), 0.0);
float NdotV = max(dot(normal, eyeDir), 0.0);
float VdotH = max(dot(eyeDir, halfVec), 0.0);
// 几何衰减
float NH2 = 2.0 * NdotH;
float g1 = (NH2 * NdotV) / VdotH;
float g2 = (NH2 * NdotL) / VdotH;
float geoAtt = min(1.0, min(g1, g2));
const float F0 = 0.6;
const float k = 0.2;
// 涅菲尔效果
float fresnel = pow(1.0 - VdotH, 5.0);
fresnel *= (1.0 - F0);
fresnel += F0;
float spec = (fresnel * geoAtt) / (NdotV * NdotL * 3.14);
color = texture(samplerEnvMap, reflect(-wViewVec, wNormal));
color = vec4(color.rgb * NdotL * (k + spec * (1.0 - k)), 1.0);
}
break;
case 2: // 折射
{
vec3 wViewVec = mat3(inInvModelView) * normalize(inViewVec);
vec3 wNormal = mat3(inInvModelView) * inNormal;
color = texture(samplerEnvMap, refract(-wViewVec, wNormal, 1.0/1.6));
}
break;
}
// 无色调映射
//outColor0.rgb = color.rgb ;
// Reinhard色调映射
//outColor0.rgb = color.rgb / (color.rgb + vec3(1.0));
// 曝光色调映射 填入附件0
outColor0.rgb = vec3(1.0) - exp(-color.rgb * ubo.exposure);
}