1. 程式人生 > >理解PBR:從原理到實現(下)

理解PBR:從原理到實現(下)

在前面的文章中我講了虛幻4中所使用的反射率方程,其中複雜的部分是 Cook-Torrance BRDF,我們把它帶入整個積分,回顧一下完整的反射率方程。

Lo(p,v)=H(kdcdiffπ+ksD(h)F(l,h)G(l,v,h)4(nl)(nv))Li(p,l)nldl L_o(p, v) = \int\limits_H (k_d\frac{c_{diff}}{\pi} + k_s\frac{D(h)F(l,h)G(l,v,h)}{4 (n \cdot l)(n \cdot v)})L_i(p,l) n \cdot l dl

4(nl)(nv)D(h)F(l,h)G(l,v,h))Li(p,l)nldl

這樣複雜的積分是無法求解析解的,只能通過數值計算方法求數值解,在圖形領域常用的方法就是“蒙特卡洛積分(Monte Carlo integration)”。對於實時渲染來說,我們還需要祭出我們最常用的兩大法寶:預計算和湊合,咳咳,說錯了?,近似,approximation!我們還需要另外一個重要的技術,就是 IBL,Image Based Lighting!把它們組裝起來:把上面這個積分進行恰當的近似,並將能夠預計算的部分使用蒙特卡洛積分求出數值解,以貼圖的方式儲存起來。在實時渲染的時候,通過取樣貼圖取得這些預計算值,進行 Shading 計算!

再進一步的說,虛幻4使用 Split Sum Approximation 將上述積分分成兩部分進行預計算:

  • 一部分的計算結果儲存到一個 Cube Map 上,管它叫做“Pre-Filtered Environment Map”;
  • 另外一部分的計算結果儲存為一張 R16G16 格式的2D貼圖,管它叫做:“Environment BRDF”。

【重要提示】 下面的內容需要兩個背景知識:蒙特卡洛積分Image Based Lighting,如果你對這兩不熟悉,可以先看文章後面一半的基礎知識部分。

Split Sum Approximation

下面重點講一下虛幻4在進行預計算的過程中進行了哪些公式推導和近似。我們先來看一下虛幻4文件中的公式推導的第一步:

HLi(p,l)f(l,v)cosθldl1Nk=1NLi(lk)f(lk,v)cosθlkp(lk,v) \int\limits_H L_i(p,l) f(l, v) cos \theta_l dl \approx \frac{1}{N} \sum_{k=1}^{N} \frac{L_i(l_k) f(l_k, v) cos \theta_{l_k}}{p(l_k, v)}

