LearnOpenGL->gamma矫正/HDR

在这一小节中,我们会讲解Gamma以及HDR的相关知识点,现在让我们开始吧!;

Gamma矫正

Gamma矫正

gamma矫正的具体内容直接去看LearnOpenGl官方文档https://learnopengl-cn.github.io/05%20Advanced%20Lighting/02%20Gamma%20Correction/#gamma_1,这里不做解释;

OpenGL里面的gamma矫正实现方法

1.使用OpenGL内建的sRGB帧缓冲

这里我们仅需要开启GL_FRAMEBUFFER_SRGB指令,就可以告诉OpenGL在后续的绘制命令里面,在将颜色存储到颜色缓冲之前先进行一次gamma矫正,但是这种方法的缺点会让我们失去部分控制权;

【sRBG这一个颜色空间大致对应的gamma值是2.2,也是家用设备的一个标准】;

在开启了GL_FRAMEBUFFER_SRBG指令之后,每一次像素(片段)着色器在运行后续的帧缓冲时(包括默认的帧缓冲),OpenGL都会自动执行gamma矫正。

开启GL_FRAMEBUFFER_SRBG的具体代码如下所示:

glEnable(GL_FRAMEBUFFER_SRBG);
//开启方式与深度测试的开启方式一样,采用OpenGL内置的glEnable函数;

自此,你渲染的图像就被进行gamma校正处理,你不需要做任何事情硬件就帮你处理了。有时候,你应该记得这个建议:gamma校正将把线性颜色空间转变为非线性空间,所以在最后一步进行gamma校正是极其重要的。如果你在最后输出之前就进行gamma校正,所有的后续操作都是在操作不正确的颜色值。例如,如果你使用多个帧缓冲,你可能打算让两个帧缓冲之间传递的中间结果仍然保持线性空间颜色,只是给发送给监视器的最后的那个帧缓冲应用gamma校正。

2.在每一个片段着色器最后进行一次gamma矫正

这个方法让我们可以对gamma操作拥有绝对的控制权,我们会在每一个像素着色器运行的最后面机械能一次gamma操作,所以在发送给帧缓冲之前,颜色就会被矫正;

具体代码如下:

const float gamma = 2.2;//这里加上gamma值;

void main()
{
    ....//进行光照纹理采样等一系列的操作;
    
    //运行的最后;
    FragColor.rgb = pow(FragColor.rgb,vec3(1.0/gamma));
    //这样就已经完成了gamma矫正;

}

最后一行代码,将FragColor的每个颜色元素应用有一个1.0/gamma的幂运算,校正像素着色器的颜色输出。

这个方法有个问题就是为了保持一致,你必须在像素着色器里加上这个gamma校正,所以如果你有很多像素着色器,它们可能分别用于不同物体,那么你就必须在每个着色器里都加上gamma校正了。正因如此,我们可以考虑采用上一节的帧缓冲来实现,这样我们仅需要进行一次就足够!!;

下面是without gamma以及with gamma的两张图的对比:

without gamma
with gamma

对比发现第二张图经过了gamma矫正之后,颜色明显更加亮了(尽管不是很好看);

下面附上片段着色器的代码:

#version 330 core
out vec4 FragColor;

in vec2 TexCoords;

uniform sampler2D screenTexture;//这一个采样器会采样每一帧上面的场景纹理的对应坐标的颜色值;

const float offset = 1.0/300.0;//用于核处理的偏移量的计算;

