Gamma校正

概括理解Gamma校正

人眼感受色彩变化是非线性的,对暗的颜色更有辨识度,一旦亮起来感受就不那么明显了;而我们处理光照计算是在线性空间中进行的,这样才能计算得准确,这便有了Gamma校正,帮助我们计算出“真正”的颜色。

现在大多数图片和显示器都遵循 sRGB 色彩空间,而 sRGB 本身就内置了Gamma校正规则(默认 γ≈2.2),即图片保存时已按此规则编码,显示器显示时也会按此规则转换光强。

我们在处理图片颜色的时候,需要进行“解码”,“计算”,“编码”三个步骤。解码是把图片中已Gamma编码的非线性 RGB 值(sRGB 格式)转换为线性 RGB 值,方便我们计算光照(计算得准确),而编码是把计算完成的线性 RGB 值,按 sRGB 的Gamma规则转换为非线性 RGB 值,让人眼感官看起来更舒适自然。

如果跳过解码,光照计算会用错误的非线性值,导致暗部过暗、亮部过曝;如果跳过编码,线性值会被显示器二次Gamma处理,画面整体过亮失真。

总的来说,Gamma校正包含“解码”和“编码”这两部分,“解码”是为了将颜色空间从非线性转换到线性空间去“计算”,当然如果颜色空间本身就是线性的就不用解码了,这牵扯到双重校正的问题,顾名思义,就是进行了两次gamma校正(本质是进行了两次编码),下面的内容会分析并解决此问题。

应用与效果

(1) 直接使用OpenGL内建的sRGB帧缓冲。请看下面对比图,右图开启Gamma校正。

1
glEnable(GL_FRAMEBUFFER_SRGB);

GL_FRAMEBUFFER_SRGB

需要注意的是,多渲染目标(多帧缓冲)之间传递时,必须保持线性颜色,不能提前做伽马校正。只有最后一步将颜色输出到屏幕(绑定显示器对应的帧缓冲)时,才需要开启GL_FRAMEBUFFER_SRGB;如果在中间帧缓冲传递时就开启这个选项,会导致中间结果变成非线性颜色,后续再基于这些颜色计算(比如模糊、光效),结果就会出错。

(2) 自己在片段着色器中进行gamma校正。

1
2
3
4
5
6
7
8
9
void main()
{
// 在线性空间做炫酷的光照效果
[...]
// 应用Gamma校正
float gamma = 2.2;
// gamma encode
fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}

片段着色器Gamma校正

sRGB纹理和解码

我们上面Gamma校正之后的图,有没有感觉过于亮了?因为我们现在用的纹理图片大多是 sRGB 格式,它们在保存时就已经经过一次Gamma编码,而我们又用OpenGL进行了一次Gamma编码,导致画面过曝。也就是说,我们的光照计算实际是在非线性颜色空间中,所以我们需要先解码图片,在线性颜色空间中计算光照后再编码。

解码也很简单,只需要对光照颜色进行“”Gamma校正就能转换其到线性颜色空间了:

1
2
3
float gamma = 2.2;
// gamma decode
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));

解码之后再编码便真正只进行一次编码,效果如下:

use gamma to decode

除了手动计算,OpenGL还提供了GL_SRGBGL_SRGB_ALPHA内部纹理格式让我们加载纹理就自动进行解码。

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

(需要ALPHA就指定GL_SRGB_ALPHA)

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

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

光照衰减

现实中,光照衰减和光源距离平方成反比:

1
float attenuation = 1.0 / (distance * distance);

但在显示器上效果最好的衰减方程,并不是符合物理的。

不进行gamma校正(不解码),显示在监视器上的衰减方程实际上将变成:

(1.0/distance2)2.2 (1.0 / distance ^ 2) ^ {2.2}

这时衰减会过于强烈,反而直接使用双曲线方程效果更好:

(1.0/distance)2.2 (1.0 / distance) ^ {2.2}

下面是gamma校正和衰减方程的效果组合,可以看出,不开启gamma校正时,衰减使用双曲线方程更好,开启时选用二次方程效果更好。

在Demo OpenGL27-GammaCorrection 中,我们用键盘上的G和F分别控制这两个效果(有修改,效果可能不同)。

pic1 pic2
pic3 pic4

上面的效果,图片的解码是在片段着色器中进行的,后来我修改使用OpenGL内部纹理格式加载纹理,颜色效果和上面的有些区别。

引用小结

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