反射模型和常用的反射模型中已经将渲染中PBR的机制和实现方案了解得差不多了。然而在应用中,这套PBR方案应用并不广泛。观察可以发现,PBR中的概念和参数繁多且复杂,并且并不灵活,需要考虑一个物体是导体还是绝缘体,需要考虑物体进行的是镜面反射还是漫反射,亦或是有粗糙度的微平面反射,并且这些不同类型的BxDF实现方案差异巨大。
disney BRDF是目前应用最为广泛的反射模型,几乎所有渲染器都支持此模型,或者以此为基础进行扩展。
顾名思义disney BRDF是BRDF模型,所以主要考虑的是光线的反射,对于光线的透射使用了一些近似方案来模拟,它也非常适合用于实时渲染领域。
disney BRDF的理念
disney BRDF核心理念如下(直接贴了毛星云大佬的总结,disney BRDF原文章里也有介绍):
- 应使用直观的参数,而不是物理类的晦涩参数。
- 参数应尽可能少。
- 参数在其合理范围内应该为0到1。
- 允许参数在有意义时超出正常的合理范围。
- 所有参数组合应尽可能健壮和合理。
其注重实用性大于物体正确性,对美术更加友好。
下面是disney BRDF的参数(使用metallic-roughness工作流):
- baseColor(固有色):表面颜色,通常由纹理贴图提供。
- subsurface(次表面):使用次表面近似控制漫反射形状。
- metallic(金属度):金属(0 = 电介质,1 =金属)。这是两种不同模型之间的线性混合。金属模型没有漫反射成分,并且还具有等于基础色的着色入射镜面反射。
- specular(镜面反射强度):入射镜面反射量。用于取代折射率。
- specularTint(镜面反射颜色):对美术控制的让步,用于对基础色(basecolor)的入射镜面反射进行颜色控制。掠射镜面反射仍然是非彩色的。
- roughness(粗糙度):表面粗糙度,控制漫反射和镜面反射。
- anisotropic(各向异性强度):各向异性程度。用于控制镜面反射高光的纵横比。(0 =各向同性,1 =最大各向异性。)
- sheen(光泽度):一种额外的掠射分量(grazing component),主要用于布料。
- sheenTint(光泽颜色):对sheen(光泽度)的颜色控制,也是一个美术参数。
- clearcoat(清漆强度):有特殊用途的第二个镜面波瓣(specular lobe)。
- clearcoatGloss(清漆光泽度):控制透明涂层光泽度,0 = “缎面(satin)”外观,1 = “光泽(gloss)”外观。
下面是官方提供的各参数效果示意图:
disney BRDF的实现原理
总览
disney BRDF由多种类型的BRDF混合而来,其实现原理图如下:
总体可以将其概括为DielectricBRDF与ConductorBRDF的混合,使用参数metallic控制混合比例。
对于DielectricBRDF,它由DiffuseBRDF和SpecularBRDF混合而来,依然使用Torrance–Sparrow BRDF的思想,即
对于ConductorBRDF,它只包含specular部分,因为金属材质吸收光线能力强,不再发生漫反射和次表面散射。
为了方便,下面使用金属和非金属来表示导体和绝缘体。
下面是disney BRDF的官方实现示例(disney_brdf):
# variables go here...
# [type] [name] [min val] [max val] [default val]
::begin parameters
color baseColor .82 .67 .16
float metallic 0 1 0
float subsurface 0 1 0
float specular 0 1 .5
float roughness 0 1 .5
float specularTint 0 1 0
float anisotropic 0 1 0
float sheen 0 1 0
float sheenTint 0 1 .5
float clearcoat 0 1 0
float clearcoatGloss 0 1 1
::end parameters
::begin shader
const float PI = 3.14159265358979323846;
float sqr(float x) { return x*x; }
float SchlickFresnel(float u)
{
float m = clamp(1-u, 0, 1);
float m2 = m*m;
return m2*m2*m; // pow(m,5)
}
float GTR1(float NdotH, float a)
{
if (a >= 1) return 1/PI;
float a2 = a*a;
float t = 1 + (a2-1)*NdotH*NdotH;
return (a2-1) / (PI*log(a2)*t);
}
float GTR2(float NdotH, float a)
{
float a2 = a*a;
float t = 1 + (a2-1)*NdotH*NdotH;
return a2 / (PI * t*t);
}
float GTR2_aniso(float NdotH, float HdotX, float HdotY, float ax, float ay)
{
return 1 / (PI * ax*ay * sqr( sqr(HdotX/ax) + sqr(HdotY/ay) + NdotH*NdotH ));
}
float smithG_GGX(float NdotV, float alphaG)
{
float a = alphaG*alphaG;
float b = NdotV*NdotV;
return 1 / (NdotV + sqrt(a + b - a*b));
}
float smithG_GGX_aniso(float NdotV, float VdotX, float VdotY, float ax, float ay)
{
return 1 / (NdotV + sqrt( sqr(VdotX*ax) + sqr(VdotY*ay) + sqr(NdotV) ));
}
vec3 mon2lin(vec3 x)
{
return vec3(pow(x[0], 2.2), pow(x[1], 2.2), pow(x[2], 2.2));
}
vec3 BRDF( vec3 L, vec3 V, vec3 N, vec3 X, vec3 Y )
{
float NdotL = dot(N,L);
float NdotV = dot(N,V);
if (NdotL < 0 || NdotV < 0) return vec3(0);
vec3 H = normalize(L+V);
float NdotH = dot(N,H);
float LdotH = dot(L,H);
vec3 Cdlin = mon2lin(baseColor);
float Cdlum = .3*Cdlin[0] + .6*Cdlin[1] + .1*Cdlin[2]; // luminance approx.
vec3 Ctint = Cdlum > 0 ? Cdlin/Cdlum : vec3(1); // normalize lum. to isolate hue+sat
vec3 Cspec0 = mix(specular*.08*mix(vec3(1), Ctint, specularTint), Cdlin, metallic);
vec3 Csheen = mix(vec3(1), Ctint, sheenTint);
// Diffuse fresnel - go from 1 at normal incidence to .5 at grazing
// and mix in diffuse retro-reflection based on roughness
float FL = SchlickFresnel(NdotL), FV = SchlickFresnel(NdotV);
float Fd90 = 0.5 + 2 * LdotH*LdotH * roughness;
float Fd = mix(1.0, Fd90, FL) * mix(1.0, Fd90, FV);
// Based on Hanrahan-Krueger brdf approximation of isotropic bssrdf
// 1.25 scale is used to (roughly) preserve albedo
// Fss90 used to "flatten" retroreflection based on roughness
float Fss90 = LdotH*LdotH*roughness;
float Fss = mix(1.0, Fss90, FL) * mix(1.0, Fss90, FV);
float ss = 1.25 * (Fss * (1 / (NdotL + NdotV) - .5) + .5);
// specular
float aspect = sqrt(1-anisotropic*.9);
float ax = max(.001, sqr(roughness)/aspect);
float ay = max(.001, sqr(roughness)*aspect);
float Ds = GTR2_aniso(NdotH, dot(H, X), dot(H, Y), ax, ay);
float FH = SchlickFresnel(LdotH);
vec3 Fs = mix(Cspec0, vec3(1), FH);
float Gs;
Gs = smithG_GGX_aniso(NdotL, dot(L, X), dot(L, Y), ax, ay);
Gs *= smithG_GGX_aniso(NdotV, dot(V, X), dot(V, Y), ax, ay);
// sheen
vec3 Fsheen = FH * sheen * Csheen;
// clearcoat (ior = 1.5 -> F0 = 0.04)
float Dr = GTR1(NdotH, mix(.1,.001,clearcoatGloss));
float Fr = mix(.04, 1.0, FH);
float Gr = smithG_GGX(NdotL, .25) * smithG_GGX(NdotV, .25);
return ((1/PI) * mix(Fd, ss, subsurface)*Cdlin + Fsheen)
* (1-metallic)
+ Gs*Fs*Ds + .25*clearcoat*Gr*Fr*Dr;
}
::end shader
这里将其一步步拆分,详细研究它的实现原理。
计算brdf颜色分量(反射率R,F0)
漫反射的颜色,
vec3 Cdlin = mon2lin(baseColor);
vec3 mon2lin(vec3 x)
{
return vec3(pow(x[0], 2.2), pow(x[1], 2.2), pow(x[2], 2.2));
}
对baseColor进行gamma校正的结果,因为渲染时颜色空间应为线性空间。
去除漫反射颜色的亮度信息,只保留色相和饱和度,因为后续镜面反射颜色的计算不需要亮度信息。
float Cdlum = .3*Cdlin[0] + .6*Cdlin[1] + .1*Cdlin[2]; // luminance approx.
vec3 Ctint = Cdlum > 0 ? Cdlin/Cdlum : vec3(1); // normalize lum. to isolate hue+sat
镜面反射颜色,
vec3 Cspec0 = mix(specular*.08*mix(vec3(1), Ctint, specularTint), Cdlin, metallic);
这里做了两次混合,第一次根据specularTint混合vec3(1)和Ctint,这里用到了specularTint参数,它将对非金属的镜面反射颜色进行控制,当它为0时,非金属的镜面反射颜色为白色,这是物理正确的,当它为1时,非金属的镜面反射颜色为Ctint,即baseColor去掉亮度。
上面对specularTint参数已经做了说明,这是一个对美术进行让步的参数,如果严格按照物理正确,非金属的镜面反射颜色应该为白色,而有了这个参数就可以对其进行控制。
第二次混合是根据metallic参数对非金属的镜面反射颜色和Cdlin进行混合,这很好理解,对于金属来说,其镜面反射颜色与漫反射颜色是相同的。
需要注意的是,非金属的镜面反射颜色还有specular*.08这个系数,也就是使用specular参数来控制它的镜面反射强度。specular参数是用来代替折射率IOR的,如果没有这个参数,通常应该使用菲涅尔方程结合IOR来计算反射光线的强度。
0.08这个系数则是一个实验总结的系数,默认情况下IOR为1.5,此时使用菲涅尔方程计算出的反射比例F0应为0.04,这表示当光线沿法线方向入射时,物体会反射4%的能量(将ior=1.5和带入菲涅尔方程可以计算出来)。specular参数的取值为0到1,默认为0.5,此时计算出的Cspec0正好就是0.04。所以系数0.08就是通过specular默认值和IOR默认值反推出来的。
从这里也可以看出,对于非金属,其F0应为0到0.08之间(忽略specularTint参数),对于金属,F0等于漫反射率R。
sheen颜色,它也属于漫反射
vec3 Csheen = mix(vec3(1), Ctint, sheenTint);
光泽的颜色,通过sheenTint控制,和specularTint是类似的。
这一步计算出来各个brdf的颜色以及反射率,包括漫反射率Cdlin(R),镜面反射率Cspec0(F0)。
diffuse
如果使用最简单的Lambertian漫反射,diffuse项很好计算,
diffuse = Cdlin / Pi;
然而disney中的漫反射更复杂一些,它的计算公式为,
其中,
代码实现:
float FL = SchlickFresnel(NdotL), FV = SchlickFresnel(NdotV);
float Fd90 = 0.5 + 2 * LdotH*LdotH * roughness;
float Fd = mix(1.0, Fd90, FL) * mix(1.0, Fd90, FV);
float SchlickFresnel(float u)
{
float m = clamp(1-u, 0, 1);
float m2 = m*m;
return m2*m2*m; // pow(m,5)
}
这里并没有直接乘以,这项工作放到最后面来完成。
disney漫反射相比于Lambertian漫反射,引入了SchlickFresnel项,这是一个基于观察经验的漫反射模型,它在物体边缘有更亮的漫反射效果,并且引入粗糙度对漫反射的影响。关于它更详细的解释,可以参考【渲染】Disney BSDF 深度解析。
subsurface
次表面散射被看作是漫反射的一部分,可以理解为光线进入物体后,在物体内部弹射的现象。
这一项是从Hanrahan-Krueger brdf近似而来,模拟次表面散射的效果,其公式为
实现代码:
float Fss90 = LdotH*LdotH*roughness;
float Fss = mix(1.0, Fss90, FL) * mix(1.0, Fss90, FV);
float ss = 1.25 * (Fss * (1 / (NdotL + NdotV) - .5) + .5);
FL和FV在diffuse项已经计算出来了。
specular
specular项的计算依然使用微平面模型,在Torrance–Sparrow模型中已经做过介绍,但是在实时渲染时,考虑到性能因素,并不能使用功能那么复杂的分布函数。
Schlick Fresnel
这是用来代替菲涅尔项的近似函数,其形式为
这是实时渲染领域使用得最广泛的菲涅尔函数了,其中F0在上面各颜色分量计算时介绍过,表示光线沿法线方向入射时反射能量比例,对于非金属默认是0.04。
这个函数在上面diffuse和subsurface计算时使用过其变体,只是1和F0的位置调换了,简单理解就是随着从0度变化到90度,从0变到1,物体反射的能量从F0变化到1,即光线沿法线方向入射时反射能量比例为F0,而光线以法线垂直方向入射时反射能量比例为1,正好对应掠射时发生全反射的物理现象。而diffuse和subsurface获取的能量是反射后剩余的,所以它们的变化趋势与函数相反,所以1和Fd90(或Fss90)的位置调换了。
disney BRDF使用SchlickFresnel函数加mix函数实现Schlick Fresnel,
float FH = SchlickFresnel(LdotH);
vec3 Fs = mix(Cspec0, vec3(1), FH);
法线分布函数 GTR
disney BRDF使用的法线分布函数是GTR分布,其具体形式如下:
其中参数用来控制函数的下降速度,越大,随着的增大,函数趋近0的速度越快,表示粗糙度。
通常情况下只会使用和两个函数,前者用来计算clearcoat的specular,后者用来计算物体材质的specular。
可以发现时该函数是未定义的,它被替换为如下形式,
当时,它为以下形式,
如果考虑各向异性,就使用微平面的法线分布函数中介绍的Trowbridge-Reitz模型,为了使算法更高效,其被替换成如下形式:
其中x和y表示该点的切线和副切线,它们与法线n组成一个正交基。
几个函数的实现如下:
float GTR1(float NdotH, float a)
{
if (a >= 1) return 1/PI;
float a2 = a*a;
float t = 1 + (a2-1)*NdotH*NdotH;
return (a2-1) / (PI*log(a2)*t);
}
float GTR2(float NdotH, float a)
{
float a2 = a*a;
float t = 1 + (a2-1)*NdotH*NdotH;
return a2 / (PI * t*t);
}
float GTR2_aniso(float NdotH, float HdotX, float HdotY, float ax, float ay)
{
return 1 / (PI * ax*ay * sqr( sqr(HdotX/ax) + sqr(HdotY/ay) + NdotH*NdotH ));
}
几何函数 Smith-GGX
Smith-GGX随粗糙度的变化更加平滑,其形式为
这个函数有一个特别的地方,它的分子是,如果再将考虑进去,它的分子就变成了,正好与Torrance–Sparrow模型的分母相同,所以此项可以直接约分。这也是使用Smith-GGX的优点之一,最终的光照计算不需要除以,减少了计算量。
代码实现:
float smithG_GGX(float NdotV, float alphaG)
{
float a = alphaG*alphaG;
float b = NdotV*NdotV;
return 1 / (NdotV + sqrt(a + b - a*b));
}
注意这里已经去掉分子了。
同时,它有一个各项异性的版本,
和法线分布函数一样,x和y表示其切线和副切线,对应实现如下:
float smithG_GGX_aniso(float NdotV, float VdotX, float VdotY, float ax, float ay)
{
return 1 / (NdotV + sqrt( sqr(VdotX*ax) + sqr(VdotY*ay) + sqr(NdotV) ));
}
同样去掉分子。
specular的计算
有了上面三个函数,就可以计算brdf的specular部分了,
// specular
float aspect = sqrt(1-anisotropic*.9);
float ax = max(.001, sqr(roughness)/aspect);
float ay = max(.001, sqr(roughness)*aspect);
float Ds = GTR2_aniso(NdotH, dot(H, X), dot(H, Y), ax, ay);
float FH = SchlickFresnel(LdotH);
vec3 Fs = mix(Cspec0, vec3(1), FH);
float Gs;
Gs = smithG_GGX_aniso(NdotL, dot(L, X), dot(L, Y), ax, ay);
Gs *= smithG_GGX_aniso(NdotV, dot(V, X), dot(V, Y), ax, ay);
首先通过anisotropic和roughness计算出不同方向的粗糙度ax,ay,然后使用它们计算其他3个函数。
Ds使用GTR2_aniso即GTR法线分布函数的各项异性版本计算。
Fs使用SchlickFresnel和mix插值计算。
Gs分为两个方向,分布计算光源方向和观察方向的遮蔽效果,都使用使用smithG_GGX_aniso函数。
sheen
光泽通常用来模拟天鹅绒、布料这类材质外表的光泽效果,它们通常拥有较大的掠射效应,材质边缘呈现出更强的光线反射效果。
其计算方式很简单:
vec3 Fsheen = FH * sheen * Csheen;
FH是SchlickFresnel项,即,然后乘以光泽强度和光泽颜色。
clearcoat
clearcoat可以视为物体材质上的第二个高光反射,具体的计算方法与specular相似,
// clearcoat (ior = 1.5 -> F0 = 0.04)
float Dr = GTR1(NdotH, mix(.1,.001,clearcoatGloss));
float Fr = mix(.04, 1.0, FH);
float Gr = smithG_GGX(NdotL, .25) * smithG_GGX(NdotV, .25);
clearcoat同样计算D、F、G三项,但是其中的有的参数被固定了。
计算Dr时使用各项同性的GTR1分布,但是使用clearcoatGloss代替了roughess,具体来说,
roughess = 1 - clearcoatGloss,并且这里保证其最小值不会小于0.001。
计算Fr时,F0被固定为0.04,对应常用的IOR = 1.5。
计算Gr时,使用各项同性的smithG_GGX,并且其粗糙度被固定为0.25。
最终BRDF计算
将上面所有的项组合为最终结果,
return ((1/PI) * mix(Fd, ss, subsurface)*Cdlin + Fsheen)
* (1-metallic)
+ Gs*Fs*Ds + .25*clearcoat*Gr*Fr*Dr;
}
在这里可以看到,漫反射项Fd和次表面散射项ss通过subsurface参数混合,这意味着它们的能量总和是固定的,最终的结果需要乘以Cdlin / PI(漫反射率)。
Fsheen作为漫反射的一部分加入漫反射项的计算结果。
漫反射项会乘以1 - metallic,这表示当金属度为1时,漫反射会全部消失,对应金属材质没有漫反射的物理特性。
后面的specular和clearcoat直接加上即可,对于clearcoat,这里加上了系数clearcoat来控制其效果强度,前面的0.25应该是固定的美术调节参数,防止clearcoat效果过强,因为clearcoat这一项是没有考虑能量守恒的。
disney BSDF
在disney BRDF后,disney又针对其做了扩充,提出了disney BSDF,相比与原来的BRDF,BSDF增加了透射部分,并且对于次表面散射部分有了更加完善的实现。
上面提到过,BRDF的反射模型主要分为两部分,金属与非金属,
而BSDF引入了新的透射BSDF,
原本的dielectric BRDF需要和specular BSDF混合之后,才和metallic BRDF混合成最终BSDFF。
specular BSDF
作为新增项,specular BSDF引入了两个新参数:
- IOR:折射率,控制光线反射的能量比例,在BRDF中它由specular替代,现在需要计算透射部分,又将其使用回来。
- specTrans(Transmission):控制透射光线能量比例,即透射效果强弱。
在BSDF中,严格来说光线的能量被分为3部分,
反射的能量F由菲涅尔定律计算出。
透射的能量为反射剩余的能量乘以specTrans系数。
最终剩下散射的能量,包括次表面散射部分和漫反射部分。
按照这个思路,specular BSDF将与diffuse + subsurface部分根据specTrans参数进行融合。
关于specular BSDF的计算方法,在粗糙的绝缘体中有推导,同样使用了微平面的理论。
specular BSDF只会在离线渲染中使用,第一,其计算方法太复杂,第二,只有在光线追踪算法中,才可以模拟光线透过物体时的渲染效果,在实时渲染中通常只使用alpha来实现半透明效果,要想实现更丰富的透射效果(如光线折射,磨砂玻璃),需要使用其他的技术手段模拟。