基礎知識部分
為了方便理解,首先會對(
Luminance
)的相關概念做一個簡單介紹。如果已經了解就跳到后面吧。
我們用Radiant energy(輻射能量)來描述光照的能量,單位是焦耳(J),因為光實際是以一定速度在傳播的電磁波,所以把單位時間內的傳播的
Radiant energy(能量)稱作
radiant flux
(
輻射通量),用來描述他的能量表現,單位瓦特(Watt
)。

Radiant intensity(
輻射強度
)用來指定
radiant flux
(
輻射通量)的方向,正式的來說,他是用來定義每個立體角的Radiant energy(輻射能量)
的傳遞速度,它的單位是瓦特/球面度((W
·
sr
-1
)。

輻射強度(
Radiant intensity
)通常會是一個很大的空間范圍,但對於使用像素的成像系統來說是一個很小的區域,所以使用
intensity
來作為圖像數據的度量並不合適。那么就需要一個可以對應面積的單位,我們把通過單位面積的能量(
Radiant energy
),稱作輻射照度(
Irradiance
)。

而把在每個單位面積上的輻射強度
(
Radiant intensity
),稱作輻射率(radiance
), 他的單位是瓦特/球面度/平方米。

圖像保存在文件里的(如TIFF),就是於輻射率成正比。但是像素值通常會被非線性的傳遞函數所影響。
在物理學里,輻射強度(
Radiant
intensity
)和輻射率(
radiance
),是一個很寬廣的波長范圍的集合,在顏色學中,我們要考慮人眼對光波長的可見性來定義光的單位,也就是心理物理量。把輻射強度( Radiant intensity
),用標准的光度函數(人眼視覺的亮度感與光譜強度的關聯函數)來加權運算,就可以得出Luminous intensity(發光強度)。它的單位是
坎德拉
candelas (cd)。

這個權重曲線由CIE來定義的數值和標准。它的峰值一般在555nm附近。
Luminance(亮度
)代表的是
Luminous intensity(發光強度)在單位面積的值,它的單位是坎德拉/平方米。
Luminance在一些公式里被表示為Y,與能量成正比關系,更類似intensity,通常luminance 會被標准化為一個值為1或者100的單位,用來指定為一個白色參考值,例如顯示器上的一個白色,可以用
100 cd
·
m
-2
和
Y
= 1來指定這個值。所以
Luminance通常也稱作relative
Luminance(相對亮度)。
Luminance可以按照一定的權重計算為線性光紅色,綠色,藍色三個部分,也就是
tristimulus components。
709
Y = 0.2126 R + 0.7152G + 0.0722 B

上圖,輻照度(irradicance)和的照度(illuminace)是對應的

上圖,光量的物理值和對應的心理物理量的對比表。
gamma
物理顯示設備上luminance的生成,通常和它輸入的信號不成正比的,存在非線性的關系。
例如傳統的CRT電視或顯示器
上
,是通過電子槍的發射高速電子,投射在電視平面的
熒光粉上使之發光,而通過不同輸出電壓來調節電子束功率,就可以調節顯示的Luminance。
但因為電壓的冪定律(
power-law
)響應的緣故(心理物理量和物理量並不成線性增長關系,而是冪函數的形式)。最終顯示的在屏幕上的效果受電壓影響大概是原理的Power 2.5左右(通常是2.35到2.55之間)
。而這個Power函數的指數數值,就是我們俗稱的gamma。
記作:


圖. 如圖所示是視頻信號從0~100mV所對應的
luminance值,輸入和輸出並不是線性對應的,在以8位數字模擬轉化的圖形系統里,把黑色編碼為0,白色編碼為255.
因為這個原因,所以需要對這個非線性進行補償以達到正常亮度。但就我們所知道的,即便是現在的非CRT的顯示設備還要特意保留在gamma 2.2左右。
這個的原因是?
首先,人眼接受光和照相機並不相同,我們
把
luminance的對比率從白到黑,按100~0來對應,在這個范圍內,
人眼的視覺感應和
luminance值也並不是成線性關系的,
luminance處於18%的相對值的,在人眼里已經顯示出
50%的亮度了。這里把這種對
luminance的感知反映稱為
Lightness
。這是因為比起明亮的部分,人眼對暗部的感知更加敏感。

非線性的人眼和線性的照相機的對比。
CIE使用一個標准函數
L*(讀作EL-Star),定義為修改的
luminance
立方根來表示lightness。

圖.
這里Yn是luminance為白色的值(按之前100~0來對應的話就是100),Y為當前的
luminance值。L*的范圍是0~100

luminance的相對值與
Lightness值的對應曲線。可以看出相對值0.2的時候,
Lightness已經大於50了
對比CRT顯示器 和人眼視覺的曲線,可以發現一個驚人的巧合,人眼視覺反映的曲線恰好近似是CRT曲線的逆
,人類的視覺暗部更加敏感,所以gamma的存在並不是一種缺陷,而作為非常有用的特性被保留下來了。既然輸出時的gamma值無法改變,那就只能通過對輸入信號做校正的方式,來讓顯示的內容正確。
而LCD顯示器就沒那么幸運了,為了能夠保證顯示是gamma2.2,往往需要大量的校正工作,所以不同的LCD顯示通常會有些差別,所以需要LUT(look-up table
)來確保輸入值按照希望的gamma值輸出。
Gamma- Correction
gamma校正,簡單來說就是對輸入信號做一次關於gamma的逆處理(1/gamma = 1/2.2 = 0.45)的Power),這樣兩個非線性的相逆的曲線使得最終的輸出成為線性。

