首先,先转载两篇文章,一篇来自KlayGE游戏引擎,链接为:http://www.klayge.org/?p=921 。另外一篇来自Kanglai Qian,链接为:http://qiankanglai.me/2014/12/24/gamma-correction/ 。

Gamma的传说(转载自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值。

输入和输出

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

gamma

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

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

gamma

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

gamma

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

对渲染的意义

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

float4 diff = tex2D(diffuse_texture, uv);

return diff * max(0, dot(light_dir, normal));

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

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

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。(Patrick:51怎么得到的,应该是55吧)

gamma_lines gamma_lines

总结

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


Unreal/Unity中的Gamma校正(转载自Kanglai Qian,链接:http://qiankanglai.me/2014/12/24/gamma-correction/)

之前桌子跟我讲了一个很冷的小知识:对于3D美术来说,做贴图的时候如果想要做50%灰,应该用186/255而不是127/255。这个很有意思,稍微想了下应该是和Gamma校正有关,然后算了下确实是这样的。后来我分别在Unreal里和Unity里试了下,谨以此文记录。

名词解释

具体Gamma Correction(伽马校正/Gamma校正)相关内容可以参看klayge上的gamma的传说一文。如果把颜色亮度理解为能量的话,输入和输出中是非线性的对应关系,对应下图中的红线;但是计算的时候必须使用线性对应才对,对应绿线:

gamma_lines

具体的转换公式就是2.2了~如此就能理解为什么50%灰度是186: pow(0.5, 1.0/2.2)*255,这样才保证转换到线性空间后是0.5。

Unreal

我分别做了127/255和186/255灰度图,导入到材质编辑器作为底色,即下表前两张

gamma_127_sRGB gamma_186_sRGB gamma_127_sRGB

可以看到颜色明显不同(Patrick:sRGB格式的纹理,被认为是pow了1/2.2,也就是gamma空间的数据,所以186的sRGB相当于127的RGB);但是修改颜色空间之后,就基本一致了。Windows下默认的颜色空间是sRGB,也就是需要经过Gamma校正的。

gamma

所以说最开始的说法,在默认情况下是成立的;但是Unreal也支持导入线性空间贴图,所以也能用127/255来表示50%灰。

插一句题外话就是Unreal其实在选颜色的地方也可以设置颜色空间,譬如下图中R通道的0.259其实对应的Gamma空间里的138/255。

gamma

Unity

Unity文档里有一页叫Linear Lighting对此进行了介绍:

Existing (Gamma) Pipeline: 默认设置,完全不考虑颜色空间的转换,输入什么颜色就使用什么颜色,输出的时候也不考虑转换;这个方式相当于错上加错,虽然抵销掉一部分但依然是错的。

Linear Lighting Pipeline: 输入的时候进行sRGB到RGB转换(利用硬件接口),输出的时候再转回sRGB;这个才是正确地做法,但目前只支持Windows & Mac/XBox 360/PlayStation 3,令人忧郁(Patrick:Unity5.6上Android、iOS也都支持了,但是手机不一定因为,因为硬件上只有OpenGL ES3.0才开始支持sRGB,如果在不支持sRGB的手机使用linear color space,应用程序会自动退出的,可以通过API QualitySettings.activeColorSpace 查看当前设备支持哪些color space)

gamma

简单做一个实验:读取127/255贴图亮度、乘以二后输出

gamma_unity_gammaspace gamma_unity_linearspace

右边这张图的亮度相当于pow(pow(0.5,2.2)*2, 1/2.2)*255~

话说这样其实就没法在移动平台上做PBR,除非自己读取贴图的时候手动转一下,输出的时候再转一下~(Patrick:因为光照的计算,系数很重要(diffuse是系数*基本色*nDotl,specular是系数*pow(nDoth,gloss)),在gamma空间,系数和最终结果是非线性的,而linear空间,系数和最终结果是线性变化的。非PBR的系数是可以随意调节的,所以可以通过策划调节来弥补,但是PBR的系数不能能随意调节的,结果,合理的系数得不到合理的效果。基本上Gamma算法是把系数的作用放大了(如果乘以一个系数,相当于乘以系数的2.2次幂),这样会导致光的衰减半径变小,因为迅速衰减了,或者随着intensity,光亮度匀速变大;而linear算法,系数发挥到它应有的作用。)不过因为渲染管线不受控制,除非实现一个gamma correction的后处理,不然不好控制在最后转换,相当于多了个Pass…(Patrick:TODO)

总之从细节上来看Unity还是略输一筹啊~(Patrick:目前Unity已经追上来了)


Unity中的线性空间与Gamma校正

关于线性空间、Gamma校正的概念通过上面转载的两篇文章,已经很清晰了,虽然有些本人有点想法的地方(已用红色字体在原文中加了注释),总体上还是很清晰的。然而钱大写这个文章的时候Unity还不支持移动平台linear,所以下面我分析下目前Unity中的线性空间和Gamma校正。

目前还是很多游戏是用的Gamma,主要原因是目前市面上还是有一批不支持linear的设备(iphone5及以下,android4.3以下都不支持OpenGL ES3.0,而3.0才开始支持texture和RBO的sRGB),而效果上调调系数也都没问题,但是未来这批手机被放弃了之后,linear空间因为更准确一些,所以移动平台还是有几率会使用linear的

当color space为gamma的时候,纹理设置sRGB是没有意义的,因为sRGB是设置给linear color space看的,linear color space中,sRGB的纹理会在shader运算前通过power 2.2切换到线性空间,然后再计算完毕后,通过power 1/2.2再切换回gamma空间。非sRGB纹理则会直接在shader中进行运算,比如normal map、mask、GUI。其中legacy GUI比较特殊,虽然这个纹理是gamma space的,理论上应该标记为sRGB,但是它在shader中的计算不需要linear,所以它不会在shader运算前,从gamma转linear,在运算结束后,从linear转gamma,而是会直接去运算,假装它是linear space的。/p>

当color space为gamma的时候,纹理被采样后直接去进行运算,虽然采样的值是gamma空间的,但是没办法。

将color space从gamma切换到linear的时候,很多参数(材质、光照)需要调整才能得到原来的效果,但是应该是值得的。

HDR

如果是HDR的话,shader计算完毕后,先暂时不用power 1/2.2切换回gamma空间,而是停在线性空间一会,等会再切换回gamma空间。

Lightmap

Lightmap是按照gamma空间存放。所以当linear color space的时候,Lightmap会从gamma空间转到线性空间再去计算,而Gamma color space,就不转了。所以如果切换color space,需要rebake lightmap。如果从外界导入lightmap,需要选择纹理类型:lightmap。确保lightmap按照gamma空间存放。

虽然并非全部原创,但还是希望转载请注明出处:电子设备中的画家|王烁 于 2017 年 7 月 19 日发表,原文链接(http://geekfaner.com/unity/blog1_gamma.html)