深入淺出基於物理的渲染二

高光BRDF模型公式推導

這裡讓我們來看看BRDF公式, 並講解一下,除了DFG函數之外其他係數是如何來的: f(p,omega_o,omega_i)=frac{D(omega_h)G(omega_o,omega_i)F(omega_o)}{4cos	heta_ocos	heta_i}

針對方向 omega_iomega_o ,考察方向為half vector omega_h 的微表面的入射微分輻射通量 dPhi_h . 根據輻射亮度定義可知:

dPhi_h=L_i(omega_i)domega_i dA^ot(omega_h)=L_i(omega_i)domega_i cos	heta_hdA(omega_h)

其中 dA(omega_h) 是法線為 omega_h 的微表面微分面積,dA^ot(omega_h)dA(omega_h) 在入射光方向 omega_i 上的投影微分面積。

針對方向為 omega_h 的微表面微分面積如下:

dA(omega_h)=D(omega_h)domega_hdA 代入此公式,可得:

dPhi_h=L_i(omega_i)domega_icos	heta_hD(omega_h)domega_hdA

微表面根據菲涅爾定理反射光線,出射通量如下: dPhi_o=F(omega_o)dPhi_h

根據輻射亮度定義,出射的輻射亮度為:

L(omega_o)=frac{dPhi_o}{domega_ocos	heta_odA}

=frac{F(omega_o)dPhi_h}{domega_ocos	heta_odA}

=frac{F(omega_o)L_i(omega_i)domega_icos	heta_hD(omega_h)domega_h}{domega_ocos	heta_o} ( frac{dw_o}{dw_h}=4cos	heta_h 見下面證明)

=frac{F(omega_o)L_i(omega_i)D(omega_h)domega_i}{4cos	heta_o}

由BRDF定義可得:

f(p,omega_o,omega_i)=frac{L_o(omega_o)}{dE_i(omega_i)}

=frac{F(omega_o)L_i(omega_i)D(omega_h)domega_i}{4L_i(omega_i)domega_icos	heta_icos	heta_o}

=frac{F(omega_o)D(omega_h)}{4cos	heta_icos	heta_o}

模型還要包括了一個幾何衰減項,它描述了被遮擋或處於陰影下的微平面的比例。它可以跟Fresnel項一樣被放入到推導之中。於是整個的公式模型就是:

f(p,omega_o,omega_i)=frac{D(omega_h)G(omega_o,omega_i)F(omega_o)}{4cos	heta_ocos	heta_i}

如下圖[4],考慮面向 omega_o 的球面坐標系立體角 domega_idomega_h 分別是 sin	heta_id	heta_idphisin	heta_hd	heta_hdphi 並且 	heta_i=2	heta_h 所以:

frac{dw_i}{dw_h}=frac{sin	heta_id	heta_idphi}{sin	heta_hd	heta_hdphi}=frac{sin2	heta_h2d	heta_h}{sin	heta_hd	heta_h}=4cos	heta_h

下面換一種證明方法[5]:假設 h_r 為法線方向(0,0,1), 將 domega_o沿著i方向整體平移,並保持 domega_o 不變(球坐標系整個移動過去)

由half vector定義可知:ar{h}_r=i+oh_r 與i和o等角。 domega_o 與o垂直, domega_0 投影到垂直矢量 h_r 方向為 domega_ocos	heta_i 。由上圖可知,投影立體角與立體角 domega_h (與h垂直)關係是:

frac{domega_h}{domega_ocos	heta_i}=(frac{h_r}{i+o})^2Rightarrow frac{dw_h}{dw_o}=frac{ocdot h}{left| i+o 
ight|^2} (注意立體角是面積,所以要取平方)

按照圖等腰三角形ioh,可得 ocdot h=frac{left| o + i 
ight|}{2} ,所以:

{domega_h}=frac{domega_o}{4(ocdot h)}Rightarrowfrac{domega_o}{domega_h}=4cos	heta_h

精確光源(Punctual Light Sources)

經典計算機圖形學中光源有point, directional和spot,由於它們無限小且無限明亮,它們在物理上是不現實的,但它們在許多情況下確實產生了合理的結果並且在計算上非常方便。這些光源都可以抽象成「精確光源」的概念,不考慮從光源到表面之間的衰減。精確光源可以用光源顏色 c_{light} 和光源方向矢量 l_c 兩個參數表示。 為了方便藝術家, c_{light} 並不是對光源強度的直接輻射測量, 而是定義為:白色的Lambert( c_{diff}=1 )表面被平行於表面法線l_c=n )的光照亮的顏色