這個約等於確不是“湊合”,等式右邊就是蒙特卡洛積分公式,其中 p(lk,v)p(l_k, v) 就是概率分佈函式:pdf,要說明的是:對於渲染方程,pdf 是一個歸一化函式(normalized function),即在半球域內的積分值為 1 。 (上面公式中的cosθlcos \theta_l就是我們之前公式中的nln \cdot l

接下來,出於效能方面的考慮,下一步是一個頗有道理的近似。

1Nk=1NLi(lk)f(lk,v)cosθlkp(lk,v)(1Nk=1NLi(lk))(1Nk=1Nf(lk,v)cosθlkp(lk,v)) \frac{1}{N} \sum_{k=1}^{N} \frac{L_i(l_k) f(l_k, v) cos \theta_{l_k}}{p(l_k, v)} \approx (\frac{1}{N} \sum_{k=1}^{N} L_i(l_k) )(\frac{1}{N} \sum_{k=1}^{N} \frac{f(l_k, v) cos \theta_{l_k}}{p(l_k, v)} )

也就是把第一步的蒙特卡洛公式分拆為兩個\sum來運算,這樣就可以分別進行預計算。這是非常重要的一步,Epic 的大牛給它起了個名字,就叫做:Split Sum Approximation。名字起的很好,就是把一個 \sum 拆分成了 \sum \cdot \sum !這兩個 \sum 要分別使用蒙特卡洛積分去計算,並且都使用了重要性取樣(Importance Sampling)。重要性取樣先單獨講一下。

重要性取樣(Importance Sampling)

蒙特卡洛積分的一個焦點就是所謂的“取樣(Sampling)”。對於渲染方程,我們能計算的入射光的數量是有限的,所以計算結果和理想積分值會有偏差,也就是會產生渲染結果中的 noise 。另外一方面,我們對渲染方程的行為是有一個粗略的概念的,例如對於非常光滑的表面,那麼我們應該在出射光方向(觀察方向)的鏡面反射方向周圍分配更多的樣本。這種取樣的分佈控制就是通過 pdf 函式實現的。

在虛幻4中使用了基於 GGX 分佈函式的重要性取樣來提高蒙特卡洛積分的精確度。GGX 函式的一個重要引數就是粗糙度。我們可以看一下虛幻4中對應的程式碼:“Epic Games\UE_4.20\Engine\Shaders\Private\MonteCarlo.ush”,其中 ImportanceSampleGGX(E, a2) 的第一個引數 E,它的取值是 Hammersley Sequence;第二個引數 a2 的取值是粗糙度的四次方。

float4 ImportanceSampleGGX( float2 E, float a2 )
{
	float Phi = 2 * PI * E.x;
	float CosTheta = sqrt( (1 - E.y) / ( 1 + (a2 - 1) * E.y ) );
	float SinTheta = sqrt( 1 - CosTheta * CosTheta );

	float3 H;
	H.x = SinTheta * cos( Phi );
	H.y = SinTheta * sin( Phi );
	H.z = CosTheta;
	
	float d = ( CosTheta * a2 - CosTheta ) * CosTheta + 1;
	float D = a2 / ( PI*d*d );
	float PDF = D * CosTheta;

	return float4( H, PDF );
}

Hammersley Sequence

這裡還需要說明一下 Hammersley Sequence,這是一個低差異序列(Low-Discrepancy Sequence),你可以粗略的理解為:它生成一系列分佈更為均勻的隨機數。下面這個圖就是 Hammersley 計算的結果示意圖。

hammersley

下面是它在虛幻4中的實現程式碼,你可以在“Epic Games\UE_4.20\Engine\Shaders\Private\MonteCarlo.ush”檔案中找到它:

float2 Hammersley( uint Index, uint NumSamples, uint2 Random )
{
  float E1 = frac( (float)Index / NumSamples + float( Random.x & 0xffff ) / (1<<16) );
  float E2 = float( ReverseBits32(Index) ^ Random.y ) * 2.3283064365386963e-10;
  return float2( E1, E2 );
}

GGX Distribution

前面講到虛幻4在蒙特卡洛積分中使用的 pdf 函式是一個基於 GGX 的分佈函式,這又是什麼意思呢?

ggx_pdf

我們想要達到的取樣分佈如上圖所示,圖中黃色的部分,也就是出射光方向的反射方向,取樣更多;黃色區域外的部分取樣更少。黃色區域的分佈收到物體粗糙度的影響,因為反射向量的分佈就是收到微平面的法向量分佈影響的!而微平面的法向量分佈(其實是hh向量,說法向量為了更直觀),我們在上篇文章中提到了,使用的是 Trowbridge-Reitz GGX 模型。因為 D(h)D(h)hh 向量的分佈,所以 pdf 函式要在它的基礎上附加一些計算,就形成了 ImportanceSampleGGX() 函數了!這個公式推導在 Disney 的分享1中有詳細過程,這裡就不深究了。

計算Split Sum的第一部分

讓我們來聚焦Split Sum的第一部分:

1Nk=1NLi(lk) \frac{1}{N} \sum_{k=1}^{N} L_i(l_k)

這個公式是比較直觀的,就是對環境貼圖進行卷積,它的計算結果仍然是一個 Cube Map,也就是“Pre-Filtered Environment Map”!

這裡有一個問題就是,使用 GGX 概率分佈函式的話,需要觀察方向V和表面法線N,這兩個都是執行時才能得到的,咋整啊?在虛幻4的預計算中,假設 N=V=R ,也就是觀察角度為 0!? 這是引入誤差最大的一個近似。

虛幻4使用下面這個函式:PrefilterEnvMap(),生成“Pre-Filtered Environment Map”。這個函式有兩個引數:粗糙度和反射方向:

  1. 每一個 Mip Map Level 對應一個“粗糙度”值;
  2. 對於每個 Mip Map Level,每一個貼影象素(texel)就對應一個反射方向;

對於每個 Mip Map Level 上的每個 texel 執行此函式進行卷積,即可得到這部分積分的預計算結果。這個函式計算的過程中用到了Hammersley()和ImportanceSampleGGX(),已經在上面的“重要性取樣”一節介紹過了啊。

float3 PrefilterEnvMap(float Roughness, float3 R)
{
    float3 N = R;
    float3 V = R;
    float3 PrefilteredColor = 0;
    const uint NumSamples = 1024;
    for (uint i = 0; i < NumSamples; i++ )
    {
        float2 Xi = Hammersley(i, NumSamples);
        float3 H = ImportanceSampleGGX(Xi, Roughness, N);
        float3 L = 2 * dot(V, H) * H - V;
        float NoL = saturate(dot(N, L));
        if (NoL > 0) {
            PrefilteredColor += EnvMap.SampleLevel(EnvMapSampler, L, 0).rgb * NoL;
            TotalWeight += NoL;
        }
    }
    return PrefilteredColor / TotalWeight;
}

計算Split Sum的第二部分

我們來看一下Split Sum第二部分的公式:

1Nk=1Nf(lk,v)cosθlkp(lk,v)=Hf(l,v)cosθldl \frac{1}{N} \sum_{k=1}^{N} \frac{f(l_k, v) cos \theta_{l_k}}{p(l_k, v)} = \int\limits_H f(l, v) cos \theta_l dl

這部分計算的結果就是下面這樣一個2D查詢表,儲存到一個 R16G16 的2D貼圖中,叫做“Environment BRDF”。

lut

這個是怎麼計算得出的呢?

把 Schlick 菲涅爾近似公式:F(v,h)=F0+(1F0)(1vh)5F(v, h) = F_0 + (1-F_0)(1 - v \cdot h)^5 帶入上述積分,發現 F0F_0 可以從積分中提取出來,Epic 文件中的公式為:

Hf(l,v)cosθldl=F0Hf(l,v)F(v,h)(1(1vh)5)cosθldl+Hf(l,v)F(v,h)(1vh)5cosθldl \int\limits_H f(l, v) cos \theta_l dl = F_0 \int\limits_H \frac {f(l, v)}{F(v,h)}(1 - (1 - v \cdot h)^5) cos \theta_l dl + \int\limits_H \frac {f(l, v)}{F(v,h)} (1 - v \cdot h)^5 cos \theta_l dl

右側變為兩個積分之後,可以分開求解,輸出為兩個值:(F_0的縮放, F_0的偏移量),也就是:

Hf(l,v)cosθldl=F0EnvB