我相信几乎全部作图像处理方面的人都听过伽马校订(Gamma Correction)这一个名词,但真正明白它是什么、为何要有它、以及怎么用它的人其实很少。我也不例外。
最初我查过一些资料,但不少文章的说法都不同,有些很晦涩难懂。直到我最近在看《Real Time Rendering,3rd Edition》这本书的时候,才开始慢慢对它有所理解。
本人才疏学浅,写的这篇文章极可能成为网上另外一篇误导你的“伽马传说”,但我尽量把目前了解的资料和可能存在的疏漏写在这里。若有错误,还望指出。
css
关于这个方面,龚大写过一篇文章,但我认为其中的说法有不许确的地方。
从我找到的资料来看,人们使用伽马曲线来进行显示最开始是源于一个巧合:在早期,CRT几乎是惟一的显示设备。但CRR有个特性,它的输入电压和显示出来的亮度关系不是线性的,而是一个相似幂律(pow-law)曲线的关系,而这个关系又刚好跟人眼对光的敏感度是相反的。这个巧合意味着,虽然CRT显示关系是非线性的,但对人类来讲感知上极可能是一致的。
我来详细地解释一下这个事件:在好久好久之前(其实没多久),全世界都在使用一种叫CRT的显示设备。这类设备的显示机制是,使用一个电压轰击它屏幕上的一种图层,这个图层就能够发亮,咱们就能够看到图像了。可是,人们发现,咦,若是把电压调高两倍,屏幕亮度并无提升两倍啊!典型的CRT显示器的伽马曲线大体是一个伽马值为2.5的幂律曲线。显示器的这类伽马也称为display gamma。因为这个问题的存在,那么图像捕捉设备就须要进行一个伽马校订,它们使用的伽马叫作encoding gamma。因此,一个完整的图像系统须要2个伽马值:
- encoding gamma:它描述了encoding transfer function,即图像设备捕捉到的场景亮度值(scene radiance values)和编码的像素值(encoded pixel values)之间的关系。
- display gamma:它描述了display transfer function,即编码的像素值和显示的亮度(displayed radiance)之间的关系。
以下图所示:
html
今天很幸运听了知乎上韩世麟童鞋的讲解。在听了他的讲座后,我听到了另外一个版本的伽马传说。和上面的讨论不一样,他认为伽马的来源彻底是因为人眼的特性形成的。对伽马的理解和职业颇有关系,长期从事摄影、视觉领域相关的工做的人可能更有发言权。我以为这个版本更加可信。感兴趣的同窗能够直接去知乎上领略一下。
我在这里来大体讲一下他的理解。
事情的原由能够从在真实环境中拍摄一张图片提及。摄像机的原理能够简化为,把进入到镜头内的光线亮度编码成图像(例如一张JEPG)中的像素。这样很简单啦,若是采集到的亮度是0,像素就是0,亮度是1,像素就是1,亮度是0.5,像素就是0.5。这里,就是这里,出现了一点问题!若是咱们假设只用8位空间来存储像素的话,觉得着0-1能够表示256种颜色,没错吧?可是,人眼有的特性,就是对光的灵敏度在不一样亮度是不同的。仍是这张图Youtube: Color is Broken:
c++
其实,对伽马传说的理解就算有误差,也不会影响咱们对伽马校订的使用。咱们只要知道,根据sRGB标准,大部分显示器使用了2.2的display gamma来显示图像。app
前面提到了,和渲染相关的是encoding gamma。咱们知道了,显示器在显示的时候,会用display gamma把显示的像素进行display transfer以后再转换成显示的亮度值。因此,咱们要在这以前,像图像捕捉设备那样,对图像先进行一个encoding transfer,与此相关的就是encoding gamma了。
而不幸的是,在游戏界长期以来都忽视了伽马校订的问题,也形成了为何咱们渲染出来的游戏老是暗沉沉的,老是和真实世界不像。less
回到渲染的时候。咱们来看看没有正确进行伽马校订到底会有什么问题。
如下实验均在Unity中进行。
性能
咱们来看一个最简单的场景:在场景中放置一个球,使用默认的Diffuse材质,打一个平行光:
ui
混合实际上是很是容易受伽马的影响。咱们仍是在Unity里建立一个场景,使用下面的shader渲染三个Quad:编码
Shader "Custom/Gamma Correction For Quad" {
Properties {
_MainTex ("Base (RGB)", 2D) = "white" {}
_Color ("Color", Color) = (1, 1, 1, 1)
}
SubShader {
Tags
{
"Queue" = "Transparent"
"IgnoreProjector" = "True"
"RenderType" = "Transparent"
}
Pass {
// Blend One One
Blend SrcAlpha OneMinusSrcAlpha
CGPROGRAM
#pragma vertex vert
#pragma fragment frag
#include "UnityCG.cginc"
sampler2D _MainTex;
float4 _Color;
struct v2f {
float4 pos : SV_POSITION;
float4 uv : TEXCOORD0;
float4 normal : TEXCOORD1;
};
v2f vert(appdata_base i) {
v2f o;
o.pos = mul(UNITY_MATRIX_MVP, i.vertex);
o.uv = i.texcoord;
return o;
}
float4 circle(float2 pos, float2 center, float radius, float3 color, float antialias) {
float d = length(pos - center) - radius;
float t = smoothstep(0, antialias, d);
return float4(color, 1.0 - t);
}
float4 frag(v2f i) : SV_Target {
float4 background = float4(0.0);
float4 layer1 = circle(i.uv, float2(0.5, 0.5), 0.3, _Color.rgb, 0.2);
float4 fragColor = float4(0.0);
fragColor = lerp(fragColor, layer1, layer1.a);
// fragColor = pow(fragColor, 1.0/1.8);
return fragColor;
}
ENDCG
}
}
FallBack "Diffuse"
}
上面的shader其实很简单,就是在Quad上画了个边缘模糊的圆,而后使用了混合模式来会屏幕进行混合。咱们在场景中画三个这样不一样颜色的圆,三种颜色分别是(0.78, 0, 1),(1, 0.78, 0),(0, 1, 0.78):
atom
shader中非线性的输入最有可能的来源就是纹理了。
为了直接显示时能够正确显示,大多数图像文件都进行了提早的校订,即已经使用了一个encoding gamma对像素值编码。但这意味着它们是非线性的,若是在shader中直接使用会形成在非线性空间的计算,使得结果和真实世界的结果不一致。
spa
在计算纹理的Mipmap时也须要注意。若是纹理存储在非线性空间中,那么在计算mipmap时就会在非线性空间里计算。因为mipmap的计算是种线性计算——即降采样的过程,须要对某个方形区域内的像素去平均值,这样就会获得错误的结果。正确的作法是,把非线性的纹理转换到线性空间后再计算Mipmap。
因为未进行伽马校订而形成的混合问题其实很是常见,不只仅是在渲染中才遇到的。
Youtube上有一个颇有意思的视频,很是建议你们看一下。里面讲的就是,因为在混合前未对非线性纹理进行转换,形成了混合纯色时,在纯色边界处出现了黑边。用数学公式来阐述这一现象就是:
咱们的目标是:保证全部的输入都转换到线性空间,并在线性空间下作各类光照计算,最后的输出在经过一个encoding gamma进行伽马校订后进行显示。
在Unity中,有一个专门的设置是为伽马校订服务的,具体能够参见官方文档(Linear Lighting)。
简单来讲就是靠Edit -> Project Settings -> Player -> Other Settings中的设置:
sRGB模式是在近代的GPU上才有的东西。若是不支持sRGB,咱们就须要本身在shader中进行伽马校订。对非线性输入纹理的校订一般代码以下:
float3 diffuseCol = pow(tex2D( diffTex, texCoord ), 2.2 );
在最后输出前,对输出像素值的校订代码一般长下面这样:
fragColor.rgb = pow(fragColor.rgb, 1.0/2.2);
return fragColor;
可是,手工对输出像素进行伽马校订在使用混合的时候会出现问题。这是由于,校订后致使写入color buffer的颜色是非线性的,这样混合就发生在非线性空间中。一种解决方法时,在中间计算时不要对输出进行伽马校订,在最后进行一个屏幕后处理操做对最后的输出进行伽马校订,但很显然这会形成性能问题。
还有一些细节问题,例如在进行屏幕后处理的时候,要当心咱们目前正在处理的图像究竟是不是已经伽马校订后的。
总之,一切工做都是为了“保证全部的输入都转换到线性空间,并在线性空间下作各类光照计算,最后的输出(最最最最后的输出)进行伽马校订后再显示”。
虽然Unity的这个设置很是方便,可是其支持的平台有限,目前还不支持移动平台。也就是说,在安卓、iOS上咱们没法使用这个设置。所以,对于移动平台,咱们须要像上面给的代码那样,手动对非线性纹理进行转换,并在最后输出时再进行一次转换。但这又会致使混合错误的问题。
若是咱们在Edit -> Project Settings -> Player -> Other Settings中使用了Linear Space,那么以前的光照、混合问题均可以解决(这里的解决是说和真实场景更接近)。但在处理纹理时须要注意,全部Unity会把全部输入纹理都设置成sRGB格式,也就说,全部纹理都会被硬件当成一个非线性纹理,使用一个display gamma(一般是2.2)进行处理后,再传递给shader。但有时,输入纹理并非非线性纹理就会发生问题。
例如,咱们绘制一个亮度为127/255的纹理,传给shader后乘以2后进行显示:
伽马校订一直是个众说纷纭的故事,固然我写的这篇也极可能会有一些错误,若是您能指出不胜感激。
即使关于一些细节问题说法不少,但本质是不变的。GPU Gems上的一段话能够说明伽马校订的重要性:
This is one reason why most (but not all) CG for film looks much better than games—a reason that has nothing to do with the polygon counts, shading, or artistic skills of game creators. (It’s also sometimes a reason why otherwise well-made film CG looks poor—because the color palettes and gammas have been mismatched by a careless compositor.)
最后,给出GPU Gems中的一段总结,如下步骤应该在游戏开发中应用:
1. 假设大部分游戏使用没有校订过的显示器,这些显示器的display gamma能够粗略地认为是2.2。(对于更高质量要求的游戏,可让你的游戏提供一个伽马校订表格,来让用户选择合适的伽马值。)
2. 在对非线性纹理(也就是那些在没有校订的显示器上看起来是正确的纹理)进行采样时,而这些纹理又提供了光照或者颜色信息,咱们须要把采样结果使用一个伽马值转换到线性空间中。不要对已经在线性颜色空间中的纹理,例如一些HDR光照纹理、法线纹理、凹凸纹理(bump heights)、或者其余包含非颜色信息的纹理,进行这样的处理。对于非线性纹理,尽可能使用sRGB纹理格式。
3. 在显示前,对最后的像素值应用一个伽马校订(即便用1/gamma对其进行处理)。尽可能使用sRGB frame-buffer extensions来进行有效自动的伽马校订,这样能够保证正确的混合。
所幸的是,在Unity中,上面的过程能够经过设置Edit -> Project Settings -> Player -> Other Settings->Color Space轻松地完成,须要注意的是对纹理的处理。但不幸的是,不支持移动平台。
最后,一句忠告,在游戏渲染的时候必定要考虑伽马校订的问题,不然就很可贵到很是真实的效果。
下面有一些文章是我以为很好的资料,可是其中有不少说法是有争议的,但愿你们能本身评估: