UnrealEngine4 PBR Shading Model 概述


  雖然是概述,但內容並還是有些多,寫上一篇PBR概念概述后,也在考慮怎么繼續下去,最后還是覺得先多寫一些東西再慢慢總結,所以還是盡量把這些年PBR相關的Paper精粹沉淀下來吧。
 
  因為UE4開源的緣故,所以一開始還從它入手。相關的ppt和notebook可以從下面的鏈接下載,同期的黑色行動2(black op2)的PBR使用也是很有參考價值的,加上本文里也有OP2的IBL近似方法的介紹,如果沒看過那也很值得下載的。
 
  UE4的paper里的PBR介紹包括三部分:Shading Model ,Lighitng Model ,Material Model,這篇就先從Shading Model,也就是使用的BRDF開始吧,但要滿足一個游戲的所有渲染效果,靠一個通用的BRDF也是無法達到的,所以也只能算是個概述吧,隨着使用和學習的應用,也會繼續補完Shading Model的介紹的。
 

首先,PBR最大的特點還是引入了微平面概念


着色平面不再是一個完美的反射平面,而是想象成更多微小的反射平面組成。所以也就有了粗糙度的概念
 
Diffuse BRDF
  可以選擇簡單的Lambert或支持 Microfacet Oren-Nayar
UE4默認使用的是 Lambert

 
Specular BRDF
支持微平面概念的 Cook-Torrance  Microfacet BRDF   ,在直接照明和間接照明的Shading Model里使用 
 
 
  由Fresnel項,NDF(Normal Distribution Function)項  Geometry Factor項來組成,以獲得更加物理的效果
F D G項會根據設備性能選擇最適合的公式
暫定 F是 Schlick’的近似
 
F0是垂直入射時的反射率(法線方向的Specular Reflectance),一般也就是存在Specular Color map里的數值了。    
 
NDF(Normal Distribution Function) GGX
 這個是能量守恆因子( normalization factor ),用來保證出射光 < 入射光的,具體的求導會放在另外一篇里一同解說。
Geometry Factor  GGX
  直接光照還是比較簡單的,將公式和需要的參數直接套入現有的渲染管線就可以了。Forward Rendering還好,多加幾個參數也沒有影像,如果是Deferred Rendering的話,就需要把F0(Specular Color)放入到Gbuffer了。但這樣對於以前CE那種Gbuffer還是有影響的。
 
孤島危機2  Deferred Lighting Slim G-Buffer
§ A8B8G8R8 World Space BF Normals 24bpp + Glossiness 8bpp RT1
§ Readback D24S8 Depth + Stencil bits for tagging indoor surfaces 8pp RT0
 
孤島危機3  Hybrid Deferred Rendering  Thin G-Buffer 2.0  只傳入Specular Color的灰度值
 
羅馬之子 Deferred Shading , PBR需要完整的Specular Color

而UE4是提供了forward rendering和 Deferred Shading兩種方案,因為Computrer Shader可以在TBDR(Tile Based Deferred Rendering)上的應用,所以 Deferred   lighting這種方法基本上到PBR上已經基本沒有什么優勢了。基本上一個比較完整的GBuffer+TBDR管線算是現在的主流設計方式了。這些到了后面PBR渲染管線設計時再具體描述吧。
 
Image Based Lighting
使用預處理的環境光貼圖來做光源的間接照明方案。
原始公式IBL公式,u是入射光方向,v是視點方向,Li是每一個入射光,也就是Environment Map的信息,f是我們前面提到的BRDF着色模型
 
重要度采樣(Importance Sampling)
 
  原始公式是要對周圍光照做一個均勻的隨機Sampling(Hammersley 隨機采樣),但像光滑材質上,大量的光會聚集在Specular方向上(鏡面反射方向),均勻采樣無法獲得准確的結果。在無法改變采用分布的情況下,使用PDF(probability density function  概率采樣函數)是一個近似解決的方法,把PDF(p)在公式里作為分母使用,PDF是0~1的一個浮點數,在接近Specular方向,這種采樣數需要較高的地方,PDF值會變得較低,提高了最后采樣的數值(間接來說就是提升了次數),相反,在采樣數較低的地方,PDF值會更高,間接減少采樣次數   。也就有了下面這個公式的近似。 
 
float3 ImportanceSampleGGX( float2 Xi, float Roughness , float3 N )
{
    float a = Roughness * Roughness;
    float Phi = 2 * PI * Xi.x;
    float CosTheta = sqrt( (1 - Xi.y) / ( 1 + (a*a - 1) * Xi.y ) );
    float SinTheta = sqrt( 1 - CosTheta * CosTheta );
    float3 H;
    H.x = SinTheta * cos( Phi );
    H.y = SinTheta * sin( Phi );
    H.z = CosTheta;
    float3 UpVector = abs(N.z) < 0.999 ? float3(0,0,1) : float3(1,0,0);
    float3 TangentX = normalize( cross( UpVector , N ) );
    float3 TangentY = cross( N, TangentX );
    // Tangent to world space
    return TangentX * H.x + TangentY * H.y + N * H.z;
}
float3 SpecularIBL( float3 SpecularColor , float Roughness , float3 N, float3 V )
{
    float3 SpecularLighting = 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 NoV = saturate( dot( N, V ) );
        float NoL = saturate( dot( N, L ) );
        float NoH = saturate( dot( N, H ) );
        float VoH = saturate( dot( V, H ) );
        if( NoL > 0 )
        {
            float3 SampleColor = EnvMap.SampleLevel( EnvMapSampler , L, 0 ).rgb;
            float G = G_Smith( Roughness , NoV, NoL );
            float Fc = pow( 1 - VoH, 5 );
            float3 F = (1 - Fc) * SpecularColor + Fc;
            // Incident light = SampleColor * NoL
            // Microfacet specular = D*G*F / (4*NoL*NoV)
            // pdf = D * NoH / (4 * VoH)
            SpecularLighting += SampleColor * F * G * VoH / (NoH * NoV);
        }
    }
    return SpecularLighting / NumSamples;
}

上面是計算Specular間接光的shader 偽代碼,1024次對實時的GPU來說還是很難的,需要對公式做拆分

把上面的公式拆分成兩部分,而第1個部分和環境光貼圖相關的,可以一起進行預計算,也是下面要說到的 Pre-Filtered Environment Map
 
Pre-Filtered Environment Map
  而UE4在拆分時還是做了一些額外的改動,那就是第1個部分里的除了采樣環境光外,為了更多預計算,把第2部分里基於GGX的PDF也放到了預處理里,PDF公式里需要的V(視口向量)和N(法線),所以這里只能就只能假設n = v = r了。
  1. 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;
    }

    PrefilterEnvMap生成部分的shader代碼。

而后面的部分,我們可以通過Schlick近似的Fresnel公式來進行拆分。

  這個時候,我們可以把方程看成是F0 * Scale + Offset的形式了,F0也就是Spcecualr Color可以從材質獲取,也就是說,我們把Scale和Offest預計算出來。並通過roughness和NdotV,也就是costheta作為LUT的查找項
這樣就可以把公式重新組合起來:
float2 IntegrateBRDF( float Roughness , float NoV )
{
    float3 V;
    V.x = sqrt( 1.0f - NoV * NoV ); // sin
    V.y = 0;
    V.z = NoV; // cos
    float A = 0;
    float B = 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( L.z );
        float NoH = saturate( H.z );
        float VoH = saturate( dot( V, H ) );
        if( NoL > 0 )
        {
            float G = G_Smith( Roughness , NoV, NoL );
            float G_Vis = G * VoH / (NoH * NoV);
            float Fc = pow( 1 - VoH, 5 );
            A += (1 - Fc) * G_Vis;
            B += Fc * G_Vis;
        }
    }
    return float2( A, B ) / NumSamples;
}

最后把第一部分pre-fileter的cubemap和第2部分計算的部分相乘,就都出IBL的最終結果了

float3 ApproximateSpecularIBL( float3 SpecularColor , float Roughness , float3 N, float3 V )
{
    float NoV = saturate( dot( N, V ) );
    float3 R = 2 * dot( V, N ) * N - V;
    float3 PrefilteredColor = PrefilterEnvMap( Roughness , R );
    float2 EnvBRDF = IntegrateBRDF( Roughness , NoV );
    return PrefilteredColor * ( SpecularColor * EnvBRDF.x + EnvBRDF.y );
}

