(翻译)Physically Based Rendering i

2023-06-08  本文已影响0人  Dragon_boy

不包括前言、附录和待定章节

标记

符号 定义
v 观察方向
l 入射光方向
n 表面法线
h lv之间的半矢量
f BRDF
f_d BRDF的漫反射组件
f_r BRDF的高光组件
\alpha 粗糙度,重映射自输入的perceptualRoughness
\sigma 漫反射率
\Omega 球形域
f_0 法线入射的反射率
f_{90} 平行入射的反射率
\chi^+(a) 赫维赛德函数(如果a>0则为1否则为0
n_{ior} 某一界面的折射系数(IOR)
\big<n\cdot l\big> 点积,范围裁切至[0..1]
\big<a\big> 将数值范围裁切至[0..1]

材质系统

这一章节会介绍几个用于简要描述不同材质表面特性的材质模型,例如各向异性或清漆,实际讲述过程中会把部分模型精炼为单个,例如,标准模型,清漆模型和各向异性模型可以组合起来,形成一个更加通用的模型。关于所实现的材质模型,详情请见材质文档。

标准模型

材质模型在数学上可以用BSDF来描述,BSDF即双向散射分布函数,它由两种函数构成,BRDF和BTDF,即双向反射分布函数和双向透射分布函数。

毕竟我们只会去试着为常见的表面建模,因此对于我们的标准材质模型,只关注BRDF而忽略BTDF,或者尽量去近似描述。总而言之,我们的标准材质模型只能够较为准确地模拟反射,各项同性,非电解质或是金属表面。

BRDF分为两个组件:

· 漫反射f_d

· 高光f_r

表面,表面法线,入射光和上述组件之间的关系如下图:

上述过程可以描述为:

f(v,l)=f_d(v,l)+f_r(v,l) \tag{1}

这一公式描述了单一方向的入射光遭遇表面反射后的过程,而完整的渲染公式需要将入射光方向沿半球积分。

一般的表面通常不是平坦的,所以我们需要一个模型来表示这一点。

微表面BRDF应运而生,它在微观层面表明了表面并不光滑,由无数随机排列的单元组成,称之为微表面,见下图:

只有那些法线方向在入射光和观察方向之间的微表面可以反射可见光,见下图:

然而啊,并不是所有符合条件的微表面可以反射光,BRDF将这些被抛弃的微表面看作是被遮挡和在阴影中,见下图:

微表面BRDF受粗糙度这一因素影响,它描述了表面在微观层面的光滑程度,表面越光滑,反射光方向更紧密,也就更明显,表面越粗糙,则反射光方向更发散,看起来也就更模糊,见下图:

用户输入的粗糙度系数被称为感知粗糙度(perceptualRoughness),roughness这一变量映射自此。

微表面模型描述为(x表示高光或者漫反射组件):

f_x(v,l)=\frac{1}{|n\cdot v||n\cdot l|}\int_{\Omega}D(m,\alpha)G(v,l,m)f_m(v,l,m)(v\cdot m)(l\cdot m)dm \tag{2}

D描述了微表面的分布,G描述了微表面的可见性。由于上述公式对高光和漫反射都成立,那么不同之处就在于f_m

需要注意的是,这一公式是用于在半球上积分的:

上述图表表明了,在宏观层面,表面可以视为平坦,我们可以假设单一方向的光入射到表面上的某一点,用于简化公式。

而在微观层面,表面并不平坦,我们就不能假设光线是单一的了(我们可以假设是一堆平行入射光)。由于微表面会向不同方向散射入射的一堆平行光,我们就必须在半球内积分。

显而易见,对每个着色单元都去计算完整的半球积分是不现实的,因此很需要对积分进行近似计算。

非电解质和金属

为了更好的理解上述的公式和概念,我们必须得理解金属和非金属表面之间的区别。

当入射光碰撞到表面,光会被反射为两个组件,漫反射和高光反射,这一概念如下图:

这一模型简要地描述了光与面反应的过程,实际上,部分入射光会穿过表面,在内部散射,然后作为漫反射离开表面,如下图:

金属和非电解质之间的区别就在于此,纯金属材质是不存在次表面散射的,这也就意味着不存在漫反射组件(之后我们可以看到这对高光组件的初始颜色有影响)。散射只会发生在非电解质中,也就是同时拥有高光和漫反射组件。

为了恰当地描述BRDF,我们必须把非电解质和金属区分开,见下图:

能量守恒

能量守恒是BRDF重要的一部分。能量守恒的BRDF描述了一个状态,即高光和漫反射能量的总和小于入射光能量的总和。如果没有能量守恒的BRDF,艺术家必须手动确保反射光的量不会超过入射光。

高光BRDF

对于高光部分,f_m是一个可以用菲涅尔原则建模的镜像BRDF,如下,F为Cook-Torrance近似:

f_r(v,l)=\frac{D(h,\alpha)G(v,l,\alpha)F(v,h,f_0)}{4(n\cdot v)(n\cdot l)} \tag{3}

实时渲染中,对于DGF我们必须使用近似结果。

法线分布函数(高光D)

实时渲染中,通常使用GGX分布。

D_{GGX}(h,\alpha)=\frac{\alpha^2}{\pi((n\cdot h)^2(\alpha^2-1)+1)^2} \tag{4}

GLSL实现:

float D_GGX(float NoH, float roughness){
    float a = NoH * roughness;
    float k = roughness / (1.0 - NoH * NoH + a * a);
    return k * k * (1.0 / PI);
}

我们可以使用半精度浮点来优化,不过我们需要改变一下公式,因为计算用半精度浮点计算1-(n\cdot h)^2时会有两个问题,首先,可能会遇到浮点截断的问题,因为(n\cdot h)^2的结果接近1,其次,n\cdot h1附近的精度不够。

解决方案来自于拉格朗日等式:

|a\times b|^2=|a|^2|b|^2-(a\cdot b)^2 \tag{5}

由于nh都是单位矢量,|n\times h|^2=1-(n\cdot h)^2,这也就让我们可以用简单的叉积来计算半精度1-(n\cdot h)^2,下面是优化后的实现:

#define MEDIUMP_FLT_MAX    65504.0
#define saturateMediump(x) min(x, MEDIUMP_FLT_MAX)

float D_GGX(float roughness, float NoH, const vec3 n, const vec3 h) {
    vec3 NxH = cross(n, h);
    float a = NoH * roughness;
    float k = roughness / (dot(NxH, NxH) + a * a);
    float d = k * k * (1.0 / PI);
    return saturateMediump(d);
}

几何阴影(高光G)

Smith公式如下:

G(v,l,\alpha)=G_1(l,\alpha)G_1(v,\alpha) \tag{6}

G_1可以使用不同的模型,通常被设置为GGX公式:

G_1(v,\alpha) = G_{GGX}(v,\alpha)=\frac{2(n\cdot v)}{n\cdot v+\sqrt{\alpha^2+(1-\alpha^2)(n\cdot v)^2}} \tag{7}

完整的Smith-GGX公式因此为:

G(v,l,\alpha)=\frac{2(n\cdot l)}{n\cdot l + \sqrt{\alpha ^ 2 + (1-\alpha ^2)(n\cdot l)^2}}\frac{2(n\cdot v)}{n\cdot v +\sqrt{\alpha ^2+(1-\alpha^2)(n\cdot v)^2}} \tag{8}

可以看到,分子2(n\cdot l)2(n\cdot v)类似,因此可以提出一个名为可见性的函数V来简化原有的函数f_r

f_r(v,l)=D(h,\alpha)V(v,l,\alpha)F(v,h,f_0) \tag{9}

其中:

V(v,l,\alpha)=\frac{G(v,l,\alpha)}{4(n\cdot v)(n\cdot l)}=V_1(l,\alpha)V_1(v,\alpha) \tag{10}

同时:

V_1(v,\alpha)=\frac{1}{n\cdot v + \sqrt{\alpha^2+(1-\alpha^2)(n\cdot v)^2}} \tag{11}

将微表面的高度的影响纳入其中可以得到更准确的结果,高度修正Smith函数如下:

G(v,l,h,\alpha) = \frac{\chi^+(v\cdot h)\chi^+(l\cdot h)}{1+\Lambda(v)+\Lambda(l)} \tag{12}
\Lambda(m)=\frac{-1+\sqrt{1+\alpha^2tan^2(\theta_m)}}{2}=\frac{-1+\sqrt{1+\alpha^2\frac{(1-cos^2(\theta_m))}{cos^2(\theta_m)}}}{2} \tag{13}

n\cdot v代替cos(\theta_m),可得:

\Lambda(v)=\frac{1}{2}\Big(\frac{\sqrt{\alpha^2+(1-\alpha^2)(n\cdot v)^2}}{n\cdot v}-1\Big) \tag{14}

以上可得可见性函数:

V(v,l,\alpha)=\frac{0.5}{n\cdot l\sqrt{(n\cdot v)^2(1-\alpha^2) + \alpha^2}+n\cdot v\sqrt{(n\cdot l)^2(1-\alpha^2)+\alpha^2}} \tag{15}

GLSL实现如下:

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);
}

