Unity BRDF

Created by miccall (转载请注明出处 miccall.tech)

Unity BRDF cginc 文件 , 里面主要写了三个 BRDF计算 :

  1. BRDF1_Unity_PBS
  2. BRDF2_Unity_PBS
  3. BRDF3_Unity_PBS
  • 第一个 BRDF 是基于基于主要物理的BRDF ,源自迪士尼的作品,基于Torrance-Sparrow微小面模型 。
  • 第二个是 基于Minimalist CookTorrance 的 BRDF
  • 第三个 不是基于 microfacet的修正标准化 Blinn-Phong BRDF ,实现使用 Lookup 纹理来提高性能

一 . Torrance-Sparrow 模型 :

一种比较古老的光照模型, 是基于物理计算的 。 其基本公式为 :

BRDF = kD / pi + kS (D V F)/ 4
I = BRDF
NdotL

kd 项 diffuse
Ks 是 specular
D 项 取绝与unity定义的宏 ,blinn 和 ggx 支持
V 项 是smith参数 SmithVisibilityTerm
F 为 菲涅尔 项

额外要说一下的就是 Roughness 和 smoothness

Roughness 分 PerceptualRoughness 和 Roughness ,他们的关系是
Roughness = perceptualRoughness * perceptualRoughness ;

perceptualRoughness 相对比较直观 ,但是我们计算要用 Roughness 。

同理 smoothness 也一样 ,但是我们一般不用 perceptualSmoothness

而涉及到的问题是 smoothness 和 Roughness 的转化 ,他们的关系是


half SmoothnessToRoughness(half smoothness)
{
    return (1 - smoothness) * (1 - smoothness);
}

也就是说, PerceptualRoughness = 1 - smoothness

其次我们开始计算BRDF ,
漫反射项 ,BRDF 用了 DisneyDiffuse :


    // Note: Disney diffuse must be multiply by diffuseAlbedo / PI. This is done outside of this function.
    half DisneyDiffuse(half NdotV, half NdotL, half LdotH, half perceptualRoughness)
    {
        half fd90 = 0.5 + 2 * LdotH * LdotH * perceptualRoughness;
        // Two schlick fresnel term
        half lightScatter   = (1 + (fd90 - 1) * Pow5(1 - NdotL));
        half viewScatter    = (1 + (fd90 - 1) * Pow5(1 - NdotV));

        return lightScatter * viewScatter;
    }

    // 计算 :
    half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl;

D 项 , 使用了 GGX 项 :

inline float GGXTerm (float NdotH, float roughness)
{
    float a2 = roughness * roughness;
    float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; // 2 mad
    return UNITY_INV_PI * a2 / (d * d + 1e-7f); // This function is not intended to be running on Mobile,
                                            // therefore epsilon is smaller than what can be represented by half
}

V 项 :

