1. 程式人生 > 實用技巧 >Gamma、Linear、sRGB 和Unity Color Space,你真懂了嗎?

Gamma、Linear、sRGB 和Unity Color Space,你真懂了嗎?

“為什麼我渲染出來的場景,總是感覺和真實世界不像呢?”

遊戲從業者或多或少都聽過Linear、Gamma、sRGB和伽馬校正這些術語,網際網路上也有很多科普的資料,但是它們似乎又都沒有講很"清楚"。

遊戲界(特別是中小團隊)很容易忽略這些概念造成的影響。長遠來看,作為遊戲從業者的你應該理解這些術語的含義,理解它們的本質聯絡,理解選擇Linear或Gamma 空間帶來的工作流變化。

本文將會簡單介紹Gamma、Linear、sRGB和伽馬校正的概念。接著通過例項解析統一到線性空間的步驟,最後介紹如何在Unity中實施相應的工作流。

什麼是Linear、Gamma、sRGB和伽馬校正?

在物理世界中,如果光的強度增加一倍,那麼亮度也會增加一倍,這是線性關係。

而歷史上最早的顯示器(陰極射線管)顯示影象的時候,電壓增加一倍,亮度並不跟著增加一倍。即輸出亮度和電壓並不是成線性關係的,而是呈亮度增加量等於電壓增加量的2.2次冪的非線性關係:

2.2也叫做該顯示器的Gamma值,現代顯示器的Gamma值也都大約是2.2。

這種關係意味著當電壓線性變化時,相對於真實世界來說,亮度的變化在暗處變換較慢,暗佔據的資料範圍更廣,顏色整體會偏暗。

如圖,直線代表物理世界的線性空間(Linear Space),下曲線是顯示器輸出的Gamma2.2空間(Gamma Space)。

橫座標表示電壓,縱座標表示亮度

好了,正常情況下,人眼看物理世界感知到了正常的亮度。而如果顯示器輸出一個顏色後再被你看到,即相當於走了一次Gamma2.2曲線的調整,這下子顏色就變暗了。如果我們在顯示器輸出之前,做一個操作把顯示器的Gamma2.2影響平衡掉,那就和人眼直接觀察物理世界一樣了!這個平衡的操作就叫做伽馬校正。

在數學上,伽馬校正是一個約0.45的冪運算(和上面的2.2次冪互為逆運算):

左(Gamma0.45) 中(Gamma2.2) 右(線性物理空間)

經過0.45冪運算,再由顯示器經過2.2次冪輸出,最後的顏色就和實際物理空間的一致了。

最後,什麼是sRGB呢?1996年,微軟和惠普一起開發了一種標準sRGB色彩空間。這種標準得到許多業界廠商的支援。sRGB對應的是Gamma0.45所在的空間。

為什麼sRGB在Gamma0.45空間?

假設你用數碼相機拍一張照片,你看了看照相機螢幕上顯示的結果和物理世界是一樣的。可是照相機要怎麼儲存這張圖片,使得它在所有顯示器上都一樣呢? 可別忘了所有顯示器都帶Gamma2.2。反推一下,那照片只能儲存在Gamma0.45空間,經過顯示器的Gamma2.2調整後,才和你現在看到的一樣。換句話說,sRGB格式相當於對物理空間的顏色做了一次伽馬校正。

還有另外一種解釋,和人眼對暗的感知更加敏感的事實有關。

如圖,在真實世界中(下方),如果光的強度從0.0逐步增加到1.0,那麼亮度應該是線性增加的。但是對於人眼來說(上方),感知到的亮度變化卻不是線性的,而是在暗的地方有更多的細節。換句話說,我們應該用更大的資料範圍來存暗色,用較小的資料範圍來存亮色。這就是sRGB格式做的,定義在Gamma0.45空間。而且還有一個好處就是,由於顯示器自帶Gamma2.2,所以我們不需要額外操作顯示器就能顯示回正確的顏色。

以上內容,看完後還是不懂也沒關係,在繼續之前你可以先死記住以下幾個知識點:

  • 顯示器的輸出在Gamma2.2空間。
  • 伽馬校正會將顏色轉換到Gamma0.45空間。
  • 伽馬校正和顯示器輸出平衡之後,結果就是Gamma1.0的線性空間。
  • sRGB對應Gamma0.45空間。

統一到線性空間

現在假設你對上文的概念有一定認識了,我們來講重點吧。

在Gamma 或 Linear空間的渲染結果是不同的,從表現上說,在Gamma Space中渲染會偏暗,在Linear Space中渲染會更接近物理世界,更真實:

左(Gamma Space),右(Linear Space)

為什麼Linear Space更真實?

你可以這麼想,物理世界中的顏色和光照規律都是線上性空間描述的對吧?(光強度增加了一倍,亮度也增加一倍)。 而計算機圖形學是物理世界視覺的數學模型,Shader中顏色插值、光照的計算自然也是線上性空間描述的。如果你用一個非線性空間的輸入,又線上性空間中計算,那結果就會有一點“不自然”。

換句話說,如果所有的輸入,計算,輸出,都能統一線上性空間中,那麼結果是最真實的,玩家會說這個遊戲畫質很強很真實。事實上因為計算這一步已經是線上性空間描述的了,所以只要保證輸入輸出是線上性空間就行了。

所以為什麼你的遊戲畫面不真實呢?因為你可能對此混亂了,你的輸入或輸出在Gamma Space,又沒搞清楚每個紋理應該在什麼Space,甚至也不知道有沒用伽馬校正,渲染結果怎麼會真實呢?

現在假設我們的目標是獲得最真實的渲染,因此需要統一渲染過程線上性空間,怎麼做呢?