计算开根开销较大,我们可以用近似结果:

V(v,l,\alpha)=\frac{0.5}{n\cdot l(n\cdot v(1-\alpha)+\alpha)+n\cdot v(n\cdot l(1-\alpha)+\alpha)} \tag{16}

实现如下:

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);
}

这一公式也可表示为插值形式:

V(v,l,\alpha)=\frac{0.5}{lerp(2(n\cdot l)(n\cdot v),n\cdot l +n\cdot v, \alpha)} \tag{17}

菲涅尔(高光F)

菲涅尔效果极为重要,这一效果构建了一个基于事实的模型,即观察者能看到的反射光的量取决于观察角度,大水体可以很好地解释这一现象,见下图:

当垂直看向水面时,我们可以直直透过水面看到水底,然而,当观察非常远的水面时,水面的高光反射会非常明显。

光反射的程度并不仅仅取决于观察角度,也取决于材质的折射系数IOR,沿法线方向观察时,光反射的程度记录为f_0,与折射率有关,沿接近表面的方向观察时,光反射的程度记录为f_{90},越光滑越高。

更术语化地解释就是,菲涅尔定义了光在两种不同介质间是如何反射和折射的,或着说是反射和透射的能量比例。Schlick描述了一种开销较小的菲涅尔近似计算:

F_{Schlick}(v,h,f_0,f_{90})=f_0+(f_{90}-f_0)(1-v\cdot h)^5 \tag{18}