void main()
{
    FragColor = texture(screenTexture,TexCoords);//进行采样,并返回FragColor的颜色值;
 //---//反相处理;
    //FragColor = vec4(vec3(1.0-texture(screenTexture,TexCoords)),1.0);
 //---//灰度处理1,取三个颜色分量的平均值,将其运用在最终输出的片段颜色的每一个分量上;
    //vec4 tempColor = texture(screenTexture, TexCoords);//时刻要记住通过纹理坐标取得的颜色值均为四分量;
    //float averge = (tempColor.r+tempColor.g+tempColor.b)/3.0;//取rgb三个分量,相加并相乘;
    //FragColor = vec4(averge,averge,averge,1.0);
//---//灰度处理2,由于人眼会对绿色更加敏感一些,而对蓝色不那么敏感,所以为了获取物理上更精确的效果,我们需要使用加权的(Weighted)通道:
    //vec4 tempColor = texture(screenTexture,TexCoords);
    //float averge = tempColor.r * 0.2126 + tempColor.g * 0.7152+tempColor.b * 0.0722;
    //FragColor = vec4(averge,averge,averge,1.0);
//---//核处理;
    vec2 offsets[9] = vec2[](
        vec2(-offset,  offset), // 左上
        vec2( 0.0f,    offset), // 正上
        vec2( offset,  offset), // 右上
        vec2(-offset,  0.0f),   // 左
        vec2( 0.0f,    0.0f),   // 中
        vec2( offset,  0.0f),   // 右
        vec2(-offset, -offset), // 左下
        vec2( 0.0f,   -offset), // 正下
        vec2( offset, -offset)  // 右下
    );

    float kenel[9]= float[](//注意到加权值的所有系数相加应该为1!!;
        -1,-1,-1,
        -1, 9,-1,
        -1,-1,-1
    );

    vec3 Sampler1[9];
    for(int i = 0;i<9;i++)
    {
        Sampler1[i] = vec3(texture(screenTexture,TexCoords.st+offsets[i]));//注意这句代码的写法;
    }
    vec3 col = vec3(0.0,0.0,0.0);
    for(int i = 0;i<9;i++)
    {//对邻近包括自己在内的九个值进行权重的相乘;
        col+=Sampler1[i]*kenel[i];
    }
    //FragColor = vec4(col,1.0);//最后输出锐化的效果;


//---//模糊:
    float blurred[9] = float[](
        1.0/16,2.0/16,1.0/16,
        2.0/16,4.0/16,2.0/16,
        1.0/16,2.0/16,1.0/16
    );
    vec3 Sampler2[9];
    for(int i = 0;i<9;i++)
    {
        Sampler2[i] = vec3(texture(screenTexture,TexCoords.st+offsets[i]));//注意这句代码的写法;
    }
    vec3 dol = vec3(0.0,0.0,0.0);
    for(int i = 0;i<9;i++)
    {//对邻近包括自己在内的九个值进行权重的相乘;
        dol+=Sampler2[i]*blurred[i];
    }
    //FragColor += vec4(dol,1.0);

//---//边缘检测
    float kenel3[9]= float[](//注意到加权值的所有系数相加应该为1!!;
        1,1,1,
        1, -8,-1,
        1,1,1
    );

    vec3 Sampler3[9];
    for(int i = 0;i<9;i++)
    {
        Sampler3[i] = vec3(texture(screenTexture,TexCoords.st+offsets[i]));//注意这句代码的写法;
    }
    vec3 eol = vec3(0.0,0.0,0.0);
    for(int i = 0;i<9;i++)
    {//对邻近包括自己在内的九个值进行权重的相乘;
        eol+=Sampler3[i]*kenel3[i];
    }
    //FragColor += vec4(eol,1.0);

    //最后进行一次gamma较正
    float gamma = 2.2;
    FragColor.rgb = pow(FragColor.rgb,vec3(1.0/gamma));//加上了这一步;
}

这些单行代码代表了gamma校正的实现。不太令人印象深刻,但当你进行gamma校正的时候有一些额外的事情别忘了考虑。

sRGB纹理

因为监视器总是在sRGB空间中显示应用了gamma的颜色,无论什么时候当你在计算机上绘制、编辑或者画出一个图片的时候,你所选的颜色都是根据你在监视器上看到的那种。这实际意味着所有你创建或编辑的图片并不是在线性空间,而是在sRGB空间中(译注:sRGB空间定义的gamma接近于2.2),假如在你的屏幕上对暗红色翻一倍,便是根据你所感知到的亮度进行的,并不等于将红色元素加倍。

结果就是纹理编辑者,所创建的所有纹理都是在sRGB空间中的纹理,所以如果我们在渲染应用中使用这些纹理,我们必须考虑到这点。在我们应用gamma校正之前,这不是个问题,因为纹理在sRGB空间创建和展示,同样我们还是在sRGB空间中使用,从而不必gamma校正纹理显示也没问题。然而,现在我们是把所有东西都放在线性空间中展示的,纹理颜色就会变坏,如下图展示的那样:

这里的sRGB空间实际上就是已经经过了一次gamma矫正后的图像,它已经不是在线性空间里面,因此这个时候如果我们在后续操作中继续采用一次gamma矫正,就会使得进行了两次gamma矫正!!!;

纹理图像实在太亮了,发生这种情况是因为,它们实际上进行了两次gamma校正!想一想,当我们基于监视器上看到的情况创建一个图像,我们就已经对颜色值进行了gamma校正,所以再次显示在监视器上就没错。由于我们在渲染中又进行了一次gamma校正,图片就实在太亮了。

为了修复这个问题,我们得确保纹理制作者是在线性空间中进行创作的。但是,由于大多数纹理制作者并不知道什么是gamma校正,并且在sRGB空间中进行创作更简单,这也许不是一个好办法。

重校

另一个解决方案是重校,或把这些sRGB纹理在进行任何颜色值的计算前变回线性空间。我们可以这样做:

因为此时在sRGB空间中已经进行了一次较正,因此我们要重新乘上gamma = 2.2的系数,就可以把位于sRGB空间中变化到线性空间中】;

