Gamma校正

起因

前段时间尝试在SP中实现unity中的效果,碰到一个问题就是我们项目的Unity使用的是gamma空间(因为低端机对于linear空间是不支持的,但这种情况下应该手动gamma校正,不过我们项目没有对颜色做任何处理),而SP只能使用linear,所以为了在SP中实现Unity的「gamma」效果,需要在SP的shader中手动反向gamma校正。

先来尝试一下一般的情况,也就是Unity设置为gamma空间,但通过在shader中手动校正,达到线性空间的效果,目标是使得linear的效果=gamma+手动校正。

原理简介

这方面找到了冯乐乐写的这篇博文我理解的伽马校正(Gamma Correction),非常详细的讲解了原理和方法。如果只是想知道怎么gamma校正,这里做一个能够被快速接受的解释。

首先看一下 真实场景 -> 摄像机捕捉 -> 显示器显示 这个过程中经过的变换

其中,display gamma是显示设备的技术客观存在的,为了校正这个误差,在图像捕捉设备中加了一个encoding gamma,使得end-to-end gamma,也就是两者的乘积,为1,这样真实场景的亮度值就等于显示器的值了。

后来,微软联合爱普生、惠普提供了sRGB标准,推荐显示器中display gamma值为2.2,那么encoding gamma的值为1/2.2。而对于渲染的人来说,并没有图像捕捉设备这一环节,但是显示器的2.2却客观存在,所以我们需要在输出显示颜色之前,使用encoding gamma,才能达到模仿真实环境,人眼看着更舒服的效果。

Unity的颜色空间设置

Unity中可以设置两种颜色模式,在Edit->Project Settings->Player->Color Space,其中gamma代表什么都不干,一般颜色会暗很多。而选择linear的话,Unity会把所有的输入颜色,包括纹理等,默认使用sRGB,先转换到线性空间,也就是pow2.2(sRGB也可以关掉,但是gamma空间下默认都是关,点击开关不会产生影响),在线性空间下做各种运算,然后在输出前pow1.0/2.2,最后交给显示器自动pow2.2,最后看到的就是线性的了。所以实际上,选择linear的话,shader输出的是gamma空间的颜色,是显示器把它变成了线性(有点绕),而选择gamma空间的话,关键不是空间错了,而是它在非线性空间下做了混合,最后显示器转成线性空间就会出问题。也就是说,如果不做混合,这两个选项的结果是一样的。

冯乐乐的文章中给出了gamma颜色模式下,颜色混合产生的问题,左边为gamma,右边为linear。

所以如果不设置成linear,颜色会变得难看不仅是变暗这么简单,有些地方还会出现明显不符合常理的颜色,比如上图交界处居然出现了蓝色。

而遗憾的是,根据Unity官方文档关于线性空间的描述,Android设备需要支持OpenGL ES 3.0和至少Android 4.3的系统,IOS需要Metal graphics API(系统至少为IOS 8),所以除非放弃这一部分用户,否则只能使用gamma,但是为了正常的颜色表现,我们需要在shader中进行手动的gamma校正。

实践

我们来试试看,将空间设置为gamma,然后手动校正,在输出前pow1.0/2.2,由于我忽略了贴图没有转到线性空间的问题,所以得到了这样的结果。


将贴图pow2.2后就使linear的效果=gamma+手动校正了

最后尝试在SP中手动反向校正,也就是使gamma=linear+反向校正,顺利使SP的效果和gamma下的Unity一致。

提高效率

Unity实际上有提供关于颜色空间转换的接口,函数名不带exact的就是近似算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
inline float GammaToLinearSpaceExact (float value)
{
if (value <= 0.04045F)
return value / 12.92F;
else if (value < 1.0F)
return pow((value + 0.055F)/1.055F, 2.4F);
else
return pow(value, 2.2F);
}
inline half3 GammaToLinearSpace (half3 sRGB)
{
// Approximate version from http://chilliant.blogspot.com.au/2012/08/srgb-approximations-for-hlsl.html?m=1
return sRGB * (sRGB * (sRGB * 0.305306011h + 0.682171111h) + 0.012522878h);

// Precise version, useful for debugging.
//return half3(GammaToLinearSpaceExact(sRGB.r), GammaToLinearSpaceExact(sRGB.g), GammaToLinearSpaceExact(sRGB.b));
}
inline float LinearToGammaSpaceExact (float value)
{
if (value <= 0.0F)
return 0.0F;
else if (value <= 0.0031308F)
return 12.92F * value;
else if (value < 1.0F)
return 1.055F * pow(value, 0.4166667F) - 0.055F;
else
return pow(value, 0.45454545F);
}
inline half3 LinearToGammaSpace (half3 linRGB)
{
linRGB = max(linRGB, half3(0.h, 0.h, 0.h));
// An almost-perfect approximation from http://chilliant.blogspot.com.au/2012/08/srgb-approximations-for-hlsl.html?m=1
return max(1.055h * pow(linRGB, 0.416666667h) - 0.055h, 0.h);

// Exact version, useful for debugging.
//return half3(LinearToGammaSpaceExact(linRGB.r), LinearToGammaSpaceExact(linRGB.g), LinearToGammaSpaceExact(linRGB.b))
}