筆者介紹:姜雪偉,IT公司技術合伙人,IT高級講師,CSDN社區專家,特邀編輯,暢銷書作者,國家專利發明人;已出版書籍:《手把手教你架構3D游戲引擎》電子工業出版社和《Unity3D實戰核心技術詳解》電子工業出版社等。
CSDN視頻網址:http://edu.csdn.net/lecturer/144
先介紹一下關于Gamma產生的原因,原因有二:一是,人眼對自然亮度感知是非線性的(韋伯定律);二是,我們用來記錄/展示畫面的媒介上,灰階預算是有限的(無論紙張還是屏幕)。為了在灰階預算有限的前提下,協調自然亮度和主觀灰階感受這二者的映射關系,Gamma就產生了。
拿游戲舉例,游戲制作的場景最終要顯示在屏幕上,顯示器有一個物理特性就是兩倍的輸入電壓產生的不是兩倍的亮度。輸入電壓產生約為輸入電壓的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),實線代表監視器顯示的顏色。如果我們把一個點線線性的顏色翻一倍,結果就是這個值的兩倍。比如,光的顏色向量
代表的是暗紅色。如果我們在線性空間中把它翻倍,就會變成 ,就像你在圖中看到的那樣。然而,由于我們定義的顏色仍然需要輸出的監視器上,監視器上顯示的實際顏色就會是 。在這兒問題就出現了:當我們將理想中直線上的那個暗紅色翻一倍時,在監視器上實際上亮度翻了4.5倍以上!直到現在,我們還一直假設我們所有的工作都是在線性空間中進行的(注:Gamma為1),但最終還是要把所喲的顏色輸出到監視器上,所以我們配置的所有顏色和光照變量從物理角度來看都是不正確的,在我們的監視器上很少能夠正確地顯示。出于這個原因,我們(以及藝術家)通常將光照值設置得比本來更亮一些(由于監視器會將其亮度顯示的更暗一些),如果不是這樣,在線性空間里計算出來的光照就會不正確。同時,還要記住,監視器所顯示出來的圖像和線性圖像的最小亮度是相同的,它們最大的亮度也是相同的;只是中間亮度部分會被壓暗。
因為所有中間亮度都是線性空間計算出來的(注:計算的時候假設Gamma為1)監視器顯以后,實際上都會不正確。當使用更高級的光照算法時,這個問題會變得越來越明顯,你可以看看下圖:
Gamma校正(Gamma Correction)的思路是在最終的顏色輸出上應用監視器Gamma的倒數?;仡^看前面的Gamma曲線圖,你會有一個短劃線,它是監視器Gamma曲線的翻轉曲線。我們在顏色顯示到監視器的時候把每個顏色輸出都加上這個翻轉的Gamma曲線,這樣應用了監視器Gamma以后最終的顏色將會變為線性的。我們所得到的中間色調就會更亮,所以雖然監視器使它們變暗,但是我們又將其平衡回來了。
我們來看另一個例子。還是那個暗紅色
。在將顏色顯示到監視器之前,我們先對顏色應用Gamma校正曲線。線性的顏色顯示在監視器上相當于降低了 次冪的亮度,所以倒數就是 次冪。Gamma校正后的暗紅色就會成為 。校正后的顏色接著被發送給監視器,最終顯示出來的顏色是 。你會發現使用了Gamma校正,監視器最終會顯示出我們在應用中設置的那種線性的顏色。2.2通常是是大多數顯示設備的大概平均gamma值?;趃amma2.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空間中顯示應用了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校正,顯示在監視器上的衰減方程實際上將變成如下所示:
若不進行gamma校正,將產生更強烈的衰減。這也解釋了為什么雙曲線不用gamma校正時看起來更真實,因為它實際變成了
這和物理公式是很相似的。
總而言之,gamma校正使你可以在線性空間中進行操作。因為線性空間更符合物理世界,大多數物理公式現在都可以獲得較好效果,比如真實的光的衰減。你的光照越真實,使用gamma校正獲得漂亮的效果就越容易。
最后,把gamma校正的Shader源代碼奉上,先給讀者展示的是頂點著色器源代碼:
#version 330 corelayout (location = 0) in vec3 position;layout (location = 1) in vec3 normal;layout (location = 2) in vec2 texCoords;out VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;} vs_out;uniform mat4 PRojection;uniform mat4 view;void main(){gl_Position = projection * view * vec4(position, 1.0f);vs_out.FragPos = position;vs_out.Normal = normal;vs_out.TexCoords = texCoords;}片段著色器源代碼如下所示:#version 330 coreout vec4 FragColor;in VS_OUT {vec3 FragPos;vec3 Normal;vec2 TexCoords;} fs_in;uniform sampler2D floorTexture;uniform vec3 lightPositions[4];uniform vec3 lightColors[4];uniform vec3 viewPos;uniform bool gamma;vec3 BlinnPhong(vec3 normal, vec3 fragPos, vec3 lightPos, vec3 lightColor){// Diffusevec3 lightDir = normalize(lightPos - fragPos);float diff = max(dot(lightDir, normal), 0.0);vec3 diffuse = diff * lightColor;// Specularvec3 viewDir = normalize(viewPos - fragPos);vec3 reflectDir = reflect(-lightDir, normal);float spec = 0.0;vec3 halfwayDir = normalize(lightDir + viewDir);spec = pow(max(dot(normal, halfwayDir), 0.0), 64.0);vec3 specular = spec * lightColor;// Simple attenuationfloat max_distance = 1.5;float distance = length(lightPos - fragPos);float attenuation = 1.0 / (gamma ? distance * distance : distance);diffuse *= attenuation;specular *= attenuation;return diffuse + specular;}void main(){vec3 color = texture(floorTexture, fs_in.TexCoords).rgb;vec3 lighting = vec3(0.0);for(int i = 0; i < 4; ++i)lighting += BlinnPhong(normalize(fs_in.Normal), fs_in.FragPos, lightPositions[i], lightColors[i]);color *= lighting;if(gamma)color = pow(color, vec3(1.0/2.2));FragColor = vec4(color, 1.0f);}C++核心代碼如下所示:// Clear the colorbufferglClearColor(0.1f, 0.1f, 0.1f, 1.0f);glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);// Draw objectsshader.Use();glm::mat4 view = camera.GetViewMatrix();glm::mat4 projection = glm::perspective(camera.Zoom, (float)SCR_WIDTH / (float)SCR_HEIGHT, 0.1f, 100.0f);glUniformMatrix4fv(glGetUniformLocation(shader.Program, "view"), 1, GL_FALSE, glm::value_ptr(view));glUniformMatrix4fv(glGetUniformLocation(shader.Program, "projection"), 1, GL_FALSE, glm::value_ptr(projection));// Set light uniformsglUniform3fv(glGetUniformLocation(shader.Program, "lightPositions"), 4, &lightPositions[0][0]);glUniform3fv(glGetUniformLocation(shader.Program, "lightColors"), 4, &lightColors[0][0]);glUniform3fv(glGetUniformLocation(shader.Program, "viewPos"), 1, &camera.Position[0]);glUniform1i(glGetUniformLocation(shader.Program, "gamma"), gammaEnabled);// FloorglBindVertexArray(planeVAO);glBindTexture(GL_TEXTURE_2D, gammaEnabled ? floorTextureGammaCorrected : floorTexture);glDrawArrays(GL_TRIANGLES, 0, 6);glBindVertexArray(0);std::cout << (gammaEnabled ? "Gamma enabled" : "Gamma disabled") << std::endl;// Swap the buffersglfwSwapBuffers(window);
新聞熱點
疑難解答