精確光源的主要優點是它們極大地簡化了反射方程L_o(v)=int_Omega f(l,v)otimes L_i(l)(ncdot l)domega_i

在這裡,我們定義一個小的以 l_c 天頂角為 varepsilon 的面光源,微面光源照亮表面上一個點的入射輻射亮度為 L_{tiny}(l) . 該方程有兩個屬性[6]

forall l|angle(l,l_c) > varepsilon , L_{tiny}(l)=0

如果 lc=n , c_{light}=frac{c_{white}}{pi}int_Omega L_{tiny}(ncdot l)domega_i

第一個屬性表示如果給定入射方向 l ,並且 ll_c 的夾角大於 varepsilon, 那麼亮度為0,換句話說,光源不會在其角度 varepsilon 範圍外產生任何光線。第二個性質是從 c_{light} 的定義而來,通過反射方程和漫反射方程 f_{lambert}(l,v)=frac{c_{diff}}{pi} (並設置 c_{diff}=1 ).當 varepsilon 趨於0時:

lc=n , c_{light}=lim_{varepsilon 
ightarrow 0}left({frac{1}{pi}int_Omega L_{tiny}(ncdot l)domega_i}
ight)

由於 lc=n 並且 varepsilon=0 ,我們可以認為 (ncdot l)=1 ,可以得到:

c_{light}=lim_{varepsilon 
ightarrow 0}left({frac{1}{pi}int_Omega L_{tiny}domega_i}
ight)

請注意,該等式與 l_c 入射方向無關,所以對於任何精確光源方向都是如此,不只是 l_c=n 。 簡單的重排將極限值的積分分離出來:

lim_{varepsilon 
ightarrow 0}left({int_Omega L_{tiny}domega_i}
ight)=pi c_{light}

現在我們可以把得到的微面光源等式帶入BRDF( l 方向任意)中,並觀察當 varepsilon 趨近於0時,極限的行為, 注意這個公式的 n 是被照亮表面上點的法線。

L_o(v)=int_Omega f(l,v)otimes L_{tiny}(l)(ncdot l)domega_i

=f(l,v)otimeslim_{varepsilon 
ightarrow 0}{int_{Omega}L_{tiny}(l)domega_i}(ncdot l) (因為 varepsilon
ightarrow0 ,可提出沒有變化的項)

=pi f(l,v)otimes c_{light}(ncdot l)

對比與原始的積分反射方程,對於精確光源,我們可以用簡單直接的計算方法代替積分來計算BRDF。注意這裡產生了新的係數 pi ,這也是為什麼最終Shader中經常見不到 pi 這個係數的原因, 漫反射直接消除了,高光反射則中從D項消除了這個係數,這樣讓美術比較容易調節,當然也有引擎沒有消除這個係數的,比如UE。

能量守恆

當你使用漫反射和鏡面反射的能量守恆模型時,很容易確定組合模型也必須是節能的。只需要確保漫反射和高光反射的顏色總和不能超過1:

k_d+k_sleq1

如果是高光流pbr, 漫反射和高光都有貼圖,需要美術自己加去平衡。

如果是金屬流pbr. 只輸入一張albedo貼圖,可通過金屬度來計算高光顏色:

specColor = lerp(0.04, albedo, metallic);

diffuseColor = albedo * lerp(1-0.04, 0, metallic);

0.04是一般非金屬電解質最常見的反射係數。

整合渲染方程 L_o(p,omega_o)=L_e(p, omega_o)+int_{Omega}f(p,omega_o,omega_i) L_i(p,omega_i)cos	heta_idomega_i

帶入漫反射和高光反射公式後,我們可以得到如下方程:

L_o(p,omega_o)=L_e(p, omega_o)+int_{Omega}left( frac{k_d}{pi}+k_sfrac{DFG}{4(ncdotomega_o)(ncdotomega_i)} 
ight) L_i(p,omega_i)(ncdotomega_i)domega_i

其中L_e 是自發光輻射亮度, L_i 是入射輻射亮度, L_o 是出射光輻射亮度, omega_i 是入射方向, omega_o 是出射方向, n 是法線方向, p 表示所在點, k_dk_s 是能量守恆係數。

現在選擇下面的公式來實現GGX Shader

D_{GGX}(h)=frac{a^2}{pi ((ncdot h)^2(a^2-1)+1)^2}

