1. 程式人生 > >CSharpGL(55)我是這樣理解PBR的

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的一種極大的改進。它用幾個貼圖幫助求解積分,所以顯得難以理解,難以實現。其實也就那麼回事。

 

更新