1. 程式人生 > >猴子都能看懂的PBR(才怪)

猴子都能看懂的PBR(才怪)

也不知道怎麼搞的,PBR(Physicallly-Based-Rendering 基於物理渲染)突然成了一個……你會了就好像什麼都會,不會就好像什麼都不會的標尺了……

嘛,其實PBR也和其他渲染技術類似,雖然是比GPUSkinMesh之類“單純”的技術要複雜,但也未見得比完整的FFT Ocean實現複雜度更高。如果只是想實現以及應用的話,其實也沒有那麼的難。

而我也不太理解,為何PBR相關的中文資料會那麼的少,因為它基本已經可以認為是現代寫實渲染的基本了,是一個必須瞭解的知識點。如果做現代寫實風格的遊戲,你不懂PBR,確實可以認為什麼都不會。但同樣也因為它是如此的基礎,即使你完全不會,依靠引擎自帶的功能也不怎麼會影響到工作(Unity自帶的Standard就是一個完整的PBR實現)

然而PBR不同於其他渲染技術,其實並沒有太多可自定義擴充套件的地方。如果你需要對官方提供的PBR做修改,也就是一些效能調優,因為PBR的目標就是一致性,並不需要針對不同需求的靈活處理。所以在坐的各位,除非自己編寫引擎,其實並不太需要知道PBR的具體實現方法,只需要知道PBR材質引數代表的物理意義,和美術一樣知道如何使用即可。不過做一些擴充套件瞭解也沒啥壞處,瞭解原理,至少可以在效能耗費上心裡有個底。

上面是篇相對比較簡短,而且術語和公式較少(比較像人話)的PBR的基礎教程翻譯。我下面所寫的內容,更多是對這篇文章內難點的解釋和補充,所以希望大家不管看不看的懂,起碼要先過一遍,然後進行結合閱讀。

基礎概念:微平面和輻射度

這是兩個對於PBR非常重要的概念,和光線追蹤一同,構成了PBR的物理基礎。

然而,其實你並不一定非要去了解它們。因為它們的出現基本上只是為了證明:PBR裡的“數學公式”,是符合物理的。

所以如果你不在乎這個“證明”,選擇直接相信的話,就只需要記憶幾個由微平面推匯出的“結論公式”,而輻射度,也可以簡單地理解成顏色值。

至於光線追蹤……我們一般使用的BRDF模型並不需要涉及到(但之後包含折射的BSDF,包含次表面反射的BSSRDF就必須使用光追了,而光追暫時是難以在當代硬體上廣泛運用的)

不過,如果不知道這些細節,許多公式的細節你是無法理解的,因為你甚至連它們的單位都不知道。雖然我很想從細處去說這個,但恐怕會違背這篇文章的主旨。個人建議你們還是去原文了解一下。

在這裡我只補充幾個容易迷惑的點:

  • 光源的單位是輻射強度I(每單位角),但在不隨距離衰減的直線光中,它與輻射度等價。
  • 螢幕的最終顏色值是輻射度L(每單位角單位面積),這也被稱為輻射率,或者輻射亮度。輻射度等價於以往的“顏色”。
  • 輻射度是最常用的物理量,其他物理量更多情況下是作為公式中的中間值,或者輸入引數存在。
  • 微平面是微觀而非巨集觀的,所以並不會和任何外部的巨集觀物理量產生互動,但微平面會導致一些巨集觀的統計結果(諸如散射)。這就和我們並不會真的去研究電子如何繞著原子核旋轉一樣,以這種方式理解微平面會比較妥當。

反射率方程

L_{o}(p,w_{o})=\int_{\Omega}(k_{d}\frac{c}{\pi}+ k_{s} \frac{DGF}{4(w_{o}\cdot n)(w_{i}\cdot n)})L_{i}(p,w_{i})(w_{i}\cdot n)dw_{i}

這是PBR的核心,也是主要的勸退點。

翻譯成自然語言,大概是這樣的:

輸出顏色=\int_{\Omega}(漫反射比例\frac{紋理顏色}{\pi}+ 鏡面反射比例 \frac{鏡面高光\times 幾何遮蔽\times 菲涅爾效應}{4(viewDir\cdot normal)(lightDir\cdot normal)})光源顏色(lightDir \cdot normal) dw_{i}

