pkwzsqsdly 发表于 2020-11-25 09:28

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, data.boxMin, data.boxMax);
        #endif

        #ifdef _GLOSSYREFLECTIONS_OFF
                specular = unity_IndirectSpecColor.rgb;
        #else
                half3 env0 = Unity_GlossyEnvironment (UNITY_PASS_TEXCUBE(unity_SpecCube0), data.probeHDR, 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
页: [1]
查看完整版本: Unity的PBR扩展(二)——PBS代码剖析