四、镜面反射项(Specular)
整个镜面反射的公式结构如下:
F(l,h) : 菲涅尔方程(Fresnel Equation),描述不同的表面角下表面所反射的光线所占的比率。
G(l,v,h) : 几何函数(Geometry Function),描述微平面自成阴影的属性,即m = h的未被遮蔽的表面点的百分比。
分母 4(n·l)(n·v):校正因子(correctionfactor),作为微观几何的局部空间和整个宏观表面的局部空间之间变换的微平面量的校正。
4.1 D项-法线分布函数
D(h):法线分布函数(Normal Distribution Function),描述微面元法线分布的概率,即正确朝向的法线的浓度,即具有正确朝向,能够将来自L的光反射到V的表面点的相对于表面面积的浓度。
4.11 大致原理
把一个平面哪怕是镜子放大到微观层面,会发现表面会是凸凹不平的表面,以此来理解就容易懂了,看下面的示意图。
微平面粗糙度对材质外观的影响(图片来自Moving Frostbite to PBR,SIGGRAPH 2014)
4.12 公式
4.13 效果展示
上图是几种NDF模型的测试对比
在示例模型中的NDF效果变化不大,只有GND和前三个差别比较明显一些,需要观察下方球体的变化更直观一些,可以看下图。
GND的反射光很窄很暗,看起来就没有了反射光的效果
4.14 相关代码
在实例中收集了4种实现方法,其中第一种D_GGXTerm是直接摘录UnityStandardBRDF.cginc文件中的方法。
//D项 NDF法线分布项 inline float D_GGXTerm (float NdotH, float roughness) { float a2 = roughness * roughness; float d = (NdotH * a2 - NdotH) * NdotH + 1.0f; return UNITY_INV_PI * a2 / (d * d + 1e-7f); } float D_GTR1( float dotNH,float alpha) { float a2 = alpha * alpha; float cos2th = dotNH * dotNH; float den = (1.0 + (a2 - 1.0) * cos2th); return (a2 - 1.0) / (UNITY_PI * log(a2) * den); } float D_GTR2(float dotNH,float alpha) { float a2 = alpha * alpha; float cos2th = dotNH * dotNH; float den = (1.0 + (a2 - 1.0) * cos2th); return a2 / (UNITY_PI * den * den); } inline float GaussianNormalDistribution(float NdotH,float roughness) { float roughnessSqr = roughness*roughness; float thetaH = acos(NdotH); return exp(-thetaH*thetaH/roughnessSqr); }
UNITY_INV_PI:通过查看上面一段代码,其中 UNITY_INV_PI 是内置宏中圆周率倒数的意思,还有更多相关内置宏如下:
UNITY_PI | 圆周率 (3.14159265359f) |
UNITY_TWO_PI | 2倍圆周率 (6.28318530718f) |
UNITY_FOUR_PI | 4倍圆周率 (12.56637061436f) |
UNITY_INV_PI | 圆周率的倒数(0.31830988618f) |
UNITY_INV_TWO_PI | 2倍圆周率的倒数 (0.15915494309f) |
UNITY_INV_FOUR_PI | 4倍圆周率的倒数(0.07957747155f) |
UNITY_HALF_PI | 半圆周率 (1.57079632679f) |
UNITY_INV_HALF_PI | 半圆周率的倒数(0.636619772367f) |
在代码中最后有个 "1e-7f",查阅了一下,是指float单精度,在内存中应该是 1 个符号位, 8个指数位 和 23个有效数据位。而 2^23 ~ 10^ 7, 由此得到 1e-7这个数的原由。
4.2 F项-菲涅尔方程
F(l,h) : 菲涅尔方程(Fresnel Equation),描述不同的表面角下表面所反射的光线所占的比率。
图片来源:Physically Based Rendering in Filament
菲涅尔效应示意图
有关菲涅尔效应这里不做过多的赘述,最直观的物理现象如上图,在观察平静水面时,会发现越远处的反射越强,越是近处时却能透射的看到水底,Fresnel公式如下:
4.21公式
4.22 反射率
在宏观层面看到的菲涅尔效应实际上是微观层面微平面菲涅尔效应的平均值,于是,不同材质的菲涅尔效应的表象是不一样的。
导体:菲涅尔效应就很弱,因为它本身的反射率就很强。
绝缘体:菲涅尔效应很明显。
上图也一定程度上反映了上文所说的应用
4.23 有关F0的问题
可以理解为视角与光线反射时的夹角,如下图的(F0)位置时,观察视线与反射回的光线夹角为0度,同样有个F90的值,即视线与球体法线刚好90度时即为(F90)。
有关Fresnel菲涅尔的直观表述就这些吧,需要了解更详细的内容可以去查阅:
4.24效果展示
展示效果如上图,仔细查看时,左三列图的变化都很小,在图片中可能观察不出来,在Unity中还是能看到微小变化,只有同时开启镜面反射的D项和G项时,变化就非常明显了,看最右一列的变化。
4.25 相关代码
内置文件中放有三个函数,其中FresnelLerpFast相比FresnelLerp的区别仅是在Pow4和Pow5的区别,也就是少乘一次,所以标个Fast吧。
1、 FresnelTrem:是用在镜面反射的F项中。
2、 FresnelLerp:插值的是用在环境光的F项,多了一个half3类型的F90这个数值可以后面有关环境光照时详细分析。
3、 FresnelLerpFast:只是少相乘一次,在代码中也有标明。
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); }
我还额外收集了两段有关菲涅尔的其它算法:
//菲涅尔项 inline float SchlickFresnel(float u) { float m = clamp(1-u, 0, 1); float m2 = m*m; return m2*m2*m; // pow(m,5) } inline float3 SchlickFresnelFunction(float LdotH,float3 SpecularColor){ return SpecularColor + (1 - SpecularColor)* SchlickFresnel(LdotH); } inline float SphericalGaussianFresnel(float LdotH,float SpecularColor) { float power = ((-5.55473 * LdotH) - 6.98316) * LdotH; return SpecularColor + (1 - SpecularColor) * pow(2,power); }
4.3 G项-未遮挡函数
G(l,v,h) : 几何函数(Geometry Function),描述微平面自成阴影的属性,即m = h的未被遮蔽的表面点的百分比。
4.31公式
明明是G项的公式,为什么下面会写成V(v,l,a),其实它是结合整个镜面反射的公式简化后的结果:
公式演化过程:
完整的Smith-GGX方程如下:
仔细观察上面的公式并联系整个镜面反射的公式时,你可以简化相关公式:
演变为:
考虑到微平面的用高度来关联和遮掩阴影,会得到更精准的效果,所以又定义了相关的Smith函数:
有了上面一大堆公式后,就可以把代码编写如下:
float V_SmithGGXCorrelated(float NoV, float NoL, float roughness) { float a2 = roughness * roughness; float GGXV = NoL * sqrt(NoV * NoV * (1.0 - a2) + a2); float GGXL = NoV * sqrt(NoL * NoL * (1.0 - a2) + a2); return 0.5 / (GGXV + GGXL); }
为了节省运算,特别是在移动端应用时,可以省去开方把公式演变为:
float V_SmithGGXCorrelatedFast(float NoV, float NoL, float roughness) { float a = roughness; float GGXV = NoL * (NoV * (1.0 - a) + a); float GGXL = NoV * (NoL * (1.0 - a) + a); return 0.5 / (GGXV + GGXL); }
4.32效果展示
漫反射和镜面反射D项关闭时几何遮蔽效果,下方球体的变化比较明显
漫反射和镜面反射D项开启时几何遮蔽效果
4.33 相关代码
inline float SmithJointFilament (float NdotL, float NdotV, float roughness) { half a = roughness; half a2 = a * a; half lambdaV = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2); half lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2); return 0.5f / (lambdaV + lambdaL + 1e-5f); } inline float SmithJointGGXTerm (float NdotL, float NdotV, float roughness) { float a = roughness; float lambdaV = NdotL * (NdotV * (1 - a) + a); float lambdaL = NdotV * (NdotL * (1 - a) + a); #if defined(SHADER_API_SWITCH) return 0.5f / (lambdaV + lambdaL + 1e-4f); #else return 0.5f / (lambdaV + lambdaL + 1e-5f); #endif }
整理自定义函数集的G项片段为:
//G项 几何遮蔽函数 inline float SmithJointFilament (float NdotL, float NdotV, float roughness) { half a = roughness; half a2 = a * a; half lambdaV = NdotL * sqrt((-NdotV * a2 + NdotV) * NdotV + a2); half lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2); return 0.5f / (lambdaV + lambdaL + 1e-5f); } inline float smithG_GGX(float NdotV, float alphaG) { float a = alphaG*alphaG; float b = NdotV*NdotV; return 1 / (NdotV + sqrt(a + b - a*b)); } inline float GGXGeometricShadowingFunction (float NdotL, float NdotV, float roughness){ float roughnessSqr = roughness*roughness; float NdotLSqr = NdotL*NdotL; float NdotVSqr = NdotV*NdotV; float SmithL = (2 * NdotL)/ (NdotL + sqrt(roughnessSqr + ( 1-roughnessSqr) * NdotLSqr)); float SmithV = (2 * NdotV)/ (NdotV + sqrt(roughnessSqr + ( 1-roughnessSqr) * NdotVSqr)); float Gs = (SmithL * SmithV); return Gs; } inline float SchlickBeckmanGeometricShadowingFunction (float NdotL, float NdotV,float roughness){ float roughnessSqr = roughness*roughness; float k = roughnessSqr * 0.797884560802865; float SmithL = (NdotL)/ (NdotL * (1- k) + k); float SmithV = (NdotV)/ (NdotV * (1- k) + k); float Gs = (SmithL * SmithV); return Gs; }
4.4 Anisotropic各项异性
各向异性的应用也非常之广,如常见的头像高光、光盘的反光、平底锅底、不锈钢盆等。在这个实例中其实用不上各向异性的效果,不过也可以附加这个功能,加个开关关闭就好了,以方便其它实例的应用。
4.41 公式
查看公式时会发现挺复杂,如果不想深入了解的话,美术师记得模型上与UV的分布有关系,如做头发时,尽量以横竖展开UV会好些。光照上与切线和副切线有关。
在此引用Google Filament中的各向异性公式:
以下是提升效果的一个公式,但代价比较昂贵:
4.42效果展示
同向异性在示例模型上的效果展示
在此实例中,开启各向异性效果后,拖动偏移时,就如同流光一般。
各向异性在材质球上的效果展示
4.43相关代码
//GGX各向异性 inline float GTR2_aniso(float NdotH, float HdotX, float HdotY, float ax, float ay) { return 1 / (UNITY_PI * ax*ay * Pows2( Pows2(HdotX/ax) + Pows2(HdotY/ay) + NdotH*NdotH )); } inline float GRT2_Anisotropic(float roughness,float aniso,float NdotH,float H,float T,float B){ float ht = dot(T, H); float bh = dot(B, H); float at = max(0.001, Pows2(roughness)/aniso); float ab = max(0.001, Pows2(roughness)*aniso); return GTR2_aniso(NdotH,ht,bh,at,ab); } //Google Filament 各向异性 inline float GGX_Aniso(float NoH, float3 h,float3 t, float3 b, float at, float ab) { float ToH = dot(t, h); float BoH = dot(b, h); float a2 = at * ab; float3 v = float3(ab * ToH, at * BoH, a2 * NoH); float v2 = dot(v, v); float w2 = a2 / v2; return a2 * w2 * w2 * (1.0 / UNITY_PI); } inline float GGX_Anisotropic(float roughness,float aniso,float NdotH,float3 H,float3 T,float3 B){ float at = max(roughness * (1.0 + aniso), 0.001); float ab = max(roughness * (1.0 - aniso), 0.001); return GGX_Aniso(NdotH,H,T,B,at,ab); }
5.1粗糙度(Roughness)
Unity内置Standard中粗糙度的反射光效果对比图
手写PBR中粗糙度的反射光效果图
如上两图,Unity内置和自己手写的粗糙度对反射光的效果对比,期间开启了漫反射和镜面反射项DFG项,关闭了环境光照部分。
之所以选模型的这个观察角度是因为左侧有贴图在粗糙度上的明显变化,左上黑色塑料被触摸多了之后脱漆的光滑变化,以及米黄色金属脱漆露出金属的材质表现。