本文同时发布在我的个人博客上:https://dragon_boy.gitee.io
当我们计算好场景中的每个像素的颜色后我们需要将其显示在显示器上。在过去我们往往使用CRT显示器,这种显示器由于某些物理属性,增加电压的倍数并不会增加同种倍数的亮度。两倍的输入电压会造成2.2倍的亮度提升,这个2.2的数值被粗略的称为显示器的伽马值。这个数值也恰好和人类感知的亮度相关。下面给出物理线性亮度变化和人类察觉的线性亮度变化:
上一行的人类感知线性亮度更像是我们眼睛看到的亮度变化。然而当我们考虑物理亮度数值(光子层面)时,下一行反而是正确的亮度变化。所以就会造成很奇怪的感知现象,真实的和我们看见的是不一样的(我们更容易察觉亮度不是很高的变化)。
正是由于人的眼睛像上面那一行一样感知亮度,所以显示器使用一个幂运算来增加输入的物理亮度来非线性映射到上一行那样的亮度变化。
这个非线性映射的确可以输出让我们眼睛更舒服的亮度,但对于渲染层面就有一个问题:所有的颜色和亮度选项我们需要使用我们从显示器感知的来配置,因此所有的配置是非线性配置。看下面的图表:
中间的点线代表线性空间的的颜色变化,实线代表显示器的颜色空间。如果我们在线性空间将颜色值增加两倍,结果就一是两倍。例如,一个颜色向量(0.5, 0.0, 0.0),2倍后就是(1.0, 0.0, 0.0)。但问题在于,原颜色向量在显示器上为(0.218, 0.0, 0.0),如果就这么在线性空间增加两倍,在显示器上相当于增加了4、5倍亮度。
到目前的章节为止,我们都是在假设我们在线性空间操作颜色,但实际上我们是在显示器的输出空间来操作颜色,所以我们配置的颜色在物理层面并不准确,只是在显示器上看起来正确(我们眼睛观察的)。针对这个理由,我们一般会将颜色设置的比它本应有的亮度高一些(显示器会降低亮度显示),这样会让大多数线性空间的计算不正确。注意上面的图表,CRT和线性空间的曲线从同一点出发并在同一位置结束,在这中间的值会降低亮度显示。
因为颜色是基于显示的输出配置的,所有的在线性空间的中间值在物理层面上都是不正确的。下面的图片显示了这种区别:
可以看到如果使用了伽马矫正,颜色显示的非常好,在亮度角度处会有比较丰富的细节。如果没有伽马校正,为了获得比较正确的亮度显示,会浪费许多时间在颜色调整上。
伽马校正
说白了,伽马校正就是对线性空间的颜色值进行反向伽马值的幂运算,到时候在显示器上进行显示时,显示器会对进行伽马矫正过的颜色值进行针对自己伽马值的幂运算,这样的话,我们只需要在线性空间调整颜色即可,在显示器上显示的颜色也在线性空间。
下面给出一个例子。我们有一个深红色的颜色,我们首先进行伽马校正,颜色为,最后显示在显示器上会再进行一次幂运算,颜色为,可以看到使用了伽马校正,最终显示的颜色和我们在线性空间设置的一样。
注意:2.2是大多是显示器的平均估计伽马值,通过进行此伽马值的幂运算得到的颜色空间被称为sRGB颜色空间(不完全是,但很接近)。每个显示器都有自己的伽马值,但2.2适用于大多数显示器,并可以得到很好的效果。
在OpenGL中有两种方式使用伽马校正:
- 使用OpenGL的sRGB帧缓冲。
- 在片元着色器中进行手动伽马校正。
第一种方法可能最简单,但可控性更低。通过开启GL_FRAMEBUFFER_SRGB我们告知OpenGL在每个绘制命令在存储颜色进颜色缓冲前先进行伽马校正。sRGB是和2.2伽马值相关的颜色空间,对大多数设备都适用。在开启GL_FRAMEBUFFER_SRGB后,OpenGL会在片元着色器执行后对片元的自动进行伽马校正,包括所有帧缓冲。
这么开启GL_FRAMEBUFFER_SRGB:
glEnable(GL_FRAMEBUFFER_SRGB);
使用这个命令我们需要记住,伽马校正会将线性空间的颜色转化到非线性空间,所以我们一定要在最后阶段进行伽马校正,以免影响其它重要的操作。
第二个方法如下,我们在片元着色器的输出颜色的最后使用伽马校正:
void main()
{
// 在线性空间进行的操作
[...]
// 使用伽马矫正
float gamma = 2.2;
FragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}
这一方法的问题在于我们需要对所有相关的片元着色器使用相同的操作。操作可能过于繁琐,但我们可以使用帧缓冲绘制一个屏幕大小的平面来进行后期处理,这样我们只需要在一个片元着色器中进行伽马校正。
sRGB纹理
在我们绘制纹理,图片时,我们会根据自己在屏幕上看到的颜色来选择颜色,这也就表示我们绘制的图片的颜色处在非线性空间即sRGB空间。
所以说,我们在使用这些纹理时我们也需要将纹理处在sRGB空间的问题考虑在内。同样,由于我们是在线性空间进行渲染相关计算,我们可能会想去进行伽马校正,但同样会造成视觉上的问题:
可以看到,使用了伽马矫正的话,纹理太亮了,这是因为它其实进行了两次伽马校正。当我们创建纹理时,我们基于我们在显示器上看到的颜色创建,我们会对这张纹理进行伽马校正来保证在显示器上会看起来不错,接着我们又在渲染时进行了伽马校正,这样就会导致图片看起来太亮了。
为了解决这一问题,我们需要保证我们是在线性空间创建纹理。然而,在sRGB空间创建纹理更为容易,而且大多数工具并不支持线性空间的纹理,所以,这不是一个好办法。
另一个方法是在片元着色器中进行。我们在进行颜色计算前先手动将sRGB纹理转化到线性空间:
float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));
如果针对每次纹理计算都这么做,可是不小的工作量。幸运地是,OpenGL提供了另一种方式,GL_SRGB和GL_SRGB_ALPHA纹理格式。
如果我们在创建纹理时将纹理格式设置为GL_SRGB,那么OpenGL会自动将颜色转化为线性空间:
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);
我们需要注意的是并不是所有的纹理都在sRGB空间,用于计算颜色的纹理(像漫反射纹理)大多数都在sRGB空间,用于检索灯光参数的纹理(如高光纹理和法线纹理)大多数在线性空间。
衰减
之前讲过,光的衰减根据距离可以有平方衰减:
float attenuation = 1.0 / (distance * distance);
以及线性衰减:
float attenuation = 1.0 / distance;
我们对上面两种衰减使用伽马校正来进行对比:
这种差异的原因时灯光衰减会改变亮度,由于我们不使用线性空间观察场景,我们会使用在显示器上观看的更好的方式来衰减,但这在物理层面并不正确。对于我们的平方衰减,如果我们不使用伽马矫正,那么公式就会变成这样:
,这样的话在显示器上衰减的程度就太大了,线性衰减也是一样的。
伽马矫正允许我们在线性空间进行光照计算,因为在线性空间计算的值在物理层面更为正确,大多数物理等式也能给出很棒的结果。使用的光照越复杂,使用伽马校正就可以得到更真实的结果。
最后,贴出原文地址供参考:https://learnopengl.com/Advanced-Lighting/Gamma-Correction