Created by miccall (转载请注明出处 miccall.tech)
Unity BRDF cginc 文件 , 里面主要写了三个 BRDF计算 :
- BRDF1_Unity_PBS
- BRDF2_Unity_PBS
- 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);
}