Gamma Correction & sRGB texture

236 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

显示器gamma值的由来

gamma其实是阴极管射线显示器CRT的特性,亮度并不会随着电压线性增加,而是按幂函数变化,其幂的值就是gamma。 gamma 上图右边的曲线是CRT的gamma曲线,通常gamma值为2.2,这样当电压为50%时,亮度如果是线性的那么应该也是50%的亮度,由于gamma的存在,亮度为50%^2.2 = 21.8%,亮度要低很多。

gamma encode

为了解决CRT的gamma问题,人们并没有去调节CRT本身,也许太难搞了,或者问题发现时已经有太多的CRT。总之结果就是反方向搞了图形采集设备,如相机等,本来相机采集到的亮度是线性的,但存储的时候采用了gamma编码,即采用CRT gamma值的倒数作为幂的幂函数。如上图的左边的曲线。这个操作被称作gamma校正。因为通过gamma编码的亮度值,通过CRT显示时gamma效果被抵消了,最终得到的是线性关系。有一种说法是gamma的存在是为了迎合人眼对低亮度更敏感,好吧,人眼确实对低亮度更敏感,但是这个和gamma没关系,人们发现了CRT的gamma特性后想办法使用gamma encode校正了最终的效果,如果为了迎合人眼就不需要校正了。另外CRT已经不主流了,但是gamma还是被保留了下来,可能是为了兼容各类输入设备吧。

图形渲染中的gamma问题

我们在图形渲染中使用的颜色是按照线性空间考虑的,比如我们可能希望亮度0.8是亮度0.4的两倍,但是由于显示器最终显示时是按照gamma曲线去显示,所以实际上亮度数值翻倍并不能让最终显示器显示的亮度翻倍。如果需要线性变化,那么我们需要进行gamma correction。

gamma correction

在图形渲染中进行gamma correction和输入设备进行gamma encode的原理一样,采用gamma值的倒数的幂函数处理最终的颜色。比如在shader中这样做:

void main()
{
    // do super fancy lighting in linear space
    [...]
    // apply gamma correction
    float gamma = 2.2;
    FragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}

对于OpenGL,也可以直接使用 glEnable(GL_FRAMEBUFFER_SRGB); 让frame buffer中按sRGB保存颜色。但是需要注意的是,gamma校正只能最终做一次,比如你将当前场景渲染到一个FBO中,然后再使用该FBO进行后期处理,那么这些FBO就不能进行gamme校正,而是让他保留使用线性空间。那么自然对于这些FBO不能直接开启GL_FRAMEBUFFER_SRGB。

sRGB texture

上面提到sRGB,即standard Red Green Blue色彩标准。对于采用sRGB标准的设备,会在存储图片时进行gamma encode,这样我们得到的图片里面的颜色是位于gamma空间,而不再是在线性空间了。比如我们渲染使用的贴图,直接读入显存中,就是sRGB编码的。如果我们不在程序中做gamma校正,那么没问题,这些贴图的gamma encode会和显示器的gamma曲线抵消掉,如果你仅仅是显示图片,那么是正确的。但是我们做gamma校正是因为我们要在线性空间计算颜色,那么texture作为输入的颜色源,确是gamma空间的,这就不对了。因此我们做gamma校正时,需要处理读入的贴图:

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

当然仅当贴图中存储的是颜色时才有必要处理,如果存储的是mask,法线之类就不需要处理了。 对于OpenGL,可以在创建texture时指定其是sRGB,这样就不需要在shader中转换了。

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

效果对比

gamma correction 左图是没有进行gamme correction的场景,右图是gamma correction后的,右图感觉是不是更自然些?当然也偏亮些。

正确的Linear/SRGB转换公式

其实上面使用的pow(2.2)公式是一个近似公式,正确的线性/SRGB转换公式要更复杂一些。例如:

float Convert_sRGB_FromLinear (float theLinearValue) {
  return theLinearValue <= 0.0031308f
       ? theLinearValue * 12.92f
       : powf (theLinearValue, 1.0f/2.4f) * 1.055f - 0.055f;
}
float Convert_sRGB_ToLinear (float thesRGBValue) {
  return thesRGBValue <= 0.04045f
       ? thesRGBValue / 12.92f
       : powf ((thesRGBValue + 0.055f) / 1.055f, 2.4f);
}

当然不怎么要求正确性的时候简单的pow(2.2)也就够了。甚至有些时候为了优化,直接使用gamma 2.0近似,这样就可以使用 x*x, sqrt()来转换,比使用pow更快。

参考资料

Gamma-Correction What is the correct gamma correction function? sRGB color space in OpenGL