【由于在sRGB空间中,我们已经对原来的FragColor乘上了1/gamma,因此经过显示器的2.2次幂忠厚就会输出会原来的颜色;然而我们在计算光线时是要在线性空间上实现的,因此例如经常存储在sRGB空间的漫反射贴图diffuseColor就应该进行一次gamma变化,使得其可以变化到线性空间中机械能光照模型等等的计算!!】;

代码如下:

float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));
//为存储在sRGB空间的diffuseColor进行一次gamma2.2的幂次计算返回线性空间;

为每个sRGB空间的纹理做这件事非常烦人。幸好,OpenGL给我们提供了另一个方案来解决我们的麻烦,这就是GL_SRGB和GL_SRGB_ALPHA内部纹理格式。

如果我们在OpenGL中创建了一个纹理,把它指定为以上两种sRGB纹理格式其中之一,OpenGL将自动把颜色校正到线性空间中,这样我们所使用的所有颜色值都是在线性空间中的了。我们可以这样把一个纹理指定为一个sRGB纹理:

glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
//同样是生成纹理的操作,不同之处仅仅在于我们将纹理的存储格式指定为GL_SRGB,只要我们将纹理的内部格式设定为GL_SRGB,则后续不需要进行sRGB空间纹理的线性变化操作,OpenGL会自动将我们存储的纹理变换到线性空间上去,之后就可以直接进行光照等等的计算;

同样是生成纹理的操作,不同之处仅仅在于我们将纹理的存储格式指定为GL_SRGB,只要我们将纹理的内部格式设定为GL_SRGB,则后续不需要进行sRGB空间纹理的线性变化操作,OpenGL会自动将我们存储的纹理变换到线性空间上去,之后就可以直接进行光照等等的计算!!

如果你还打算在你的纹理中引入alpha元素,必究必须将纹理的内部格式指定为GL_SRGB_ALPHA。

因为不是所有纹理都是在sRGB空间中的所以当你把纹理指定为sRGB纹理时要格外小心。比如diffuse纹理,这种为物体上色的纹理几乎都是在sRGB空间中的。而为了获取光照参数的纹理,像specular贴图和法线贴图几乎都在线性空间中,所以如果你把它们也配置为sRGB纹理的话,光照就坏掉了。指定sRGB纹理时要当心。

将diffuse纹理定义为sRGB纹理之后,你将获得你所期望的视觉输出,但这次每个物体都会只进行一次gamma校正。

衰减

在使用了gamma校正之后,另一个不同之处是光照衰减(Attenuation)真实的物理世界中,光照的衰减和光源的距离的平方成反比。

float attenuation = 1.0 / (distance * distance);

然而,当我们使用这个衰减公式的时候,衰减效果总是过于强烈,光只能照亮一小圈,看起来并不真实。出于这个原因,我们使用在基本光照教程中所讨论的那种衰减方程,它给了我们更大的控制权,此外我们还可以使用双曲线函数:

float attenuation = 1.0 / distance;

//下面是二次函数曲线;
//float attenuation = 1.0 / (light.constant + light.linear * distance + light.quadratic * (distance * distance)); 

双曲线比使用二次函数变体在不用gamma校正的时候看起来更真实,不过但我们开启gamma校正以后线性衰减看起来太弱了,符合物理的二次函数突然出现了更好的效果。下图显示了其中的不同:

这种差异产生的原因是,光的衰减方程改变了亮度值,而且屏幕上显示出来的也不是线性空间,在监视器上效果最好的衰减方程,并不是符合物理的。想想平方衰减方程,如果我们使用这个方程,而且不进行gamma校正,显示在监视器上的衰减方程实际上将变成(1.0/distance2)2.2。若不进行gamma校正,将产生更强烈的衰减。这也解释了为什么双曲线不用gamma校正时看起来更真实,因为它实际变成了(1.0/distance)2.2=1.0/distance2.2这和物理公式是很相似的。

这一节比较简单,下面我们会进入学习PBR基于物理的渲染前的最后一节:HDR;

HDR

这里先总结一下HDR的基本原理:

由于LDR会将我们的光照限制在0.0到1.0之间,因此如果当有好多个光源照射同一个Frag片段时,最后的光照系数就有可能大于1.0,而LDR会将超过1.0的部分均限制在1.0这一个边界上,此时如果有多个片段均超过1.0时,由于LDR的作用会被限制在了1.0处,此时就会看到一大片的高亮白色,很多的细节都会被遮掩;

而HDR的作用就在于其可以存储超过1.0部分的颜色值,并通过一系列算法将其转换到0.0到1.0的部分这样会使得细节更加的突出,视觉效果更好,具体我们会先设置缓冲纹理颜色的内部格式为浮点帧缓冲,这样我们渲染场景就会以浮点帧缓冲(可以理解为帧缓冲中的一个颜色附件)的形式附加到帧缓冲上面,之后再在屏铺四边形上对纹理进行采样,我们就可以获得到大于1.0的颜色片段,之后在片段着色器中再进行色调映射,就可以得到最终的显示更多细节的效果!!;

