什么是伽马校正

作者:追风剑情 发布于:2017-10-28 12:33 分类:Shader

      要想渲染出更符合真实光照环境的场景就需要使用线性空间。而Unity默认的空间是伽马空间,在伽马空间下进行渲染会导致很多非线性空间下的计算,从而引入了一些误差。而要把伽马空间转换到线性空间,就需要进行伽马校正(Camma Correction)

伽马校正中的伽马一词来源伽马曲线。通常,伽马曲线的表达如下:

1111.png

其中指数部分发音就是伽马。最开始的时候,人们使用伽马曲线来对拍摄的图像进行伽马编码(gamma encoding)。事情的起因可以从真实环境中拍摄一张图片说起。摄像机的原理可以简化为,把进入到镜头内的光线亮度编码成图像(例如一张JEPG)中的像素。如果采集到的亮度是0,像素就是0亮度是1,像素就是1亮度是0.5,像素就是0.5。如果我们只用8位空间来存储像素的每个通道的话,这意味着0~1区间可以对应256种不同的亮度值。但是,后来人们发现,人眼有一个有趣的特性,就是对光的灵敏度在不同亮度上是不一样的。在正常的光照条件下,人眼对较暗区域的变化更加敏感(亮度上的线性变化对人眼感知来说是非均匀的)。伽马校正可以让我们用更多的值来表示暗部,用更少的值来表示亮部,这样存储空间就可以被充分利用起来了,摄影设备如果使用了8位空间来存储照片的话,会使用大约为0.45的编码伽马来对输入的亮度进行编码,得到一张编码后的图像。因此,图像中的0.5像素值对应的亮度其实并不是0.5,而大约为0.22。这是因为:

2222.png(输入0.5,输出0.22,伽马编码0.45)

对拍摄图像使用的伽马编码使得我们可以充分利用图像的存储空间。但当把图片放到显示器显示时,我们应该对图像再进行一次解码操作,使得屏幕输出的亮度和捕捉到的亮度是符合线性的。这时,人们发现了一个奇妙的巧合——CRT显示器本身几乎已经自动做了这个解码操作!这又从何说起呢?在早期,CRT(Cathode Ray Tube,阴极射线管)几乎是唯一的显示设备。这类设备的显示机制是,使用一个电压轰击它屏幕上的一种图层,这个图层就可以发亮,我们就可以看到图像了。但CRT显示器有一个特性,它的输入电压和显示出来的亮度关系不是线性的,也就是说,如果我们把输入电压调高两倍,屏幕亮度并没有提高两倍。我们把显示器的这个伽马曲线称为显示伽马(diplay gamma)。非常巧合的是,CRT的显示伽马值大约就是编码伽马的倒数。CRT显示器的这种特性,正好补偿了图像捕捉设备的伽马曲线,人们想,“天呐,太棒了,我们不需要做任何调整就可以让拍摄的图像在电脑上看起来和原来的一样了!”虽然现在CRT设备很少见了,并且后来出现的显示设备有着不同的伽马响应曲线,但是,人们仍在硬件上做了调整来提供兼容性。

真实场景->摄像机(场景亮度->编码伽马->像素值)->显示器(像素值->显示伽马->显示的亮度)->显示图像