這里需要注意一點 : EPIC在ppt里提供的shader代碼,並不是實際運行的代碼,也就是說PrefilterEnvMap和 IntegrateBRDF這兩個函數還是ALU方式的實現,而實際上是應該用LUT的方式來替換的。也就是下面的shader代碼

half3 EnvBRDF( half3 SpecularColor, half Roughness, half NoV )
{
    // Importance sampled preintegrated G * F
    float2 AB = Texture2DSampleLevel( PreIntegratedGF, PreIntegratedGFSampler, float2( NoV, Roughness ), 0 ).rg;
    // Anything less than 2% is physically impossible and is instead considered to be shadowing 
    float3 GF = SpecularColor * AB.x + saturate( 50.0 * SpecularColor.g ) * AB.y;
    return GF;
}

PreIntegratedGF就是我們前面提到的那張紅綠的LUT圖,這里最后算得的結果,才是UE4最終選擇的近似方案,也是

里后面的部分,而前面部分則保存在AmbientCubemap里,對AmbientCubemap采樣
  1. floatMip=ComputeCubemapMipFromRoughness(GBuffer.Roughness,AmbientCubemapMipAdjust.w );
    float3SampleColor=TextureCubeSampleLevel(AmbientCubemap,AmbientCubemapSampler, R,Mip).rgb;
    
    SpecularContribution+=SampleColor*EnvBRDF(GBuffer.SpecularColor,GBuffer.Roughness,NoV);

    再把結果相乘,就得到了最終的Specular的顏色。

不同精度條件下的渲染效果
上一排是完全在shader里計算的,中間是按正規拆分后近似的結果(PDF在shader里計算),下面則是完全近似的方法(PDF放在了Pre-Filtered EnvMap里,假設r = n = v)
 
同樣的近似方法,在非金屬(絕緣體)上的效果比較
 
PS:最后這里還是想注釋一下,也就是PrefilterEnvMap具體的生成算法,以及如何根據不同的粗糙度生成mip,ue4的ppt里並沒有涉及,這個我想會在以后的文章里具體介紹吧。
 
  另外,在GLES2.0的移動設備上,因為texutre sample最高只有8張,UE4為了節省LUT,還提供另外一種更加近似的方式,應該是參考了黑色行動2(后面簡稱ops2吧)里的方法,
在他們的paper里把這個叫做“ground truth”
 
這里還是是想介紹一下這種方法,這里我們可以看到 ops2里也做了和ue4類似的拆分。
 
ground truth的近似
前面部分是Pre-filtered Environment map,后面則是 Environment  BRDF,不過他這並沒有PDF,而是直接把D項( Normal Distribution Function )放到前面去預計算了。
 
  然后再說后面部分的拆分,前面提到,UE4是通過 Fresnel公式的F0提出來,做成F0 * Scale + Offset的方式,再Scale和Offset索引的存到了一張2D LUT上。靠roughness和NoV(N dot V)來查找
而ops2的方法,也一樣是從Schlick的Fresnel公式入手來拆分(這里rf0和F0是一樣的)。
這里是從加法這里做拆分
  大概就變成了 rf0 * a1+ (1-rf0) * a0的形式,這個公式很容易理解為一個關於rf0的一個線性插值公式。所以只要能計算出a1 (rf0 = 1)和a0(rf0 = 0),就可以通過線性公式求出任意rf0情況下的結果了。
接下來就是想辦法來近似出a0和a1的曲線函數了
ground truth   rf0 = 0 的 曲線
ground truth  rf0 = 1   的 曲線
 
然后他們整出兩個近似的曲線函數出來
float a0( float g, float NoV )
{
    float t1 = 11.4 * pow( g, 3 ) + 0.1;
    float t2 = NoV + ( 0.10.09 * g );
    return (1 – exp( -t1 * t2 ) ) * 1.32 * exp2( -10.3 * NoV );
}

float a1( float g, gloat NoV )
{
    float t1 = max( 1.3360.486 * g, 1);
    float t2 = 0.06 + 3.25 * g + 12.8 * pow( g, 3 );
    float t3 = NoV + min( 0.1250.1 * g, 0.1 );
    return min( t1 – exp2( -t2 * t3 ), 1 );
}