先解釋下這個公式遺留的部分。半球積分( \int_{\Omega} ………… dw_{i} ),表示的是多光源下光照的疊加。之所以非要寫成半球積分而不是 ∑,是為了相容環境光照。如果你只考慮單個不衰減的直線光照的話,這部分其實可以直接去掉(並不是說數學上可以直接化簡,而是因為這是一個特例)

輸出顏色=(漫反射比例\frac{紋理顏色}{\pi}+ 鏡面反射比例 \frac{鏡面高光\times 幾何遮蔽\times 菲涅爾效應}{4(viewDir\cdot normal)(lightDir\cdot normal)})光源顏色(lightDir \cdot normal)

看到這個lightDir • normal大家都應該很熟悉,如果將鏡面反射係數設定為0,漫反射係數設定為1,公式就和單純的Lambert漫反射基本一致:

輸出顏色=(\frac{紋理顏色}{\pi})光源顏色(lightDir \cdot normal)

不一致的部分是這個除π。因為它把亮度除低了,就只能相應調高光源的亮度補回來。看似彆扭,但是回頭一想,光源的亮度,難道不就應該比周圍的物品高上很多嗎?因為即使是直射,也還是會有很多光線被散射到其他方向,只有少部分才正常投射到了人眼中,漫反射的性質就是如此,之前不除π的做法其實才是錯誤的。

(至於為什麼除的是π,是因為 \int_{2π}cosθ_{o}dω_{o}=π ,如果散射的光線最後都能彙集到一點的話,積分的結果就是會再乘一個π。所以分散的時候就需要除π。)

另外還有一個地方容易讓人迷惑,按說經過半球積分彙集了不同方向的光線後,返回的結果應該是輻照度E(每單位面積),而這個反射率公式左邊卻是L(每單位角單位面積),這在單位上就說不過去。

實際上,是因為這個公式經過了化簡,把一些中間引數給約掉了,剩下的部分形成了這樣的結構。這篇文章有推導過程:PBR Step by Step(三)BRDFs

從“非數學”的角度考慮的話,也可以認為是這個單位面積彙集的不同方向的光線最後都融合並反射了出去,我們從中重新取了一條光線作為結果。

鏡面反射部分( \frac{鏡面高光\times 幾何遮蔽\times 菲涅爾效應}{4(viewDir\cdot normal)(lightDir\cdot normal)} ),這部分是個叫做Cook-Torrance的BRDF光照公式,分子上的三個係數含義如下:

鏡面高光:正態分佈函式 Normal Distribution Function

(引數:normal,viewDir,lightDir,粗糙度)

這裡和傳統的BlinnPhong高光模型一樣,是用半形向量h,也就是viewdir和lightdir的中間向量h,和normal求點乘來決定高光亮度的。

瞭解的朋友都知道,BlinnPhong其實相對於它的前身Phong,並不是那麼的“物理”(視線越接近水平和光線反射的物理原理越不一致),所以我看到PBR依然在使用BlinnPhong是有點意外的。

也就說明,兩個都不完全“物理”的公式,還是看上去和物理效果更接近的,比實際“更物理”的吃香。經驗公式最終獲得了勝利(括弧笑)。

而綜合了散射係數的具體的公式如下:

D =\frac{α^{2}}{π((n⋅h)^{2}(α^{2}−1)+1)^{2}}

這個公式也是前人的勞動成果,我也不知道是物理推導的結果還是“看上去對就好”的經驗公式。但在不同的粗糙度α取值下,它確實和BlinnPhong通過pow實現的效果方向一致,擁有類似的結果。但它的取值是0-1,效果變化也很平滑,比起Skininess那種沒譜的引數更容易控制。

當然更重要的是不會輻射出多餘的光,D不會大於1/π(除π的原因和上面漫反射部分一致)

當α非常接近0的時候,光照集中在一點,其他方向會完全看不到光線。這是符合現實的。

幾何遮蔽:幾何函式 Geometry function

(引數:normal,viewDir,lightDir,粗糙度)

這是一個其他傳統光照模型不具有的特徵,體現了光在物體粗糙面上反射時的損耗。

G =\frac{n⋅l}{lerp(n⋅l,1,k)}\times\frac{n⋅v}{lerp(n⋅v,1,k)}

直接光照時: k = \frac{(α+1)^{2}}{8} ,間接光照(IBL)時: k = \frac{α^{2}}{2}

