Gamma Correction 灰阶校正
Advanced-Lighting/Gamma-Correction
一旦我们计算出场景的最终像素颜色,我们将不得不在监视器上显示它们。在过去的数字成像时代,大多数显示器都是阴极射线管(CRT)显示器。这些监视器有一种物理特性,即两倍的输入电压不会导致两倍的亮度。将输入电压加倍后,显示器的伽玛亮度大约等于2.2的指数关系。这碰巧(巧合地)也与人类测量亮度的方式紧密匹配,因为亮度也以类似(反向)的权力关系显示。为了更好地理解这一切意味着什么,请看下面的图片:
在人眼看来,上面这条线似乎是正确的亮度范围,将亮度加倍(例如从0.1到0.2)确实看起来是亮度的两倍,而且有很好的一致性差异。然而,当我们讨论光的物理亮度时,例如离开光源的光子数量,底部刻度实际上显示了正确的亮度。在最下面的图中,将亮度增加一倍,恢复到正确的物理亮度,但由于我们的眼睛对亮度的感知不同(更容易受到深色变化的影响),它看起来很奇怪。
因为人类的眼睛更喜欢看到最上层的亮度颜色,所以显示器(直到今天)使用权力关系来显示输出颜色,这样原始的物理亮度颜色就映射到最上层的非线性亮度颜色。
这种非线性映射的监视器输出亮度更令人愉快的结果对于我们的眼睛,但当谈到渲染图形有一个问题:所有的颜色和亮度选项配置在我们的应用程序是基于我们认为从监视,因此所有的选项实际上是非线性亮度/颜色选项。请看下面的图表:
虚线表示线性空间中的颜色/光值,实线表示显示器显示的颜色空间。如果我们在线性空间中使一种颜色加倍,其结果确实是该值的加倍。例如,取一个光的颜色向量(0.5,0.0,0.0),它表示一个半深红色的光。如果我们将线性空间中的光加倍,它就会变成(1.0,0.0,0.0),正如你在图表中看到的那样。但是,原始颜色在监视器上显示为(0.218,0.0,0.0),正如您从图中看到的那样。问题就从这里开始出现了:一旦我们在线性空间中将暗红色的光加倍,它在显示器上的亮度实际上会增加4.5倍以上!
在这一章之前,我们假设我们是在线性空间中工作,但我们实际上是在显示器的输出空间中工作,所以我们配置的所有颜色和照明变量在物理上都不是正确的,只是看起来(有点)对我们的显示器。由于这个原因,我们(和艺术家)通常设置的照明值比它们应该的亮度要高(因为显示器使它们变暗),这导致了大多数线性空间计算错误。注意,显示器(CRT)和线性图形都开始和结束在同一位置;它是中间值被显示变暗。
因为颜色是根据显示器的输出来配置的,所以在线性空间中所有的中间(照明)计算都是不正确的。这变得更加明显,因为更先进的照明算法是到位的,正如你可以在下图看到:
你可以看到,通过gamma校正,(更新的)颜色值一起工作得更好,较暗的区域显示更多的细节。总的来说,一个更好的图像质量与一些小的修改。
没有正确的校正这个监视器伽马,照明看起来是错误的,艺术家将有一个困难的时间得到真实和好看的结果。解决办法是应用伽马校正。
Gamma correction
伽马校正的想法是在显示到监视器之前,将监视器伽马的倒数应用到最终输出的颜色。回顾本章早些时候的伽马曲线图,我们看到了另一条虚线,它是监视器的伽马曲线的逆。我们用这个逆伽马曲线乘以每一个线性输出颜色(使它们更亮),一旦这些颜色在监视器上显示出来,监视器的伽马曲线就被应用,结果颜色就变成线性的了。我们有效地使中间的颜色变亮,这样当显示器使它们变暗时,就能平衡所有的颜色。
我们再举一个例子。再次使用深红色在显示这个颜色到监视器之前,我们首先应用伽马校正曲线的颜色值。显示器显示的线性颜色大致被缩放到的幂次,因此反过来需要将颜色缩放到的幂次。伽马校正那深红色的颜色从而变得。然后将校正后的颜色输入显示器,结果显示为。
默认gamma值为2.2,粗略估计了大多数显示器的平均gamma值。这个伽马值为2.2的颜色空间被称为sRGB颜色空间(不是100%精确的,但是很接近)。每个监视器都有自己的伽马曲线,但伽马值2.2在大多数监视器上给出了良好的结果。由于这个原因,游戏经常允许玩家改变游戏的伽马设置,因为它会随着显示器的变化而变化。
有两种方法应用伽马校正到你的场景:
- 通过使用OpenGL内置的sRGB framebuffer支持。
- 通过在碎片着色器(s)中自己做伽马校正。
第一个选项可能是最简单的,但也给您较少的控制。通过启用GL_FRAMEBUFFER_SRGB,你告诉OpenGL,每个后续的绘图命令应该首先伽玛正确的颜色(从sRGB颜色空间),然后将它们存储在颜色缓冲区(s)。sRGB是一个颜色空间,大致对应于2.2的伽马值和大多数设备的标准。在启用GL_FRAMEBUFFER_SRGB之后,OpenGL自动在每个片段着色器运行到所有后续的framebuffer之后执行gamma校正,包括默认的framebuffer。
启用GL_FRAMEBUFFER_SRGB就像调用glEnable一样简单:
glEnable(GL_FRAMEBUFFER_SRGB);
从现在起,你的渲染图像将伽玛纠正,因为这是由硬件做,它是完全免费的。在使用这种方法(和另一种方法)时,你应该记住的是伽马校正(也)将颜色从线性空间转换为非线性空间,所以你只在最后一步和最后一步进行伽马校正是非常重要的。如果在最终输出之前对颜色进行gamma-correct,那么对这些颜色的所有后续操作将对不正确的值进行操作。例如,如果您使用多个framebuffer,您可能希望传递到framebuffer之间的中间结果保持线性空间,并且只有最后一个framebuffer在发送到监视器之前应用gamma校正。
第二种方法需要更多的工作,但也给了我们对伽马操作的完全控制。我们应用gamma校正在每个相关的片段着色运行的结束,所以最终的颜色gamma校正被发送到监视器之前:
void main()
{// do super fancy lighting in linear space[...]// apply gamma correctionfloat gamma = 2.2;FragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}
最后一行代码有效地将fragColor的每个单独的颜色组件提升到1.0/gamma,纠正了这个fragment shader运行的输出颜色。
这个方法的一个问题是,为了保持一致,你必须对每个碎片着色器应用gamma校正,这有助于最终的输出。如果你有多个对象的一打片段着色器,你必须添加gamma校正代码到每一个着色器。一个简单的解决方案是在渲染循环中引入一个后期处理阶段,并在后期处理的四轴上应用gamma校正,这是你只需要做一次的最后一步。
那条线代表了伽马校正的技术实现。不是所有太令人印象深刻,但有一些额外的事情,你必须考虑当做伽马校正。
sRGB textures
因为显示器显示的颜色与伽玛应用,无论何时你画,编辑,或在你的计算机上画一幅画,你是选择的颜色基于你所看到的显示器。这有效地意味着你创建或编辑的所有图片不在线性空间中,但在sRGB空间中,例如,在你的屏幕上根据感知亮度加倍暗红色,并不等于加倍红色成分。
因此,当纹理艺术家通过眼睛创建艺术时,所有纹理的值都在sRGB空间中,所以如果我们在渲染应用程序中使用这些纹理,我们必须考虑到这一点。在我们知道伽马校正之前,这不是一个真正的问题,因为纹理看起来很好,在sRGB空间,这是相同的空间,我们工作;纹理完全按照原样显示,这很好。然而,现在我们在线性空间中显示所有的东西,纹理颜色将会如下图所示:
纹理图像太亮了,这是因为它进行了两次伽马校正!想想看,当我们创建一个基于我们在监视器上看到的图像时,我们有效地gamma校正了图像的颜色值,使它在监视器上看起来正确。因为我们在渲染器中再次进行了gamma校正,图像最终变得太亮了。
为了解决这个问题,我们必须确保纹理艺术家在线性空间工作。然而,因为它更容易在sRGB空间工作,而且大多数工具甚至不适当地支持线性纹理,这可能不是首选的解决方案。
另一个解决方案是重新纠正或转换这些sRGB纹理到线性空间之前做任何计算他们的颜色值。我们可以这样做:
float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));
在sRGB空间中为每个纹理做这个是相当麻烦的。幸运的是,OpenGL通过提供GL_SRGB和GL_SRGB_ALPHA内部纹理格式为我们提供了另一个解决方案。
如果我们用这两种sRGB纹理格式在OpenGL中创建纹理,一旦我们使用它们,OpenGL会自动将颜色修正为线性空间,允许我们在线性空间中正常工作。我们可以指定一个纹理作为一个sRGB纹理如下:
glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data);
如果你还想在你的纹理中包含alpha组件,你必须指定纹理的内部格式为GL_SRGB_ALPHA。
当你在sRGB空间中指定纹理时,你应该小心,因为并不是所有的纹理都在sRGB空间中。用于着色对象的纹理(如漫反射纹理)几乎总是在sRGB空间中。用于检索照明参数的纹理(如镜面贴图和法线贴图)几乎总是在线性空间中,所以如果你要配置这些sRGB纹理,照明看起来会很奇怪。要注意你指定为sRGB的纹理。
使用我们的漫反射纹理指定为sRGB纹理,你会再次得到你期望的视觉输出,但这一次所有的伽马校正只有一次。
Attenuation
另一个与伽马校正不同的是光照衰减。在真实的物理世界中,光线衰减与光源距离的平方成反比。在普通英语中,它的意思是光强度随着到光源的距离的平方而减小,如下图所示:
float attenuation = 1.0 / (distance * distance);
然而,当使用这个方程时,衰减效果通常太强烈,给光一个小半径看起来不太合适。由于这个原因,我们使用了其他的衰减函数(就像我们在基本照明章节中讨论的那样)来给予更多的控制,或者使用线性等效:
float attenuation = 1.0 / distance;
与没有伽马校正的二次变量相比,线性等效给出了更可信的结果,但当我们使伽马校正时,线性衰减看起来太弱,而物理上正确的二次衰减突然给出了更好的结果。下图显示了不同之处:
造成这种差异的原因是光线衰减函数会改变亮度,因为我们没有在线性空间中可视化我们的场景,所以我们选择了在显示器上看起来最好的衰减函数,但在物理上是不正确的。想想平方衰减函数:如果我们使用这个函数而不进行伽马校正,当在显示器上显示时,衰减函数有效地变成:(1.0/distance2)2.2(1.0/distance2)2.2。这造成了比我们最初预期的更大的衰减。这也解释了为什么线性等价更有意义
我们在基本照明章节中讨论的更高级的衰减函数在gamma校正的场景中仍然有它的位置,因为它在精确的衰减上给予了更多的控制(但是当然在一个gamma校正的场景中需要不同的参数)。
你可以在这里here. 找到这个简单演示场景的源代码。通过按下空格键,我们在gamma校正和未校正的场景之间进行切换,这两个场景都使用了相应的纹理和衰减。这不是最令人印象深刻的演示,但它确实展示了如何实际应用所有技术。
总而言之,伽马校正允许我们在线性空间中做所有的着色/照明计算。因为线性空间在物理世界中是有意义的,现在大多数物理方程实际上给出了很好的结果(比如真实的光衰减)。你的光线变得越高级,就越容易得到好看的(和真实的)伽马校正结果。这也是为什么我们建议你只有在gamma校正到位后才真正调整你的照明参数。
Additional resources
- What every coder should know about gamma: a well written in-depth article by John Novak about gamma correction.
- www.cambridgeincolour.com: more about gamma and gamma correction.
- blog.wolfire.com: blog post by David Rosen about the benefit of gamma correction in graphics rendering.
- renderwonk.com: some extra practical considerations.