下面我们根据文档来进行探讨。

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

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

解决这个问题有两个方案:

1.减小光源的强度从而保证场景内没有一个片段亮于1.0。然而这并不是一个好的方案,因为你需要使用不切实际的光照参数。

2.一个更好的方案是让颜色暂时超过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_RGB16F, GL_RGBA16F, GL_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);  

这一步是在帧缓冲中创建一个纹理附件的步骤中进行的!!目的是为了在后续对场景的渲染中,将每一帧的场景以浮点帧缓冲的形式附加到帧缓冲上面,并在后续屏铺四边形的纹理采样中进行计算,设置为浮点帧缓冲的形式当作颜色附件就可以使得屏幕中的每一个像素可以接受高于1.0的光线值!!】;

默认的帧缓冲默认一个颜色分量只占用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色调映射

最简单的色调映射算法是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.rgb/(hdrColor.rgb+vec3(1.0,1.0,1.0)); //这一步将色调映射到0.0-1.0的范围内;
    
    //进行gamma较正;
    mapped = pow(mapped,vec3(1.0/gamma));//gamma较正,用pow函数;

    //输出最终的颜色;
    FragColor = vec4(mapped,1.0); 
}

由于我们存储再附加在帧缓冲内的浮点帧缓冲的颜色值可能大于1.0,而最后我们会利用默认的帧缓冲来对屏铺三角形进行纹理的采样绘制,并且默认的帧缓冲只会接收范围在0.0-1.0之间的颜色值,因此我们必须要先进行色调映射,将我们在浮点帧缓冲中接收的大于1.0的值进行转换,转换到0.0-1.0之间,才可以被默认的帧缓冲进行接收】;

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

现在你可以看到在隧道的尽头木头纹理变得可见了。用了这个非常简单地色调映射算法,我们可以合适的看到存在浮点帧缓冲中整个范围的HDR值,使我们能在不丢失细节的前提下,对场景光照有精确的控制。

曝光参数

另一个有趣的色调映射应用是曝光(Exposure)参数的使用。你可能还记得之前我们在介绍里讲到的,HDR图片包含在不同曝光等级的细节。如果我们有一个场景要展现日夜交替,我们当然会在白天使用低曝光,在夜间使用高曝光,就像人眼调节方式一样。有了这个曝光参数,我们可以去设置可以同时在白天和夜晚不同光照条件工作的光照参数,我们只需要调整曝光参数就行了。

....
uniform float exposure;//曝光值;
const float gamma = 2.2;

uniform sampler2D hdrBuffer;
....

void main()
{
    //先进行采样;
    vec3 hdrColor = texture(hdrBuffer,TexCoords).rgb;//采样,修改rgb;

    //进行曝光色调映射;
    vec3 mapped = vec3(1.0) - exp(-hdrColor*exposure);

    //进行gamma较正;
    mapped = pow(mapped,vec3(1.0/gamma));

    //进行输出;
    FragColor = vec4(mapped,1.0);//转为四维向量并输出;
}

这里说明一下曝光色调映射函数:vec3 mapped = vec3(1.0) - exp(-hdrColor*exposure);

1.首先用vec3(1.0)去减另外一个正值,目的在于保证最后的值在0.0到1.0之间;

2.其次,对于exp(-hdrColor*exposure),当曝光系数越大,最后的值每一个分量就会越小,因此mapped的每一个分量就会越大,也就会越亮,对于暗处的场景我们就可以更加清晰地得到一些细节;当曝光系数较小时,mapped的值每一个分量就会越小,就会暗一些,这对于较亮部分就会变得暗一些,也就可以更好地看到亮处的细节。

文档中的这副图就很清晰地展现了不同曝光度的影响;

有疑问的话请回去认真看文档的内容:

https://learnopengl-cn.github.io/05%20Advanced%20Lighting/06%20HDR/#_1

下面是一些资料汇总:

1.色彩较正中的gamma值是什么

https://www.zhihu.com/question/27467127/answer/37602200

2.更多关于gamma和gamma校正的内容。

cambridgeincolour.com

3.David Rosen关于在渲染领域使用gamma校正的好处。

wolfire.com

4. 一些额外的实践上的思考

renderwonk.com

5.如果泛光效果不被应用HDR渲染还有好处吗?: 一个StackExchange问题,其中有一个答案非常详细地解释HDR渲染的好处。

6.什么是色调映射? 它与HDR有什么联系?: 另一个非常有趣的答案,用了大量图片解释色调映射。

结束。开始PBR之旅!!;

猜你喜欢

转载自blog.csdn.net/2201_75303014/article/details/129019605
今日推荐