1 - Gamma校正

我们渲染好的画面最终要在显示器上显示,而有的显示器显示画面过亮,有的显示器显示画面过暗,这时候就需要Gamma校正技术,让它们显示的画面亮度尽可能跟原渲染出的画面亮度相同。

Gamma校正原理

人眼与CRT显示器

对于以前的CRT显示器,输入一定电压,显示出来的亮度总是输入电压的2.2次幂,这是显示器Gamma。

对于以前的人眼,感知亮度和实际亮度通常是2次幂的关系:

可以发现,在较亮的区域,人眼感知亮度和实际上真实的物理亮度差不多;但在较暗区域,区别比较大。但为什么是2次幂的关系,例如人眼感知亮度从0.4~0.8变亮了一倍,而在实际亮度中0.4跟人眼感知亮度0.8差不多,因此是2次幂关系。

因此,人眼和CRT显示器是相近的,它能让我们看到的灰阶亮度更全面。

线性空间

但一个更灵魂的问题来了,艺术家在渲染颜色时,还是盯着显示器来渲染的,这导致他们渲染的颜色实际上是非线性的(线性就是Gamma值为1):

在此图中,先看中间和下边的线。

他们在线性空间里调参(计算机永远是对的),但由于显示器里显示的是非线性结果(显示器Gamma误差),导致他们觉得他们看见的颜色是错的,于是他们开始调整,直至看见的颜色是对的,但实际上线性空间中的颜色已经错了。例如,要调一个暗红色\((0.5,0.0,0.0)\),实际上从显示器看到的是\((0.5,0.0,0.0)^{2.2}=(0.218,0.0,0.0)\),导致他们觉得暗了,于是继续调,当他们调到“理想”的暗红色后,实际上的颜色可能是\((0.73,0.0,0.0)\)

同时从上图可以发现,显示器显示出来的颜色和线性空间中的颜色的最大/最小亮度是相同的,只是中间亮度部分会被压暗。例如下图中,该亮的地方亮,该暗的地方暗,经过Gamma校正后只有介于它们之间的暗色被调亮了点:

Gamma校正

再看那个曲线图,最上边的线是显示器Gamma曲线的翻转曲线。Gamma校正的思路就是 在最终的颜色输出上应用监视器Gamma的倒数(即该曲线)。这样子显示器得到的颜色就是正确的了。

还是以那个暗红色为例,在将其发送给显示器前,先对它进行Gamma校正,\((0.5,0.0,0.0)^{1/2.2}=(0.73,0.0,0.0)\),然后将Gamma校正后的颜色让显示器进行显示,这时候显示器显示出的颜色就是\((0.73,0.0,0.0)^{2.2}=(0.5,0.0,0.0)\)了,艺术家和电脑一眼叮真,不需要继续调,显示的结果终于是计算机中正确的线性结果了。

随着显示器科技的发展,Gamma2.2现在已经是一个标准了,基于Gamma2.2的颜色空间叫做 sRGB颜色空间。为了更加适配各种各样的显示器,Gamma值可以随便调(例如游戏里的Gamma相关设置)。

OpenGL中的Gamma校正

在OpenGL中使用Gamma校正主要有两种方法:使用OpenGL内建的sRGB帧缓冲;在着色器中自行编写Gamma校正代码

使用内建sRGB帧缓冲

只需在 渲染的最后一步启用Gamma校正即可 ,例如如果使用多个帧缓冲,你可能打算让两个帧缓冲之间传递的中间结果仍然保持线性空间颜色,只是给发送给监视器的最后的那个帧缓冲应用gamma校正:

// 启用Gamma校正
glEnable(GL_FRAMEBUFFER_SRGB);

结果如下:

未开启Gamma校正
开启Gamma校正

在着色器中实现Gamma校正

在每个片段着色器的末尾进行Gamma校正有点麻烦,且容易出现bug。加入帧缓冲后,只需在帧缓冲的最后进行Gamma校正:

// 进行Gamma校正
float gamma = 2.2;
FragColor.rgb = pow(FragColor.rgb, vec3(1.0 / gamma));

结果如下:

sRGB纹理

现在,绝大部分显示器都是在sRGB空间显示颜色,而不是在Linear空间。sRGB空间相当于最上边Gamma Correction 1/2.2这条曲线,不需经过手动Gamma校正,直接让显示器显示正确的Linear空间颜色。

通常艺术家们在sRGB空间中创建作品,我们也在sRGB空间中观看渲染结果(已经经过一次Gamma校正了:sRGB –(Gamma)-> Linear -> 显示器),因此不需要额外的操作我们就能看到正确的颜色。但在LearnOpenGL这个程序中,我们对sRGB纹理显式地又进行了一次Gamma校正(sRGB –(Gamma)-> Linear –(Our Gamma)-> 显示器),导致最终渲染出的图片过亮,如上边的结果。

为了解决这个问题,可以让所有艺术家到线性空间去创作(这显然不可能);也能进行重校,把sRGB纹理在进行任何颜色值计算前将其变回线性空间。这里采用第二种方法。

sRGB纹理重校

漫反射纹理通常是艺术家创作的,所以这里以它为sRGB纹理重校的例子。我们得为每个纹理进行如下操作,将sRGB纹理在进行任何颜色值计算前将其变回Linear空间:

float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));

这样做有点麻烦,但OpenGL为我们提供了GL_SRGBGL_SRGB_ALPHA纹理格式来帮助我们解决问题,在加载并创建纹理的时候就能解决此问题了。

我们将天空盒和模型的diffuse纹理指定为GL_SRGB(_ALPHA)来让OpenGL自动将颜色校正到Linear空间中,类似语句如下:

glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

经过重校后的渲染结果如下:

sRGB和Linear纹理的选择

有关Linear纹理和sRGB纹理的选择(摘抄参考资料):

所有需要人眼参与被创作出来的纹理,都应是sRGB(如美术画出来的图)。所有通过计算机计算出来的纹理(如噪声,Mask,LightMap)都应是Linear。

这很好解释,人眼看东西才需要考虑显示特性和校正的问题。而对计算机来说不需要,在计算机看来只是普通数据,自然直接选择Linear是最好的。

总而言之,gamma校正使你可以在线性空间中进行操作。因为线性空间更符合物理世界,大多数物理公式现在都可以获得较好效果,比如真实的光的衰减。你的光照越真实,使用gamma校正获得漂亮的效果就越容易。这也正是为什么当引进gamma校正时,建议只去调整光照参数的原因。

参考资料

  • Gamma校正 - LearnOpenGL CN (learnopengl-cn.github.io)
  • Gamma、Linear、sRGB 和Unity Color Space,你真懂了吗? - 知乎 (zhihu.com)