一個gamma校正的例子,實現代表的是CRT的行為 power 2.2,虛線代表的是反函數,也就是gamma校正的行為power 0.45,而點線則是后的線性函數。
在
HDTV這類的
視頻系統里。
通常會把luminance計算為按一定權重分配為
三組
線性光照(
(tristimulus
),分為紅色,綠色和藍色。這里一個比較標准的格式是
Rec. 709來對信號進行編碼
709
Y = 0.2126 R + 0.7152G + 0.0722 B
使用Rec. 709 transfer function 可以把這些線性值轉化為非線性,這個函數主要也是基於
0.45(1/2.2)的Power函數實現的。


將顏色值轉化為非線性的的公式。
注,通常電視的gamma值是2.5,這里用的使用0.45(1/2.2)的緣故,是因為一般電視周圍的環境比較暗,會產生
simultaneous contrast現象,昏暗背景會丟失對比度,所以把輸入時校正gamma值調第,


圖,Simultaneous contrast 明亮和昏暗環境下,對比度會有差別。
上面是視頻系統里的校正方法,回到平時工作的計算機操作系統里,因為顯示器的gamma的緣故,我們在系統里看到的也需要進行gamma校正才能顯示正確。所以也就有了sRGB色彩空間的概念。
sRGB(standard RGB )色彩空間最早是微軟和惠普開發的一種標准的RGB格式,目的就是為了在顯示器和打印機上使用,而sRGB和上面提到的
Rec. 709非常相似,使用了相同的原色( primary chromaticities
)和白點(
white point chromaticities
)的,唯一不同的是,
Rec. 709對應的是gamma為2.5的顯示設備,而sRGB選擇的是gamma為2.2的設備。這種標准得到了許多業內廠商的支持,數碼相機,攝像機,掃描儀等等,都有sRGB的支持。所以一般我們也會默認的認為
導入到
操作系統里的圖片都是sRGB格式的。

圖
sRGB和
Rec. 709相同的色度表(chromaticity diagram
)
,三角內是原色,D65位置是原點,
如上圖所示,sRGB 顏色空間的值通常會是3個浮點值,在0.0~1.0之間,超出范圍的會被clip掉,在圖形計算中,會編碼為
8位的無符號整數,范圍是
0~255。因為也是為了gamma校正的目的,sRGB也是非線性的。大致近似是一個y = x^2.2的曲線,而實際的曲線要更負責一些,見下面的公式。

線性空間和sRGB空間之間的轉化關系。
OUT.Color.xyz = (OUT.Color.xyz < 0.0031308) ? 12.92 * OUT.Color.xyz : 1.055 * pow(OUT.Color.xyz, 1.0 / 2.4) - float3(0.055, 0.055, 0.055);
ce3的UpscaleImagePS里,使用的就是最准確的公司來轉化到sRGB空間,一般都是簡化的pow(x, 1.0/2.2)了

圖。線性空間和sRGB的對比,和gamma2.2的曲線幾乎重合。這樣的好處是sRGB可以儲存更多低luminance RGB值,更適合人類的視覺。
如上圖所示的結果,sRGB曲線和非常的接近gamma2.2,所以一般sRGB轉化函數經常就近似為一個gamma函數就可以了。所以,對於圖片來講,如果他是sRGB空間的話,那么也就可以說它是gamma校正過的了。

早期和游戲渲染相關的gamma校正的資料里,對每個gamma空間和線性空間的定義比較含糊,為了方便進入下一個章節,這里適當的做一下總結。
圖像的Gamma
通常所說的sRGB空間 = gamma 0.45,
照相機或其他Raw軟件捕捉的圖像,通常會轉為標准的JPEG或TIFF文件,也就是編碼為gamma 1/2.2的圖像文件。但Raw是保存在線性空間的。一般不使用color profile的情況,都是默認的gamma 1/2.2,比如PNG和GIF文件,只有jpg文件有時會使用"save for the web"的設定


線性Raw圖像 gamma = 1.0 gamma校正的圖像 gamma = 1/2.2
顯示的Gamma
通常的gamma2.2,
現在的顯示器通常已經把gamma2.2作為標准,所以不用太擔心不同gamma值所帶來的優缺點,老的蘋果顯示器可能用的是gamma1.8。

上圖是,不同顯示gamma值和對應曲線的對比,其中,藍線為圖片的gamma校正值(gamma 1/2.22),紅線為設置的顯示gamma值,紫色則為最終的系統gamma值。
系統 Gamma
表現的是gamma校正和顯示gamma同時作用在圖片上的效果,當系統gamma是線性時,可以保證你的光照計算正確的顯示。
基本理論都介紹后,下面開始介紹渲染中的gamma校正
游戲中的Gamma校正
結合一些網站和ppt上的信息,再重新回顧一下游戲里gamma的問題,下面的圖例,左邊和右邊是水平的黑白交叉線,中間上面的是稍暗的方塊,下面是稍亮的,如果我們稍微離屏幕遠一點,可以看到左邊和右邊部分,是黑和白(0和255)做了顏色平均也就是128. 一眼看上去,和中下面的亮灰色方塊很接近。那實際下面的顏色是128么?

結果是否定的
,實際上是下面是187比0~255的一半要高。而造成這種情況的原因,就是因為是gamma的非線性空間運算導致的。

下面以naughty dog的John Hable的
Uncharted 2: HDR Lighting 這篇ppt里的內容來解釋這個問題。
唯一可惜的就是這里把sRGB空間和顯示空間都統稱為gmama空間了,我稍微做了下修改

上圖是從0到255的灰度圖,計算0和255的平均值后顏色,並不是128的顏色,而是187的。

當我們把一個值輸出給顯示器時,一般會認為luminance增長會和我們輸入的數量呈線性的增長,例如把25傳送2次就是50,但實際上,所以的顯示器都會有gamma2.2的曲線,我們把我們的值用0.0~1.0范圍替換0~255的話,輸出的
luminance大概是pow(x, 2.2
)。

把灰度圖分為32級的例子,這里的gamma值標准的1/2.2,可以看到,線性空間的暗色調信息不足,並導致明亮的色調過量。另外,gamma空間下灰度的均勻分布在整個色調范圍內。

上面的圖里,左邊的圖是gamma1/2.2(接近0.45,其實也就是sRGB空間),中間是線性的沒有變化,右邊偏暗的圖像是gamma2.2、
注:原文這里把0.45和2.2都稱作gamma空間,這里為了后面理解方便,我就直接把左側圖的空間寫作sRGB空間了。

實際上,通過power函數,是可以在gamma空間之間轉化的,從左邊的圖片開始(sRGB空間),我們可以通過pow(x,2.2)轉為中間的圖像(線性空間),而且也可以再次使用pow(x,2.2)轉為右邊的圖像(gamma2.2空間)。同樣,我們也可以通過逆函數pow(x, 1/2.2)返回來。

假設我們生產了一種沒有考慮任何gamma的因素的照相機,那么傳感器就會把入射光(左圖)編碼為0~255范圍的像素,然后把這個照片存儲在電腦硬盤上。當用戶在操作系統里打開時看到屏幕上顯示的圖像時,顯示器會將右邊的gamma2.2的顯示出來。那么用戶因為在看到的效果不符合真實世界看到圖像,而認為我們的照相機是有問題的。

你也可以試着和用戶解釋,說攝像機是正確的,而實際上每個人的顯示器都是錯的(包括照相機背面的LCD預覽),但結果是肯定的。如果我們把圖形保存在sRGB空間
(
gamma0.45 =
1/2.2),光射入傳感器后,照相機會對它進行一次pow(x,1/2.2),然后再編碼到0~255的等級,並保存到電腦硬盤上。實際上,包括手機,網絡攝像頭,單反照相機都是這么做的,唯一有差別的就是計算機視覺相機(computer vision cameras)。當
圖形保存在sRGB空間時,會更明亮和柔和,但當用戶在顯示器屏幕上查看時,就會看到正確的結果。

關於gamma必須認識到的一點就是,任何時候在計算機屏幕上看到的圖像(右邊),實際保存在硬盤里的圖片都要更亮也更柔和一些,就如左圖所示。
那么接下來要解釋它對計算機圖形的影響。

左側圖的所有光照計算,都是在sRGB空間,右側的圖的光照是在線性空間。
左側圖錯誤的
有幾個原因,首先,他有一個真實的柔軟的衰減,這看起來並不正確,同時你可以看到很多色調變化(hue-shifting
),特別是Specular高光的顏色,從白色到黃色到綠色再到白色。而看 右邊的圖,它更像一個有白色Specular高光的Diffuse Surface。右邊的圖個更符合光照模式下的顯示效果。要強調的是,這里討論的是渲染“正確”,而不是看起來效果很好。

如果沒有做任何關於gamma的處理的話,那么基本上會遵循上圖的流程,假設有一個比較典型的shader
-
float specular =...; float3 color=tex2D(samp,uv.xy); float diffuse = saturate(dot(N,L)); float3 finalColor = color * diffuse + specular; return finalColor;
首先要讀取Texture,而這張Texture在你的硬盤里是保存在sRGB空間的,所以這張圖實際比你在屏幕上看到的要亮的多,不飽和度也要高,所以你要把一張過亮的Texture,來應用你的光照模型,得到中間的結果,然后你的顯示器會使用pow(x,2.2)把這張圖變暗,並輸出最終結果。那么該如何修改呢?

這里是修改過的shader
-
float specular =...; float3 color=pow(tex2D(samp,uv.xy),2.2); float diffuse = saturate(dot(N,L)); float3 finalColor = pow(color * diffuse + specular,1/2.2); return finalColor
唯一的區別就是2個pow指令,當Texture從硬盤讀取后,他是在sRGB空間(第一張圖),但pow(x,2.2)會把它轉變到線性空間(第2張圖),然后我們進行光照計算(第3張),然后我們使用pow(x, 1/2.2)把他轉回sRGB空間,然后再把sRGB空間的圖像傳輸給顯示器,顯示器會適用一個pow(x,2.2)並把最終圖像顯示出來。
因為pow指令在shader里還是有消耗還是比較大的,所以,可以使用硬件的Sample State來免費的使用,在讀貼圖時,使用D3DSAMP_SRGBTEXTURE,在寫framebuffer(render target)時,可以使用D3DRS_SRGBWRITEENABLE,然后就可以使用修改前的shader,硬件State會幫你免費的轉化。

上圖是一個真實排球的圖像,右上的圖像是在線性空間,右下是在sRGB空間,可以看到線性圖像和真實圖像在衰減上更匹配。所以說,如何讓你的圖形不但正確而且好看,是需要不斷去練習的。
Lighting的情況講完后,是Naty Hoffman的blog上的Blend的示例,用公式的方法來描述這種問題。
正確的進行Blend的方法是和Shading計算一樣在線性空間進行,這里以Alpha-Blending為例,也類似其他的Blend Mode。下面的等式中,用小寫的c代表sRGB空間,大寫C代表線性空間(x為通用值,適用於每種空間
)。要注意的是,alpha值作為物理覆蓋量,一直在線性空間沒有轉化。
條件都清楚了,下面是Blend 公式:

因為給予GPU的是顏色是線性空間,而在Blend操作后,輸出的Render Target顏色是sRGB空間,我們希望GPU可以這樣做:

D3D11和OpenGL擴展特性可以完成這種行為,但是D3D9級別的GPU會先轉為sRGB空間再進行混合。

在這種情況下,shader在線性空間計算顏色,而Blend計算在sRGB空間進行,這樣在理論上並不正確,如果你的Blending只用來表現一些特殊效果,那結果可能不會太糟糕,如果你是每個light 1個pass的情況,並把光照在硬件上Blending來合並,那么,所有的光照會在sRGB空間合並,那么會失去gamma校正渲染管線的優勢。不論是你用硬件轉換,還是在shader里轉化,情況都會很壞,因為這兩種轉換都是在Blend前進行的。
如果你不想在sRGB空間進行Blending,又找不到正確的支持跨顏色空間進行Blending的方法,你可以在HDR線性空間緩沖進行Blending,然后在后處理中轉化為
sRGB空間。如果
HDR Buffer並不實際,也可以在LDR線性空間Buffer進行。
GPU硬件的自動轉化不但無法保證Blend正確,如果是premultiplied alpha,可能會得到更差的結果。

匯總
根據上面的事例,解決問題似乎只需要兩次pow指令或者用次sRGB硬件狀態,但實際開發中,需要注意的地方還是很多的。
輸入到渲染管線的圖像默認都是非線性的sRGB貼圖
渲染管線里會使用的圖像來源,大致可以分為,直接照片采集,美術繪制以及RenderTarget的輸出
照片數據,也就是我們所有掃描,或數碼拍照的圖像應該都是gamma校正后的,sRGB空間的貼圖。前提是照片的采集步驟正確,這個會1在后面的美術部分詳細描述。
讓美術人員手繪,因為本身美術所用的顯示器是gamma影響,如果不使用顏色表的話,那么畫出來的圖也是在顯示器的顏色空間里。具體方法后述。
網絡上找來的圖,也不一定就是可信的。
在DCC工具(UE4也有)中使用顏色吸管工具時,獲取的顏色值都需要從sRGB空間轉為線性空間再輸入給渲染管線,包括材質的顏色和光的顏色,而且只有0~1之間的值才能正確的轉化,並不適合light intensity 那種無邊界的值。
如果美術師,在DCC繪制頂點顏色(Vertex Color)的話,那么通常是可視化的在sRGB空間進行的,需要轉化為線性空間值來加載(通常在模型導出時進行)。手繪的ambient occlusion灰度信息保存在頂點顏色里的話也需要進行轉化為線性空間。頂點顏色基本上不需要保存在sRGB空間里,因為高精度的插值可能會導致錯誤。
另外,比如在延遲渲染里,需要RenderTarget輸出的Specular和Diffuse的accumulation buffer作為光照信息來對場景着色,也就是要求輸出的圖像是sRGB空間,
不論是RT的輸出,還是讀取保存的Texture,都需要打開sRGB狀態。一些預烘培的lightmap也一樣要注意這個問題。
MipMap的處理
創建Texture的Mip map,最簡單的方法就是每個更低等級的mip,是通過上一個更高分辨率中,相鄰四個像素總和的1/4來計算。
一個經常會被忽視的問題,就是Mip-maps也是需要轉到線性空間計算的,不過大多數的庫還是支持帶gamma校正的 MIP生成的(
NVTextureTools和D3DX
),把要轉化的Texture和要生成Mips的顏色空間指定好,圖形庫就會幫你 把輸入的Texture轉到線性空間,將紋理過濾,並最終轉為合適的顏色空間輸出(一般是sRGB)。
如果你不使用現成的庫,而是自己來寫圖片壓縮和生成工具,又沒有注意而直接在sRGB空間做mip的過濾的話,那么你最后會得到一個比正確結果更暗的顯示。

如上圖所示,中間的圖是黑白像素線的交錯,左邊的圖是把中間的圖在線性空間降采樣,而右圖是直接在sRGB空間。如果你的顯示器設置正確的話,左圖和中間的圖會表現出相同的亮度。這是因為,中間圖在線性空間平均得出50%的灰度,換算到sRGB空間就是186的值,而右圖直接把128作為sRGB空間值來保存。那么轉到線性空間就是一個只有21.4%的灰度,所以會看的更暗一些。
正確的方法
sRGB空間(0,1) --> pow(x, 2.2)轉線性空間 --> 線性空間里計算 (0 + 1)/2 = 0.5 --> pow(x, 1/2.2)轉回sRGB --> 保存在sRGB空間的是0.73
而錯誤的計算方法
sRGB空間(0 ,1) --> sRGB空間里計算 (0 + 1 )/2 = 0.5 --> 保存在sRGB的空間的值是0.5
所以,當帶mipmap的Texture加載並輸出到顯示器上時,錯誤的sRGB空間的計算導致了最后的結果要更暗。
參考鏈接