文/陳嘉棟 公眾號(hào):慕容的游戲編程

前言的前言

這篇小文其實(shí)是在清明節(jié)前后起的頭,不過后來一度擱筆。一直到這周末才又想起來起的這個(gè)頭還沒有寫完,所以還是直接用一個(gè)月前的開頭,再將過程和結(jié)尾補(bǔ)齊。

前言

結(jié)束了在南方一周的出差,清明時(shí)節(jié)回到了剛好下過雪并且和南方有20多度溫差的北京之后,終于有時(shí)間來寫點(diǎn)文字了。這篇小文,我主要想來聊一聊在使用Unity時(shí)和gamma校正相關(guān)的話題。事實(shí)上關(guān)于Gamma校正的來源歷史以及理論知識(shí)已經(jīng)有很多相關(guān)的文章了,比如龔大的《gamma的傳說》、Nvidia的Gpu Gems的文章等等。所以我在理論知識(shí)上只是稍作著墨,主要還是要來聊聊Unity中的Gamma校正的相關(guān)內(nèi)容。

顯示器和gamma校正

關(guān)于gamma校正來源的說法很多,具體可以參考龔大的《gamma的傳說》的顯示器說以及樂樂的《我理解的伽馬校正》中所提到的人眼視覺特點(diǎn)說。兩者都有道理,并且客觀上這兩個(gè)說法發(fā)生了有趣的巧合,最后達(dá)到了一個(gè)還不錯(cuò)的效果。

簡單來說,過去的CRT顯示器存在一個(gè)特點(diǎn),即屏幕上顯示的顏色對(duì)于傳遞而來的原始值并不是線性的(非線性) ,在這里非線性意味著以一個(gè)比率增加某個(gè)顏色分量,并不會(huì)導(dǎo)致顯示器屏幕上的光強(qiáng)度增加相同的比例。舉一個(gè)例子,假如我們將一個(gè)顏色的紅色分量變成之前的兩倍,顯示器的屏幕所顯示的紅光并不會(huì)變成之前的兩倍。

事實(shí)上CRT顯示器的輸入和輸出之間的關(guān)系近似于一個(gè)指數(shù)關(guān)系,而這個(gè)指數(shù)便是我們常常聽到的gamma。典型的gamma范圍在2.0到2.4之間,一般該值常常以2.2作為折中。雖然后來的LCD并不存在這個(gè)特點(diǎn),但是為了保證兼容,也選擇了和當(dāng)年CRT一樣的非線性特性。

111629bcjot3m6o3offzz7.jpg
上圖中的紅色實(shí)心線是在gamma = 2.2的情況下,顯示器實(shí)際顯示色彩強(qiáng)度的方式。 這一部分是由顯示器的特性導(dǎo)致的。所以如果圖片不做任何處理,經(jīng)過pow(2.2)的操作之后顯然會(huì)變得更暗,所以gamma校正就顯得很有必要了。而gamma校正要做的事情也十分簡單,即通過pow(1/2.2)將顏色強(qiáng)度提高,也就是上方的紅色虛線,這樣經(jīng)過顯示器時(shí)就會(huì)將顯示器的pow(2.2)抵消掉。

同時(shí),人眼對(duì)暗部的變化更加敏感,而對(duì)亮部變化其實(shí)不是很敏感。這可以以攝像作為一個(gè)例子,使用攝像機(jī)時(shí),攝像機(jī)會(huì)把進(jìn)入到鏡頭內(nèi)的光線亮度編碼成圖像中的像素。

例如人們看到下面這張圖,會(huì)自然而然的認(rèn)為中間的地方即灰度為0.5的地方。

111629cn6840e7u4ur2u6n.jpg
事實(shí)上,攝像機(jī)“看到的”光線亮度如下圖,上圖中間部分的灰度其實(shí)只是在0.2左右。

111629a0454r9997tx71k0.jpg
所以在只有8bit的情況下,沒有必要在亮部浪費(fèi)過多,這樣就可以表現(xiàn)更多暗部的細(xì)節(jié)變化,所以實(shí)際亮度只有0.2經(jīng)過gamma校正后實(shí)際被編碼成了0.5的像素值。

當(dāng)然,我個(gè)人認(rèn)為顯示器的特性是gamma校正出現(xiàn)的主要原因,而人眼對(duì)暗部的敏感而出現(xiàn)的gamma校正則更像是為了適應(yīng)顯示器這種特性而為的一種編碼策略的“優(yōu)化”。