常量f_0表示在法向入射时的高光反射率,非电解质无色,金属有色,它的值取决于接触面的折射系数,GLSL实现如下:

vec3 F_Schlick(float u, vec3 f0, float f90) {
    return f0 + (vec3(f90) - f0) * pow(1.0 - u, 5.0);
}

这一函数可以看作是法向入射高光反射率和平行入射高光发射率之间进行插值。观察现实生活中的材质后,可以得出结论,非电解质和金属材质在极端入射余角处都存在无色的高光反射率,即菲涅尔反射率在90°时为1。

f_{90}设置为1,可以进一步简化实现:

vec3 F_Schlick(float u, vec3 f0) {
    float f = pow(1.0 - u, 5.0);
    return f + f0 * (1.0 - f);
}

漫反射BRDF

在漫反射中,f_m是一个兰伯特函数,那么相应的BRDF为:

f_d(v,l)=\frac{\sigma}{\pi}\frac{1}{|n\cdot v||n\cdot l|}\int_\Omega D(m,\alpha)G(v,l,m)(v\cdot m)(l\cdot m)dm \tag{19}

这里的实现会简化为最简单的兰伯特BRDF,即全局的漫反射:

f_d(v,l)=\frac{\sigma}{\pi} \tag{20}

实际操作中,漫反射率\sigma会在之后乘上,实现如下:

float Fd_Lambert() {
    return 1.0 / PI;
}

vec3 Fd = diffuseColor * Fd_Lambert();

兰伯特BRDF很明显非常有效率,结果非常接近复杂的模型。

然而,漫反射部分理论上会和高光部分相关在一起,同时考虑表面粗糙度的影响。Disney漫反射BRDF和Oren-Nayar模型都考虑了粗糙度,在极端入射余角处还加入了一些回归反射。基于对实时渲染的考虑,我们不太需要这种细枝末节的效果提升,而且实现还比较复杂,就不予考虑了。

不过嘛,还是要介绍一下,Disney漫反射BRDF如下:

f_d(v,l)=\frac{\sigma}{\pi}F_{Schlick}(n,l,1,f_{90})F_{Schlick}(n,v,1,f_{90})\tag{21}

其中:

f_{90}=0.5+2\cdot \alpha cos^2(\theta_d)\tag{22}

实现如下:

float F_Schlick(float u, float f0, float f90) {
    return f0 + (f90 - f0) * pow(1.0 - u, 5.0);
}

float Fd_Burley(float NoV, float NoL, float LoH, float roughness) {
    float f90 = 0.5 + 2.0 * roughness * LoH * LoH;
    float lightScatter = F_Schlick(NoL, 1.0, f90);
    float viewScatter = F_Schlick(NoV, 1.0, f90);
    return lightScatter * viewScatter * (1.0 / PI);
}

下图展示了简易兰伯特漫反射BRDF和高质量Disney漫反射BRDF之间的区别:

两者非常相似,但是Disney漫反射展示了一些极端入射余角下的回归反射效果,注意看左侧边缘。

我们可以让使用者根据设备性能在简易或者高质量模型间选择。

标准模型总结

高光部分:Cook-Torrance高光微观模型,包含GGX法线分布函数,Smith-GGX高度修正可见性函数,Schlick菲涅尔函数。

漫反射部分:兰伯特漫反射模型。

完整的实现如下:

float D_GGX(float NoH, float a) {
    float a2 = a * a;
    float f = (NoH * a2 - NoH) * NoH + 1.0;
    return a2 / (PI * f * f);
}

vec3 F_Schlick(float u, vec3 f0) {
    return f0 + (vec3(1.0) - f0) * pow(1.0 - u, 5.0);
}

float V_SmithGGXCorrelated(float NoV, float NoL, 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);
}

float Fd_Lambert() {
    return 1.0 / PI;
}

void BRDF(...) {
    vec3 h = normalize(v + l);

    float NoV = abs(dot(n, v)) + 1e-5;
    float NoL = clamp(dot(n, l), 0.0, 1.0);
    float NoH = clamp(dot(n, h), 0.0, 1.0);
    float LoH = clamp(dot(l, h), 0.0, 1.0);

    // perceptually linear roughness to roughness (see parameterization)
    float roughness = perceptualRoughness * perceptualRoughness;

    float D = D_GGX(NoH, a);
    vec3  F = F_Schlick(LoH, f0);
    float V = V_SmithGGXCorrelated(NoV, NoL, roughness);

    // specular BRDF
    vec3 Fr = (D * V) * F;

    // diffuse BRDF
    vec3 Fd = diffuseColor * Fd_Lambert();

    // apply lighting...
}

优化BRDF

之前提到过,能量守恒很关键,而我们之前提到过的BRDF也因此面临两个问题。

漫反射中的能量增长

兰伯特漫反射BRDF并没有考虑反射光,因此无法参与漫散射的计算。

高光反射中的能量损失

我们之前提到过的Cook-Torrance BRDF对微观层面的光反射进行了建模,但也只考虑了单次反射,这样子去近似会在高粗糙度的位置损失能量,表面并不是能量守恒的。下图展示了为何会发生能量损失的问题:

在单次反射模型中,一束光线会击中表面,反射到另一个微表面上,然后会因为遮挡和阴影被剔除,如果我们考虑多次反射,同样的光线可能会反射离开表面,被观察到。

