Unity Shader 伽马空间与线性空间

前言

首先这篇文章是Unity工程师所写的,本人只是为了作为学习和方便查找进行了搬运。Unity一直是我使用的最多的工具,而伽马空间和线性空间的使用也一直是困惑我的地方,因此在博客中进行了搬运。向提供这么好的工具的Unity工程师致敬。原文链接这里

什么是伽马空间

通常物体呈现出来的颜色和我们用眼睛看到屏幕上的最终颜色是不一样的。

下图01中有三张图片。左边偏亮的才是真实的颜色。这里的真实颜色指的是物体的颜色波长是多长,在光谱中对应的信息是什么样子的,眼睛对这个颜色的反馈是什么样子的。实际上我们在屏幕上看到的都是右边的颜色,显示器在显示的时候做了伽马变换,我们把这个指数叫做伽马。
图1
图 01

中间的图,当伽马值是1的时候,和原来的颜色是一样的,伽马从1到2.2的时候,这个颜色是逐渐加深的,但是纯黑(0,0,0)和纯白(1,1,1)是不会变的,只是让中间颜色的亮度有变化。因为我们从来没有见过真实的颜色是什么样的,都是通过屏幕上看到的,所以我们理所应当地认为右边是真实的颜色。

再看一个更形象的情况,图02中有二张图片。左边是我们眼睛看到的或者是照相机看到的,右边是我们实际在显示器上看到它是什么样子的。右边的图片其实是原始图,左边图片经过了伽马校正,我们在显示器里看到的东西比它本身的亮度暗一点,显示器内部有伽玛变换。
这里写图片描述
图 02

那么我们看现在这个图变暗了,如果想让它正常一点,为了解决这个问题,我们会怎么办?我们可以反向做一个伽马校正,把暗的部分校正到变亮的部分,它们的值是倒数的关系,完全相同的还原回这个亮度本身的值。

这时又出现了另一个问题。例如:当我们输出到屏幕的时候已经变暗了,这时再做校正,是找不到任何一种方法能在这中间插一个步骤来校正它。我们最后想了一个办法,如果在输出的时候没法把它校正,能不能把之前的颜色提前校正回来?
这里写图片描述
图 03

如图03所示,这是一个对比图,首先它是完全没有校正过的,最左侧是它本身的颜色,中间这个是可以拿照相机或者拿摄像机,或者你自己眼睛看到真实的亮度,照相机把真实亮度存下来,不做任何的处理,这是中间图的样子,但是经过屏幕显示的时候变暗了。
这里写图片描述
图 04

我们应该怎么做才能让它不变暗呢? 把最左边原始的亮度存储的时候,我们提前做一些反向的伽马校正,把伽马校正这个阶段提前到图片存储的时候,这个时候显示器又要让这个画面变暗,一亮一暗抵消了之后就是原来的亮度了,看到所有的图片也就正常了,不会普遍的偏暗,我们看着比较舒服的样子。
这里写图片描述
图 05

这样就解决了伽玛变换产生的问题。示例中伽马值是2.2,每个显示器的伽马值都是可能不一样的,图片到底应该用哪个伽马值做校正?数据预先校正完之后保存在图片中了,无法对所有的显示器分别做校正。所以人们发明了Color Profile,可以称为颜色空间或者色彩空间。
这里写图片描述
图 06

颜色空间的概念是很大的,在这里我们狭义地理解它为:它解释了数值到波长的对应关系。当以RGB保存图片的时候,图06中的的颜色空间说明了RGB数值如何变换的颜色波长;反过来,显示器也有Color Profile,它说明了一个特定波长的颜色要怎么样用它屏幕的RGB亮度表现出来。当显示一张图片时,根据图片的Color Profile把它的颜色变换到波长,显示器显示该波长时,也知道应该如何设置它的RGB亮度来表示这个波长的颜色,这样一张图片可以在不同的显示器上看起来都相同。

Color Profile一般也会定义伽马值,来定义一个伽马变换,这个伽马值是多少,也是包含在这个Color Profile里边的,通过伽马值就知道它的变换曲线是什么样子的。这是从显示器那一端可以拿到当前的Color Profile,甚至可以指定用哪一种Color Profile,你可以看到选不同配置的时候,桌面或者图片显示出来的颜色是不一样的。

通过这种配置,图片中的数据对应到颜色亮度的变换是可以不一样的。系统设置里一般会把伽马值翻译成响应曲线,你可以看到RGB分别有响应曲线,下面是对应的曲线,这个值不同的时候,这个曲线有变化,和刚才看到的现象是一样的。
这里写图片描述
图 07

sRGB是一种最常用的Color Profile,是由微软和惠普联合制定的一个规范,它的使用如此广泛以至于我们经常把sRGB等同于了伽马颜色空间。如果显示一个颜色,所有的显示设备都用了sRGB的颜色空间,存储一个图片或者保存一个视频也好或者其它图片工具也使用sRGB的编码,因为都在一个颜色空间下,所以输入和显示是可以免去转换的。

在不同设备上显示颜色要把图片颜色转成波长,再把波长放在显示器上,根据显示器的Color Profile可以得出显示器应该怎么显示这个值,才能体现出它的原本颜色,这中间有一个计算过程,如果我们都使用同一种Colo Profile,例如:sRGB,就没有这个过程了,图片存的时候就是sRGB的颜色空间,显示器接收的时候已经是同样的颜色空间了,RGB数值可以直接输出,省略了转换的过程。

sRGB也有伽玛变换,它的伽马值近似等于2.02。图07中右边图颜色部分解释了它的伽马值,伽马值其实不是一个固定值,而是一条曲线。

下面我们看一下Unity里边用伽马颜色空间时到底做什么?如果不做任何的伽马校正,一般的流程是什么样子的?
这里写图片描述
图 08

如图08所示,最左边是贴图,大家看着可能觉得有点暗,这是正常的,因为显示端做了伽玛变换。这张图在Unity里参与光照计算,计算结果保存到Frame Buffer,这也是正常的结果。但是显示器输出Frame Buffer的时候,结果看起来变暗了,我们看到后觉得这不是我们想要的结果。

如图09所示,这就是我们提到的保存图片时的伽玛校正,可以看到,原始的这张图经过伽马校正提亮,传到Unity里面做光照计算。通过Frame Buffer可以看到它的计算结果是特别亮的,经过显示器的伽马变换之后,这个亮度被压了下来,你就会觉得这个效果是正常的。但是这个亮度是让人觉得正常的,代表它的结果是正确的吗?其实不是这样的,后面我们再来详谈。
这里写图片描述
图 09

什么是线性空间

什么是线性颜色空间呢?这里要提到二个底层图形API:sRGB Frame Buffer和sRGB Sampler。

我们拿刚才的渲染流程对比一下,上面分支的流程里输出是偏暗的,正常的亮度被显示器压低了,我们没有办法在显示器输出前进行校正。但是sRGB Frame Buffer可以,如果使用sRGB Frame Buffer,它能够在结果输出到显示器这个阶段做sRGB伽玛校正,如图10所示,最后的显示是正常的。
这里写图片描述
图 10

sRGB Frame Buffer是由硬件支持的,就像刚才所做的变换用Shader也可以实现,sRGB Frame Buffer的转换速度要比它快。安卓手机只有OpenGL ES3.0才可以支持它,所以你会看到开启线性空间必须指定Graphics API为OpenGL ES3.0。需要要注意的是Alpha值不做任何变换的,它只支持每通道8位的格式。为什么只支持8位格式?我们后面会说到。
这里写图片描述
图 11

你目前能用到的大部分作图软件都是在sRGB的空间下制作的,软件内的预览和图片的保存都经过了伽玛校正。这个在伽玛颜色空间下没问题,但是在线性颜色空间里,贴图的颜色应该是线性的。所以当使用线性空间时,经过sRGB校正的贴图应该被还原。有二种方法,可以把图退回给美术重做,或者你可以使用sRGB Sampler。
这里写图片描述
图 12

sRGB Sampler也是硬件支持的一个特性,如果勾选sRGB选项,硬件会认为图是sRGB编码的,在采样颜色的时候会先做一次sRGB反向转换,再把结果返回,所以Shader读到的颜色是校正之前的颜色。
这里写图片描述
图 13

如果贴图是线性存储的,则不需要勾选sRGB选项,采样得到的颜色值就是直接从贴图中取得。
这里写图片描述
图 14

为什么要用gamma颜色空间

那么为什么我们之前要用伽马颜色空间?