並進一步的做優化

  1. float a0f( float g, float NoV )
    {
        float t1 = 0.095 + g * ( 0.6 + 4.19 * g );
        float t2 = NoV + 0.025;
        return t1 * t2 * exp2( 114 * NoV );
    }
    float a1f( float g, float NoV )
    {
        float t1 = 9.5 * g * NoV;
        return 0.4 + 0.6 * (1 – exp2( -t1 ) );
    }

  2. rf0(ground truth)是點線,a0是實線,a0f是線段

rf1(ground truth)是點線,a1是實線,a1f是線段
但這個曲線被美術人員反映環境光反射效果過亮,特別是dielectric(電介質/絕緣體/非金屬)和gloss低的情況。所以就把rf0 = 0.04這個對非金屬比較通用的值,作為求a0的參數
 
ground truth rf0 = 0.04的 曲線 不在是a0曲線,而叫a004曲線了- -
那么,擬合出來的更廉價的 a004曲線的公式
  1. float a004( float g, float NoV )
    {
        float t = min( 0.475 * g, exp2( -9.28 * NoV ) );
        return ( t + 0.0275 ) * g + 0.015;
    }

  2. ground truth rf0 =0.04是點線 a004是實線

另外,因為在游戲里,金屬的使用情況並不多,也就是說a1(rf0 = 1)在實際計算插值時,貢獻的參數並不是那么占主要的,所以,可以做a1f做進一步粗糙近似成a1vf
float a1vf( float g )
{
    return 0.25 * g + 0.75;
}

再用a004和a1vf算出新的a0r

  1. float a0r( float g, float NoV )
    {
        return ( a004( g, NoV ) - a1vf( g ) * 0.04 ) / 0.96;
    }


      至此,a0和a1的最終近似版本也完成了,前面我們提到實際計算就是關於rf0的插值運算
    這里我們把rf0提出來
    rf0 * a1+ (1-rf0) * a0 = rf0 (a1 - a0) + a0  ,那么最后的Environment BRDF近似公式

  1. float3 EnvironmentBRDF( float g, float NoV, float3 rf0 )
    {
        float4 t = float4( 1/0.96, 0.475, (0.0275 - 0.25 * 0.04)/0.96, 0.25 );
        t *= float4( g, g, g, g );
        t += float4( 0, 0, (0.015 - 0.75 * 0.04)/0.96, 0.75 );
        float a0 = t.x * min( t.y, exp2( -9.28 * NoV ) ) + t.z;
        float a1 = t.w;
        return saturate( a0 + rf0 * ( a1 - a0 ) );
    }

    OP2的近似方法就先講到這里了,PPT的公式推導還是太簡單,建議還是看notebook的吧,如果有問題可以留言給我討論

UE4的mobile PBR
接下來繼續說UE4,他的近似公式也是照抄op2的,
材質為金屬時的近似公式
  1. half3 EnvBRDFApprox( half3 SpecularColor, half Roughness, half NoV )
    {
        const half4 c0 = { -1, -0.0275, -0.572, 0.022 };
        const half4 c1 = { 1, 0.0425, 1.04, -0.04 };
        half4 r = Roughness * c0 + c1;
        half a004 = min( r.x * r.x, exp2( -9.28 * NoV ) ) * r.x + r.y;
        half2 AB = half2( -1.04, 1.04 ) * a004 + r.zw;
        return SpecularColor * AB.x + AB.y;
    }

    材質為非金屬時的近似公式

  1. half EnvBRDFApproxNonmetal( half Roughness, half NoV )
    {
        // Same as EnvBRDFApprox( 0.04, Roughness, NoV )
        const half2 c0 = { -1, -0.0275 };
        const half2 c1 = { 1, 0.0425 };
        half2 r = Roughness * c0 + c1;
        return min( r.x * r.x, exp2( -9.28 * NoV ) ) * r.x + r.y;
    }

    在非金屬的情況下,Specular沒有顏色而只是一個亮度,這里就假設為0.04了

然后還可以進一步優化,就是在roughness = 1的時候,不在運行上面的擬合函數,而是直接 給出一個擬合結果就可以了
DiffuseColor+=SpecularColor*0.45;
SpecularColor=0;

下面是和使用黑色行動2里的擬合方式的對比效果

使用LUT的
 
移動平台,用ALU擬合替代LUT的
 
最后還有一個  Directional Light的近似,不過感覺還是光照模式說完再寫好一些。
 
就暫時到此為止了,如果有錯誤還請留言或直接聯系我,這里先感謝了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM