【視頻開發】伽馬校訂(gamma correction)學習筆記

我相信幾乎全部作圖像處理方面的人都聽過伽馬校訂(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

這裏寫圖片描述


而encoding gamma和display gamma的乘積就是真個圖像系統的end-to-end gamma。若是這個乘積是1,那麼顯示出來的亮度就是和捕捉到的真實場景的亮度是成比例的。 

上面的情景是對於捕捉的相片。那麼對於咱們渲染的圖像來講,咱們須要的是一個encoding gamma。若是咱們沒有用一個encoding gamma對shader的輸出進行校訂,而是直接顯示在屏幕上,那麼因爲display gamma的存在就會使畫面失真。 

至此爲止,就是龔大 所說的伽馬傳說 。由此,龔大認爲所有的問題都出在CRT問題上,跟人眼沒有任何關係。 

可是,在《Real-time Rendering》一書中,指出了這種乘積爲1的end-to-end gamma的問題。看起來,乘積爲1的話,可讓顯示器精確重現原始場景的視覺條件。可是,因爲原始場景的觀察條件和顯示的版本之間存在兩個差別:1)首先是,咱們可以顯示的亮度值其實和真實場景的亮度值差了好幾個數量級,說通俗點,就是顯示器的精度根本達不到真實場景的顏色精度(大天然的顏色種類幾乎是無窮多的,而若是使用8-bit的編碼,咱們只能顯示256^3種顏色);2)這是一種稱爲surround effect的現象。在真實的場景中,原始的場景填充了填充了觀察者的全部視野,而顯示的亮度每每只侷限在一個被周圍環境包圍的屏幕上。這兩個差異使得感知對比度相較於原始場景明顯降低了。也就是咱們一開始說的,對光的靈敏度對不一樣亮度是不同的。以下圖所示(來源: Youtube: Color is Broken ): 
這裏寫圖片描述


爲了中和這種現象,因此咱們須要乘積不是1的end-to-end gamma,來保證顯示的亮度結果在感知上和原始場景是一致的。根據《Real-time Rendering》一書中,推薦的值在電影院這種漆黑的環境中爲1.5,在明亮的室內這個值爲1.125。 

我的電腦使用的一個標準叫sRGB,它使用的encoding gamma大約是0.45(也就是1/2.2)。這個值就是爲了配合display gamma爲2.5的設備工做的。這樣,end-to-end gamma就是0.45 * 2.5 = 1.125了。 

這意味着,雖然CRT的display gamma是2.5,但咱們使用的encoding gamma應該是1.125/2.5 = 1/2.2,而不是1/2.5。這樣才能保證end-to-end gamma爲1.125,從而在視覺上進行了補償。 

雖然如今CRT設備不多見了,但爲了保證這種感知一致性(這是它一直沿用至今的很重要的一點),同時也爲了對已有圖像的兼容性(以前不少圖像使用了encoding gamma對圖像進行了編碼),因此仍在使用這種伽馬編碼。並且,如今的LCD雖然有不一樣的響應曲線(即display gamma不是2.5),可是在硬件上作了調整來提供兼容性。 

重要: 上面的說法主要來源於Real-time Rendering》一書。 

來自其餘領域的伽馬傳說


今天很幸運聽了知乎上韓世麟童鞋的講解。在聽了他的講座後,我聽到了另外一個版本的伽馬傳說。和上面的討論不一樣,他認爲伽馬的來源徹底是因爲人眼的特性形成的。對伽馬的理解和職業頗有關係,長期從事攝影、視覺領域相關的工做的人可能更有發言權。我以爲這個版本更加可信。感興趣的同窗能夠直接去知乎上領略一下。 

我在這裏來大體講一下他的理解。 

事情的原由能夠從在真實環境中拍攝一張圖片提及。攝像機的原理能夠簡化爲,把進入到鏡頭內的光線亮度編碼成圖像(例如一張JEPG)中的像素。這樣很簡單啦,若是採集到的亮度是0,像素就是0,亮度是1,像素就是1,亮度是0.5,像素就是0.5。這裏,就是這裏,出現了一點問題!若是咱們假設只用8位空間來存儲像素的話,覺得着0-1能夠表示256種顏色,沒錯吧?可是,人眼有的特性,就是對光的靈敏度在不一樣亮度是不同的。仍是這張圖Youtube: Color is Broken: 
c++

這裏寫圖片描述


這張圖說明一件事情,即亮度上的線性變化在人眼看來是非均勻的,再通俗點,從0亮度變到0.01亮度,人眼是能夠察覺到的,但從0.99變到1.0,人眼可能就根本差異不出來,以爲它們是一個顏色。也就是說,人眼對暗部的變化更加敏感,而對亮部變化其實不是很敏感。也就是說,人眼認爲的中灰其實不在亮度爲0.5的地方,而是在大約亮度爲0.18的地方(18度灰)。強烈建議去看一下Youtube上的視頻, Color is Broken 。 

那麼,這和拍照有什麼關係呢?若是在8位圖中,咱們仍然用0.5亮度編碼成0.5的像素,那麼暗部和亮部區域咱們都使用了128種顏色來表示,但實際上,亮部區域使用這麼多種其實相對於暗部來講是種存儲浪費。不浪費的作法是,咱們應該把人眼認爲的中灰亮度放在像素值爲0.5的地方,也就是說,0.18亮度應該編碼成0.5像素值。這樣存儲空間就能夠充分利用起來了。因此,攝影設備若是使用了8位空間存儲照片的話,會用大約爲0.45的encoding gamma來對輸入的亮度編碼,獲得一張圖像。0.45這個值徹底是因爲人眼的特性測量獲得的。 

那麼顯示的時候到了。有了一張圖片,顯示的時候咱們仍是要把它還原成原來的亮度值進行顯示。畢竟,0.454只是爲了充分利用存儲空間而已。咱們假設一下,當年CRT設備的輸入電壓和產生亮度之間徹底是線性關係,咱們仍是要進行伽馬校訂的。這是爲了把用0.45伽馬編碼後的圖像正確重如今屏幕上。巧合的是,當年人們發現CRT顯示器居然符合冪律曲線!人們想,「天哪,太棒了,咱們不須要作任何調整就可讓拍攝的圖像在電腦上看起來和原來的同樣了」。這就是咱們一直說的「那個巧合」。當年,CRT的display gamma是2.5,這樣致使最後的end-to-end gamma大約是0.45 * 2.5 = 1.125,實際上是非1的。 

直到後來,微軟聯合愛普生、惠普提供了sRGB標準,推薦顯示器中display gamma值爲2.2。這樣,配合0.45的encoding gamma就能夠保證end-to-end gamma爲1了。固然,上一節提到的兩個觀察差別,有些時候咱們其實更但願end-to-end gamma非1的結果,例如,在電影院這種暗沉沉的環境中,end-to-end gamma爲1.5咱們人看起來更爽、更舒服,而在明亮的辦公室這種環境中1.125的end-to-end gamma值更舒服、更漂亮。因此,咱們能夠根據環境的不一樣,去選擇使用什麼樣的display gamma。 

總之, 伽馬校訂一直沿用至今說究竟是人眼特性決定的 。你會說,伽馬這麼麻煩,何時能夠捨棄它呢?按 韓世麟童鞋 的說法,若是有一天咱們對圖像的存儲空間可以大大提高,通用的格式再也不是8位的時候,例如是32位的時候,伽馬就沒有用了。由於,咱們不須要爲了提升精度而把18度灰編碼成0.5像素,由於咱們有足夠多的顏色空間能夠利用,不須要考慮人眼的特性。 

好啦,上面就是來自攝影、建築領域的見解和理解。但願這兩種見解可讓你們更深地理解伽馬校訂的存在乎義。 

這和渲染有什麼關係


其實,對伽馬傳說的理解就算有誤差,也不會影響咱們對伽馬校訂的使用。咱們只要知道,根據sRGB標準,大部分顯示器使用了2.2的display gamma來顯示圖像。app

前面提到了,和渲染相關的是encoding gamma。咱們知道了,顯示器在顯示的時候,會用display gamma把顯示的像素進行display transfer以後再轉換成顯示的亮度值。因此,咱們要在這以前,像圖像捕捉設備那樣,對圖像先進行一個encoding transfer,與此相關的就是encoding gamma了。 

而不幸的是,在遊戲界長期以來都忽視了伽馬校訂的問題,也形成了爲何咱們渲染出來的遊戲老是暗沉沉的,老是和真實世界不像。less

回到渲染的時候。咱們來看看沒有正確進行伽馬校訂到底會有什麼問題。 

如下實驗均在Unity中進行。 

性能

光照


咱們來看一個最簡單的場景:在場景中放置一個球,使用默認的Diffuse材質,打一個平行光: 
ui

Gamma


看起來很對是嗎?但實際上,這和咱們在真實場景中看到的是不同的。在真實的場景中,若是咱們把一個球放在平行光下,它是長這個樣子的: 
Linear


假設球上有一點B,它的法線和光線方向成60°,還有一點A,它的法線和光線方向成90°。那麼,在shader中計算diffuse的時候,咱們會得出B的輸出是(0.5, 0.5, 0.5),A的輸出的(1.0, 1.0, 1.0)。 

在第一張圖中,咱們沒有進行伽馬校訂。所以,在把像素值轉換到屏幕亮度時並非線性關係,也就是說B點的亮度其實並非A亮度的一半,在Mac顯示器上,這個亮度只有A亮度的1/1.8唄,約爲四分之一。在第二章圖中,咱們進行了伽馬校訂,此時的亮度纔是真正跟像素值成正比的。 

混合


混合實際上是很是容易受伽馬的影響。咱們仍是在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"
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62


上面的shader其實很簡單,就是在Quad上畫了個邊緣模糊的圓,而後使用了混合模式來會屏幕進行混合。咱們在場景中畫三個這樣不一樣顏色的圓,三種顏色分別是(0.78, 0, 1),(1, 0.78, 0),(0, 1, 0.78): 
atom

這裏寫圖片描述


看出問題了嗎?在不一樣顏色的交接處出現了不正常的漸變。例如,從綠色(0, 1, 0.78)到紅色(0.78, 0, 1)的漸變中,居然出現了藍色。 

正確的顯示結果應該是: 
這裏寫圖片描述


第一張圖的問題出在,在混合後進行輸出時,顯示器進行了display transfer,致使接縫處顏色變暗。 

非線性輸入


shader中非線性的輸入最有可能的來源就是紋理了。 

爲了直接顯示時能夠正確顯示,大多數圖像文件都進行了提早的校訂,即已經使用了一個encoding gamma對像素值編碼。但這意味着它們是非線性的,若是在shader中直接使用會形成在非線性空間的計算,使得結果和真實世界的結果不一致。 

spa

Mipmaps


在計算紋理的Mipmap時也須要注意。若是紋理存儲在非線性空間中,那麼在計算mipmap時就會在非線性空間裏計算。因爲mipmap的計算是種線性計算——即降採樣的過程,須要對某個方形區域內的像素去平均值,這樣就會獲得錯誤的結果。正確的作法是,把非線性的紋理轉換到線性空間後再計算Mipmap。 

擴展


因爲未進行伽馬校訂而形成的混合問題其實很是常見,不只僅是在渲染中才遇到的。 

Youtube上有一個頗有意思的視頻,很是建議你們看一下。裏面講的就是,因爲在混合前未對非線性紋理進行轉換,形成了混合純色時,在純色邊界處出現了黑邊。用數學公式來闡述這一現象就是: 

x1gamma+y1gamma2<(x+y2)1gamma

咱們能夠把 x1gamma y1gamma 當作是兩個非線性空間的紋理,若是直接對它們進行混合(如取平均值),獲得的結果實際要暗於在線性空間下取平均值再伽馬校訂的結果。 

因此,在處理非線性紋理時必定要格外當心。


進行伽馬校訂


咱們的目標是:保證全部的輸入都轉換到線性空間,並在線性空間下作各類光照計算,最後的輸出在經過一個encoding gamma進行伽馬校訂後進行顯示。 

在Unity中,有一個專門的設置是爲伽馬校訂服務的,具體能夠參見官方文檔(Linear Lighting)。 

簡單來講就是靠Edit -> Project Settings -> Player -> Other Settings中的設置: 

這裏寫圖片描述


它有兩個選項:一個是Gamma Space,一個Linear Space。 

- 當選擇Gamma Space時,實際上就是「聽任模式」,不會對shader的輸入進行任何處理,即便輸入多是非線性的;也不會對輸出像素進行任何處理,這意味着輸出的像素會通過顯示器的display gamma轉換後獲得非預期的亮度,一般表現爲整個場景會比較昏暗。

  • 當選擇Linear Space時,Unity會背地裏把輸入紋理設置爲sRGB模式,這種模式下硬件在對紋理進行採樣時會自動將其轉換到線性空間中;而且,也會設置一個sRGB格式的buffer,此時GPU會在shader寫入color buffer前自動進行伽馬校訂。若是此時開啓了混合(像咱們以前的那樣),在每次混合是,以前buffer中存儲的顏色值會先從新轉換回線性空間中,而後再進行混合,完成後再進行伽馬校訂,最後把校訂後的混合結果寫入color buffer中。這裏須要注意,Alpha通道是不會參與伽馬校訂的。 

sRGB模式是在近代的GPU上纔有的東西。若是不支持sRGB,咱們就須要本身在shader中進行伽馬校訂。對非線性輸入紋理的校訂一般代碼以下:

float3 diffuseCol = pow(tex2D( diffTex, texCoord ), 2.2 ); 
  • 1


在最後輸出前,對輸出像素值的校訂代碼一般長下面這樣:

fragColor.rgb = pow(fragColor.rgb, 1.0/2.2);
return fragColor;
  • 1
  • 2


可是,手工對輸出像素進行伽馬校訂在使用混合的時候會出現問題。這是由於,校訂後致使寫入color buffer的顏色是非線性的,這樣混合就發生在非線性空間中。一種解決方法時,在中間計算時不要對輸出進行伽馬校訂,在最後進行一個屏幕後處理操做對最後的輸出進行伽馬校訂,但很顯然這會形成性能問題。 

還有一些細節問題,例如在進行屏幕後處理的時候,要當心咱們目前正在處理的圖像究竟是不是已經伽馬校訂後的。 

總之,一切工做都是爲了「保證全部的輸入都轉換到線性空間,並在線性空間下作各類光照計算,最後的輸出(最最最最後的輸出)進行伽馬校訂後再顯示」。 

雖然Unity的這個設置很是方便,可是其支持的平臺有限,目前還不支持移動平臺。也就是說,在安卓、iOS上咱們沒法使用這個設置。所以,對於移動平臺,咱們須要像上面給的代碼那樣,手動對非線性紋理進行轉換,並在最後輸出時再進行一次轉換。但這又會致使混合錯誤的問題。 

在Unity中使用Linear Space


若是咱們在Edit -> Project Settings -> Player -> Other Settings中使用了Linear Space,那麼以前的光照、混合問題均可以解決(這裏的解決是說和真實場景更接近)。但在處理紋理時須要注意,全部Unity會把全部輸入紋理都設置成sRGB格式,也就說,全部紋理都會被硬件當成一個非線性紋理,使用一個display gamma(一般是2.2)進行處理後,再傳遞給shader。但有時,輸入紋理並非非線性紋理就會發生問題。 

例如,咱們繪製一個亮度爲127/255的紋理,傳給shader後乘以2後進行顯示: 

這裏寫圖片描述 這裏寫圖片描述

能夠看出,Gamma Space的反而更加正確。這是由於,咱們的輸入紋理已是線性了,而Unity錯誤地又進行了sRGB的轉換處理。這樣一來,右邊顯示的亮度實際是,(pow(0.5, 2.2) * 2, 1/2.2)。 

爲了告訴Unity,「嘿,這張紋理就是線性的,不用你再處理啦」,能夠在Texture的面板中設置: 
這裏寫圖片描述

上面的「Bypass sRGB Sample」就是告訴Untiy要繞過sRGB處理,「它是啥就是啥!」。 

這樣設置後,就能夠獲得正確採樣結果了。 

寫在最後


伽馬校訂一直是個衆說紛紜的故事,固然我寫的這篇也極可能會有一些錯誤,若是您能指出不勝感激。 

即使關於一些細節問題說法不少,但本質是不變的。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輕鬆地完成,須要注意的是對紋理的處理。但不幸的是,不支持移動平臺。 

最後,一句忠告,在遊戲渲染的時候必定要考慮伽馬校訂的問題,不然就很可貴到很是真實的效果

下面有一些文章是我以爲很好的資料,可是其中有不少說法是有爭議的,但願你們能本身評估: 

相關文章
相關標籤/搜索