有一个历史原因,以前的显示器是模拟信号,电子打到显示屏上,就能看到像素发光。它的电压和亮度并不是线性的增长关系,而是近似于调伽马曲线,所以以前的显示器自带伽马变换的。

另一个重要的原因和精度有关,就像我们刚才说,RGB 8位的贴图才有sRGB格式,sRGB格式的精度会更高。
这里写图片描述
图 15

那么同样是8位的表示方式,为什么精度会有区别呢?这和人眼有关系,并不是说绝对精度会变高,而是说人眼对不同亮度的区域的反应是不一样的。

例如:图15中的渐变图是经过伽马变换的,下边那一个是没有经过任何校正的,也就是说下面这个图从左到右亮度是均匀变化的。你会发现下面这张图右边基本上是纯白,但是它其实是有均匀变化的,这说明我们人眼对亮部的识别特别差,对暗部的识别高一些。

假如说对右边这部分区域也用8位做编码的话,亮度值的渐变对人眼来说是看不到任何变化的,人眼识别不了那么清晰的渐变过程,位数少一些没有关系。对暗度反而有比较高的敏感度,需要更多的位数。这就是为什么说sRGB精度高,是因为我们可以从图像中看到更多信息。

精度如何变高的呢?如图16所示,这是经过伽马校正以后的曲线,纵轴是sRGB格式的数据,横轴是原始数据。原始值和编码后的值基本是一样的,原始数据较小的区间,编码后的阈值变得比较大。
这里写图片描述
图 16

这是我们期望的,同样跨度的编码值可以表示更细微的过渡。这就是为什么要用伽马颜色空间非常重要的原因,我们制作贴图时对精度就是有要求的。

那么线性空间的好处在哪里?是因为光照计算会出错,在伽马颜色空间下做光照计算分两步。举个简单乘法的例子,第一步:Shader计算结果,计算方式为贴图颜色×光照系数,贴图颜色是经过伽玛校正的,所以它要带着幂运算。第二步:输出计算结果到屏幕,屏幕颜色等于是输出的颜色。
这里写图片描述
图 17

我们想要的正确的结果是什么样子?我们并不是要在贴图颜色上做校正,贴图颜色就是直接乘以光照系数得到输出的颜色,只有在输出到屏幕的时候,把输出颜色做伽马校正。
这里写图片描述
图 18

这二种方式比较相似,做一个对比,你就能看到,左边是做贴图上的伽马校正,并且在伽马颜色空间下的计算结果,右边是正确的计算结果,二者是不一样的。线性颜色空间的计算是和正确的计算方法一致的。

这里写图片描述
图 19

图20中的这几张图片更能说出问题。贴图一般都不是灰度图,RGB通道间的差异是不一样的,比如任的皮肤贴图,红通道的值会高于其它二个通道。经过伽马校正之后,红通道和其他通道的差异就会被放大,用它做光照计算,会发现红通道会提升得异常的高。
这里写图片描述
图 20

二张图中左边是线性正确值,右边的曝光结果不对,因为这种色温比较温和的颜色,会迅速的曝光,存的这个值在经过sRGB变换了之后已经比原来的值大了。注意右边上眼皮轮廓应用的地方,你会发现有一些黑、有一些蓝色,也是一样的问题,当你做光照计算的时候,冷色调和暖色调的差别已经拉开了,把光照系数加上去,差异变得更大,这就是你发现结果不对的原因。

选择颜色空间

你刚才看到只有在光照计算的时候,伽马空间的弊端会暴露的特别明显,什么时候分别采用合适的颜色空间呢?

在光照效果来说,只有要求真实光照的话,才要考虑线性空间,真实光照并不是说看起来好,而是说数值上就是正确的。要求数值正确是指要求在不同的光照环境下光照结果都正常。
这里写图片描述
图 21

如果要求的是物理写实的效果,才只能考虑使用线性空间。其实在其他的情况下,二种方式都是可以的,用伽马空间也是可以的。
这里写图片描述
图 22

2D游戏推荐使用伽马空间,这不是Unity的考虑,而是美术工作流的考虑,需要花很长时间培训美术,转到线性空间做图方式做图,这是成本比较高的,所以推荐伽马空间。3D游戏推荐使用线性空间,尤其使用PBR时线性空间是前提,伽马空间是做不了的。
这里写图片描述
图 23

  • 9
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值