Unity的PBR擴充套件(二)——PBS程式碼剖析
PBR可理解為是一套渲染標準,其核心是PBS(Physically Based Shading)著色模型,具體實現由各大渲染引擎自己負責。
Unity的PBS實現封裝為Standard,UE4中實現封裝為Default Lit。
BRDF
BRDF(雙向反射分佈函式)光照模型是PBS的重要組成部分,用於描述光在物體表面的反射情況。該模型基於微表面理論,認為光在物體表面反射的光量是物體表面的所有微小表面漫反射和鏡面反射光量的總和,符合能量守恆:
1。反射的光總量不大於入射的光總量,且漫反射和鏡面反射是互斥關係;
2。粗糙的表面反射的光線分散且暗,光滑的表面反射集中且亮。
Unity的BRDF的內部實現檔案為UnityStandardBRDF。cginc,主要實現函式為BRDF?_Unity_PBS。Unity的BRDF實現按平臺分為3個檔次,這裡討論的是針對Console/PC平臺,光照模型更加精確的第1檔實現BRDF1_Unity_PBS。
BRDF的漫反射部分為Disney漫反射模型,該計算模型基於表面粗糙度,主要實現程式碼為:
half nlPow5 = Pow5 (1-nl);
half nvPow5 = Pow5 (1-nv);
half Fd90 = 0。5 + 2 * lh * lh * roughness;
half disneyDiffuse = (1 + (Fd90-1) * nlPow5) * (1 + (Fd90-1) * nvPow5);
half diffuseTerm = disneyDiffuse * nl;
程式碼中diffuseTerm 為計算得到的漫反射部分。
Disney漫反射模型與不考慮表面粗糙度的Lambert漫反射模型實際效果區別不大,所以在Unity的第2,3檔中diffuse計算用的是更簡單的Lambert模型。
BRDF的鏡面反射部分基於Torrance-Sparrow微表面模型,公式類似為:
主要實現程式碼為:
#if UNITY_BRDF_GGX
half V = SmithJointGGXVisibilityTerm (nl, nv, roughness);
half D = GGXTerm (nh, roughness);
#else
half V = SmithBeckmannVisibilityTerm (nl, nv, roughness);
half D = NDFBlinnPhongNormalizedTerm (nh, RoughnessToSpecPower (roughness));
#endif
F = FresnelTerm (specColor, lh);
half specularTerm = (V * D * F) * (UNITY_PI/4);
程式碼中specularTerm 為計算得到的鏡面反射部分,實現上基本遵守了Torrance-Sparrow的公式。
其中,法線分佈D和幾何衰減G按是否採用GGX計算模型會有些不同。這裡附一張不同粗糙度的法線分佈函式(NDF)曲線示意圖:
上圖中,X軸為half半形向量和表面normal的夾角弧度,Y軸為NDF返回值,可看出smoothness越高的函式曲線越陡峭,可解釋
“粗糙的表面反射的光線分散且暗,光滑的表面反射集中且亮”
能量守恆。
公式中的Frensnel部分的程式碼實現為:
inline half3 FresnelTerm (half3 F0, half cosA)
{
half t = Pow5 (1 - cosA); // ala Schlick interpoliation
return F0 + (1-F0) * t;
}
F = FresnelTerm (specColor, lh)
FresnelTerm 的函式曲線符合之前《理論基礎》文章所示的
Fresnel曲線
:
其中,FresnelTerm 函式的第1個引數specColor對應著示意圖中的Base Reflectivities。
接下來分析
“Diffuse和Specular互斥”
能量守恆。UnityStandardUtils。cginc檔案包含了主要內部實現程式碼:
inline half OneMinusReflectivityFromMetallic(half metallic)
{
// We‘ll need oneMinusReflectivity, so
// 1-reflectivity = 1-lerp(dielectricSpec, 1, metallic) = lerp(1-dielectricSpec, 0, metallic)
// store (1-dielectricSpec) in unity_ColorSpaceDielectricSpec。a, then
// 1-reflectivity = lerp(alpha, 0, metallic) = alpha + metallic*(0 - alpha) =
// = alpha - metallic * alpha
half oneMinusDielectricSpec = unity_ColorSpaceDielectricSpec。a;
return oneMinusDielectricSpec - metallic * oneMinusDielectricSpec;
}
inline half3 DiffuseAndSpecularFromMetallic (half3 albedo, half metallic, out half3 specColor, out half oneMinusReflectivity)
{
specColor = lerp (unity_ColorSpaceDielectricSpec。rgb, albedo, metallic);
oneMinusReflectivity = OneMinusReflectivityFromMetallic(metallic);
return albedo * oneMinusReflectivity;
}
程式碼根據金屬度計算漫反射和鏡面反射比例,當metallic為1時,反射率接近1,函式返回的diffColor接近0,表示幾乎不反射漫反射。
Unity的內建變數unity_ColorSpaceDielectricSpec定義了絕緣體的高光顏色和反射率,不完全為0,是一個經驗值。
Unity還提供了specular setup工作流程來控制漫反射和鏡面反射比例。內部實現程式碼為:
// Diffuse/Spec Energy conservation
inline half3 EnergyConservationBetweenDiffuseAndSpecular (half3 albedo, half3 specColor, out half oneMinusReflectivity)
{
oneMinusReflectivity = 1 - SpecularStrength(specColor);
#if !UNITY_CONSERVE_ENERGY
return albedo;
#elif UNITY_CONSERVE_ENERGY_MONOCHROME
return albedo * oneMinusReflectivity;
#else
return albedo * (half3(1,1,1) - specColor);
#endif
}
程式碼中用1減去鏡面反射比例,得到漫反射比例。當傳入的specColor為白色時,SpecularStrength返回1,結果漫反射比例為0,發生完美鏡面反射。
計算得到的diffColor和specColor作為比例係數用於最終漫反射和鏡面反射計算:
// BRDF = kD / pi + kS * (D * V * F) / 4
程式碼中的 kD和kS對應著diffColor和specColor。
IBL
在材質上反應出周圍的環境也是PBS的重要組成部分。在光照模型中一般把周圍的環境當作一個大的光源來對待,不過環境光不同於實時光,而是作為間接光(indirect light)透過IBL( Image Based Lighting)來實現。間接光計算也包含漫反射部分和鏡面反射部分。
UnityGlobalIllumination。cginc檔案包含了主要內部實現程式碼:
inline UnityGI UnityGI_Base(UnityGIInput data, half occlusion, half3 normalWorld)
{
UnityGI o_gi;
ResetUnityGI(o_gi);
。。。。。
#if UNITY_SHOULD_SAMPLE_SH
o_gi。indirect。diffuse = ShadeSHPerPixel (normalWorld, data。ambient, data。worldPos);
#endif
#if defined(LIGHTMAP_ON)
// Baked lightmaps
fixed4 bakedColorTex = UNITY_SAMPLE_TEX2D(unity_Lightmap, data。lightmapUV。xy);
half3 bakedColor = DecodeLightmap(bakedColorTex);
#ifdef DIRLIGHTMAP_COMBINED
……
#elif DIRLIGHTMAP_SEPARATE
。。。。。
#else // not directional lightmap
o_gi。indirect。diffuse = bakedColor;
……
#endif
#endif
#ifdef DYNAMICLIGHTMAP_ON
……
#endif
o_gi。indirect。diffuse *= occlusion;
return o_gi;
}
Unity內建了unity_Lightmap、unity_SHAr等全域性變數,來從預先烘焙好的Lightmap貼圖或light probe中讀取顏色,其中UNITY_SHOULD_SAMPLE_SH程式碼段處理的是從light probe中讀取顏色值。一般渲染時靜態物體讀取lightmap,非靜態物體讀取light probe。
UnityGI_Base函式返回的顏色值為間接光的漫反射部分。
inline half3 UnityGI_IndirectSpecular(UnityGIInput data, half occlusion, half3 normalWorld, Unity_GlossyEnvironmentData glossIn)
{
half3 specular;
#if UNITY_SPECCUBE_BOX_PROJECTION
// we will tweak reflUVW in glossIn directly (as we pass it to Unity_GlossyEnvironment twice), so keep original to pass into BoxProjectedCubemapDirection
half3 originalReflUVW = glossIn。reflUVW;
#endif
#if UNITY_SPECCUBE_BOX_PROJECTION
glossIn。reflUVW = BoxProjectedCubemapDirection (originalReflUVW, data。worldPos, data。probePosition[0], data。boxMin[0], data。boxMax[0]);
#endif
#ifdef _GLOSSYREFLECTIONS_OFF
specular = unity_IndirectSpecColor。rgb;
#else
half3 env0 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE(unity_SpecCube0), data。probeHDR[0], glossIn);
#if UNITY_SPECCUBE_BLENDING
……
#else
specular = env0;
#endif
#endif
return specular * occlusion;
}
Unity用reflection probe來儲存預先烘焙好的環境光反射貼圖,透過內建變數unity_SpecCube0,unity_SpecCube1訪問。
UnityGI_IndirectSpecular返回的顏色值為間接光的鏡面反射部分。
另外,
“粗糙的表面反射的光線分散且暗,光滑的表面反射集中且亮”
能量守恆在這裡同樣被遵守,函式輸入引數包含粗糙度資訊,用於環境光貼圖的LOD取值:
half mip = roughness * UNITY_SPECCUBE_LOD_STEPS;
half4 rgbm = UNITY_SAMPLE_TEXCUBE_LOD(tex, glossIn。reflUVW, mip);
表面越粗糙,用於取樣mipmap貼圖的LOD值越高,UNITY_SAMPLE_TEXCUBE_LOD取樣的結果越模糊,反之亦然。
BRDF+IBL
正如光照計算公式中多個光源的強度是疊加關係,PBS模型光照計算的結果是實時光BRDF與間接光IBL之和。BRDF1_Unity_PBS函式最後的顏色返回值程式碼:
half grazingTerm = saturate(oneMinusRoughness + (1-oneMinusReflectivity));
half3 color = diffColor * (gi。diffuse + light。color * diffuseTerm)
+ specularTerm * light。color * FresnelTerm (specColor, lh)
+ surfaceReduction * gi。specular * FresnelLerp (specColor, grazingTerm, nv);
附自定義Standard shader得到的結果分解圖:
參考文獻:
http://
simonstechblog。blogspot。com
/2011/12/microfacet-brdf。html