我相信幾乎全部作圖像處理方面的人都聽過伽馬校訂(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輕鬆地完成,須要注意的是對紋理的處理。但不幸的是,不支持移動平臺。
最後,一句忠告,在遊戲渲染的時候必定要考慮伽馬校訂的問題,不然就很可貴到很是真實的效果。
下面有一些文章是我以爲很好的資料,可是其中有不少說法是有爭議的,但願你們能本身評估: