第三十一章
高級光照 • 伽馬矯正
一旦我們計算出場景的最終像素顏色,我們就必須將它們顯示在螢幕上。在數位成像的早期,大多數螢幕都是陰極射線管(CRT)螢幕。這些螢幕具有物理特性,即兩倍的輸入電壓並不會產生兩倍的亮度。將輸入電壓加倍會產生一個與指數關係約為 2.2(稱為螢幕的伽馬值)相等的亮度。這(巧合地)也與人類測量亮度的方式非常吻合,因為亮度也以類似的(反向)冪關係顯示。為了更好地理解這一切意味著什麼,請看下圖:
頂部那一行看起來像是人眼正確的亮度比例尺,將亮度加倍(例如從 0.1 到 0.2)確實看起來亮度是原來的兩倍,並且具有良好的一致性差異。然而,當我們談論光的物理亮度,例如從光源發出的光子數量時,底部比例尺實際上顯示了正確的亮度。在底部比例尺上,亮度加倍會返回正確的物理亮度,但由於我們的眼睛感知亮度的方式不同(對暗色的變化更敏感),所以看起來很奇怪。
因為人眼偏好根據頂部比例尺來觀看亮度顏色,所以顯示器(即使是今天)仍然使用冪次關係來顯示輸出顏色,以便將原始的物理亮度顏色映射到頂部比例尺中的非線性亮度顏色。
顯示器的這種非線性映射確實為我們的眼睛輸出了更令人愉悅的亮度結果,但是當談到渲染圖形時,存在一個問題:我們在應用程式中配置的所有顏色和亮度選項都基於我們從顯示器中感知到的內容,因此所有選項實際上都是非線性的亮度/顏色選項。請看下面的圖表:
虛線表示線性空間中的顏色/光線值,實線表示顯示器顯示的色彩空間。如果我們將線性空間中的顏色加倍,其結果確實是值的兩倍。例如,取一個光的顏色向量 (0.5, 0.0, 0.0),它代表一種半暗的紅色光。如果我們將這種光在線性空間中加倍,它將變成 (1.0, 0.0, 0.0),如圖所示。然而,原始顏色在顯示器上顯示為 (0.218, 0.0, 0.0),你可以從圖中看到。問題開始浮現:一旦我們將線性空間中的暗紅色光加倍,它在顯示器上實際亮度會增加 4.5 倍以上!
直到本章為止,我們都假設我們在線性空間中工作,但我們實際上一直在顯示器的輸出空間中工作,因此我們配置的所有顏色和照明變數都不是物理正確的,而只是在我們的顯示器上看起來(某種程度上)正確。因此,我們(和藝術家)通常將照明值設定得比實際應有的亮度高得多(因為顯示器會將它們變暗),這導致大多數線性空間計算不正確。請注意,顯示器(CRT)和線性圖都從相同的位置開始和結束;是顯示器使中間值變暗了。
由於顏色是根據顯示器的輸出配置的,因此線性空間中的所有中間(照明)計算都是物理上不正確的。當採用更高級的照明演算法時,這會變得更加明顯,如下圖所示:
你可以看到,透過伽馬校正,(更新後的)顏色值能更好地協同工作,較暗的區域也能顯示更多細節。總體而言,只需稍作修改即可獲得更好的影像品質。
如果沒有正確校正顯示器伽馬,光照會看起來不正確,藝術家也很難獲得逼真且美觀的結果。解決方案是應用 伽馬校正
。
伽馬校正
伽馬校正的理念是在將最終輸出顏色顯示到顯示器之前,對其應用顯示器伽馬的反轉。回顧本章前面伽馬曲線圖,我們看到另一條「虛線」,它是顯示器伽馬曲線的反轉。我們將每個線性輸出顏色乘以這個反轉伽馬曲線(使其變亮),一旦顏色顯示在顯示器上,顯示器的伽馬曲線就會被應用,並且最終的顏色會變成線性的。我們有效地增亮了中間顏色,以便一旦顯示器將其變暗,所有內容都會平衡。
讓我們再舉一個例子。假設我們再次有暗紅色 $(0.5, 0.0, 0.0)$。在將此顏色顯示到顯示器之前,我們首先將伽馬校正曲線應用於顏色值。顯示器顯示的線性顏色大約以 $2.2$ 的冪次縮放,因此反轉需要以 $1/2.2$ 的冪次縮放顏色。因此,伽馬校正後的暗紅色變為 $(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)$。你可以看到,透過使用伽馬校正,顯示器現在最終顯示的顏色與我們在應用程式中線性設定的顏色相同。
伽馬值 2.2 是一個預設的伽馬值,它大致估計了大多數顯示器的平均伽馬值。由於這個 2.2 的伽馬值而產生的色彩空間稱為 sRGB
色彩空間(不完全精確,但很接近)。每個顯示器都有自己的伽馬曲線,但 2.2 的伽馬值在大多數顯示器上都能產生良好的效果。因此,遊戲通常允許玩家更改遊戲的伽馬設定,因為它會因顯示器而異。
有兩種方法可以將伽馬校正應用於你的場景:
- 透過使用 OpenGL 內建的 sRGB 幀緩衝支援。
- 透過在片段著色器中自行進行伽馬校正。
第一個選項可能最簡單,但控制較少。透過啟用 GL_FRAMEBUFFER_SRGB
,你告訴 OpenGL 每個後續的繪圖命令在將顏色儲存到顏色緩衝區之前,應該先對顏色進行伽馬校正(從 sRGB 色彩空間)。sRGB 是一種色彩空間,大致對應於 2.2 的伽馬值,並且是大多數裝置的標準。啟用 GL_FRAMEBUFFER_SRGB
後,OpenGL 會在每次片段著色器執行後自動對所有後續的幀緩衝區(包括預設幀緩衝區)執行伽馬校正。
啟用 GL_FRAMEBUFFER_SRGB
就像呼叫 glEnable
一樣簡單:
glEnable(GL_FRAMEBUFFER_SRGB);
從現在開始,你渲染的影像將會進行伽馬校正,由於這是由硬體完成的,因此完全沒有額外開銷。使用這種方法(以及另一種方法)時需要記住的是,伽馬校正也會將顏色從線性空間轉換為非線性空間,因此只在最後一步進行伽馬校正非常重要。如果你在最終輸出之前對顏色進行伽馬校正,則對這些顏色進行的所有後續操作都將在不正確的值上進行。例如,如果你使用多個幀緩衝區,你可能希望在幀緩衝區之間傳遞的中間結果保持在線性空間中,並且只讓最後一個幀緩衝區在發送到顯示器之前應用伽馬校正。
第二種方法需要更多工作,但也讓我們可以完全控制伽馬操作。我們在每次相關的片段著色器執行結束時應用伽馬校正,這樣最終顏色在發送到顯示器之前就會進行伽馬校正:
void main()
{
// do super fancy lighting in linear space
[...]
// apply gamma correction
float gamma = 2.2;
FragColor.rgb = pow(fragColor.rgb, vec3(1.0/gamma));
}
最後一行程式碼有效地將 fragColor
的每個顏色分量提升到 1.0/gamma
,校正了這個片段著色器運行的輸出顏色。
這種方法的一個問題是,為了保持一致性,你必須將伽馬校正應用到每個有助於最終輸出的片段著色器。如果你有多個物件的十幾個片段著色器,你必須將伽馬校正程式碼添加到每個著色器中。一個更簡單的解決方案是在你的渲染迴圈中引入一個後處理階段,並在後處理的四邊形上應用伽馬校正作為最後一步,這樣你只需要做一次。
那一條線代表了伽馬校正的技術實現。雖然不是那麼令人印象深刻,但在進行伽馬校正時還有一些額外的事情需要考慮。
sRGB 紋理
由於顯示器以應用伽馬的方式顯示顏色,因此每當你在電腦上繪製、編輯或描繪圖片時,你都是根據你在顯示器上看到的內容來選擇顏色。這有效地意味著你創建或編輯的所有圖片都不在線性空間中,而是在 sRGB 空間中,例如,根據感知亮度將螢幕上的暗紅色加倍,並不等於紅色分量加倍。
因此,當紋理藝術家憑藉肉眼創作藝術時,所有紋理的值都在 sRGB 空間中,所以如果我們在渲染應用程式中使用這些紋理時,我們必須考慮這一點。在我們了解伽馬校正之前,這實際上不是問題,因為紋理在 sRGB 空間中看起來很好,這與我們工作的空間相同;紋理完全按照它們的樣子顯示,這很好。然而,現在我們將所有內容顯示在線性空間中,紋理顏色將會出現偏差,如下圖所示:
紋理圖像太亮了,這是因為它實際上被伽馬校正了兩次!想想看,當我們根據在顯示器上看到的內容創建圖像時,我們有效地對圖像的顏色值進行了伽馬校正,使其在顯示器上看起來正確。因為我們隨後又在渲染器中進行伽馬校正,所以圖像最終會變得太亮。
要解決這個問題,我們必須確保紋理藝術家在線性空間中工作。然而,由於在 sRGB 空間中工作更容易,而且大多數工具甚至不支援線性紋理,這可能不是首選的解決方案。
另一個解決方案是在對 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, data);
如果你也想在紋理中包含 alpha 分量,你必須將紋理的內部格式指定為 GL_SRGB_ALPHA
。
在 sRGB 空間中指定紋理時應該小心,因為並非所有紋理都實際在 sRGB 空間中。用於著色物體(如漫反射紋理)的紋理幾乎總是在 sRGB 空間中。用於檢索光照參數(如鏡面貼圖和法線貼圖)的紋理幾乎總是在線性空間中,所以如果你將這些紋理配置為 sRGB 紋理,光照會看起來很奇怪。請注意你將哪些紋理指定為 sRGB。
透過將我們的漫反射紋理指定為 sRGB 紋理,你將再次獲得預期的視覺輸出,但這次所有內容都只進行了一次伽馬校正。
衰減
伽馬校正還有一個不同的地方是光照衰減。在真實的物理世界中,光照衰減與光源距離的平方呈反比關係。用通俗的語言來說,就是光強度隨光源距離的平方而減小,如下所示:
float attenuation = 1.0 / (distance * distance);
然而,當使用這個方程式時,衰減效果通常會過於強烈,導致光線半徑很小,看起來不符合物理現實。因此,會使用其他衰減函數(就像我們在基礎光照章節中討論的那樣),這些函數提供更多的控制,或者使用線性等效函數:
float attenuation = 1.0 / distance;
與其二次方變體相比,線性等效函數在沒有伽馬校正的情況下提供了更合理的效果,但當我們啟用伽馬校正時,線性衰減看起來太弱,而物理上正確的二次衰減突然提供了更好的結果。下圖顯示了差異:
造成這種差異的原因是光衰減函數會改變亮度,由於我們沒有在線性空間中可視化我們的場景,我們選擇了在我們的顯示器上看起來最好的衰減函數,但它們並不符合物理原理。想想平方衰減函數:如果我們在沒有伽馬校正的情況下使用這個函數,當顯示在顯示器上時,衰減函數實際上會變成:$(1.0 / \text{distance}^2)^{2.2}$。這會產生比我們最初預期的更大的衰減。這也解釋了為什麼在沒有伽馬校正的情況下,線性等效函數更有意義,因為它實際上會變成 $(1.0 / \text{distance})^{2.2} = 1.0 / \text{distance}^{2.2}$,這與其物理等效函數更為相似。
我們在基礎光照章節中討論的更高級的衰減函數在伽馬校正場景中仍然有其作用,因為它提供了對精確衰減的更多控制(但當然在伽馬校正場景中需要不同的參數)。
你可以在這裡找到這個簡單範例場景的原始碼。透過按下空格鍵,我們可以在伽馬校正和未校正的場景之間切換,兩個場景都使用其紋理和衰減等效值。這不是最令人印象深刻的範例,但它確實展示了如何實際應用所有技術。
總結來說,伽馬校正允許我們在線性空間中進行所有著色器/光照計算。由於線性空間在物理世界中是合理的,因此大多數物理方程式現在實際上都能提供良好的結果(例如真實的光照衰減)。你的光照越複雜,使用伽馬校正就越容易獲得美觀(和逼真)的結果。這也是為什麼建議在伽馬校正到位後才真正調整光照參數的原因。
額外資源
- 每個程式設計師都應該知道的伽馬知識:一篇由 John Novak 撰寫的關於伽馬校正的深度好文。
- www.cambridgeincolour.com:更多關於伽馬和伽馬校正的資訊。
- blog.wolfire.com:David Rosen 撰寫的部落格文章,關於在圖形渲染中伽馬校正的好處。
- renderwonk.com:一些額外的實務考量。
- 1. 介绍
- 2. 开始 • 认识 OpenGL
- 3. 开始 • 创建一个窗口
- 4. 开始 • Hello, 窗口
- 5. 开始 • Hello, 三角形
- 6. 开始 • 著色器
- 7. 开始 • 紋理
- 8. 开始 • 轉換
- 9. 开始 • 座標系統
- 10. 开始 • 相機
- 11. 光 • 顏色
- 12. 光 • 基本光照
- 13. 光 • 材質
- 14. 光 • 光照貼圖
- 15. 光 • 光源
- 16. 光 • 多重光源
- 17. 模型載入 • Assimp
- 18. 模型載入 • Mesh
- 19. 模型載入 • Model
- 20. 高級 OpenGL • 深度測試
- 21. 高級 OpenGL • 模板測試
- 22. 高級 OpenGL • 混合
- 23. 高級 OpenGL • 面剔除
- 24. 高級 OpenGL • Framebuffers
- 25. 高級 OpenGL • Cubemaps
- 26. Advanced-OpenGL • Advanced-Data
- 27. Advanced-OpenGL • Advanced-GLSL
- 28. Advanced-OpenGL • Instancing
- 29. Advanced-OpenGL • Anti-Aliasing
- 30. 高級光照
- 31. 高級光照 • 伽馬矯正
- 32. 高級光照 • Shadow-Mapping
- 33. Advanced-Lighting • Point-Shadows
- 34. 高級光照 • 法線貼圖 (Normal Mapping)
- 35. 高級光照 • 視差貼圖(Parallax Mapping)
- 36. 高級光照 • HDR
- 37. 高級光照 • 輝光(Bloom)
- 38. 高級光照 • 延遲著色
- 39. 高級光照 • SSAO