V_{GGX}(l,v,h)=frac{0.5}{(ncdot l)*((ncdot v)*(1-a)+a)+((ncdot v))*((ncdot l)*(1-a)+a)} F_{Schlick}(F_{0}, l, h) = F_{0}+(1-F_{0})(1-vcdot h)^{5}

以下Shader代碼基於Unity cg格式

Shader "topameng/LightingGGX" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _BumpMap ("Normalmap", 2D) = "bump" {} [Gamma]_Metallic ("Metallic", Range(0,1)) = 0.0 _RoughRange("Roughness range", Vector) = (1,1,1,1) _BRDFLut("BRDF Lut", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma target 3.0 #include "UnityPBSLighting.cginc" #pragma surface surf GGX fullforwardshadows exclude_path:deferred struct Input { float2 uv_MainTex; float2 uv_BumpMap; }; sampler2D _MainTex; sampler2D _BumpMap; sampler2D _BRDFLut; half4 _Color; half _Metallic; half4 _RoughRange; UNITY_INSTANCING_CBUFFER_START(Props) UNITY_INSTANCING_CBUFFER_END struct SurfaceOutputGGX { half3 Albedo; half3 Specular; half3 Normal; half3 Emission; half Smoothness; half Occlusion; half Alpha; }; // [Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"] inline half3 F_Schlick(half3 f0, half vh) { half fc = pow(1 - vh, 5); // 1 sub, 3 mul return fc + (1 - fc) * f0; // 1 add, 3 mad } // GGX / Trowbridge-Reitz // [Walter et al. 2007, "Microfacet models for refraction through rough surfaces"] inline half D_GGX(half roughness, half nh) { half a = roughness * roughness; half a2 = a * a; half d = (nh * a2 - nh) * nh + 1.00001h; // 2 mad return a2 / (d * d + 1e-7); // 4 mul, 1 rcp } // Appoximation of joint Smith term for GGX // [Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"] inline half Vis_SmithJointApprox(half roughness, half nv, half nl) { half a = roughness * roughness; half smithV = nl * ( nv * ( 1 - a ) + a ); half smithL = nv * ( nl * ( 1 - a ) + a ); return 0.5 / (smithV + smithL + 1e-5f); } inline half4 LightingGGX(SurfaceOutputGGX s, half3 viewDir, UnityGI gi) { half3 normal = normalize(s.Normal); half roughness = 1 - s.Smoothness; half3 specColor = s.Specular; half3 lightDir = gi.light.dir; half3 halfDir = normalize(lightDir + viewDir); half nv = saturate(dot(normal, viewDir)); half nl = saturate(dot(normal, lightDir)); half nh = saturate(dot(normal, halfDir)); half lh = saturate(dot(lightDir, halfDir)); half2 AB = tex2D(_BRDFLut, float2(nv, roughness)).xy; half3 F0 = specColor * AB.x + AB.y; //D half D = D_GGX(roughness, nh); half V = Vis_SmithJointApprox(roughness, nv, nl); half3 F = F_Schlick(specColor, lh); half3 specular = D * V * F; half3 color = (s.Albedo + specular) * _LightColor0.rgb * nl + s.Albedo * gi.indirect.diffuse + gi.indirect.specular * F0; return half4(color, 1); } inline void LightingGGX_GI (SurfaceOutputGGX s, UnityGIInput data, inout UnityGI gi) { UNITY_GI(gi, s, data); } void surf (Input IN, inout SurfaceOutputGGX o) { half4 albedo = tex2D (_MainTex, IN.uv_MainTex) * _Color; half3 normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); half3 specColor = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, _Metallic); half oneMinusReflectivity = (1 - _Metallic) * unity_ColorSpaceDielectricSpec.a; half roughness = lerp(_RoughRange.x, _RoughRange.y, albedo.a); o.Albedo = albedo * oneMinusReflectivity; o.Specular = specColor; o.Normal = normal; o.Smoothness = 1 - roughness; o.Alpha = 1; o.Occlusion = 1; } ENDCG } FallBack "Diffuse"}

注:上面僅為示意代碼,不代表可以工程應用。

代碼沒有涉及到IBL部分,關於IBL的計算和擬合會在新的章節里進行

LightingGGX_GI 函數是Unity Surface Shader中負責設置UnityGI 變數的函數,也就是在LightingGGX函數參數中的UnityGI gi, UnityGI定義的結構如下

struct UnityLight{ half3 color; //光源顏色 half3 dir; //光源方向};struct UnityIndirect{ half3 diffuse; //低頻漫反射信息 half3 specular; //高頻高光信息};struct UnityGI{ UnityLight light; //直接光源信息 UnityIndirect indirect; //間接光源信息};

LightingGGX_GI 主要為gi.indirect 賦值,UnityIndirect 中的diffuse 由球諧SH函數以及lightmap(如果改物件被烘培)提供, specular 通過反射探針Reflection Probe對應的cubemap查詢獲取,包括mipmap選取等,將在IBL章節展開。

對於Phong我們選擇下面的公式來組合Shader

D_{Blinn}(h)=frac{1}{pialpha^2}(rcdot l)^{(frac{2}{alpha^2}-2)}

V_{Kelemen}(l,v,h)=frac{1}{4(lcdot h)^2}

F_{Schlick}(F_{0}, l, h) = F_{0}+(1-F_{0})(1-vcdot h)^{5}

這裡補充下,UE Mobile環境使用的D項,對歸一化Blinn-Phong做了球形高斯近似

D_{Blinn}(h)=frac{1}{pialpha^2}(ncdot h)^{(frac{2}{alpha^2}-2)}approx=frac{1}{pialpha^2}(rcdot l)^{(frac{2}{alpha^2}-2)}

r=2(ncdot v)n-v

按球形高斯近似公式 x^{n}approx e^{(n+0.775)(x-1)} 和對數換底公式 log_{a}^{e} = frac{1}{log_{e}^{a}}=frac{1}{lna}

可得:

 2^{frac{x}{ln2}}= (2^{frac{1}{ln2}})^x=(2^{log_{2}^{e}})^{x} =e^x

x^{n}approx e^{(n+0.775)(x-1)}=2^{frac{(n+0.775)(x-1)}{ln2}}

(rcdot l)^{frac{1}{2a^{2}}-frac{1}{2}}=e^{(frac{1}{2a^2}+0.275)(rcdot l-1)}

=2^{(frac{(frac{1}{2a^2}+0.275)rcdot l - (frac{1}{2a^2}+0.275)}{ln2}}=2 ^{(frac{0.72134752}{a^2}+0.39674113)rcdot l-frac{0.72134752}{a^2}+0.39674113)}

對於現代GPU作用不大,主要對於PS3,XBOX360是個優化

Shader "topameng/LightingPhong" { Properties { _Color ("Color", Color) = (1,1,1,1) _MainTex ("Albedo (RGB)", 2D) = "white" {} _BumpMap ("Normalmap", 2D) = "bump" {} [Gamma]_Metallic ("Metallic", Range(0,1)) = 0.0 _RoughRange("Roughness range", Vector) = (1,1,1,1) _BRDFLut("BRDF Lut", 2D) = "white" {} } SubShader { Tags { "RenderType"="Opaque" } LOD 200 CGPROGRAM #pragma target 3.0 #include "UnityPBSLighting.cginc" #pragma surface surf Phong fullforwardshadows exclude_path:deferred struct Input { float2 uv_MainTex; float2 uv_BumpMap; float3 worldViewDir; }; sampler2D _MainTex; sampler2D _BumpMap; sampler2D _BRDFLut; half4 _Color; half _Metallic; half4 _RoughRange; UNITY_INSTANCING_CBUFFER_START(Props) UNITY_INSTANCING_CBUFFER_END struct SurfaceOutputPhong { half3 Albedo; half3 Specular; half3 Normal; half3 Emission; half3 ViewDir; half Smoothness; half Occlusion; half Alpha; }; // [Schlick 1994, "An Inexpensive BRDF Model for Physically-Based Rendering"] inline half3 F_Schlick(half3 f0, half vh) { half fc = pow(1 - vh, 5); // 1 sub, 3 mul return fc + (1 - fc) * f0; // 1 add, 3 mad } half PhongApprox(half roughness, half rl) { half a = roughness * roughness; // 1 mul //!! Ronin Hack? a = max(a, 0.012); // avoid underflow in FP16, next sqr should be bigger than 6.1e-5 half a2 = a * a; // 1 mul half rcp_a2 = 1/a2; // 1 rcp //half rcp_a2 = exp2( -6.88886882 * Roughness + 6.88886882 ); // Spherical Gaussian approximation: pow( x, n ) ~= exp( (n + 0.775) * (x - 1) ) // Phong: n = 0.5 / a2 - 0.5 // 0.5 / ln(2), 0.275 / ln(2) half c = 0.72134752 * rcp_a2 + 0.39674113; // 1 mad half p = rcp_a2 * exp2(c * rl - c); // 2 mad, 1 exp2, 1 mul // Total 7 instr return min(p, rcp_a2); // Avoid overflow/underflow on Mali GPUs } inline half4 LightingPhong(SurfaceOutputPhong s, half3 viewDir, UnityGI gi) { half3 normal = normalize(s.Normal); half roughness = 1 - s.Smoothness; half3 specColor = s.Specular; half3 lightDir = gi.light.dir; half3 halfDir = normalize(lightDir + viewDir); half nv = saturate(dot(normal, viewDir)); half nl = saturate(dot(normal, lightDir)); half lh = saturate(dot(lightDir, halfDir)); half2 AB = tex2D(_BRDFLut, float2(nv, roughness)).xy; half3 F0 = specColor * AB.x + AB.y; half3 r = reflect(-s.ViewDir, normal); half rl = saturate(dot(r, lightDir)); //D half D = PhongApprox(roughness, rl); half V = 0.25 / (lh * lh); half3 F = F_Schlick(specColor, lh); half3 specular = D * V * F; half3 color = (s.Albedo + specular) * _LightColor0.rgb * nl + s.Albedo * gi.indirect.diffuse + gi.indirect.specular * F0; return half4(color, 1); } inline void LightingPhong_GI (SurfaceOutputPhong s, UnityGIInput data, inout UnityGI gi) { UNITY_GI(gi, s, data); } void surf (Input IN, inout SurfaceOutputPhong o) { half4 albedo = tex2D (_MainTex, IN.uv_MainTex) * _Color; half3 normal = UnpackNormal(tex2D(_BumpMap, IN.uv_BumpMap)); half3 specColor = lerp(unity_ColorSpaceDielectricSpec.rgb, albedo, _Metallic); half oneMinusReflectivity = (1 - _Metallic) * unity_ColorSpaceDielectricSpec.a; half roughness = lerp(_RoughRange.x, _RoughRange.y, albedo.a); o.Albedo = albedo * oneMinusReflectivity; o.Specular = specColor; o.Normal = normal; o.Smoothness = 1 - roughness; o.Alpha = 1; o.Occlusion = 1; o.ViewDir = IN.worldViewDir; } ENDCG } FallBack "Diffuse"}

注:上面僅為示意測試代碼,不代表可以工程應用。

PhongApprox 取自UE Mobile Shader[7]。計算過程見上一章。這裡添加了V和F項,實際上UE中的Shader完全省略掉了V和F,這樣就可以省掉更多的計算指令,比如不需要halfDir, 不需要lh等。其實也可以認為UE, V項對應 G_{implect}=(ncdot l)(ncdot v) , F 簡單取SpecularColor。

在Unity Shader里也有一個簡化VF的操作,把VF合併為[8] : VF = frac{specColor}{(lh*lh)*(roughness^2+0.5)}

對應Shader輸入參數如下,記得Unity需要切換為線性空間渲染:

BRDF Lut 貼圖可以參考基於物理的環境光渲染一自己生成,或直接取用

下圖是一個鋼鐵金屬球效果對比

如果只看高光信息,對比如下:

通過對比可以看出GGX的高光有更柔和的衰減。

不過UE mobile的pbr實現的早一些。而Unity pbr在u5才開始實現,前面也是phong版本,直到後面才變成首選GGX演算法,並且到Unity5.3.2 IBL項才包含surfaceReduction並且穩定下來。

內容可能會不斷修改,謝絕轉載。

參考文獻:

[0]TomasAkenine-M?ller, Real-Time Rendering

[1]Eric Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"

[2]Brian Karis, Specular BRDF Reference

[3]Trent, Physically Based Shading and Image Based Lighting

[4]Matt Pharr, Physically Based Rendering From Theory to Implementation

[5]Bruce Walter等, Microfacet Models for Refraction through Rough Surfaces

[6]Naty Hoffman, Physically-Based Shading Models in Film and Game Production

[7] Brian Karis, 移動平台上基於物理的著色

[8]Renaldas Zioma, Optimizing PBR for Mobile


推薦閱讀:

深入淺出基於物理的渲染一
《Real Time Rendering》之Vertex Blending/Skinning

TAG:計算機圖形學 | 實時渲染 | 遊戲開發 |