注:統一在Linear空間是最真實的,但不代表不統一就是錯的。一般來說,如果是畫質要求高的作品(如3A)等,那麼都是統一的。沒這方面要求的則未必是統一的,還有一些專案追求非真實的渲染,它們也未必需要統一。

統一到線性空間的過程是看起來是這樣的,用圖中橙色的框表示(現在看不懂圖沒關係,跟著後面的步驟來一步步看):

我們從橙色框的左上角出發。

第一步,輸入的紋理如果是sRGB(Gamma0.45),那我們要進行一個操作轉換到線性空間。這個操作叫做Remove Gamma Correction,在數學上是一個2.2的冪運算。如果輸入不是sRGB,而是已經線上性空間的紋理了呢?那就可以跳過Remove Gamma Correction了。

注:美術輸出資源時都是在sRGB空間的,但Normal Map等其他電腦計算出來的紋理則一般線上性空間,即Linear Texture。詳見後文!

第二步,現在輸入已經線上性空間了,那麼進行Shader中光照、插值等計算後就是比較真實的結果了(上文解釋了哦~),如果不對sRGB進行Remove Gamma Correction直接就進入Shader計算,那算出來的就會不自然,就像前面那兩張球的光照結果一樣。

第三步,Shader計算完成後,需要進行Gamma Correction,從線性空間變換到Gamma0.45空間,在數學上是一個約為0.45的冪運算。如果不進行Gamma Correction輸出會怎麼樣?那顯示器就會將顏色從線性空間轉換到Gamma2.2空間,接著再被你看到,結果會更暗。

第四步,經過了前面的Gamma Correction,顯示器輸出在了線性空間,這就和人眼看物理世界的過程是一樣的了!

我們再舉個例子,我們取sRGB紋理裡面的一個畫素,假設其值為0.73。那麼在統一線性空間的過程中,它的值是怎麼變化的?

第一步,0.73(上曲線) * [Remove Gamma Correction] = 0.5(直線)。()

第二步,0.5(直線) * [Shader] = 0.5(直線)(假設我們的Shader啥也不幹保持顏色不變)

第三步,0.5(直線) * [Gamma Correction] = 0.73(上曲線)。()

第四步,0.73(上曲線) * [顯示器] = 0.5(直線)。()

如果不進行Gamma Correction,就會變暗,因為第三步不存在了,第四步就會變成:

0.5(直線) * [顯示器] = 0.218(下曲線)。()

再對照上面的圖琢磨琢磨?

Unity中的Color Space

我們回到Unity,在ProjectSetting中,你可以選擇Gamma 或 Linear作為Color Space:

這兩者有什麼區別呢?

如果選擇了Gamma,那Unity不會對輸入和輸出做任何處理,換句話說,Remove Gamma Correction 、Gamma Correction都不會發生,除非你自己手動實現。

如果選了Linear,那麼就是上文提到的統一線性空間的流程了。對於sRGB紋理,Unity在進行紋理取樣之前會自動進行Remove Gamma Correction,對於Linear紋理則沒有這一步。而在輸出前,Unity會自動進行Gamma Correction再讓顯示器輸出。

怎麼告訴Unity紋理是sRGB還是Linear呢?對於特定用途的紋理,你可以直接設定他們所屬的型別:如Normal Map、Light Map等都是Linear,設定好型別Unity自己會處理他們。

還有一些紋理不是上面的任何型別,但又已經線上性空間了(比如說Mask紋理、噪聲圖),那你需要取消sRGB這個選項讓它跳過Remove Gamma Correction過程:

到底什麼紋理應該是sRGB,什麼是Linear?

關於這一點,我個人有一個理解:所有需要人眼參與被創作出來的紋理,都應是sRGB(如美術畫出來的圖)。所有通過計算機計算出來的紋理(如噪聲,Mask,LightMap)都應是Linear。

這很好解釋,人眼看東西才需要考慮顯示特性和校正的問題。而對計算機來說不需要,在計算機看來只是普通資料,自然直接選擇Linear是最好的。

除了紋理外,在Linear Space下,Shaderlab中的顏色輸入也會被認為是sRGB顏色,會自動進行Gamma Correction Removed。

有時候你可能需要想讓一個Float變數也進行Gamma Correction Removed,那麼就需要在ShaderLab中使用[Gamma]字首:

[Gamma]_Metallic("Metallic",Range(0,1))=0

如上面的程式碼,來自官方的Standard Shader原始碼,其中的_Metallic這一項就帶了[Gamma]字首,表示在Lienar Space下Unity要將其認為在sRGB空間,進行Gamma Correction Removed。

擴充套件:為什麼官方原始碼中_Metallic項需要加[Gamma]?這和底層的光照計算中考慮能量守恆的部分有關,Metallic代表了物體的“金屬度”,如果值越大則反射(高光)越強,漫反射會越弱。在實際的計算中,這個強弱的計算和Color Space有關,所以需要加上[Gamma]項。

雖然Linear是最真實的,但是Gamma畢竟少了中間處理,渲染開銷會更低,效率會更高。上文也說過不真實不代表是錯的,畢竟圖形學第一定律:如果它看上去是對的,那麼它就是對的。

注:在Android上,Linear只在OpenGL ES 3.0和Android 4.3以上支援,iOS則只有Metal才支援。

在早期移動端上不支援Linear Space流程,所以需要考慮更多。不過隨著現在手機遊戲的發展,越來越多追求真實的專案出現,很多專案都選擇直接在Linear Space下工作。

一旦確定好Color Space,那麼就需要渲染工程師、技術美術和美術商量和統一好工作流了。在中小團隊或專案中,這些概念很容易被忽略,導致工作流混亂,渲染效果不盡人意。現在你懂了嗎?