目前业界广泛采用的Microfacet Cook-Torrance BRDF形式如下:
基于此公式,本系列之前我们也实现过一版PBR基础:Vulkan_PBR—基于物理的渲染基础。
其中:
关于基于物理的渲染理念,其实跟图形学中对几何体的建模尺度有一定关联。图形学中,对几何体外观的建模,总会假设一定的建模尺度和观察尺度:
传统光照模型中,一般只将几何体建模到中尺度的法线贴图(Normal Map)层面。虽说Blinn-Phong等分布也是基于微平面理论推导而来,但并没有配套粗糙度贴图(Roughness Map)为其提供亚像素级精度的细节,而且传统的NDF一般都没有经过归一化,不满足能量守恒,容易出现失真。
而在基于物理的渲染工作流中,通过将粗糙度贴图(Roughness Map)与微平面归一化的法线分布函数结合使用,将需渲染的几何体的建模尺度细化到了微观尺度(Microscale)的亚像素层面,对材质的微观表现更加定量,所以能够带来更加接近真实的渲染质量和更全面的材质外观质感把控。
最常见的法线分布函数是各向同性(isotropic)的,它们围绕由宏观表面法线n定义的轴旋转对称(rotationally symmetrical)。本文将对其中在历史上使用和讨论较为广泛的几种法线分布函数,可以总结如下:
Beckmann [1963]
Blinn-Phong [1977]
GGX [2007] / Trowbridge-Reitz [1975]
Trowbridge-Reitz(GTR)[2012]
Beckmann分布是光学业界开发的第一批微平面模型中使用的法线分布。[Beckmann 1963],也是Cook-Torrance BRDF在提出时选择的NDF [Cook 1981] [Cook 1982]。
Beckmann NDF具备形状不变性(shape-invariant)
正确归一化后,Beckmann分布具有以下形式:
UE4中对Beckmann分布的实现代码如下:
// [Beckmann 1963, "The scattering of electromagnetic waves from rough surfaces"]
float D_Beckmann(float NoH, float a )
{
float NoH2 = NoH * NoH;
return exp( (NoH2 - 1) / (a * a * NoH2) ) / ( PI * a * a * NoH2 * NoH2 );
}
运行效果见下图:
Blinn-Phong分布的前身Phong分布是计算机图形学文献中提出的最早的着色方程之一。
Blinn-Phong法线分布函数由Blinn推导出,作为(非基于物理的)Phong着色模型的改进,以更好地拟合微平面BRDF的结构。
Blinn-Phong 分布不具备形状不变性(shape-invariant)。
下式是较为主流的归一化的Blinn-Phong(Normalized Blinn-Phong)的形式:
其中,幂αp是Blinn-Phong NDF的“粗糙度参数”;高值表示光滑表面,低值表示粗糙表面。对于非常光滑的曲面,值可以任意高(一个完美的镜面αp=∞),并且通过将αp设置为0可以实现最大随机曲面(均匀NDF)。
αp参数不便于艺术家操纵或直接绘制,因为它带来的视觉变化非常不均匀。出于这个原因,经常让美术师们操纵“界面值”,即通过非线性函数从中导出αp。UE4中,则采用映射 [公式] , 那么得到的Blinn-Phong的形式为:
UE4中对Blinn-Phong的实现代码如下:
// [Blinn 1977, "Models of light reflection for computer synthesized pictures"]
float D_Blinn( float NoH, float a2)
{
float n = 2 / (a2*a2) - 2;
return (n+2) / (2*PI) * pow( NoH, n ); // 1 mad, 1 exp, 1 mul, 1 log
}
运行效果见下图:
GGX即Trowbridge-Reitz分布,最初由Trowbridge和Reitz [Trowbridge 1975]推导出,在Blinn 1977年的论文 [Blinn 1977]中也有推荐此分布函数,但一直没有受到图形学界的太多关注。30多年后,Trowbridge-Reitz分布被Walter等人独立重新发现[Walter 2007],并将其命名为GGX分布。
在[Walter 2007]重新发现并提出GGX分布之后,GGX分布采用风潮开始在电影 [Burley 2012]和游戏 [Karis 2013][Lagarde 2014]行业中广泛传播,成为了如今游戏行业和电影行业中最常用的法线分布函数。
GGX分布的公式为:
在流行的模型中,GGX拥有最长的尾部。这是GGX能够日益普及的主要原因:
GGX分布具备形状不变性(shape-invariant),而与其对标的GTR等分布不具备形状不变性,这是GGX能普及的深层次原因。
在迪士尼原理着色模型(Disney principled shading model)中,Burley推荐将粗糙度控制以α= r2暴露给用户,其中r是0到1之间的用户界面粗糙度参数值,以让分布以更线性的方式变化。这种方式实用性较好,不少使用GGX分布的引擎与游戏都采用了这种映射,如UE4和Unity。
// GGX / Trowbridge-Reitz
// [Walter et al. 2007, "Microfacet models for refraction through rough surfaces"]
float D_GGXT(float NoH , float a)
{
float d = (NoH * NoH) * (a * a -1) + 1;
return (a*a) / ( PI*d*d );
}
运行效果见下图:
Burley [ Burley 2012]根据对Berry,GGX等分布的观察,提出了广义的Trowbridge-Reitz(Generalized-Trowbridge-Reitz,GTR)法线分布函数,其目标是允许更多地控制NDF的形状,特别是分布的尾部:
其中,γ参数用于控制尾部形状。 当γ= 2时,GTR等同于GGX。 随着γ的值减小,分布的尾部变得更长。而随着γ值的增加,分布的尾部变得更短。上式中:
γ=1时,GTR即Berry分布
γ=2时,GTR即GGX(Trowbridge-Reitz)分布
以下为各种γ值的GTR分布曲线与θh的关系图示:
GTR分布不具备形状不变性(shape-invariant),导致其发布以来,无法被广泛使用。
以下是γ= 1和γ= 2时GTR分布的Shader实现代码:
// Generalized-Trowbridge-Reitz distribution
float D_GTR1(float alpha, float dotNH)
{
float a2 = alpha * alpha;
float cos2th = dotNH * dotNH;
float den = (1.0 + (a2 - 1.0) * cos2th);
return (a2 - 1.0) / (PI * log(a2) * den);
}
float D_GTR2(float alpha, float dotNH)
{
float a2 = alpha * alpha;
float cos2th = dotNH * dotNH;
float den = (1.0 + (a2 - 1.0) * cos2th);
return a2 / (PI * den * den);
}
现实世界中,大多数材质具有各向同性的表面外观,但有些特殊材质的微观结构具有显著的各向异性(Anisotropy),从而显著影响其外观。
创建各向异性NDF的常用方法是基于现有各向同性NDF进行推导。而推导所使用的方法是通用的,可以应用于任何形状不变的(shape-invariant)各向同性NDF,这便是GGX等形状不变的NDF能更加普及的另一个原因。
各项异性的Beckmann分布形式如下:
其中,m为微表面法线(可以理解为half半矢量),n为宏观表面法线,t为切线方向,b为副法线方向。
以下为UE4中Anisotropic Beckmann分布的Shader实现代码:
// Anisotropic Beckmann
float D_Beckmann_aniso( float ax, float ay, float NoH, float3 H, float3 X, float3 Y )
{
float XoH = dot( X, H );
float YoH = dot( Y, H );
float d = - (XoH*XoH / (ax*ax) + YoH*YoH / (ay*ay)) / NoH*NoH;
return exp(d) / ( PI * ax*ay * NoH * NoH * NoH * NoH );
}
各项异性的GGX分布形式如下:
以下为UE4中Anisotropic GGX分布的Shader实现代码:
// Anisotropic GGX
// [Burley 2012, "Physically-Based Shading at Disney"]
float D_GGXaniso( float ax, float ay, float NoH, float3 H, float3 X, float3 Y )
{
float XoH = dot( X, H );
float YoH = dot( Y, H );
float d = XoH*XoH / (ax*ax) + YoH*YoH / (ay*ay) + NoH*NoH;
return 1 / ( PI * ax*ay * d*d );
}
其中,X为tangent,t切线方向,Y为binormal,b,副法线方向
需要注意的是,将法线贴图与各向异性BRDF组合时,重要的是要确保法线贴图扰动(perturbs)切线和副切线矢量以及法线。
对于菲涅尔(Fresnel)项,业界方案一般都采用Schlick的Fresnel近似,因为计算成本低廉,而且精度足够:
vec3 F_Schlick(float cosTheta, float metallic)
{
vec3 F0 = mix(vec3(0.04), materialcolor(), metallic); // * material.specular
vec3 F = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
return F;
}
运行效果见下图:
vec3 Fresnel_UE4(float cosTheta, float metallic)
{
vec3 F0 = mix(vec3(0.04), materialcolor(), metallic); // * material.specular
vec3 F = F0 + (1.0 - F0) * pow(2, ((-5.55473) * cosTheta - 6.98316) * cosTheta);
return F;
}
运行效果见下图:
菲涅尔项的常见模型可以总结如下:
Cook-Torrance [1982]
Schlick [1994]
Gotanta [2014]
在Microfacet Specular BRDF的D,G,F三项中,如果说法线分布函数是最核心的一项,那么几何函数则是核心的辅助项,而且是三项中最复杂的一项。
历史上主流的几何函数建模,按提出或归纳的时间进行排序,可以总结为:
其中,Smith遮蔽函数(Smith masking function)是现在业界所采用的主流遮蔽函数,Eric Heitz在2014年[Heitz 2014]将其拓展为Smith联合遮蔽阴影函数(Smith Joint Masking-Shadowing Function),该函数具有四种形式:
其中,高度相关遮蔽阴影型(Height-Correlated Masking and Shadowing),以及其近似,是目前业界采用的主流遮蔽阴影函数。
几何函数具有两种主要形式:G1和G2,其中:
默认情况下,microfacet BRDF中使用的几何函数代指G2
图中几何函数的两种主要形式:G1和G2。G1为微平面在单个方向(光照方向L或观察方向V)上可见比例。G2为微平面在光照方向L和观察方向V两个方向上可见比例(图片来自GDC 2017, PBR Diffuse Lighting for GGX+SmithMicrosurfaces, Earl Hammon )
几何函数与法线分布函数作为Microfacet Specular BRDF中的重要两项,两者之间具有紧密的联系:
游戏和电影工业对GGX-Smith遮蔽函数的选用方面,可以总结为两个主要阶段:
SIGGRAPH 2014之前,Smith分离的遮蔽阴影函数
SIGGRAPH 2014之后,Smith相关的遮蔽阴影函数
而这两个阶段的演变,主要在于2014年Eric Heitz于JCGT 2014上发表了著名的paper 《Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs》,以及其后续在SIGGPRAPH 2014上进行的同名的talk。
Disney参考了 [Walter 2007]的近似方法,使用Smith GGX导出的G项,并将粗糙度参数进行重映射以减少光泽表面的极端增益,即将α 从[0,1]重映射到[0.5, 1],α的值为(0.5 + roughness/2)^2。从而使几何项的粗糙度变化更加平滑,更便于美术人员的使用。
以下为Disney 实现的Smith GGX的几何项的表达式:
其中UE4在SIGGRAPH 2013上公布的方案为基于Schlick近似,将k映射为k=a/2,去匹配GGX Smith方程,并采用了Disney对粗糙度的重映射:
在2014年,Heitz在JCGT 2014发表了著名的paper 《Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs》,以及后续在SIGGPRAPH 2014上进行了同名的talk,将游戏和电影业界对遮蔽阴影函数(The Smith Joint Masking-Shadowing Function)的理解上升到了一个新的层次。
UE4 ,Frostbite 和Unity等引擎都受到Heitz的启发,为了得到更精确的几何遮挡关系,开始考虑入射阴影和出射遮蔽之间的相关性,并在后续更新中各自转向了Smith联合遮蔽阴影函数(The Smith Joint Masking-Shadowing Function)的高度相关遮蔽阴影形式(Height-Correlated Masking and Shadowing),并相应地都做了一些近似与优化。
下面将分别对其进行总结。
UE4采用的 GGX-Smith Correlated Joint Approximate为:
实现代码如下:
// Appoximation of joint Smith term for GGX
// [Heitz 2014, "Understanding the Masking-Shadowing Function in Microfacet-Based BRDFs"]
float Vis_SmithJointApprox( float NoL , float NoV,float a)
{
float a2 = a*a;
float Vis_SmithV = NoL * ( NoV * ( 1 - a2 ) + a2 );
float Vis_SmithL = NoV * ( NoL * ( 1 - a2 ) + a2 );
return 0.5 /( Vis_SmithV + Vis_SmithL );
}
运行效果见下图:
Unity HDRP采用的GGX-Smith Correlated Joint Approximate为:
实现代码如下:
float V_SmithJointGGX(float NdotL, float NdotV, float roughness)
{
float a2 = roughness*roughness;
float lambdaV = NdotL * (NdotV * (1 - roughness) + roughness);
float lambdaL = NdotV * sqrt((-NdotL * a2 + NdotL) * NdotL + a2);
return 0.5 / (lambdaV + lambdaL);
}
运行效果见下图:
Google Filament渲染器采用的 GGX-Smith Correlated Joint Approximate为:
float V_SmithGGXCorrelated(float NoL, float NoV, float a)
{
float a2 = a * a;
float GGXL = NoV * sqrt((-NoL * a2 + NoL) * NoL + a2);
float GGXV = NoL * sqrt((-NoV * a2 + NoV) * NoV + a2);
return 0.5 / (GGXV + GGXL);
}
运行效果见下图:
Hammon[Hammon 2017]在GDC 2017上提出,UE4在2013年提出的近似G1可以导出由高度相关的Vis项的高效近似:
其中,lerp表示线性插值算子, lerp(x, y, s) = x * (1 − s) + y * s
mix(x,y,a) a控制混合结果 return x(1-a) +y*a 返回 线性混合的值
float G_RespawnEntertainment(float NoL, float NoV, float a)
{
float lerp = mix(2*abs(NoL)*abs(NoV),abs(NoL)+abs(NoV),a);
return 0.5 / lerp;
}
运行效果见下图:
使用了上述多种DFG组合,最终个人感觉以下公式效果最佳:
// Normal Distribution function
float D_GGX(float dotNH, float roughness)
{
float alpha = roughness * roughness;
float alpha2 = alpha * alpha;
float denom = dotNH * dotNH * (alpha2 - 1.0) + 1.0;
return (alpha2)/(PI * denom*denom);
}
// Fresnel function
vec3 F_Schlick(float cosTheta, float metallic)
{
vec3 F0 = mix(vec3(0.04), materialcolor(), metallic); // * material.specular
vec3 F = F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
return F;
}
// Geometric Shadowing function
float G_SchlicksmithGGX(float dotNL, float dotNV, float roughness)
{
float r = (roughness + 1.0);
float k = (r*r) / 8.0;
float GL = dotNL / (dotNL * (1.0 - k) + k);
float GV = dotNV / (dotNV * (1.0 - k) + k);
return GL * GV;
}
运行效果见下图: