Gamma校正与线性空间
基础知识部分
为了方便理解,首先会对(Luminance)的相关概念做一个简单介绍。如果已经了解就跳到后面吧。
我们用Radiant energy(辐射能量)来描述光照的能量,单位是焦耳(J),因为光实际是以一定速度在传播的电磁波,所以把单位时间内的传播的Radiant energy(能量)称作radiant flux(辐射通量),用来描述他的能量表现,单位瓦特(Watt)。
Radiant intensity(辐射强度)用来指定radiant flux(辐射通量)的方向,正式的来说,他是用来定义每个立体角的Radiant energy(辐射能量)的传递速度,它的单位是瓦特/球面度((W · sr-1)。
辐射强度(Radiant intensity)通常会是一个很大的空间范围,但对于使用像素的成像系统来说是一个很小的区域,所以使用intensity来作为图像数据的度量并不合适。那么就需要一个可以对应面积的单位,我们把通过单位面积的能量(Radiant energy),称作辐射照度( Irradiance)。
而把在每个单位面积上的辐射强度(Radiant intensity),称作辐射率(radiance), 他的单位是瓦特/球面度/平方米。
图像保存在文件里的(如TIFF),就是于辐射率成正比。但是像素值通常会被非线性的传递函数所影响。
在物理学里,辐射强度(Radiant intensity)和辐射率(radiance),是一个很宽广的波长范围的集合,在颜色学中,我们要考虑人眼对光波长的可见性来定义光的单位,也就是心理物理量。把辐射强度( Radiant intensity),用标准的光度函数(人眼视觉的亮度感与光谱强度的关联函数)来加权运算,就可以得出Luminous intensity(发光强度)。它的单位是坎德拉candelas (cd)。
这个权重曲线由CIE来定义的数值和标准。它的峰值一般在555nm附近。
Luminance(亮度)代表的是Luminous intensity(发光强度)在单位面积的值,它的单位是坎德拉/平方米。Luminance在一些公式里被表示为Y,与能量成正比关系,更类似intensity,通常luminance 会被标准化为一个值为1或者100的单位,用来指定为一个白色参考值,例如显示器上的一个白色,可以用100 cd · m-2和Y= 1来指定这个值。所以Luminance通常也称作relative Luminance(相对亮度)。
Luminance可以按照一定的权重计算为线性光红色,绿色,蓝色三个部分,也就是tristimulus components。
709Y = 0.2126 R + 0.7152G + 0.0722 B
上图,辐照度(irradicance)和的照度(illuminace)是对应的
上图,光量的物理值和对应的心理物理量的对比表。
gamma
物理显示设备上luminance的生成,通常和它输入的信号不成正比的,存在非线性的关系。例如传统的CRT电视或显示器上,是通过电子枪的发射高速电子,投射在电视平面的荧光粉上使之发光,而通过不同输出电压来调节电子束功率,就可以调节显示的Luminance。但因为电压的幂定律( power-law)响应的缘故(心理物理量和物理量并不成线性增长关系,而是幂函数的形式)。最终显示的在屏幕上的效果受电压影响大概是原理的Power 2.5左右(通常是2.35到2.55之间) 。而这个Power函数的指数数值,就是我们俗称的gamma。记作:
图. 如图所示是视频信号从0~100mV所对应的luminance值,输入和输出并不是线性对应的,在以8位数字模拟转化的图形系统里,把黑色编码为0,白色编码为255.
因为这个原因,所以需要对这个非线性进行补偿以达到正常亮度。但就我们所知道的,即便是现在的非CRT的显示设备还要特意保留在gamma 2.2左右。 这个的原因是?
首先,人眼接受光和照相机并不相同,我们把luminance的对比率从白到黑,按100~0来对应,在这个范围内,人眼的视觉感应和luminance值也并不是成线性关系的,luminance处于18%的相对值的,在人眼里已经显示出50%的亮度了。这里把这种对luminance的感知反映称为Lightness。这是因为比起明亮的部分,人眼对暗部的感知更加敏感。
非线性的人眼和线性的照相机的对比。
CIE使用一个标准函数L*(读作EL-Star),定义为修改的luminance立方根来表示lightness。
图. 这里Yn是luminance为白色的值(按之前100~0来对应的话就是100),Y为当前的luminance值。L*的范围是0~100
luminance的相对值与Lightness值的对应曲线。可以看出相对值0.2的时候,Lightness已经大于50了
对比CRT显示器 和人眼视觉的曲线,可以发现一个惊人的巧合,人眼视觉反映的曲线恰好近似是CRT曲线的逆,人类的视觉暗部更加敏感,所以gamma的存在并不是一种缺陷,而作为非常有用的特性被保留下来了。既然输出时的gamma值无法改变,那就只能通过对输入信号做校正的方式,来让显示的内容正确。
而LCD显示器就没那么幸运了,为了能够保证显示是gamma2.2,往往需要大量的校正工作,所以不同的LCD显示通常会有些差别,所以需要LUT(look-up table)来确保输入值按照希望的gamma值输出。
Gamma- Correction
gamma校正,简单来说就是对输入信号做一次关于gamma的逆处理(1/gamma = 1/2.2 = 0.45)的Power),这样两个非线性的相逆的曲线使得最终的输出成为线性。
一个gamma校正的例子,实现代表的是CRT的行为 power 2.2,虚线代表的是反函数,也就是gamma校正的行为power 0.45,而点线则是后的线性函数。
在HDTV这类的视频系统里。通常会把luminance计算为按一定权重分配为三组线性光照((tristimulus),分为红色,绿色和蓝色。这里一个比较标准的格式是Rec. 709来对信号进行编码
709Y = 0.2126 R + 0.7152G + 0.0722 B
使用Rec. 709 transfer function 可以把这些线性值转化为非线性,这个函数主要也是基于0.45(1/2.2)的Power函数实现的。
将颜色值转化为非线性的的公式。
注,通常电视的gamma值是2.5,这里用的使用0.45(1/2.2)的缘故,是因为一般电视周围的环境比较暗,会产生simultaneous contrast现象,昏暗背景会丢失对比度,所以把输入时校正gamma值调第,
使最终的输出为power 1.13左右,来缓解这种现象的影响。
图,Simultaneous contrast 明亮和昏暗环境下,对比度会有差别。
上面是视频系统里的校正方法,回到平时工作的计算机操作系统里,因为显示器的gamma的缘故,我们在系统里看到的也需要进行gamma校正才能显示正确。所以也就有了sRGB色彩空间的概念。
sRGB(standard RGB )色彩空间最早是微软和惠普开发的一种标准的RGB格式,目的就是为了在显示器和打印机上使用,而sRGB和上面提到的Rec. 709非常相似,使用了相同的原色( primary chromaticities)和白点(white point chromaticities)的,唯一不同的是,Rec. 709对应的是gamma为2.5的显示设备,而sRGB选择的是gamma为2.2的设备。这种标准得到了许多业内厂商的支持,数码相机,摄像机,扫描仪等等,都有sRGB的支持。所以一般我们也会默认的认为导入到操作系统里的图片都是sRGB格式的。
图 sRGB和Rec. 709相同的色度表(chromaticity diagram),三角内是原色,D65位置是原点,
如上图所示,sRGB 颜色空间的值通常会是3个浮点值,在0.0~1.0之间,超出范围的会被clip掉,在图形计算中,会编码为8位的无符号整数,范围是0~255。因为也是为了gamma校正的目的,sRGB也是非线性的。大致近似是一个y = x^2.2的曲线,而实际的曲线要更负责一些,见下面的公式。
线性空间和sRGB空间之间的转化关系。
OUT.Color.xyz = (OUT.Color.xyz < 0.0031308) ? 12.92 * OUT.Color.xyz : 1.055 * pow(OUT.Color.xyz, 1.0 / 2.4) - float3(0.055, 0.055, 0.055);
ce3的UpscaleImagePS里,使用的就是最准确的公司来转化到sRGB空间,一般都是简化的pow(x, 1.0/2.2)了
图。线性空间和sRGB的对比,和gamma2.2的曲线几乎重合。这样的好处是sRGB可以储存更多低luminance RGB值,更适合人类的视觉。
如上图所示的结果,sRGB曲线和非常的接近gamma2.2,所以一般sRGB转化函数经常就近似为一个gamma函数就可以了。所以,对于图片来讲,如果他是sRGB空间的话,那么也就可以说它是gamma校正过的了。
早期和游戏渲染相关的gamma校正的资料里,对每个gamma空间和线性空间的定义比较含糊,为了方便进入下一个章节,这里适当的做一下总结。
图像的Gamma
通常所说的sRGB空间 = gamma 0.45,照相机或其他Raw软件捕捉的图像,通常会转为标准的JPEG或TIFF文件,也就是编码为gamma 1/2.2的图像文件。但Raw是保存在线性空间的。一般不使用color profile的情况,都是默认的gamma 1/2.2,比如PNG和GIF文件,只有jpg文件有时会使用"save for the web"的设定
线性Raw图像 gamma = 1.0 gamma校正的图像 gamma = 1/2.2
显示的Gamma
通常的gamma2.2,现在的显示器通常已经把gamma2.2作为标准,所以不用太担心不同gamma值所带来的优缺点,老的苹果显示器可能用的是gamma1.8。
上图是,不同显示gamma值和对应曲线的对比,其中,蓝线为图片的gamma校正值(gamma 1/2.22),红线为设置的显示gamma值,紫色则为最终的系统gamma值。
系统 Gamma
表现的是gamma校正和显示gamma同时作用在图片上的效果,当系统gamma是线性时,可以保证你的光照计算正确的显示。
基本理论都介绍后,下面开始介绍渲染中的gamma校正
游戏中的Gamma校正
结合一些网站和ppt上的信息,再重新回顾一下游戏里gamma的问题,下面的图例,左边和右边是水平的黑白交叉线,中间上面的是稍暗的方块,下面是稍亮的,如果我们稍微离屏幕远一点,可以看到左边和右边部分,是黑和白(0和255)做了颜色平均也就是128. 一眼看上去,和中下面的亮灰色方块很接近。那实际下面的颜色是128么?
结果是否定的,实际上是下面是187比0~255的一半要高。而造成这种情况的原因,就是因为是gamma的非线性空间运算导致的。
下面以naughty dog的John Hable的 Uncharted 2: HDR Lighting 这篇ppt里的内容来解释这个问题。
唯一可惜的就是这里把sRGB空间和显示空间都统称为gmama空间了,我稍微做了下修改
上图是从0到255的灰度图,计算0和255的平均值后颜色,并不是128的颜色,而是187的。
当我们把一个值输出给显示器时,一般会认为luminance增长会和我们输入的数量呈线性的增长,例如把25传送2次就是50,但实际上,所以的显示器都会有gamma2.2的曲线,我们把我们的值用0.0~1.0范围替换0~255的话,输出的luminance大概是pow(x, 2.2)。
把灰度图分为32级的例子,这里的gamma值标准的1/2.2,可以看到,线性空间的暗色调信息不足,并导致明亮的色调过量。另外,gamma空间下灰度的均匀分布在整个色调范围内。
上面的图里,左边的图是gamma1/2.2(接近0.45,其实也就是sRGB空间),中间是线性的没有变化,右边偏暗的图像是gamma2.2、
注:原文这里把0.45和2.2都称作gamma空间,这里为了后面理解方便,我就直接把左侧图的空间写作sRGB空间了。
实际上,通过power函数,是可以在gamma空间之间转化的,从左边的图片开始(sRGB空间),我们可以通过pow(x,2.2)转为中间的图像(线性空间),而且也可以再次使用pow(x,2.2)转为右边的图像(gamma2.2空间)。同样,我们也可以通过逆函数pow(x, 1/2.2)返回来。
假设我们生产了一种没有考虑任何gamma的因素的照相机,那么传感器就会把入射光(左图)编码为0~255范围的像素,然后把这个照片存储在电脑硬盘上。当用户在操作系统里打开时看到屏幕上显示的图像时,显示器会将右边的gamma2.2的显示出来。那么用户因为在看到的效果不符合真实世界看到图像,而认为我们的照相机是有问题的。
你也可以试着和用户解释,说摄像机是正确的,而实际上每个人的显示器都是错的(包括照相机背面的LCD预览),但结果是肯定的。如果我们把图形保存在sRGB空间(gamma0.45 = 1/2.2),光射入传感器后,照相机会对它进行一次pow(x,1/2.2),然后再编码到0~255的等级,并保存到电脑硬盘上。实际上,包括手机,网络摄像头,单反照相机都是这么做的,唯一有差别的就是计算机视觉相机(computer vision cameras)。当图形保存在sRGB空间时,会更明亮和柔和,但当用户在显示器屏幕上查看时,就会看到正确的结果。
关于gamma必须认识到的一点就是,任何时候在计算机屏幕上看到的图像(右边),实际保存在硬盘里的图片都要更亮也更柔和一些,就如左图所示。那么接下来要解释它对计算机图形的影响。
左侧图的所有光照计算,都是在sRGB空间,右侧的图的光照是在线性空间。
左侧图错误的有几个原因,首先,他有一个真实的柔软的衰减,这看起来并不正确,同时你可以看到很多色调变化(hue-shifting),特别是Specular高光的颜色,从白色到黄色到绿色再到白色。而看 右边的图,它更像一个有白色Specular高光的Diffuse Surface。右边的图个更符合光照模式下的显示效果。要强调的是,这里讨论的是渲染“正确”,而不是看起来效果很好。
如果没有做任何关于gamma的处理的话,那么基本上会遵循上图的流程,假设有一个比较典型的shader
-
float specular =...; float3 color=tex2D(samp,uv.xy); float diffuse = saturate(dot(N,L)); float3 finalColor = color * diffuse + specular; return finalColor;
首先要读取Texture,而这张Texture在你的硬盘里是保存在sRGB空间的,所以这张图实际比你在屏幕上看到的要亮的多,不饱和度也要高,所以你要把一张过亮的Texture,来应用你的光照模型,得到中间的结果,然后你的显示器会使用pow(x,2.2)把这张图变暗,并输出最终结果。那么该如何修改呢?
这里是修改过的shader
-
float specular =...; float3 color=pow(tex2D(samp,uv.xy),2.2); float diffuse = saturate(dot(N,L)); float3 finalColor = pow(color * diffuse + specular,1/2.2); return finalColor
唯一的区别就是2个pow指令,当Texture从硬盘读取后,他是在sRGB空间(第一张图),但pow(x,2.2)会把它转变到线性空间(第2张图),然后我们进行光照计算(第3张),然后我们使用pow(x, 1/2.2)把他转回sRGB空间,然后再把sRGB空间的图像传输给显示器,显示器会适用一个pow(x,2.2)并把最终图像显示出来。
因为pow指令在shader里还是有消耗还是比较大的,所以,可以使用硬件的Sample State来免费的使用,在读贴图时,使用D3DSAMP_SRGBTEXTURE,在写framebuffer(render target)时,可以使用D3DRS_SRGBWRITEENABLE,然后就可以使用修改前的shader,硬件State会帮你免费的转化。
上图是一个真实排球的图像,右上的图像是在线性空间,右下是在sRGB空间,可以看到线性图像和真实图像在衰减上更匹配。所以说,如何让你的图形不但正确而且好看,是需要不断去练习的。
Lighting的情况讲完后,是Naty Hoffman的blog上的Blend的示例,用公式的方法来描述这种问题。
正确的进行Blend的方法是和Shading计算一样在线性空间进行,这里以Alpha-Blending为例,也类似其他的Blend Mode。下面的等式中,用小写的c代表sRGB空间,大写C代表线性空间(x为通用值,适用于每种空间)。要注意的是,alpha值作为物理覆盖量,一直在线性空间没有转化。
条件都清楚了,下面是Blend 公式:
因为给予GPU的是颜色是线性空间,而在Blend操作后,输出的Render Target颜色是sRGB空间,我们希望GPU可以这样做:
D3D11和OpenGL扩展特性可以完成这种行为,但是D3D9级别的GPU会先转为sRGB空间再进行混合。
在这种情况下,shader在线性空间计算颜色,而Blend计算在sRGB空间进行,这样在理论上并不正确,如果你的Blending只用来表现一些特殊效果,那结果可能不会太糟糕,如果你是每个light 1个pass的情况,并把光照在硬件上Blending来合并,那么,所有的光照会在sRGB空间合并,那么会失去gamma校正渲染管线的优势。不论是你用硬件转换,还是在shader里转化,情况都会很坏,因为这两种转换都是在Blend前进行的。
如果你不想在sRGB空间进行Blending,又找不到正确的支持跨颜色空间进行Blending的方法,你可以在HDR线性空间缓冲进行Blending,然后在后处理中转化为sRGB空间。如果HDR Buffer并不实际,也可以在LDR线性空间Buffer进行。
GPU硬件的自动转化不但无法保证Blend正确,如果是premultiplied alpha,可能会得到更差的结果。
汇总
根据上面的事例,解决问题似乎只需要两次pow指令或者用次sRGB硬件状态,但实际开发中,需要注意的地方还是很多的。
输入到渲染管线的图像默认都是非线性的sRGB贴图
渲染管线里会使用的图像来源,大致可以分为,直接照片采集,美术绘制以及RenderTarget的输出
照片数据,也就是我们所有扫描,或数码拍照的图像应该都是gamma校正后的,sRGB空间的贴图。前提是照片的采集步骤正确,这个会1在后面的美术部分详细描述。
让美术人员手绘,因为本身美术所用的显示器是gamma影响,如果不使用颜色表的话,那么画出来的图也是在显示器的颜色空间里。具体方法后述。网络上找来的图,也不一定就是可信的。
在DCC工具(UE4也有)中使用颜色吸管工具时,获取的颜色值都需要从sRGB空间转为线性空间再输入给渲染管线,包括材质的颜色和光的颜色,而且只有0~1之间的值才能正确的转化,并不适合light intensity 那种无边界的值。
如果美术师,在DCC绘制顶点颜色(Vertex Color)的话,那么通常是可视化的在sRGB空间进行的,需要转化为线性空间值来加载(通常在模型导出时进行)。手绘的ambient occlusion灰度信息保存在顶点颜色里的话也需要进行转化为线性空间。顶点颜色基本上不需要保存在sRGB空间里,因为高精度的插值可能会导致错误。
另外,比如在延迟渲染里,需要RenderTarget输出的Specular和Diffuse的accumulation buffer作为光照信息来对场景着色,也就是要求输出的图像是sRGB空间,不论是RT的输出,还是读取保存的Texture,都需要打开sRGB状态。一些预烘培的lightmap也一样要注意这个问题。
MipMap的处理
创建Texture的Mip map,最简单的方法就是每个更低等级的mip,是通过上一个更高分辨率中,相邻四个像素总和的1/4来计算。
一个经常会被忽视的问题,就是Mip-maps也是需要转到线性空间计算的,不过大多数的库还是支持带gamma校正的 MIP生成的(NVTextureTools和D3DX),把要转化的Texture和要生成Mips的颜色空间指定好,图形库就会帮你 把输入的Texture转到线性空间,将纹理过滤,并最终转为合适的颜色空间输出(一般是sRGB)。
如果你不使用现成的库,而是自己来写图片压缩和生成工具,又没有注意而直接在sRGB空间做mip的过滤的话,那么你最后会得到一个比正确结果更暗的显示。
如上图所示,中间的图是黑白像素线的交错,左边的图是把中间的图在线性空间降采样,而右图是直接在sRGB空间。如果你的显示器设置正确的话,左图和中间的图会表现出相同的亮度。这是因为,中间图在线性空间平均得出50%的灰度,换算到sRGB空间就是186的值,而右图直接把128作为sRGB空间值来保存。那么转到线性空间就是一个只有21.4%的灰度,所以会看的更暗一些。
正确的方法
sRGB空间(0,1) --> pow(x, 2.2)转线性空间 --> 线性空间里计算 (0 + 1)/2 = 0.5 --> pow(x, 1/2.2)转回sRGB --> 保存在sRGB空间的是0.73
而错误的计算方法
sRGB空间(0 ,1) --> sRGB空间里计算 (0 + 1 )/2 = 0.5 --> 保存在sRGB的空间的值是0.5
所以,当带mipmap的Texture加载并输出到显示器上时,错误的sRGB空间的计算导致了最后的结果要更暗。
参考链接