硬件實(shí)現(xiàn)

sRGB 顏色空間是一個(gè)可以直接用來在顯示器上顯示的非線性顏色空間。

以O(shè)penGL作為圖形庫為例,Unity實(shí)現(xiàn)Gamma校正以及Linear workflow借助了OpenGL的 texture_sRGB 以及 framebuffer_sRGB 相關(guān)拓展,事實(shí)上是一種硬件層面的實(shí)現(xiàn)。

EXT_texture_sRGB會(huì)對(duì)texture做pow2.2的gamma校正,將輸入從sRGB空間轉(zhuǎn)換到線性空間;而framebuffer_sRGB如果開啟,并且輸出的目標(biāo)是sRGB顏色空間,則硬件會(huì)將結(jié)果再做一次pow0.45(為了方便,下文使用0.45代替1/2.2)的gamma校正,將結(jié)果從線性空間轉(zhuǎn)換到sRGB顏色空間。這樣保證輸入的是正確的數(shù)據(jù),并且中間的計(jì)算是線性的過程,最后的結(jié)果再轉(zhuǎn)移到sRGB空間來中和顯示器的輸出,這樣就能保證一個(gè)正確的光照計(jì)算結(jié)果。

下面我們可以通過RenderDoc來分別分析一下gamma workflow和linear workflow在安卓手機(jī)上的渲染流水線。

不過我在使用RenderDoc的目前正式版本(v1.0 - 6 Mar, 2018)時(shí)遇到了一些小問題,即我無法正常的通過RenderDoc啟動(dòng)我的App,總是會(huì)報(bào)下圖中的錯(cuò)誤。

111629bllve774upqpy0ll.jpg
這其實(shí)是這個(gè)版本的一個(gè)bug,相關(guān)issue可以參考(https://github.com/baldurk/renderdoc/issues/903),解決方案的話就是不使用這個(gè)正式版,而是下載latest nightly build。

ok,回到正題。大家都知道,利用renderdoc我們可以很方便的查看渲染流水線的各種數(shù)據(jù)以及各種資源的參數(shù)等等,所以首先我們來看看在gamma空間下的整個(gè)工作流。

首先我在工程中導(dǎo)入兩張一樣的圖片,分別叫做gamma和linear,在gamma空間下一個(gè)勾選了導(dǎo)入設(shè)置中的sRGB,另一個(gè)則不勾選。

111630jfkqkqpbpse44ksb.jpg
111631mzwj1wqjq5ozk71t.jpg
可以看到兩張圖片并沒有什么變化。下面我們來抓一幀來看一看兩者在OpenGL中的紋理格式:

111631owdbdidddz7t0iwi.jpg
111632szixt5czizdickld.jpg
兩者的格式都是GL_COMPRESSED_RGB8_ETC2。有趣吧,可以看到Unity設(shè)置了Gamma空間后,圖片導(dǎo)入時(shí)無論是否選擇勾選sRGB的結(jié)果都是一樣的。

之后我們?cè)賮砜匆豢碏rameBuffer的情況:

111632gufxzxkfzkq0go0g.jpg
沒有什么意外,同樣是我們常見且習(xí)慣的格式——GL_RGBA8。

這樣整個(gè)gamma workflow的過程中沒有涉及到所謂的gamma校正,整個(gè)過程和上一節(jié)中描述的一樣——導(dǎo)入了經(jīng)過處理的圖片,最后再經(jīng)過顯示器的處理中和——傳統(tǒng)且充滿了巧合與錯(cuò)誤。

接下來,我們將整個(gè)工程切換到Linear空間。同樣,兩張一樣的圖片一個(gè)勾選sRGB,另一個(gè)不勾選。

111633xhhncw7w717kh988.jpg
111634aykx5gx7ytvx5s5v.jpg
這次就更有趣了,我們可以看到勾選了sRGB的圖片變暗了,而沒有勾選的則仍然保持原樣。并且,勾選sRGB的圖片在下面的信息中顯示是sRGB——它被作為一張sRGB紋理來看待,需要進(jìn)行g(shù)amma校正;而另一張,則顯示的是Linear——它被當(dāng)作一張Linear紋理來看待,不需要經(jīng)過gamma校正。