基于这一简单的解释,我们可以大胆推断,表面越粗糙,因为未考虑多次散射所造成的计算失误的可能性越大,能量越有可能损失,这会让粗糙的材质显得更暗,金属表面影响不大,因为都是高光反射。

单次散射效果如下:

考虑多次散射后:

我们可以将全局光照环境设置为纯白色,以更好的确认上述的现象。当能量守恒成立时,全反射金属表面(f_0=1)应该会和环境融为一体,而且无论粗糙度是多少,都理应如此。

下图展示了能量损失时,随着粗糙度提升,变暗的效果会越来越明显:

相反,能量守恒时,不会有变化:

Kulla和Conty的方法是增加一个额外的BRDF项作为能量补偿:

f_{ms}(l,v)=\frac{(1-E(l))(1-E(v))F^2_{avg}E_{avg}}{\pi(1-E_{avg})(1-F_{avg}(1-E_{avg}))}\tag{23}

其中E是高光BRDFf_r的定向反射率,f_0设为1:

E(l)=\int_{\Omega}f(l,v)(n\cdot v)dv\tag{24}

E_{avg}E的加权余弦平均:

E_{avg}=2\int^1_0E(\mu)\mu d\mu\tag{25}

同理,F_{avg}是菲涅尔项的加权余弦平均:

F_{avg}=2\int^1_0F(\mu)\mu d\mu \tag{26}

EE_{avg}都可以提前计算并存储在查找表中,其中F_{avg}在使用Schlick近似时极致简化:

F_{avg}=\frac{1+20f_0}{21}\tag{27}

这一新的BRDF项与原有的单次散射项结合在一起:

f_r(l,v)=f_{ss}(l,v)+f_{ms}(l,v)\tag{28}

据研究发现,公式27可以简化为f_0,同时可以添加一个可伸缩GGX高光项来进行能量补偿:

f_{ms}(l,v)=f_0\frac{1-E(l)}{E(l)}f_{ss}(l,v)\tag{29}

关键点在于E(l)不只可以被预计算,也可以参与到基于图像的光照预积分计算中。这一多次散射的能量补偿公式变为:

f_r(l,v)=f_{ss}(l,v)+f_0\Big(\frac{1}{r}-1\Big)f_{ss}(l,v)\tag{30}

其中r定义为:

r = \int_\Omega D(l,v)V(l,v)\big<n\cdot l\big>dl \tag{31}

我们可以把r的计算结果存储在DFG查找表中,以最小的开销实现高光能量补偿,下面是公式30的直接实现:

vec3 energyCompensation = 1.0 + f0 * (1.0 / dfg.y - 1.0);
// Scale the specular lobe to account for multiscattering
Fr *= pixel.energyCompensation;

参数化

Disney的材质模型挺不错,只是参数太多了,在实时渲染中实现太不现实,而且,咱希望咱们的标准材质模型能够通俗易懂,傻子都会调参数。

标准参数

下面的图表列举了我们所需的参数:

参数 定义
BaseColor 非金属表面的漫反射颜色,金属表面的高光颜色
Metallic 判断表面是非金属(0.0)还是金属(1.0),通常用二进制表示(0或是1)
Roughness 描述表面的粗糙程度,光滑(0.0)还是粗糙(1.0)
Reflectance 非电解质表面在法线入射方向处的菲涅尔反射率
Emissive 额外的漫反射颜色,用于模拟发光表面,这一参数通常用于带发光pass的HDR管线
Ambient occlusion 定义了某一表面点可接受的环境光的量,这是一个位于0.0和1.0之间的逐像素投影的因数,这一参数会在之后的光照环节详细讨论

下图展示了金属度,粗糙度,反射率参数对表面的影响。从上到下依次是金属度,非电解质的粗糙度,金属的粗糙度,反射率:

类型和范围

不同参数的类型与范围如下:

参数 类型和范围
BaseColor 线性RGB[0..1]
Metallic 标量[0..1]
Roughness 标量[0..1]
Reflectance 标量[0..1]
Emissive 线性RGB[0..1]+曝光补偿
Ambient occlusion 标量[0..1]

注意上述参数的类型和范围都是shader中适用的,实际调节参数时可能会使用其它的类型和范围,以便使用。

例如,基础颜色可以用sRGB空间的值表示,在传到shader前先转换到线性空间。对美术而言,可以用0到255的灰度值来表示金属度,粗糙度和菲涅尔反射率等参数,更为直观。

此外,发光参数可以被表示为色温和强度,用于模拟黑体发光。

重映射

为了让美术更加简单直观的使用标准模型,我们必须重映射基础颜色,粗糙度和菲涅尔反射率等系数。

基础颜色重映射

材质的基础颜色受一种名叫”金属质感“的概念的影响。非电解质拥有无色的高光反射,但基础颜色会作为漫反射颜色存在。金属会将基础颜色作为高光颜色,同时没有漫反射组件。

因此光照等式里会使用漫反射颜色和f_0代替基础颜色,漫反射颜色可以很简单地由基础颜色计算得到:

vec3 diffuseColor = (1.0 - metallic) * baseColor.rgb;

菲涅尔反射率重映射

非电解质

菲涅尔效果取决于f_0,使用如下重映射:

f_0=0.16\cdot reflectance^2\tag{32}

