CSharpGL(55)我是這樣理解PBR的
CSharpGL(55)我是這樣理解PBR的
簡介
PBR(Physically Based Rendering),基於物理的渲染,據說是目前最先進的實時渲染方法。它比Blinn-Phong方法的真實感更強,幾乎是照片級的效果。
下圖就是PBR的一個例子,讀者可在CSharpGL中找到。
+BIT祝威+悄悄在此留下版了個權的資訊說:
應用題
PBR雖然看起來很複雜,但仍舊是在解一個應用題,只要明確了已知條件和所求問題,就沒有什麼難以理解的了。
已知條件如下:
對於不透明的三維模型(Cube、Sphere、Teapot等等任何三維模型)上的任意一點,我們知道它的位置vec3 p、法線向量vec3 N和紋理座標vec2 texCoord。當觀察者(你,我,攝像機等等)從某個位置觀察三維模型上的這個點p時,從點p到觀察者的向量記作vec3 v或vec3 wo。照射到點p的每一束光線vec3 Li,根據某種規則,都會被點p反射到很多方向上去。觀察者看到的點p的顏色,就是所有恰好反射到v或wo方向上的光線的顏色。
(注意,為論述方便,在本文中,Li是從點p到入射光源的向量;v和wo是從點p到觀察者方向的向量;所有向量的長度都是1。)
+BIT祝威+悄悄在此留下版了個權的資訊說:所求問題:
觀察者看到的顏色是什麼?(用Lo(p, wo)表示)
解答:這個問題目前是不可能100%完美解決的,所以只給出各種近似的計算模型,湊合著用。
Blinn-Phong
Blinn-Phong模型
Blinn-Phong模型就是其中一種近似方案。
(注意,這裡“Blinn-Phong模型”中的“模型”與“三維模型”中的“模型”是兩個不同的概念。“Blinn-Phong模型”中的“模型”是對光照現象的某種計算方法。“三維模型”中的“模型”指的是三維空間中的物體的形狀。)
Blinn-Phong將物體反射到每一個方向上的光,都分為漫反射diffuse和鏡面反射specular這2個部分。它處理的光源,一般是平行光、點光源、聚光燈這種,從某一個點發射光的光源。
為什麼在PBR的文章裡要介紹Blinn-Phong?因為PBR可以被(我)認為是Blinn-Phong的進化版本。
在Blinn-Phong中,漫反射強度由N、Li共同決定:
float diffuse = dot(N, Li);
鏡面反射強度由N、Li、v共同決定:
float specular = dot(N, normalize(Li + v));
(注意,這裡的式子沒有考慮diffuse和specular小於0的情況,這是為了突出重點。)
這2種反射光加起來,配合物體的材質和光源的顏色,就得到了物體在點p處被觀察者看到的顏色:
vec3 fragColor = diffuse * material.diffuse * light.diffuse + specular * material.specular * light.specular;
當然,最後還要加上個環境光(用常量表示):
vec3 fragColor += ambientColor;
有的Blinn-Phong實現可能與此稍有不同:有的將ambient和diffuse加在一起,有的用紋理(Texture)表示物體的材質,等等。但是思路都是一樣的,不要糾結這裡。
+BIT祝威+悄悄在此留下版了個權的資訊說:Blinn-Phong的缺點
Blinn-Phong是個很不錯的模型,但是它有一個比較明顯的缺點:反射光的總量可能大於入射光的總量。也就是說,有時候物體反射的光的總強度居然比入射光還要大。這是不符合物理實際的。
例如,當Li、v都等於N(即入射光和觀察者都與法線方向重合)時,diffuse=1,specular=1,兩者相加=2>1。我們知道,Blinn-Phong將物體反射出來的每一個方向上的光,都分為漫反射diffuse和鏡面反射specular這2個部分。即使物體能夠100%反射所有的入射光,(diffuse+specular)最多也就是1而已,不可能超過1。
也就是說,Blinn-Phong雖然能保證diffuse和specular各自不超過1,但是不能保證(diffuse+specular)也不超過1。
PBR解決了這個問題。
PBR
PBR不僅保證了 (diffuse+specular)<=1 ,還有別的優點:
它能把周圍環境當作一個整體的光源,這擴大了光源的範圍。
它以真實的物理量為引數,因而對美工更友好。
它表現出照片級的真實感,且物體看起來就像本來就屬於場景中一樣。
PBR模型
PBR也將物體反射到每個方向上的光,都分為漫反射diffuse和鏡面反射specular這2個部分。
同時,它對這2種反射光的形成機制給出了自己的解釋:
如圖所示,一些入射光Li打在點p上。仔細想想,點p實際上不是數學意義上的點,而是由很多微小的平面(長度大於光的波長,小於畫素,簡稱微平面)組成的一小塊“褶子”(褶皺程度就是粗糙度roughness)。入射光Li打在褶子上,一部分會被褶子直接反射,另一部分會被吸收進褶子內部。直接反射的,就是specular部分;吸收後,在褶子內部經過若干次碰撞(組成褶子的原子、分子會不斷地反射或吸收剩下的光),有一些光會再次被反射出來,這就是diffuse部分。
PBR模型的關鍵,就在於光的波長、微平面的大小、畫素的大小這三者的大小關係。由於光的波長遠遠小於微平面的尺寸,所以就不用考慮光的衍射等現象。由於微平面的尺寸遠遠小於一個畫素,所以可以將一個個畫素視為一個個“褶子”。這樣一來,雖然入射光的diffuse部分,其出射位置與入射位置不完全相同,但仍舊在同一個畫素範圍內,所以可以視作位置相同。
(有人會說,會不會有的光在褶子內部被反射的很遠,最終超出了一個畫素的範圍呢?答案是,會。那麼,這種情況如何處理呢?PBR的答案是,忽略不計。)
“褶子”只是一個稱呼,事實上完美光滑的“褶子”,即微平面的排列完全平整,一點都不褶(光學平滑)是存在的,你可以在高階望遠鏡上找到。當然了,這是微平面級別的完美光滑,不是原子級別的。原子級別的完美光滑,據我所知還做不到。
+BIT祝威+悄悄在此留下版了個權的資訊說:PBR認為 (diffuse+specular)==1 始終成立。那麼,先算出其中一個,自然就得知另一個了(1-specular)。
Specular部分
菲涅耳方程F
當你站在清澈的海邊、河邊、湖邊,低頭向下看時,能夠看到水面下的沙石泥土,但平視遠處的水面時,就只能看到強烈的反光,很難看到水面下的景象。這種現象被稱為菲涅耳(Fresnel)效應。更多圖文介紹可以參考(http://blog.sina.com.cn/s/blog_798bec050100rigq.html)。
這種現象說明,入射光被拆分後,specular所佔的比例,與入射光Li和觀察者v的方向有關。當然,它還與物質的材質有關。菲涅耳方程(Fresnel Equation)給出了一個計算specular的公式。不過那玩意計算起來比較費時,業界一般用它的一個近似版本:
1 vec3 fresnelSchlick(float cosTheta, vec3 F0) 2 { 3 return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); 4 }
當然,其他版本的F函式也是存在的。
其中的cosTheta = max(0, dot(v, normalize(v + Li))) 。可見“它與入射光Li和觀察者v的方向有關”,此言不虛。
其中的F0就是物質的材質屬性。每種材質都一個對應的F0常數。
其返回結果為vec3 specular,就是說,黃金、白銀、鋼鐵、巧克力,材質對光的RGB通道的反射能力不同。嗯這很科學。
有了specular,當然就有了 vec3 diffuse = vec3(1, 1, 1) - specular 。我們稍後再討論diffuse。
幾何函式G
菲涅耳公式給出的,是在入射光Li和觀察者v條件下,specular所佔的比例。但是,褶子是粗糙的,會遮擋住specular的一部分。
+BIT祝威+悄悄在此留下版了個權的資訊說:
因此,需要計算出沒有被遮擋的比例,這就是幾何函式(Geometry Function):
(Kdirect是指平行光、點光源、聚光燈這樣的光源應採用的公式;KIBL是將整個圖片作為光源時應採用的公式。α表示表面粗糙度)
1 float GeometrySchlickGGX(float NdotV, float roughness) 2 { 3 float r = (roughness + 1.0); 4 float k = (r*r) / 8.0; 5 6 float nom = NdotV; 7 float denom = NdotV * (1.0 - k) + k; 8 9 return nom / denom; 10 } 11 // ---------------------------------------------------------------------------- 12 float GeometrySmith(vec3 N, vec3 V, vec3 L, float roughness) 13 { 14 float NdotV = max(dot(N, V), 0.0); 15 float NdotL = max(dot(N, L), 0.0); 16 float ggx2 = GeometrySchlickGGX(NdotV, roughness); 17 float ggx1 = GeometrySchlickGGX(NdotL, roughness); 18 19 return ggx1 * ggx2; 20 }
當然,其他版本的G函式也是存在的。
+BIT祝威+悄悄在此留下版了個權的資訊說:從引數可知,遮蔽比例與入射光方向Li、法線N、觀察者方向v和粗糙度roughness都是有關的。
法線分佈函式D
那麼,那些沒有被遮蔽的specular部分,就全部進入觀察者的眼中了嗎?並沒有。在這些順利逃出來的specular中,只有那些法線方向與(V+L)相同的微平面反射的光,才能進入觀察者眼中。
法線分佈函式(Normal Distribution Function)就給出了這個比例:
(α表示表面粗糙度)
1 float DistributionGGX(vec3 N, vec3 H, float roughness) 2 { 3 float a = roughness*roughness; 4 float a2 = a*a; 5 float NdotH = max(dot(N, H), 0.0); 6 float NdotH2 = NdotH*NdotH; 7 8 float nom = a2; 9 float denom = (NdotH2 * (a2 - 1.0) + 1.0); 10 denom = PI * denom * denom; 11 12 return nom / denom; 13 }
當然,其他版本的D函式也是存在的。
經過FGD的層層篩選,specular部分就很接近物理真實了。
漫反射常量
diffuse部分相對簡單些,用一個常數c表示材質本身的顏色,與diffuse相乘即可。當然,這也是一種近似,其他的近似函式也是存在的。
反射率方程
將上面的各種函式綜合起來,再配合一些數學系數,總的PBR公式(反射率方程)就是這樣:
總結一下就是:
反射率方程左側的意思是:觀察者在wo方向上觀察點p,他所看到的光的顏色Lo是多少?
反射率方程右側:Kd是diffuse所佔的比例,Ks是specular所佔的比例(注意Kd+Ks=1);c是材質的顏色,可以是單一的顏色vec3(r, g, b),也可以是用一個材質貼圖描述texture(texMaterial, texCoord);π是數學常數;n是點p的法線向量;wi是某個入射光線的方向;DFG是上文所述的法線分佈函式、菲涅耳函式和幾何函式;Li(p, wi)是在wi方向上照射到點p的入射光的顏色;最左邊那個長長的S和Ω符號,加上最右邊的dwi符號,是積分的意思,Ω符號表示在法線n方向上的半球範圍內積分。
右側的意思是:將所有入射光Li與其約束比例相乘,再加起來,就是我們應用題的答案。
本質上這仍舊是將diffuse和specular分別計算後再相加而已,只不過PBR對specular和diffuse的量都做了限制,從而保證其和不超過1。
其中的fr部分就是常說的BRDF函式。可見它包含了各種玩意,對物體反射光的量進行約束。
這個公式是如何推匯出來的?我不知道,暫時不是解決這個問題的時候。作為工程師,我先理解它,實現它,是第一要務。之後再從理論上推導它。
反射率方程是不能直接用shader來寫的,因為達不到實時的效能。所以我們一步步做簡化。
首先,右側可以從加法的位置上拆分為diffuse部分和specular部分:
這樣,就可以分別去研究如何實現這2個部分,最後簡單加起來就行了。
實現diffuse部分
首先,diffuse部分可以將一些常數提取出來:
+BIT祝威+悄悄在此留下版了個權的資訊說:
現在,積分內部的含義是,在半球範圍內,將所有方向上的入射光向量分別與法線相乘,再加起來。這個積分在shader中當然要用離散的方式計算。半球嘛,立體的,所以分別在水平方向和豎直方向上進行累加比較方便。
此時,我們就可以把上述方程稍微變形下:
然後變為對應的離散的形式:
從原來的積分形式變為離散形式,使用了蒙特卡羅積分原理。感興趣的同學可以自行搜尋研究一下。本文中,只要知道可以這麼轉換就行了。
在shader中表示這個離散公式的程式碼如下:
1 #version 330 core 2 out vec4 FragColor; 3 in vec3 WorldPos; 4 5 uniform samplerCube environmentMap; 6 7 const float PI = 3.14159265359; 8 9 void main() 10 { 11 vec3 N = normalize(WorldPos); 12 13 vec3 irradiance = vec3(0.0); 14 15 // tangent space calculation from origin point 16 vec3 up = vec3(0.0, 1.0, 0.0); 17 vec3 right = cross(up, N); 18 up = cross(N, right); 19 20 float sampleDelta = 0.025; 21 float nrSamples = 0.0f; 22 for(float phi = 0.0; phi < 2.0 * PI; phi += sampleDelta) 23 { 24 for(float theta = 0.0; theta < 0.5 * PI; theta += sampleDelta) 25 { 26 // spherical to cartesian (in tangent space) 27 vec3 tangentSample = vec3(sin(theta) * cos(phi), sin(theta) * sin(phi), cos(theta)); 28 // tangent space to world 29 vec3 sampleVec = tangentSample.x * right + tangentSample.y * up + tangentSample.z * N; 30 31 irradiance += texture(environmentMap, sampleVec).rgb * cos(theta) * sin(theta); 32 nrSamples++; 33 } 34 } 35 irradiance = PI * irradiance * (1.0 / float(nrSamples)); 36 37 FragColor = vec4(irradiance, 1.0); 38 }
程式碼中的雙重for迴圈,就是在離散地計算積分值。最後得到的irradiance,再乘以Kd*c,就是diffuse部分的顏色值了。這個值加上接下來馬上要講解的specular部分的顏色值,就是應用題的答案。
+BIT祝威+悄悄在此留下版了個權的資訊說:所有Fragment Shader的計算結果都會儲存到一個立方體貼圖中。這個貼圖叫做irradianceMap。這個計算過程叫做“卷積”。
注意,計算diffuse部分的輸入資料中,用到了一個立方體貼圖samplerCube environmentMap,它其實就是物體所處於的環境,也叫天空盒。這裡實際上就是將整個天空盒當作一個大光源來處理了。下圖展示了將輸入的立方體貼圖(左側)卷積後得到的irradianceMap(右側):
另外,這裡將點p選在原點(0, 0, 0)上,稍後計算specular部分時也會這樣設定。讀者會問,那就只能描述在原點處的光照嘍?也不盡然。只要在場景中的其他關鍵位置上也分別執行一遍PBR公式,就可以在整個場景中安排好這種“探針”。計算光照時,將距離物體最近的那幾個探針的顏色加權平均一下,就可以得到需要的顏色了。本文不討論“探針”的問題。
實現specular部分
現在,提取出specular部分:
這個積分裡有wi和wo兩個變數,如果要離散地計算,就得對wi和wo的所有組合都算一遍。這是達不到實時要求的。Epic遊戲公司給了一個近似公式,可以解決這個問題:
左邊的積分和上文的diffuse部分很相似,不同之處是,要對不同的粗糙度分別計算結果,並依次儲存到一個立方體貼圖的不同mipmap層上(越高的粗糙度儲存在越高(解析度小)的mipmap層上)。這個過程也是卷積,得到的貼圖是個多mipmap層的立方體貼圖,叫做prefilterMap。下圖展示了一個被卷積好了的prefilterMap:
右邊的積分,以n與wi的乘積為引數1,以粗糙度為引數2,進行卷積,得到一個普通的二維紋理,叫做brdfLUT。下圖就是:
+BIT祝威+悄悄在此留下版了個權的資訊說:
分別從卷積貼圖裡取樣,再算到公式裡就得到specular部分的顏色了。
貼圖總結
首先,我們需要從一個*.hdr檔案載入二維紋理texHDR。
然後,將texHDR轉換為天空盒紋理sampleCube environmentMap。
然後,用environmentMap分別生成irradianceMap和多mipmap層的prefilterMap。
最後,brdfLUT是獨立生成的,與別的貼圖無關。
只需載入其他的*.hdr檔案,就可以將物體置於其他天空盒下。PBR將天空盒視作光源,照射物體。這就是PBR能讓物體保持融入各個場景中原因。
下圖是我在CSharpGL中使用的newport_loft.hdr載入後的樣子:
這樣的樣式,在頭頂和腳底方向上的資料損失會多一點。不過,一般使用者關注的都是平視方向,所以沒問題。
總結
PBR是對Blinn-Phong的一種極大的改進。它用幾個貼圖幫助求解積分,所以顯得難以理解,難以實現。其實也就那麼回事。
更新