深入淺出基於物理的渲染二
高光BRDF模型公式推導
這裡讓我們來看看BRDF公式, 並講解一下,除了DFG函數之外其他係數是如何來的:
針對方向 和 ,考察方向為half vector 的微表面的入射微分輻射通量 . 根據輻射亮度定義可知:
其中 是法線為 的微表面微分面積, 為 在入射光方向 上的投影微分面積。
針對方向為 的微表面微分面積如下:
代入此公式,可得:
微表面根據菲涅爾定理反射光線,出射通量如下:
根據輻射亮度定義,出射的輻射亮度為:
( 見下面證明)
由BRDF定義可得:
模型還要包括了一個幾何衰減項,它描述了被遮擋或處於陰影下的微平面的比例。它可以跟Fresnel項一樣被放入到推導之中。於是整個的公式模型就是:
如下圖[4],考慮面向 的球面坐標系立體角 和 分別是 和 並且 所以:
下面換一種證明方法[5]:假設 為法線方向(0,0,1), 將 沿著i方向整體平移,並保持 不變(球坐標系整個移動過去)
由half vector定義可知: 且 與i和o等角。 與o垂直, 投影到垂直矢量 方向為 。由上圖可知,投影立體角與立體角 (與h垂直)關係是:
(注意立體角是面積,所以要取平方)
按照圖等腰三角形ioh,可得 ,所以:精確光源(Punctual Light Sources)
經典計算機圖形學中光源有point, directional和spot,由於它們無限小且無限明亮,它們在物理上是不現實的,但它們在許多情況下確實產生了合理的結果並且在計算上非常方便。這些光源都可以抽象成「精確光源」的概念,不考慮從光源到表面之間的衰減。精確光源可以用光源顏色 和光源方向矢量 兩個參數表示。 為了方便藝術家, 並不是對光源強度的直接輻射測量, 而是定義為:白色的Lambert( )表面被平行於表面法線( )的光照亮的顏色精確光源的主要優點是它們極大地簡化了反射方程
在這裡,我們定義一個小的以 天頂角為 的面光源,微面光源照亮表面上一個點的入射輻射亮度為 . 該方程有兩個屬性[6],
如果 ,
第一個屬性表示如果給定入射方向 ,並且 和 的夾角大於 , 那麼亮度為0,換句話說,光源不會在其角度 範圍外產生任何光線。第二個性質是從 的定義而來,通過反射方程和漫反射方程 (並設置 ).當 趨於0時:
,
由於 並且 ,我們可以認為 ,可以得到:請注意,該等式與 入射方向無關,所以對於任何精確光源方向都是如此,不只是 。 簡單的重排將極限值的積分分離出來: 現在我們可以把得到的微面光源等式帶入BRDF( 方向任意)中,並觀察當 趨近於0時,極限的行為, 注意這個公式的 是被照亮表面上點的法線。
(因為 ,可提出沒有變化的項)
對比與原始的積分反射方程,對於精確光源,我們可以用簡單直接的計算方法代替積分來計算BRDF。注意這裡產生了新的係數 ,這也是為什麼最終Shader中經常見不到 這個係數的原因, 漫反射直接消除了,高光反射則中從D項消除了這個係數,這樣讓美術比較容易調節,當然也有引擎沒有消除這個係數的,比如UE。
能量守恆
當你使用漫反射和鏡面反射的能量守恆模型時,很容易確定組合模型也必須是節能的。只需要確保漫反射和高光反射的顏色總和不能超過1:
如果是高光流pbr, 漫反射和高光都有貼圖,需要美術自己加去平衡。
如果是金屬流pbr. 只輸入一張albedo貼圖,可通過金屬度來計算高光顏色:specColor = lerp(0.04, albedo, metallic);
diffuseColor = albedo * lerp(1-0.04, 0, metallic);
0.04是一般非金屬電解質最常見的反射係數。整合渲染方程
帶入漫反射和高光反射公式後,我們可以得到如下方程:
其中 是自發光輻射亮度, 是入射輻射亮度, 是出射光輻射亮度, 是入射方向, 是出射方向, 是法線方向, 表示所在點, 和 是能量守恆係數。
現在選擇下面的公式來實現GGX Shader
以下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
這裡補充下,UE Mobile環境使用的D項,對歸一化Blinn-Phong做了球形高斯近似
按球形高斯近似公式 和對數換底公式
可得:
對於現代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項對應 , F 簡單取SpecularColor。在Unity Shader里也有一個簡化VF的操作,把VF合併為[8] :對應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