我们需要试着将f_0映射到一个可以表示常见非电解质(4\%反射率)和宝石(8\%6\%)的菲涅尔范围,重映射函数需要将输入的0.5反射率映射到4\%菲涅尔反射率,关系如下图:

如果已知折射率,菲涅尔反射率可以用以下公式计算:

f_0(n_{ior})=\frac{(n_{ior}-1)^2}{(n_{ior}+1)^2} \tag{33}

如果已知反射率,可以计算相应的折射率:

n_{ior}=\frac{2}{1-\sqrt{f_0}}-1 \tag{34}

下图描述了不同材质的菲涅尔反射率(现实生活中,没有低于2%的材质)

材质 反射率 折射率 线性值
2% 1.33 0.35
布料 4%-5.6% 1.5-1.62 05-0.59
通常的液体 2%-4% 1.33-1.5 0.35-0.5
通常的宝石 5%-16% 1.58-2.33 0.56-1.0
塑料,玻璃 4%-5% 1.5-1.58 0.5-0.56
其它的非电解质材质 2%-5% 1.33-1.58 0.35-0.56
眼球 2.5% 1.38 0.39
皮肤 2.5% 1.38 0.39
头发 4.6% 1.55 0.54
牙齿 5.8% 1.63 0.6
默认值 4% 1.5 0.5

下图列举了一些金属的f_0值,位于sRGB空间,必须被用作基础颜色。

金属 f_0 sRGB 16进制 颜色
0.97,0.96,0.91 #f7f4e8 \color{#f7f4e8}{█████}
0.91,0.92,0.92 #e8eaea \color{#e8eaea}{█████}
0.76,0.73,0.69 #c1baaf \color{#c1baaf}{█████}
0.77,0.78,0.78 #c4c6c6 \color{#c4c6c6}{█████}
0.83,0.81,0.78 #d3cec6 \color{#d3cec6}{█████}
1.00,0.85,0.57 #ffd891 \color{ #ffd891}{█████}
黄铜 0.98,0.90,0.59 #f9e596 \color{#f9e596}{█████}
紫铜 0.97,0.74,0.62 #f7bc93 \color{#f7bc93}{█████}

所有的材质在极端入射余角处的菲涅尔反射率都为100%,因此我们在计算高光BRDFf_r时将f_{90}设置为1.0:

f_{90}=1.0 \tag{35}

8soCfP12Ph19mi914zQaZz2KsGGtcANVhVVfKAnmVRqM

金属

金属的高光反射是有颜色的:

f_0=baseColor \cdot metallic \tag{36}

下面实现了非金属和金属材质的f_0计算,金属刚好可以接收基础颜色:

vec3 f0 = 0.16 * reflectance * reflectance * (1.0 - metallic) + baseColor * metallic;

粗糙度重映射和截断

用户们设置的粗糙度被称为感知粗糙度perceptualRoughness,使用下列公式映射到感知线性范围:

\alpha=perceptualRoughness^2\tag{37}

下图展示了银金属表面随粗糙度增长的样子,下方粗糙度未修改,上方重映射:

经过上述的比较,可以明显看到重映射的粗糙度区分度更大,美术更容易理解。没有重映射的话,光滑金属表面的变化会非常小。

在尝试了多种不同的重映射方法后,我们得出结论,对于实时渲染,平方重映射兼顾效果和开销,不错。

此外,需要注意,粗糙度这一参数会在不同的情境下实时参与计算,因此浮点精度会成为一个大问题。例如,mediump浮点在移动GPU上为16位半精度浮点,这在光照等式中计算类似\frac{1}{perceptualRoughness^4}这一特别小的值时会出现问题,半精度浮点最小值为2^{-14}6.1\times 10^{-5},为了避免在不能处理非常规情况的设备上出现除以0的问题,\frac{1}{roughness^4}的结果一定不能低于6.1\times 10^{-5},为此,我们必须将粗糙度截断在0.089,它的值刚好是6.274\times 10^{-5},卡得准准的。

粗糙度当然也不能设为0,这种过于明显的问题当然得避免。

由于我们也希望高光有一个最小的尺寸(接近于0的粗糙度会得到几乎看不见的高光),我们应该将shader中的粗糙度截断在一个安全的范围,这一阶段也会对地粗糙度下的高光锯齿进行一定的修正。

寒霜引擎会将可见光的粗糙度截断在0.045以减少高光锯齿,这是在使用单精度浮点时所进行的考虑(32位)。

混合和层级

咱们的模型可以简单地在不同参数间插值以达到混合不同材质的目的,比如用遮罩来混合材质。

例如,游戏教团1886里的一个例子:

材质间的混合和层级叠加就是材质模型的不同参数间的插值,下图展示了闪亮亮铬合金和粗糙塑料红球之间的插值结果,中间的样子勉强看起来挺逼真的:

自定义材质

只要理解了四个最重要参数的本质:基础颜色,金属度,粗糙度和菲涅尔反射率,设计基于物理的材质简直就是手到擒来。

我们提供了简要的引用图集来阐明上述的四种参数:

下面简单概括下如何使用我们的材质模型:

所有的材质

Base color不应该包含光照信息,除了微观遮蔽。

Metallic一般是个二进制值,纯金属为1,纯非金属为0,我们应该使用接近0或者1的值,中间值应该用于表面类型的转换,如铁到铁锈之间的变化。

非金属材质

Base color表示反射颜色,是个范围在50-240或者30-240之间的sRGB值。

Metallic应该为0或者无限接近0。

Reflectance,如果找不到合适的值,应该设置为127的sRGB值,或是线性0.5,4%反射率,不要低于sRGB 90,或是线性0.35,2%反射率。

金属材质

Base color表示高光和菲涅尔反射颜色,使用范围在67%-100%照度之间的值,或是170-255的sRGB值。氧化或是脏脏的金属应该考虑非金属部分,使用更低的照度。

Metallic应该为1或是无限接近1。

Reflectance应该被忽略(计算自基础颜色)。

清漆模型

上述标准模型在描述单层各向同性材质时挺合适,不过多层材质才是真正的主流,尤其是在标准层上叠上一层薄薄的透射层的材质,现实生活中这样的材质有很多啊,比如车漆,苏打罐,油漆板,亚克力板等。如下图,左为标准模型,右为清漆模型。

清漆层可以通过添加额外的高光项来作为标准材质模型的扩展。为了简化实现和参数,清漆层通常是各向同性的,是非金属的。基础层可以是任何一种标准模型可以描述的材质。

由于入射光会穿过清漆层,我们必须考虑能量损失,但暂时不会考虑内部的反射和折射。

清漆高光BRDF

清漆层可以用同样的Cook-Torrance微表面BRDF来建模,由于清漆层通常是各向异性且非电解的,而且粗糙度一般比较低,我们可以牺牲下效果选择开销更小的DFG方法。

据研究,标准模型里的菲涅尔和NDF的计算开销较小,我们针对Smith-GGX可见性函数进行简化:

V(l,h)=\frac{1}{4(l\cdot h)^2} \tag{38}

这一遮蔽阴影函数并不基于物理,但对于实时渲染来说也够了,结果也算可信。

总结下,我们的清漆BRDF使用GGX法线分布函数,简化过的可见性函数以及Schlick菲涅尔函数。实现如下:

float V_Kelemen(float LoH) {
    return 0.25 / (LoH * LoH);
}

菲涅尔注意事项

高光BRDF的菲涅尔效果需要f_0,这一参数可以由接触面的折射率计算得到。我们假设清漆层由聚氨酯构成,这是最常见的一种。空气与聚氨酯的接触面的折射率为1.5,由此可得f_0:

f_0(1.5)=\frac{(1.5-1)^2}{(1.5+1)^2}=0.04\tag{39}

这刚好是常见非金属材质的菲涅尔折射率,4%。

积分

因为必须考虑清漆层造成的能量损失,我们可以根据公式1重组BRDF:

f(v,l)=f_d(v,l)(1-F_c)+f_r(v,l)(1-F_c)+f_c(v,l)\tag{40}

其中F_c是清漆BRDF的菲涅尔项,f_c是清漆BRDF。

清漆参数

清漆材质模型包含标准材质模型里的所有参数,额外加了两个,如下:

参数 定义
ClearCoat 清漆层的强度,0-1之间的标量
ClearCoatRoughness 清漆层可感知到的光滑度或者粗糙度,0-1之间的标量

清漆粗糙度参数使用了和标准材质模型类似的方法去重映射和截断。

下两图展示了清漆参数是如何影响表面效果的。上面是清漆强度,0-1变化,下面是清漆粗糙度,0-1变化:

下面是清漆模型在标准模型重映射,参数化和积分后的实现:

void BRDF(...) {
    // compute Fd and Fr from standard model

    // remapping and linearization of clear coat roughness
    clearCoatPerceptualRoughness = clamp(clearCoatPerceptualRoughness, 0.089, 1.0);
    clearCoatRoughness = clearCoatPerceptualRoughness * clearCoatPerceptualRoughness;

    // clear coat BRDF
    float  Dc = D_GGX(clearCoatRoughness, NoH);
    float  Vc = V_Kelemen(clearCoatRoughness, LoH);
    float  Fc = F_Schlick(0.04, LoH) * clearCoat; // clear coat strength
    float Frc = (Dc * Vc) * Fc;

    // account for energy loss in the base layer
    return color * ((Fd + Fr * (1.0 - Fc)) * (1.0 - Fc) + Frc);
}

基础层修正

有了清漆层后,因为它的表面变成了空气与对应材质的接触面,我们需要去重新计算f_0。基础层的f_0就相应地基于清漆材质接触面去进行重新计算。

我们可以通过f_0计算折射率,然后根据清漆层的折射率(1.5)计算新的折射率。

首先,计算基础层的折射率:

IOR_{base}=\frac{1+\sqrt{f_0}}{1-\sqrt{f_0}}

然后据此计算新的f_0

f_{0base}=\Big(\frac{IOR_{base}-1.5}{IOR_{base}+1.5}\Big)^2

由于清漆层的折射率固定,我们可以简化步骤:

f_{0base}=\frac{(1-5\sqrt{f_0})^2}{(5-\sqrt{f_0})^2}

本来还需要基于清漆层的折射率去修改基础层的粗糙度,不过暂且延后吧。

各向异性模型

之前讲过的标准模型只能用于描述各向同性表面,然而许多材质是各向异性的,例如拉丝金属,罐子。下面是各向同性和各向异性材质的一个对比:

各向异性高光BRDF

各项同性高光BRDF可以修改下来处理各向异性材质。比如使用各向异性的GGX NDF:

