转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=921

Gamma校正对于图形和图像来说是个常提的概念,但对于gamma的缘由和使用方法,却存在着很多传说。本文将尽可能解析gamma校正来源,破解各种迷思。

Gamma校正从何而来

有一种常见的说法,gamma来源于眼睛对光感受。我也曾经错误地采用了这种说法。在wikipedia上查到了gamma的真正来源:

开发gamma编码是用来抵消阴极射线管(CRT)显示器的输入和输出特性。电子枪的电流,也就是光的亮度,与输入的正极电压的变化是非线性的。通过gamma压缩来改变输入信号抵消了这个非线性,因此输出图像就能有预期的亮度。

所以,gamma校正和人眼特性无关,仅仅和CRT有关。更新的显示方法,比如LCD和等离子之类,为了保证兼容,也都选择了和当年CRT一样的非线性特性。(其实和系统有关,Mac OS X 10.6就用的1.8,其他系统,包括电视,都用的2.2)

Gamma计算很简单,只是个power而已,也就是:

其中的γ就是用来校正的gamma值。

输入和输出

现在让我们来看看一个输入输出的例子。假设相机是线性的,显示器也是线性的,那么输入和输出的关系就是:

Gamma 1
也就是通过相机拍照后,在显示器上看到的和真实场景的色彩一样。

可惜,现实是残酷的,显示器的gamma为2.2,所以如果相机仍然是线性的,那么结果就会变成:

Gamma 2

这样在显示器上看到的就会有明显的色彩失真。解决方法是把相机的gamma设成1/2.2,这样两次调整之后又能得到真实场景的色彩了:

Gamma 3其实从这个过程也可以看出,gamma校正是为了在输入和输出的环节中保证能和真实场景一致,而眼睛不在这个环节中,所以和眼睛对亮度的感受没有直接关系了。

对渲染的意义

前面讲的输入是对相机拍的照片而言。而对渲染来说,情况又如何呢?渲染中用到的光照都是在线性空间的。因为在设计光照的时候都是认为1的亮度是0.5的2倍。光照如此,texture又如何呢?渲染中用到的 texture一般有两个来源,一个是照片,一个是artist手工画的。前文提到了,照片是gamma = 1/2.2的。一般图象处理软件也都是在gamma空间工作的,所以artist画的图一般也可以认为是gamma = 1/2.2的。所以,我们在pixel shader常可以见到这样的代码:

1
2
float4 diff = tex2D(diffuse_texture, uv);
return diff * max(0, dot(light_dir, normal));

这样的代码对吗?不对也对

说其不对,是因为这里没显式地做gamma校正。做校正的话应该是这样的:

1
2
float4 diff = pow(tex2D(diffuse_texture, uv), 2.2f);
return pow(diff * max(0, dot(light_dir, normal)), 1 / 2.2f);

也就是说,gamma校正的过程就是把输入的texture都转换到线性空间,并把输出的调整到gamma = 1/2.2的空间。

说其,是因为如果diffuse texture如果是sRGB格式的,那么再读取的时候硬件会把它自动转到线性空间。如果render target的texture也是sRGB格式的,在输出的时候硬件也会把它自动转到线性空间。所以,如果输入和输出纹理都是sRGB,那么原先那段shader就是正确的。对于不支持sRGB的老硬件,就必须自己做pow了。

除了渲染,另一个需要注意gamma的地方就是mipmap。如果原texture是gamma =1/2.2的,那么在建立mipmap chain的时候,每一层都必须和渲染一样,先转到线性空间,计算之后再转到gamma = 1/2.2的。否则,255和0混合得到的是51,而不是128。

总结

总之,计算都要发生在线性空间,所以输入和输出需要进行gamma校正。最佳选择是采用sRGB格式,这样pow是硬件内自动实现,速度更快,代码也简单。鉴于目前很多texture的数据是gamma = 1/2.2的,而纹理格式却被错误地标记成没有sRGB的,所以需要修改它们的格式标记,并重新建立mipmap。