效果就是粗糙度越大,亮度越低。但視線和光線越接近垂直,受粗糙度的影響就越小,合情合理。

k的取值範圍都在逐漸逼近1/2。而直接光和間接光的差別是,直接光至少有1/8的吸收係數保底,而間接光沒有。這是為了讓完全光滑的物體,也能至少吸收一些光線。完全不吸收光線的物體是不應該存在的。

菲涅爾方程:Fresnel equation

(引數:normal,viewDir,金屬度)

菲涅爾方程以前一般是用在水體上的,因為水體粗糙度低反光能力強,卻又不是金屬,是菲涅爾效應最明顯的現實物體。

F = lerp((1−(n⋅v))^{5},1,F_{0})

注意:這個公式和光照方向無關。

法線和視線夾角越大(視線越接近水平),F的值也就越大,反射光的亮度也越高,這就是所有物體都具有的菲涅爾效應。即使不是金屬物體,在這種情況下都會產生和金屬物體類似的表現。而當物體本身就是金屬的時候(F0接近1),不管視線是什麼情況,F的值都會接近於1,那麼菲涅爾效應也就看不出來了。

這看似是個無關緊要的特性——那只是我們大多沒有意識到“物體應該如此”而已,但即使我們沒注意到,我們的大腦卻會依然會得出一個“不真實”的結論。其實菲涅爾效應的模擬比我們想象中要更重要,並不僅僅是在水體模擬這個情景下。

然而,對於金屬物體而言,菲涅爾其實並不完全適用。他的F0引數對不同顏色值的反射率是不同的,而且還需要和表面顏色相乘,否則我們的大腦就會通知我們它“不像金屬”,所以最終的做法是做這樣一次處理:

F0 = mix(vec3(0.04), 表面顏色, 金屬度);

這樣代入公式的結果就比較符合金屬的物理特徵,而非金屬由於F0值偏低,即使乘了表面顏色影響也不大。

注意這裡的表面顏色僅僅是給金屬物體用的,用於表現金屬物體的特殊性質,高光部分本身並不需要和物體的表面顏色相乘。

BRDF方程的配平係數:

\frac{……………………}{4(viewDir\cdot normal)(lightDir\cdot normal)}

至於這個公式剩下的分母部分,在哪裡都沒有看到它們的解釋,而且也想不出“除點乘”對應著何種物理特性,“4”這個迷之係數更是難以理解。大家都是把它當做一個配平係數直接用了,最後我也只知道這兩個點乘是和微平面有關的。

我倒是覺得它們應該不是什麼“經驗公式”,應該有推導的方法,但這個問題我確實也沒找到懂的人,所以這次也只能先放在一邊了。

最後看回這個公式:

L_{o}(p,w_{o})=\int_{\Omega}(k_{d}\frac{c}{\pi}+ k_{s} \frac{DGF}{4(w_{o}\cdot n)(w_{i}\cdot n)})L_{i}(p,w_{i})(w_{i}\cdot n)dw_{i}

最後還有兩個引數沒有解明,也就是Kd(漫反射比例)和Ks(鏡面反射比例)。

Ks(鏡面反射比例)實際上就是F。之前的公式其實並不妥當,因為Ks和F其實是重複的,只需要乘一次。所以應該是:

L_{o}(p,w_{o})=\int_{\Omega}(k_{d}\frac{c}{\pi}+ \frac{DGF}{4(w_{o}\cdot n)(w_{i}\cdot n)})L_{i}(p,w_{i})(w_{i}\cdot n)dw_{i} 。

而Kd(漫反射比例),則是(1-F)(1-金屬度),除了需要減掉F外,還要再乘一次(1-金屬度)。這是因為金屬會更多的吸收折射光線導致漫反射消失,這是金屬物質的特殊物理性質。

其實,剛才說的這幾個DGF公式都不是唯一的,因為這些公式即使是基於物理的,也還是會包含一些“只要和結果差不多就可以”的部分(比如那個1/8),因為嚴格的公式往往會為了不明顯的細節而消耗大量計算時間,不值得。

所以,他們其實也都只是“並非那麼擬合”的擬合公式。

而這幾個公式,也有一些精度更低,但效能更好的擬合版本,諸如UE4的Paper裡,菲涅爾部分使用的是這樣一個神奇的公式:

這個公式是用曲線擬合方式對之前那個菲涅爾方程的近似,通過把pow函式換成exp2,得到了更好一點的效能。

(是的,exp2比pow快,因為x^y = e^{y \ln x}

至此,PBR的直接光照部分就已經完成。

IBL(Image-Based Lighting 基於紋理的光照)

但是PBR並不只有直接光照。

如果只考慮直接光照的話,PBR渲染出來的畫面,其實和以前並沒有多少區別。PBR統一了光照的單位,保證了光能守恆,確實有它的積極意義。但是隻說畫面效果的話,確實沒啥明顯的進步。

讓PBR表現出優於上一個世代的畫面效果的,是它的環境光照部分。這部分則是由IBL實現的。實際上,他就是所謂的動態全域性光照(Gl)的正體。

實現原理僅從它的名字(Image-Based)就可以猜出來,就是cubemap(環境貼圖)。

實際上,它就是我們熟悉的環境反射貼圖,只是取樣點布點更加廣泛,而且會在相近的取樣點之間插值。對於Unity而言,就是Light Probe。

只不過,在PBR的IBL中,這張環境貼圖並不會僅僅提供非常粗糙的幾個光源的烘焙圖。它會把周圍環境的輻射度(也就是顏色)完整儲存起來,而且精度很高,高到可以形成清晰的鏡面倒影。

而PBR的材質則會把這種環境貼圖當做光源來進行取樣。如果是金屬材質,且粗糙度低,就能夠映射出周圍的環境,甚至成為“鏡子”。但不是金屬的物體也會受此影響,不僅僅會被光源照亮,還會被周圍這些“預烘焙成貼圖”的物體略微照亮。

實現上確實並不複雜,和環境貼圖的用法差不多,直接取樣cubemap獲得光照資料,然後再代入PBR的公式算出結果就行了。

這時候,大家應該回想起了之前那個討厭的半球積分( \int_{\Omega} ),那麼,我們是否應該根據這個積分,取樣整個半球的資料,然後再計算一次BRDF,最終合併出一個顏色值來呢?

這怎麼可能算得動?有腦子的人,肯定都會直接把這個計算結果直接存在環境貼圖裡好吧?

漫反射部分不需要擔心,這部分還真的就是最普通的環境貼圖,因為並沒有任何變數,直接搞出一張很糊的環境圖,再通過normal從cubemap直接取樣顏色值即可。

而高光部分,則有粗糙度α這個變數,必須需要烘焙出多個粗糙度下的環境圖。然而,不同α值下的烘焙出環境貼圖,其實主要就是模糊程度的不同,所以生成這樣一組圖:

然後合併到一張cubemap的多個mipMap層級上,再利用cubeTexLod函式,根據其粗糙度選擇特定層級的兩個mipMap層級進行三線插值,就能得到需要的半球積分過的光照顏色值了(當然,是近似的)。

float lod             = getMipLevelFromRoughness(roughness);
vec3 prefilteredColor = textureCubeLod(PrefilteredEnvMap, refVec, lod);

然後通過光照正常計算一次BRDF就好了。

這樣各個不同粗糙度和金屬度的材質就都能從同一張環境貼圖裡獲得需要的資料,並完成各自的渲染。

然而,IBL的難點並不在渲染部分。而是在預烘焙部分。

這些模糊的貼圖,雖說也不是不能直接用高斯模糊一類的方法完成。但高斯模糊畢竟也只是一種近似,效果還是比不上真正的半球積分的。

我貼的兩篇文章,其實大部分的內容都在講怎麼拆積分,通過拆解的積分寫出一個4096次sample的隨機取樣函式,算出一個平均值來,存在紋理上,生成需要的烘焙紋理。

這部分內容實在太多,需要了解的就去看原文吧。不想了解的,也可以直接把它的烘焙部分程式碼抄走。

下面依然是對一些難點的解釋:

在烘焙環境貼圖的計算裡,並沒有取當前攝像機viewDir,而是讓viewDir直接等於 w_{o}=球面normal 。

因為在生成cubemap的時候,viewDir本來也沒有意義。所以只能讓它一直朝向當前正在繪製的畫素。之後使用這個cubemap,根據當前的viewDir重新計算的時候,因為兩次的viewDir是不同的,積分合並後的結果當然也是錯的。

但也沒啥別的方法啊。

就圖片裡的結果,還算勉強可以接受吧。

vec2 Xi = Hammersley(i, SAMPLE_COUNT);
vec3 H  = ImportanceSampleGGX(Xi, N, roughness);

Hammersley叫做低差異序列,是一種特殊的生成隨機數的方法,可以生成一組“並不是那麼隨機的隨機數”。

隨機數有兩大要求:概率分佈均勻,足夠混亂。Hammersley就是一個概率分佈均勻但是並不太混亂的隨機數生成方法,作為一個隨機數是很糟糕的,但是在隨機收斂中使用它代替正常的隨機數,反而可以獲得更快的收斂速度,也就是在同樣sample數量下獲得更好的效果。

P.S. 其實這種方法也可以用到抽卡上,能夠大幅減少非洲人和歐洲人的比例,在不增加保底的同時提高抽卡體驗。

下面的ImportanceSampleGGX(重要性取樣)其實很簡單,就是“隨機正態分佈取樣”,也就是需要實現一個正態分佈的隨機數生成器。

z0 = sqrt(-2.0 * log(u1)) * cos(2 * pi * u2);
z1 = sqrt(-2.0 * log(u1)) * sin(2 * pi * u2);
(u1,u2是兩個[0-1]的隨機數)

和文中的重要性取樣程式碼對比下就能發現他們的公式是多麼的相似。

P.S. 其實也可以用到遊戲邏輯中,比如角色長期靜止時pose動作出現的時機。

除了普通的輻射度烘焙外,它還將IBL的BRDF計算過程做了預計算,放入了一個LUT查詢圖裡。

vec2 envBRDF          = texture2D(BRDFIntegrationMap, vec2(NdotV, roughness)).xy;
vec3 indirectSpecular = prefilteredColor * (F * envBRDF.x + envBRDF.y) 

用NdotV,roughness作為引數就可以直接從圖中獲得計算結果,並得到處理了視角和高光的最終顏色值。雖然紋理隨機取樣會導致cache miss,但畢竟節約了大量的計算。

這張查詢圖的生成方法,還有那個拆積分的過程……很抱歉我實在沒心情去看,實在是又臭又長。

——畢竟這個圖其實是通用的啊,複製貼上走就可以,連程式碼都不需要抄。

結語:

PBR這套東西,雖然實現一個其實並不算很難。

但是假如你非要去理解為什麼它是這樣,就確實比較費勁。而且還牽涉到了一些前置知識,不說的話也會難以前進,導致這篇文章特別的難寫。

估計能完全看懂的人的不會太多,再說有些內容我自己也沒搞懂。

不過,本來PBR這東西也不是非要看懂就是了,不求甚解,只是扒公式扒程式碼,一樣能做出東西來。

寫這篇文章的時候我倒是發現了很多自己以前對PBR的理解錯誤——然後修正過來了,也算有所收穫吧。但這對做事方面並沒有什麼幫助。

本來想把這文章寫的簡單一點,但結果還是這樣,也是沒啥辦法。

如果你們要問,要上PBR該怎麼上?

如果你想上的是真正的PBR,而不是什麼打著PBR幌子的劣化品(加了點法線高光就戛然而止),恐怕直接用unity的Standard就是現在最好的選擇。因為直接光部分實在沒啥可改的,而間接光部分,你要自己實現一套也實在太困難了,這可不是改個Shader這麼簡單的事情,怎麼樣都得依附Light ProbeGroup和配套的渲染管線。

之前Unity自己那個演示就是用的這套東西,看著也不賴不是麼?效果上應該是合格的。

但是真正的PBR效能耗費肯定是比較高的。進入PS4時代後,遊戲需求配置都在蹭蹭往上漲好吧,畢竟萬物皆法線+高光+反射+菲涅爾。我其實很懷疑,到底有多少團隊是真的需求PBR,而不是強行為了PBR而PBR。比如說,你遊戲裡半個光源都沒有,還整天追求鮮豔的畫面風格,而且沒有法線沒有高光……又或者有光源,但是並沒有按自然光的方式打光,而是各種點光源補光,光源一點都不物理,那又何必去追求材質的物理正確呢?如果模型等等都非常簡陋,一看就不像現實,那麼材質貼近現實又什麼意義?

用傳統的材質Shader,效率自由度不是都更高嗎?

——NPR也是極好的嘛。

Mark幾篇相關文章避免以後找不到: