天天看點

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

寫在前面

我相信幾乎所有做圖像處理方面的人都聽過伽馬校正(Gamma Correction)這一個名詞,但真正明白它是什麼、為什麼要有它、以及怎麼用它的人其實不多。我也不例外。

最初我查過一些資料,但很多文章的說法都不一樣,有些很晦澀難懂。直到我最近在看《Real Time Rendering,3rd Edition》這本書的時候,才開始慢慢對它有所了解。

本人才疏學淺,寫的這篇文章很可能成為網上另一篇誤導你的“伽馬傳說”,但我盡可能把目前了解的資料和可能存在的疏漏寫在這裡。如有錯誤,還望指出。

伽馬的傳說

關于這個方面,龔大寫過一篇文章,但我認為其中的說法有不準确的地方。

從我找到的資料來看,人們使用伽馬曲線來進行顯示最開始是源于一個巧合:在早期,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)之間的關系。

如下圖所示:

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

而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):

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

為了中和這種現象,是以我們需要乘積不是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:

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

這張圖說明一件事情,即亮度上的線性變化在人眼看來是非均勻的,再通俗點,從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來顯示圖像。

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

而不幸的是,在遊戲界長期以來都忽視了伽馬校正的問題,也造成了為什麼我們渲染出來的遊戲總是暗沉沉的,總是和真實世界不像。

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

以下實驗均在Unity中進行。

光照

我們來看一個最簡單的場景:在場景中放置一個球,使用預設的Diffuse材質,打一個平行光:

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

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

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

假設球上有一點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)", D) = "white" {}
        _Color ("Color", Color) = (, , , )
    }
    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(, antialias, d);
                return float4(color,  - t);
            }

            float4 frag(v2f i) : SV_Target {
                float4 background = float4();
                float4 layer1 = circle(i.uv, float2(, ), , _Color.rgb, );

                float4 fragColor = float4();
                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):

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

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

正确的顯示結果應該是:

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

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

非線性輸入

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

為了直接顯示時可以正确顯示,大多數圖像檔案都進行了提前的校正,即已經使用了一個encoding gamma對像素值編碼。但這意味着它們是非線性的,如果在shader中直接使用會造成在非線性空間的計算,使得結果和真實世界的結果不一緻。

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 Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

它有兩個選項:一個是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中進行伽馬校正。對非線性輸入紋理的校正通常代碼如下:

在最後輸出前,對輸出像素值的校正代碼通常長下面這樣:

fragColor.rgb = pow(fragColor.rgb, /);
return fragColor;
           

但是,手工對輸出像素進行伽馬校正在使用混合的時候會出現問題。這是因為,校正後導緻寫入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 Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後
【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

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

為了告訴Unity,“嘿,這張紋理就是線性的,不用你再處理啦”,可以在Texture的面闆中設定:

【圖形學】我了解的伽馬校正(Gamma Correction)寫在前面伽馬的傳說來自其他領域的伽馬傳說這和渲染有什麼關系進行伽馬校正寫在最後

上面的“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輕松地完成,需要注意的是對紋理的處理。但不幸的是,不支援移動平台。

最後,一句忠告,在遊戲渲染的時候一定要考慮伽馬校正的問題,否則就很難得到非常真實的效果。

下面有一些文章是我覺得很好的資料,但是其中有很多說法是有争議的,希望大家能自己評估:

  • http://http.developer.nvidia.com/GPUGems3/gpugems3_ch24.html
  • 《Real-Time Rendering, Third Edition》5.8. Gamma Correction
  • http://www.klayge.org/2011/02/26/gamma%E7%9A%84%E4%BC%A0%E8%AF%B4/
  • http://qiankanglai.me/misc/2014/12/24/gamma-correction/
  • http://docs.unity3d.com/Manual/LinearLighting.html
  • 知乎上的讨論,看了很淩亂~啊啊啊啊~

繼續閱讀