D_{aniso}(h,\alpha)=\frac{1}{\pi\alpha_t\alpha_b}\frac{1}{((\frac{t\cdot h}{\alpha_t})^2+(\frac{b\cdot h}{\alpha_t})^2+(n\cdot h)^2)^2}\tag{41}

这一NDF使用了两个额外的粗糙度量,\alpha_b即沿副切线方向的粗糙度,\alpha_t即沿切线方向的粗糙度。下面是一种使用anisotropy参数的计算这两种的粗糙度量的方法:

\alpha_t=\alpha
\alpha_b=lerp(0,\alpha,1-anisotropy)

还有一种开销更大,但更精确的方法:

\alpha_t =\frac{\alpha}{\sqrt{1-0.9\times anisotropy}}
\alpha_b=\alpha\sqrt{1-0.9\times anisotropy}

我们选择使用下列方法,可以得到更为明显的高光:

\alpha_t=\alpha\times (1+anisotropy)
\alpha_b=\alpha\times(1-anisotropy)

注意这一NDF需要切线和副切线方向,这些变量通常需要在法线映射时使用,因此不成问题。

实现如下:

float at = max(roughness * (1.0 + anisotropy), 0.001);
float ab = max(roughness * (1.0 - anisotropy), 0.001);

float D_GGX_Anisotropic(float NoH, const vec3 h,
        const vec3 t, const vec3 b, float at, float ab) {
    float ToH = dot(t, h);
    float BoH = dot(b, h);
    float a2 = at * ab;
    highp vec3 v = vec3(ab * ToH, at * BoH, a2 * NoH);
    highp float v2 = dot(v, v);
    float w2 = a2 / v2;
    return a2 * w2 * w2 * (1.0 / PI);
}

另外,针对高度修正GGX分布也有对应的各向异性遮罩阴影函数:

G(v,l,h,\alpha)=\frac{\chi^+(v\cdot h)\chi^+(l\cdot h)}{1+\Lambda(v)+\Lambda(l)}\tag{42}
\Lambda(m)=\frac{-1+\sqrt{1+\alpha^2_0tan^2(\theta_m)}}{2}=\frac{-1+\sqrt{1+\alpha^2_0\frac{(1-cos^2(\theta_m))}{cos^2(\theta_m)}}}{2}\tag{43}

其中:

\alpha_0=\sqrt{cos^2(\phi_0)\alpha^2_x+sin^2(\phi_0)\alpha^2_y} \tag{44}

经过推导,我们可以得到:

V_{aniso}(n\cdot l,n\cdot v,\alpha)=\frac{1}{2((n\cdot l)\hat\Lambda_v+(n\cdot v) \hat\Lambda_l)}\tag{45}
\hat\Lambda_v=\sqrt{\alpha^2_t(t\cdot v)^2+\alpha^2_b(b\cdot v)^2+(n\cdot v)^2}
\hat\Lambda_l=\sqrt{\alpha^2_t(t\cdot l)^2+\alpha^2_b(b\cdot l)^2+(n\cdot l)^2}

\hat\Lambda_v对所有的光源来说都一样,因此只需要计算一次,实现如下:

float at = max(roughness * (1.0 + anisotropy), 0.001);
float ab = max(roughness * (1.0 - anisotropy), 0.001);

float V_SmithGGXCorrelated_Anisotropic(float at, float ab, float ToV, float BoV,
        float ToL, float BoL, float NoV, float NoL) {
    float lambdaV = NoL * length(vec3(at * ToV, ab * BoV, NoV));
    float lambdaL = NoV * length(vec3(at * ToL, ab * BoL, NoL));
    float v = 0.5 / (lambdaV + lambdaL);
    return saturateMediump(v);
}

各向异性参数

各向异性模型包含标准材质模型的所有参数,只是额外多了一个:

参数 定义
Anisotropy 各向异性程度,-1..1之间的标量

这一参数不需要重映射。注意,负数意味着各向异性会贴近副切线方向。下图展示了各向异性参数在粗糙金属表面的影响,从左到右0-1:

布料模型

之前描述的材质模型都是用来模拟紧密表面的,在宏观和微观层面皆是如此,而衣服布料通常是由宽松连接的线构成的,会吸收和散射入射光。之前展示过的微表面BRDF用在布料上会让本来有纹理的表面看上去像个镜面。相对于硬表面,布料由一个大范围衰减的柔和高光项描述特性,因为前向或后向散射,光照效果更加模糊,部分布料还会显示出两种渐变的高光颜色,例如丝绒。

下图展示了传统微表面BRDF在牛仔布料上的错误效果,看起来更硬,像是塑料,更像是防水布而不是衣物。

丝绒对于布料材质模型来说是个不错的例子。下图展示了这一类型的布料,因为前向和后向散射,显示出强烈的轮廓光:

这些散射发生的原因在于纤维方向正好是布料表面方向,当入射光来自观察方向反向时,纤维会向前散射这些光,同理,入射光来自观察方向时,纤维会向后散射这些光。

由于纤维的高度可变性,我们需要在理论上构建梳理平面的能力。我们的模型不去表明这一点的话,某一可见的前向高光可能会对来自各个方向的纤维造成影响。

需要注意的是,某些布料还是可以利用标准模型来描述的,例如,皮革,丝绸和缎子。

布料高光BRDF

在布料高光BRDF中,针对丝绒布料,分布函数贡献最大,而阴影遮蔽函数几乎不太需要。分布函数本身是反转高斯分布,加入偏移量后可以模拟前向高光贡献,用来实现模糊的光照,这也被称为丝绒NDF:

D_{velvet}(v,h,\alpha)=c_{norm}(1+4exp(\frac{-cot^2\theta_h}{\alpha^2}))\tag{46}

更通用化的版本为:

D_{velvet}(v,h,\alpha)=\frac{1}{\pi(1+4\alpha^2)}(1+4\frac{exp(\frac{-cot^2\theta_h}{\alpha^2})}{sin^4\theta_h})\tag{47}

对于完整的高光BRDF,我们用一个更光滑的变体来替代传统的分母:

f_r(v,h,\alpha)=\frac{D_{velvet}(v,h,\alpha)}{4(n\cdot l + n\cdot v - (n \cdot l)(n\cdot v))}\tag{48}

丝绒NDF的实现如下,使用了半精度浮点进行优化,利用三角恒等式来避免计算高消耗的余切值,注意我们移除了菲涅尔项:

float D_Ashikhmin(float roughness, float NoH) {
    // Ashikhmin 2007, "Distribution-based BRDFs"
    float a2 = roughness * roughness;
    float cos2h = NoH * NoH;
    float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
    float sin4h = sin2h * sin2h;
    float cot2 = -cos2h / (a2 * sin2h);
    return 1.0 / (PI * (4.0 * a2 + 1.0) * sin4h) * (4.0 * exp(cot2) + sin4h);
}

还有一种不同的NDF(查理光泽),基于幂正弦而不是反转高斯,这一NDF的优点在于:参数更加自然直观,效果更柔和,函数如下:

D(m)=\frac{(2+\frac{1}{\alpha})sin(\theta)^{\frac{1}{\alpha}}}{2\pi}\tag{49}

我们使用了来自等式48的可见性项,实现如下:

float D_Charlie(float roughness, float NoH) {
    // Estevez and Kulla 2017, "Production Friendly Microfacet Sheen BRDF"
    float invAlpha  = 1.0 / roughness;
    float cos2h = NoH * NoH;
    float sin2h = max(1.0 - cos2h, 0.0078125); // 2^(-14/2), so sin2h^2 > 0 in fp16
    return (2.0 + invAlpha) * pow(sin2h, invAlpha * 0.5) / (2.0 * PI);
}

光泽颜色

为了更好的控制布料的外观,同时让用户能够由办法创建双色调高光材质,我们允许直接修改高光反射,该参数就是光泽颜色:

布料漫反射BRDF

我们的布料材质模型仍依赖兰伯特漫反射BRDF,只不过简单修改了下以保证能量守恒,同时提供了可选的次表面散射项。这一额外项并于基于物理,可以用于模拟散射,部分吸收以及在特定类型布料下的发光。

首先,这是不带次表面散射的漫反射项:

f_d(v,h)=\frac{c_{diff}}{\pi}(1-F(v,h))\tag{50}

其中F(v,h)是等式48中描述的菲涅尔项,实际操作中,我们打算忽略漫反射部分中的1-F(v,h)项,它所产生的效果不易察觉,就不浪费性能计算了。

次表面散射使用包裹漫反射光照技术实现:

f_d(v,h)=\frac{c_{diff}}{\pi}(1-F(v,h))\Big<\frac{n\cdot l + w}{(1+w)^2}\Big>\big<c_{subsurface}+n\cdot l\big>\tag{51}

其中w范围在0-1之间,定义了有多少漫反射光会包裹在明暗交接处。为了不引入其它参数,我们令w=0.5。注意,使用包裹漫反射光照后,漫反射项必须乘上n \cdot l。效果如下:

完整的布料BRDF实现如下:

// specular BRDF
float D = distributionCloth(roughness, NoH);
float V = visibilityCloth(NoV, NoL);
vec3  F = sheenColor;
vec3 Fr = (D * V) * F;

// diffuse BRDF
float diffuse = diffuse(roughness, NoV, NoL, LoH);
#if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
// energy conservative wrap diffuse
diffuse *= saturate((dot(n, light.l) + 0.5) / 2.25);
#endif
vec3 Fd = diffuse * pixel.diffuseColor;

#if defined(MATERIAL_HAS_SUBSURFACE_COLOR)
// cheap subsurface scatter
Fd *= saturate(subsurfaceColor + NoL);
vec3 color = Fd + Fr * NoL;
color *= (lightIntensity * lightAttenuation) * lightColor;
#else
vec3 color = Fd + Fr;
color *= (lightIntensity * lightAttenuation * NoL) * lightColor;
#endif

布料参数

布料材质模型包含标准材质模型的所有参数,除了metallic和reflectance,还有两个额外的参数:

参数 定义
SheenColor 高光着色,用于创造双色调高光布料(默认为0.04,用于匹配标准反射率)
SubsurfaceColor 散射和吸收后针对漫反射颜色的着色

为了创建类似丝绒的材质,基础颜色可以设置为黑色或是深色,其相应的颜色信息需要设置在光泽颜色上。为了创建类似于牛仔布或是棉布这样的材质,颜色信息设置在基础颜色上,同时使用默认的光泽颜色,或是将光泽颜色设置为基础颜色的亮度。

上一篇下一篇

猜你喜欢

热点阅读