``` cpp
inline half SmithVisibilityTerm (half NdotL, half NdotV, half k)
{
    half gL = NdotL * (1-k) + k;
    half gV = NdotV * (1-k) + k;
    return 1.0 / (gL * gV + 1e-5f); // This function is not intended to be running on Mobile,
                                    // therefore epsilon is smaller than can be represented by half
}

F 项 :

inline half3 FresnelTerm (half3 F0, half cosA)
{
    half t = Pow5 (1 - cosA);   // ala Schlick interpoliation
    return F0 + (1-F0) * t;
}
inline half3 FresnelLerp (half3 F0, half3 F90, half cosA)
{
    half t = Pow5 (1 - cosA);   // ala Schlick interpoliation
    return lerp (F0, F90, t);
}
// approximage Schlick with ^4 instead of ^5
inline half3 FresnelLerpFast (half3 F0, half3 F90, half cosA)
{
    half t = Pow4 (1 - cosA);
    return lerp (F0, F90, t);
}

所有源码 :


half4 BRDF1_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness,
float3 normal, float3 viewDir,
UnityLight light, UnityIndirect gi)
{
    float perceptualRoughness = SmoothnessToPerceptualRoughness (smoothness);
    float3 halfDir = Unity_SafeNormalize (float3(light.dir) + viewDir);

    //  NdotV对于可见像素不应该为负,但由于透视投影和法线贴图,它可能会发生
    // 在这种情况下,应修改法线以使其有效(即面向相机)并且不会导致奇怪的伪像。
    // 但是这个操作添加了很少的ALU,用户可能不想要它。 另一种方法是简单地采用NdotV的绝对值(不太正确但也有效)。
    // 以下定义允许控制它。 如果ALU在您的平台上至关重要,请将其设置为0。
    // 对于具有SmithJoint可见度函数的GGX,此校正很有意义,因为在这种情况下由于粗糙表面的高光边缘,伪影更加明显
    // Edit:现在默认禁用此代码,因为它与SpeedTree中使用的双面光源不兼容。


    #define UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV 0
    #if UNITY_HANDLE_CORRECTLY_NEGATIVE_NDOTV

    // The amount we shift the normal toward the view vector is defined by the dot product.
    half shiftAmount = dot(normal, viewDir);
    normal = shiftAmount < 0.0f ? normal + viewDir * (-shiftAmount + 1e-5f) : normal;


    // 这里应该应用重新 normalize,但由于转换很小,我们不会这样做以节省ALU。
    // normal = normalize(normal);

    half nv = saturate(dot(normal, viewDir)); // TODO: this saturate should no be necessary here
    #else
    half nv = abs(dot(normal, viewDir)); // This abs allow to limit artifact
    #endif
    half nl = saturate(dot(normal, light.dir));
    float nh = saturate(dot(normal, halfDir));
    half lv = saturate(dot(light.dir, viewDir));
    half lh = saturate(dot(light.dir, halfDir));

    // Diffuse term
    half diffuseTerm = DisneyDiffuse(nv, nl, lh, perceptualRoughness) * nl;

    // Specular term
    // HACK:理论上我们应该将diffuseTerm除以Pi而不是乘以specularTerm!
    // but 1 : 这将使着色器看起来比传统的着色器明显更暗
    // and 2 : 在unity 中 “非重要” 灯在被注入环境SH时也必须用Pi分开

    float roughness = PerceptualRoughnessToRoughness(perceptualRoughness);
#if UNITY_BRDF_GGX
    // 具有roughtness为0的GGX将意味着没有镜面反射,使用 max(roughness, 0.002) 来匹配 HDrenderloop roughtness重映射。

    roughness = max(roughness, 0.002);
    half  V = SmithJointGGXVisibilityTerm (nl, nv, roughness);
    float D = GGXTerm (nh, roughness);

#else
    // Legacy
    half V = SmithBeckmannVisibilityTerm (nl, nv, roughness);
    half D = NDFBlinnPhongNormalizedTerm (nh, PerceptualRoughnessToSpecPower(perceptualRoughness));

#endif

    half specularTerm = V*D * UNITY_PI; // Torrance-Sparrow model, Fresnel is applied later
# ifdef UNITY_COLORSPACE_GAMMA
    specularTerm = sqrt(max(1e-4h, specularTerm));
# endif

    // specularTerm * nl can be NaN on Metal in some cases, use max() to make sure it's a sane value
    specularTerm = max(0, specularTerm * nl);
#if defined(_SPECULARHIGHLIGHTS_OFF)
    specularTerm = 0.0;
#endif

    // surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(roughness^2+1)
    half surfaceReduction;
# ifdef UNITY_COLORSPACE_GAMMA

    surfaceReduction = 1.0-0.28*roughness*perceptualRoughness; 
    // 1-0.28*x^3 as approximation for (1/(x^4+1))^(1/2.2) on the domain [0;1]

# else

    surfaceReduction = 1.0 / (roughness*roughness + 1.0); // fade \in [0.5;1]
# endif

    // To provide true Lambert lighting, we need to be able to kill specular completely.
    specularTerm *= any(specColor) ? 1.0 : 0.0;
    half grazingTerm = saturate(smoothness + (1-oneMinusReflectivity));

    half3 color = diffColor * (gi.diffuse + light.color * diffuseTerm)
    + specularTerm * light.color * FresnelTerm (specColor, lh)
    + surfaceReduction * gi.specular * FresnelLerp (specColor, grazingTerm, nv);

    return half4(color, 1);
}

half4 BRDF2_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness,
float3 normal, float3 viewDir,
UnityLight light, UnityIndirect gi)
{
    float3 halfDir = Unity_SafeNormalize ( float3(light.dir) + viewDir );
    half nl = saturate(dot(normal, light.dir));
    float nh = saturate(dot(normal, halfDir));
    half nv = saturate(dot(normal, viewDir));
    float lh = saturate(dot(light.dir, halfDir));

    // Specular term
    half perceptualRoughness = SmoothnessToPerceptualRoughness (smoothness);
    half roughness = PerceptualRoughnessToRoughness(perceptualRoughness);

#if UNITY_BRDF_GGX

    // GGX 分布乘以可见性和菲涅耳的组合近似值
    // 请参阅 Siggraph 2015 移动移动图形课程的 “优化移动PBR”
    // https://community.arm.com/events/1155

    half a = roughness;
    float a2 = a*a;
    float d = nh * nh * (a2 - 1.f) + 1.00001f;
    #ifdef UNITY_COLORSPACE_GAMMA
        //更紧密的近似 ,只 在 Gamma渲染模式 
        // DVF = sqrt(DVF);
        // DVF = (a * sqrt(.25)) / (max(sqrt(0.1), lh)*sqrt(roughness + .5) * d);

        float specularTerm = a / (max(0.32f, lh) * (1.5f + roughness) * d);
    #else
        float specularTerm = a2 / (max(0.1f, lh*lh) * (roughness + 0.5f) * (d * d) * 4);
    #endif

    // 在手机上分母有溢出的风险
    // clamp 是专门为“修复”而添加的, but dx compiler (we convert bytecode to metal/gles)
    // sees that specularTerm have only non-negative terms, so it skips max(0,..) in clamp (leaving only min(100,...))

    #if defined (SHADER_API_MOBILE)
        specularTerm = specularTerm - 1e-4f;
    #endif

#else
    // Legacy
    half specularPower = PerceptualRoughnessToSpecPower(perceptualRoughness);

    //使用近似可见性函数进行修改,将粗糙度考虑在内
    // Original ((n+1)*N.H^n) / (8*Pi * L.H^3) 未考虑粗糙度 
    // 并在掠射角处产生极其明亮的镜面 

    half invV = lh * lh * smoothness + perceptualRoughness * perceptualRoughness; 
    // approx ModifiedKelemenVisibilityTerm(lh, perceptualRoughness);

    half invF = lh;
    half specularTerm = ((specularPower + 1) * pow (nh, specularPower)) / (8 * invV * invF + 1e-4h);

    #ifdef UNITY_COLORSPACE_GAMMA
        specularTerm = sqrt(max(1e-4f, specularTerm));
    #endif

#endif


#if defined (SHADER_API_MOBILE)
    specularTerm = clamp(specularTerm, 0.0, 100.0); // Prevent FP16 overflow on mobiles
#endif



#if defined(_SPECULARHIGHLIGHTS_OFF)
    specularTerm = 0.0;
#endif


    // surfaceReduction = Int D(NdotH) * NdotH * Id(NdotL>0) dH = 1/(realRoughness^2+1)
    // 1-0.28*x^3 as approximation for (1/(x^4+1))^(1/2.2) on the domain [0;1]
    // 1-x^3*(0.6-0.08*x) approximation for 1/(x^4+1)


#ifdef UNITY_COLORSPACE_GAMMA
    half surfaceReduction = 0.28;
#else
    half surfaceReduction = (0.6-0.08*perceptualRoughness);
#endif

    surfaceReduction = 1.0 - roughness*perceptualRoughness*surfaceReduction;
    half grazingTerm = saturate(smoothness + (1-oneMinusReflectivity));
    half3 color = (diffColor + specularTerm * specColor) * light.color * nl
    + gi.diffuse * diffColor
    + surfaceReduction * gi.specular * FresnelLerpFast (specColor, grazingTerm, nv);
    return half4(color, 1);

}


half4 BRDF3_Unity_PBS (half3 diffColor, half3 specColor, half oneMinusReflectivity, half smoothness,
float3 normal, float3 viewDir,
UnityLight light, UnityIndirect gi)
{
    float3 reflDir = reflect (viewDir, normal);
    half nl = saturate(dot(normal, light.dir));
    half nv = saturate(dot(normal, viewDir));

    // Vectorize Pow4 to save instructions
    half2 rlPow4AndFresnelTerm = Pow4 (float2(dot(reflDir, light.dir), 1-nv)); 

    // use R.L instead of N.H to save couple of instructions
    half rlPow4 = rlPow4AndFresnelTerm.x; 

    // power exponent must match kHorizontalWarpExp in NHxRoughness() function in GeneratedTextures.cpp
    half fresnelTerm = rlPow4AndFresnelTerm.y;
    half grazingTerm = saturate(smoothness + (1-oneMinusReflectivity));

    half3 color = BRDF3_Direct(diffColor, specColor, rlPow4, smoothness);
    color *= light.color * nl;
    color += BRDF3_Indirect(diffColor, specColor, gi, grazingTerm, fresnelTerm);
    return half4(color, 1);
}