OpenGL學習筆記一之高級光照篇二 Gamma校正

轉載自 https://learnopengl-cn.github.io/05%20Advanced%20Lighting/02%20Gamma%20Correction/

本節暫未進行完全的重寫,錯誤可能會很多。如果可能的話,請對照原文進行閱讀。如果有報告本節的錯誤,將會延遲至重寫之後進行處理。

當我們計算出場景中所有像素的最終顏色以後,我們就必須把它們顯示在監視器上。過去,大多數監視器是陰極射線管顯示器(CRT)。這些監視器有一個物理特性就是兩倍的輸入電壓產生的不是兩倍的亮度。輸入電壓產生約爲輸入電壓的2.2次冪的亮度,這叫做監視器Gamma。

譯註

Gamma也叫灰度係數,每種顯示設備都有自己的Gamma值,都不相同,有一個公式:設備輸出亮度 = 電壓的Gamma次冪,任何設備Gamma基本上都不會等於1,等於1是一種理想的線性狀態,這種理想狀態是:如果電壓和亮度都是在0到1的區間,那麼多少電壓就等於多少亮度。對於CRT,Gamma通常爲2.2,因而,輸出亮度 = 輸入電壓的2.2次冪,你可以從本節第二張圖中看到Gamma2.2實際顯示出來的總會比預期暗,相反Gamma0.45就會比理想預期亮,如果你講Gamma0.45疊加到Gamma2.2的顯示設備上,便會對偏暗的顯示效果做到校正,這個簡單的思路就是本節的核心

人類所感知的亮度恰好和CRT所顯示出來相似的指數關係非常匹配。爲了更好的理解所有含義,請看下面的圖片:

第一行是人眼所感知到的正常的灰階,亮度要增加一倍(比如從0.1到0.2)你纔會感覺比原來變亮了一倍(譯註:這裏的意思是說比如一個東西的亮度0.3,讓人感覺它比原來變亮一倍,那麼現在這個亮度應該成爲0.6,而不是0.4,也就是說人眼感知到的亮度的變化並非線性均勻分佈的。問題的關鍵在於這樣的一倍相當於一個亮度級,例如假設0.1、0.2、0.4、0.8是我們定義的四個亮度級別,在0.1和0.2之間人眼只能識別出0.15這個中間級,而雖然0.4到0.8之間的差距更大,這個區間人眼也只能識別出一個顏色)。然而,當我們談論光的物理亮度,比如光源發射光子的數量的時候,底部(第二行)的灰階顯示出的纔是物理世界真實的亮度。如底部的灰階顯示,亮度加倍時返回的也是真實的物理亮度(譯註:這裏亮度是指光子數量和正相關的亮度,即物理亮度,前面討論的是人的感知亮度;物理亮度和感知亮度的區別在於,物理亮度基於光子數量,感知亮度基於人的感覺,比如第二個灰階裏亮度0.1的光子數量是0.2的二分之一),但是由於這與我們的眼睛感知亮度不完全一致(對比較暗的顏色變化更敏感),所以它看起來有差異。

因爲人眼看到顏色的亮度更傾向於頂部的灰階,監視器使用的也是一種指數關係(電壓的2.2次冪),所以物理亮度通過監視器能夠被映射到頂部的非線性亮度;因此看起來效果不錯(譯註:CRT亮度是是電壓的2.2次冪而人眼相當於2次冪,因此CRT這個缺陷正好能滿足人的需要)。

監視器的這個非線性映射的確可以讓亮度在我們眼中看起來更好,但當渲染圖像時,會產生一個問題:我們在應用中配置的亮度和顏色是基於監視器所看到的,這樣所有的配置實際上是非線性的亮度/顏色配置。請看下圖:

點線代表線性顏色/亮度值(譯註:這表示的是理想狀態,Gamma爲1),實線代表監視器顯示的顏色。如果我們把一個點線線性的顏色翻一倍,結果就是這個值的兩倍。比如,光的顏色向量L=(0.5,0.0,0.0)代表的是暗紅色。如果我們在線性空間中把它翻倍,就會變成(1.0,0.0,0.0),就像你在圖中看到的那樣。然而,由於我們定義的顏色仍然需要輸出的監視器上,監視器上顯示的實際顏色就會是(0.218,0.0,0.0)。在這兒問題就出現了:當我們將理想中直線上的那個暗紅色翻一倍時,在監視器上實際上亮度翻了4.5倍以上!

直到現在,我們還一直假設我們所有的工作都是在線性空間中進行的(譯註:Gamma爲1),但最終還是要把所有的顏色輸出到監視器上,所以我們配置的所有顏色和光照變量從物理角度來看都是不正確的,在我們的監視器上很少能夠正確地顯示。出於這個原因,我們(以及藝術家)通常將光照值設置得比本來更亮一些(由於監視器會將其亮度顯示的更暗一些),如果不是這樣,在線性空間裏計算出來的光照就會不正確。同時,還要記住,監視器所顯示出來的圖像和線性圖像的最小亮度是相同的,它們最大的亮度也是相同的;只是中間亮度部分會被壓暗。

因爲所有中間亮度都是線性空間計算出來的(譯註:計算的時候假設Gamma爲1)監視器顯以後,實際上都會不正確。當使用更高級的光照算法時,這個問題會變得越來越明顯,你可以看看下圖:

Gamma校正

Gamma校正(Gamma Correction)的思路是在最終的顏色輸出上應用監視器Gamma的倒數。回頭看前面的Gamma曲線圖,你會有一個短劃線,它是監視器Gamma曲線的翻轉曲線。我們在顏色顯示到監視器的時候把每個顏色輸出都加上這個翻轉的Gamma曲線,這樣應用了監視器Gamma以後最終的顏色將會變爲線性的。我們所得到的中間色調就會更亮,所以雖然監視器使它們變暗,但是我們又將其平衡回來了。

我們來看另一個例子。還是那個暗紅色(0.5,0.0,0.0)。在將顏色顯示到監視器之前,我們先對顏色應用Gamma校正曲線。線性的顏色顯示在監視器上相當於降低了2.22次冪的亮度,所以倒數就是1/2.2次冪。Gamma校正後的暗紅色就會成爲(0.5,0.0,0.0)^1/2.2=(0.5,0.0,0.0)^0.45=(0.73,0.0,0.0)。校正後的顏色接着被髮送給監視器,最終顯示出來的顏色是(0.73,0.0,0.0)^2.2=(0.5,0.0,0.0)。你會發現使用了Gamma校正,監視器最終會顯示出我們在應用中設置的那種線性的顏色。

2.2通常是是大多數顯示設備的大概平均gamma值。基於gamma2.2的顏色空間叫做sRGB顏色空間。每個監視器的gamma曲線都有所不同,但是gamma2.2在大多數監視器上表現都不錯。出於這個原因,遊戲經常都會爲玩家提供改變遊戲gamma設置的選項,以適應每個監視器(譯註:現在Gamma2.2相當於一個標準,後文中你會看到。但現在你可能會問,前面不是說Gamma2.2看起來不是正好適合人眼麼,爲何還需要校正。這是因爲你在程序中設置的顏色,比如光照都是基於線性Gamma,即Gamma1,所以你理想中的亮度和實際表達出的不一樣,如果要表達出你理想中的亮度就要對這個光照進行校正)。

有兩種在你的場景中應用gamma校正的方式:

使用OpenGL內建的sRGB幀緩衝。 自己在像素着色器中進行gamma校正。 第一個選項也許是最簡單的方式,但是我們也會喪失一些控制權。開啓GL_FRAMEBUFFER_SRGB,可以告訴OpenGL每個後續的繪製命令裏,在顏色儲存到顏色緩衝之前先校正sRGB顏色。sRGB這個顏色空間大致對應於gamma2.2,它也是家用設備的一個標準。開啓GL_FRAMEBUFFER_SRGB以後,每次像素着色器運行後續幀緩衝,OpenGL將自動執行gamma校正,包括默認幀緩衝。

開啓GL_FRAMEBUFFER_SRGB簡單的調用glEnable就行:

glEnable(GL_FRAMEBUFFER_SRGB);

自此,你渲染的圖像就被進行gamma校正處理,你不需要做任何事情硬件就幫你處理了。有時候,你應該記得這個建議:gamma校正將把線性顏色空間轉變爲非線性空間,所以在最後一步進行gamma校正是極其重要的。如果你在最後輸出之前就進行gamma校正,所有的後續操作都是在操作不正確的顏色值。例如,如果你使用多個幀緩衝,你可能打算讓兩個幀緩衝之間傳遞的中間結果仍然保持線性空間顏色,只是給發送給監視器的最後的那個幀緩衝應用gamma校正。

第二個方法稍微複雜點,但同時也是我們對gamma操作有完全的控制權。我們在每個相關像素着色器運行的最後應用gamma校正,所以在發送到幀緩衝前,顏色就被校正了。

void main()
{
    // do super fancy lighting 
    [...]
    // apply gamma correction
    float gamma = 2.2;
    fragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}

最後一行代碼,將fragColor的每個顏色元素應用有一個1.0/gamma的冪運算,校正像素着色器的顏色輸出。

這個方法有個問題就是爲了保持一致,你必須在像素着色器里加上這個gamma校正,所以如果你有很多像素着色器,它們可能分別用於不同物體,那麼你就必須在每個着色器裏都加上gamma校正了。一個更簡單的方案是在你的渲染循環中引入後處理階段,在後處理四邊形上應用gamma校正,這樣你只要做一次就好了。

這些單行代碼代表了gamma校正的實現。不太令人印象深刻,但當你進行gamma校正的時候有一些額外的事情別忘了考慮。

sRGB紋理

因爲監視器總是在sRGB空間中顯示應用了gamma的顏色,無論什麼時候當你在計算機上繪製、編輯或者畫出一個圖片的時候,你所選的顏色都是根據你在監視器上看到的那種。這實際意味着所有你創建或編輯的圖片並不是在線性空間,而是在sRGB空間中(譯註:sRGB空間定義的gamma接近於2.2),假如在你的屏幕上對暗紅色翻一倍,便是根據你所感知到的亮度進行的,並不等於將紅色元素加倍。

結果就是紋理編輯者,所創建的所有紋理都是在sRGB空間中的紋理,所以如果我們在渲染應用中使用這些紋理,我們必須考慮到這點。在我們應用gamma校正之前,這不是個問題,因爲紋理在sRGB空間創建和展示,同樣我們還是在sRGB空間中使用,從而不必gamma校正紋理顯示也沒問題。然而,現在我們是把所有東西都放在線性空間中展示的,紋理顏色就會變壞,如下圖展示的那樣:

紋理圖像實在太亮了,發生這種情況是因爲,它們實際上進行了兩次gamma校正!想一想,當我們基於監視器上看到的情況創建一個圖像,我們就已經對顏色值進行了gamma校正,所以再次顯示在監視器上就沒錯。由於我們在渲染中又進行了一次gamma校正,圖片就實在太亮了。

爲了修復這個問題,我們得確保紋理製作者是在線性空間中進行創作的。但是,由於大多數紋理製作者並不知道什麼是gamma校正,並且在sRGB空間中進行創作更簡單,這也許不是一個好辦法。

另一個解決方案是重校,或把這些sRGB紋理在進行任何顏色值的計算前變回線性空間。我們可以這樣做:

float gamma = 2.2;
vec3 diffuseColor = pow(texture(diffuse, texCoords).rgb, vec3(gamma));

爲每個sRGB空間的紋理做這件事非常煩人。幸好,OpenGL給我們提供了另一個方案來解決我們的麻煩,這就是GL_SRGB和GL_SRGB_ALPHA內部紋理格式。

如果我們在OpenGL中創建了一個紋理,把它指定爲以上兩種sRGB紋理格式其中之一,OpenGL將自動把顏色校正到線性空間中,這樣我們所使用的所有顏色值都是在線性空間中的了。我們可以這樣把一個紋理指定爲一個sRGB紋理:

glTexImage2D(GL_TEXTURE_2D, 0, GL_SRGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, image);

如果你還打算在你的紋理中引入alpha元素,必究必須將紋理的內部格式指定爲GL_SRGB_ALPHA。

因爲不是所有紋理都是在sRGB空間中的所以當你把紋理指定爲sRGB紋理時要格外小心。比如diffuse紋理,這種爲物體上色的紋理幾乎都是在sRGB空間中的。而爲了獲取光照參數的紋理,像specular貼圖和法線貼圖幾乎都在線性空間中,所以如果你把它們也配置爲sRGB紋理的話,光照就壞掉了。指定sRGB紋理時要當心。

將diffuse紋理定義爲sRGB紋理之後,你將獲得你所期望的視覺輸出,但這次每個物體都會只進行一次gamma校正。

衰減

在使用了gamma校正之後,另一個不同之處是光照衰減(Attenuation)。真實的物理世界中,光照的衰減和光源的距離的平方成反比。

float attenuation = 1.0 / (distance * distance);

然而,當我們使用這個衰減公式的時候,衰減效果總是過於強烈,光只能照亮一小圈,看起來並不真實。出於這個原因,我們使用在基本光照教程中所討論的那種衰減方程,它給了我們更大的控制權,此外我們還可以使用雙曲線函數:

float attenuation = 1.0 / distance;

雙曲線比使用二次函數變體在不用gamma校正的時候看起來更真實,不過但我們開啓gamma校正以後線性衰減看起來太弱了,符合物理的二次函數突然出現了更好的效果。下圖顯示了其中的不同:

這種差異產生的原因是,光的衰減方程改變了亮度值,而且屏幕上顯示出來的也不是線性空間,在監視器上效果最好的衰減方程,並不是符合物理的。想想平方衰減方程,如果我們使用這個方程,而且不進行gamma校正,顯示在監視器上的衰減方程實際上將變成(1.0/distance^2)^2.2。若不進行gamma校正,將產生更強烈的衰減。這也解釋了爲什麼雙曲線不用gamma校正時看起來更真實,因爲它實際變成了(1.0/distance)^2.2=1.0/distance^2.2。這和物理公式是很相似的。

我們在基礎光照教程中討論的更高級的那個衰減方程在有gamma校正的場景中也仍然有用,因爲它可以讓我們對衰減擁有更多準確的控制權(不過,在進行gamma校正的場景中當然需要不同的參數)。

我創建的這個簡單的demo場景,你可以在這裏找到源碼以及頂點和像素着色器。按下空格就能在有gamma校正和無gamma校正的場景進行切換,兩個場景使用的是相同的紋理和衰減。這不是效果最好的demo,不過它能展示出如何應用所有這些技術。

總而言之,gamma校正使你可以在線性空間中進行操作。因爲線性空間更符合物理世界,大多數物理公式現在都可以獲得較好效果,比如真實的光的衰減。你的光照越真實,使用gamma校正獲得漂亮的效果就越容易。這也正是爲什麼當引進gamma校正時,建議只去調整光照參數的原因。

附加資源

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章