随后,微软联合爱普生、惠普提供了sRGB颜色空间标准,推荐显示器的显示伽马值为2.2,并配合0.45的编码伽马就可以保证最后伽马曲线之间可以相互抵消(2.2×0.45≈1)绝大多数的摄像机、PC和打印机都使用了上述的sRGB标准。

     事实上,由于游戏界长期以来都忽视了伽马校正的问题,造成了我们渲染出来的游戏总是暗沉沉的,总是和真实世界不像。由于编码伽马和显示伽马的存在,我们一不小心就可能在非线性空间下进行计算,或是使得输出的图像是非线性的。

     对于输出来说,如果我们直接输出渲染结果而不进行任何处理,在经过显示器的显示伽马处理后,会导致图像整体偏暗,出现失真的状况。

     实际上,渲染中非线性输入最有可能的来源就是纹理。为了充分利用存储空间,大多数图像文件都进行了提前的校正,即已经使用了一个编码伽马对像素值编码。但这意味着它们是非线性的,如果我们在Shader中直接使用纹理采样值就会造成在非线性空间的计算,使得结果和真实世界的结果不一致。我们在使用多级渐远纹理(mipmaps)时也需要注意。如果纹理存储在非线性空间中,那么在计算多级渐远纹理时就会在非线性空间里计算。由于多级渐远纹理的计算是种线性计算——即采样的过程,需要对某个方形区域内的像素取平均值,这样就会得到错误的结果。正确的做法是,我们要把非线性的纹理转换到线性空间后再计算多级渐远纹理。

     如上所说,伽马的存在使得我们很容易得到非线性空间下的渲染结果。在游戏渲染中,我们应该保证所有的输入都被转换到了线性空间下,并在线性空间下进行各种光照计算,最后在输出前通过一个编码伽马进行伽马校正后再输出到颜色缓冲中。Unity的颜色空间设置就可以满足我们的需求。当我们选择伽马空间时,实际上就是“放任模式”,不会对Shader的输入进行任何处理,即使输入可能是非线性的;也不会对输出像素进行任何处理,这意味着输出的像素会经过显示伽马转换后得到非预期的亮度,通常表现为整个场景会比较昏暗。当选择线性空间时。Unity会把输入纹理设置为sRGB模式,在这种模式下,硬件在对纹理进行采样时会自动将其转换到线性空间中;并且,GPU会在Shader写入颜色缓冲前自动进行伽马校正或是保持线性在后面进行伽马校正,这取决于当前的渲染配置。如果我们开启了HDR的话,渲染就会使用一个浮点精度的缓冲。这些缓冲有足够的精度不需要我们进行任何伽马校正,此时所有的混合和屏幕后处理都是在线性空间下进行的。当渲染完成要写入显示设备的后备缓冲区(back buffer)时,再进行一次最后的伽马校正。如果我们没有使用HDR,那么Unity就会把缓冲设置成sRGB格式,这种格式的缓冲就像一个普通的纹理一样,在写入缓冲前需要进行伽马校正,在读取缓冲时需要再进行一次解码操作。如果此时开启了混合,在每次混合时,硬件会首先把之前颜色缓冲中存储的颜色值转换回线性空间中,然后再与当前的颜色进行混合,完成后再进行伽马校正,最后把校正后的混合结果写入颜色缓冲中。这里需要注意,透明通道是不会参与伽马校正的

     然而,Unity的线性空间并不是所有平台都支持,例如,移动平台就无法使用线性空间。此时,我们就需要自己在Shader中进行伽马校正。对非线性输入纹理的校正代码通常如下:
float3 diffuseCol = pow(tex2D( diffTex, texCoord ), 2.2);
在最后输出前,对输出像素值的校正代码通常如下面这样:
fragColor.rgb = pow(fragColor.rgb, 1.0/2.2);
return fragColor;

      但是,手工对输出像素进行伽马校正会在使用混合时出现问题。这是因为,校正会导致写入颜色缓冲内的颜色是非线性的,这样混合就发生在非线性空间中。一种解决方法是,在中间计算时不要对输出颜色值进行伽马校正,但在最后需要进行一个屏幕后处理操作来对最后的输出进行伽马校正,也就是说我们需要保证伽马校正发生在渲染的最后一步中,但这可能会造成一定的性能损耗。

      那么,什么时候可以舍弃伽马校正呢?如果有一天我们对图像的存储空间能够大大提升,通用的格式不再是8位时,例如是32位时,伽马也许就会消失。因为,我们有足够多的颜色空间可以利用,不需要为了充分利用存储空间进行伽马编码的工作了。

File->Build Settings->Player Settings

111111.png

标签: Shader

Powered by emlog  蜀ICP备18021003号-1   sitemap

川公网安备 51019002001593号