所以勾選了sRGB的紋理變的更暗了,這是因?yàn)榻?jīng)過了pow(2.2)的gamma校正處理。

下面我們來抓一幀來看一看在linear workflow下兩者在OpenGL中的紋理格式:

111634k0uqb432uzka07m0.jpg
可以看到,勾選了sRGB選項(xiàng)的Texture在OpenGL中的格式為GL_COMPRESSED_SRGB8_ETC2,即硬件會(huì)對(duì)其作一次Pow2.2的gamma校正,將它轉(zhuǎn)化到線性空間中。

111635cge2d1ts0r7jeodr.jpg
而沒有勾選sRGB選項(xiàng)的Texture在OpenGL中的格式仍然是GL_COMPRESSED_RGB8_ETC2,所以硬件不會(huì)對(duì)它進(jìn)行pow2.2的gamma校正操作,所以針對(duì)真正的線性空間圖片不要勾選sRGB選項(xiàng)也就是這個(gè)原理——否則的話,顏色會(huì)比正確的結(jié)果更暗、數(shù)據(jù)也會(huì)錯(cuò)誤。

不過有意思的還在后面,即framebuffer的格式。下面我們就來看一看framebuffer的抓幀結(jié)果:

111636rwjybmoaxqwox9wa.jpg
framebuffer的格式為GL_SRGB8_ALPHA8,即此時(shí)保存的結(jié)果經(jīng)過了pow0.45的gamma校正,從線性空間轉(zhuǎn)換到了sRGB空間——這當(dāng)然是合理的,因?yàn)樗泻妥詈箫@示器的gamma校正——但是,有一件事情這時(shí)會(huì)變的比較棘手……


透明混合

是的,這件事情就是透明混合的問題。由于透明混合是一個(gè)線性的過程,因此在混合中作為Dst的那一方的framebuffer的數(shù)據(jù)就要是線性空間的了。

所以此時(shí)混合的操作事實(shí)上會(huì)先將framebuffer的內(nèi)容從sRGB空間再次pow2.2轉(zhuǎn)換到線性空間,和src進(jìn)行混合,再將混合后的結(jié)果pow0.45轉(zhuǎn)換回sRGB空間保存到framebuffer中。

是不是有點(diǎn)亂?

我們來寫一下公式,代入一個(gè)數(shù)據(jù)就明白了:
?

  1. ret = (srcColor^2.2 * srcAlpha + dstColor^2.2 * (1 - srcAlpha) ) ^(1/2.2)

復(fù)制代碼
ok,這時(shí)我們假設(shè)src的color值的g分量為1,alpha為0.2;dst的color值的g分量為0。則計(jì)算結(jié)果為:

0.481156505。

但是在我們的傳統(tǒng)的認(rèn)知下,或者是在Gamma workflow的情況下,這次混合的結(jié)果是什么呢?我們來寫一下混合公式:

?

?

  1. ret = srcColor * srcAlpha + dstColor * (1 - srcAlpha)
復(fù)制代碼
代入同樣的數(shù)據(jù),計(jì)算的結(jié)果為:

0.2。

兩個(gè)差別頗大的結(jié)果,而如果混合的次數(shù)越多,則結(jié)果的差別也會(huì)更大。

事實(shí)上這個(gè)問題的處理目前也并沒有一個(gè)特別十全十美的解決方案,目前常見的幾種做法大概包括以下幾種方案:

?

?

  • 使用gamma 1.0來制作資源,即在線性空間中制作資源。
  • 自己在ui的shader中對(duì)alpha進(jìn)行pow(2.2)的操作,但是這個(gè)只是稍微修復(fù)問題,并沒有真正的解決問題。
  • 仍然使用gamma space的workflow,但是涉及光照的shader自己來做pow 2.2和pow 0.45的校正。這樣的話ui還在gamma space,而光照計(jì)算在linear space。

當(dāng)然這里只是拋磚引玉,希望有更好解決方案的朋友能夠在此多多分享一下經(jīng)驗(yàn)。
銳亞教育

銳亞教育,游戲開發(fā)論壇|游戲制作人|游戲策劃|游戲開發(fā)|獨(dú)立游戲|游戲產(chǎn)業(yè)|游戲研發(fā)|游戲運(yùn)營| unity|unity3d|unity3d官網(wǎng)|unity3d 教程|金融帝國3|8k8k8k|mcafee8.5i|游戲蠻牛|蠻牛 unity|蠻牛