1-理论知识

接下来我们将接触简单的PBR,首先看看有关PBR的理论知识。

简介

PBR,即基于物理的渲染(Physically Based Rendering),是一个基于物理/现实的渲染技术的集合。PBR的目的是用符合物理规律的方式来模拟光线,这会使得渲染结果比Phong光照模型更加真实。

此外,由于它和实际物理很接近,艺术家可以使用基于物理的参数去调整表面材质,而不是用粗劣修改和调整让材质看起来正确。使用基于PBR的材质会使不论光照条件如何,该材质均正确。

PBR实际上仍是对现实的近似,PBR光照模型需要满足下列三个条件:

  • 基于微表面模型;
  • 满足能量守恒;
  • 使用一个基于物理的BRDF。

这里会学习PBR中的Metallic Roughness工作流,这是由迪士尼提出探讨并被Epic首先应用于实时渲染的PBR方案:

在学习PBR之前,需要有帧缓冲、立方体贴图、Gamma校正、HDR和法线贴图基础。

微表面模型

所有PBR的技术都基于微表面理论。这个理论认为,达到微观尺度之后的任何平面都可以用被称为微表面(Microfacets)的细小镜面来进行描绘。根据表面的粗糙度,这些细小镜面的排列方式可以相当不一致:

如果表面越粗糙,那么微表面的排列方式也就越混乱,会使反射光线更加发散,产生更宽广的镜面反射;反之,在光滑表面上的反射光线几乎朝同一方向,产生范围小并锐利的反射:

在微表面层次上,没有表面是完全光滑的。由于微表面已经微小到无法通过像素区分,假设一个粗糙度Roughness参数,然后用统计学方法来估计微表面的粗糙程度。可以通过平面的粗糙度计算出有多少比率的微表面朝向一些向量\(\mathbf{h}\)。向量\(\mathbf{h}\)就是光线\(\mathbf{l}\)和观察方向\(\mathbf{v}\)半程向量\[ \nonumber \mathbf{h}=\frac{\mathbf{l+v}}{\|\mathbf{l+v}\|} \] 朝向半程向量的微表面越多,镜面反射的锐度和强度也就越高。粗糙度通常定义在[0, 1],可以通过它估计所有微表面的朝向情况:

可以发现粗糙度越高就越像漫反射。

能量守恒

微表面近似模型遵循能量守恒(Energy Conservation):出射光的能量不应该超过入射光的能量(除了自发光材质)。例如上图中,随着镜面反射区域的增大,亮度也减少了。

为了遵循能量守恒,我们需要对漫反射光和镜面反射光作区分。当光线打到表面上时,它就会分成一个 折射部分(refraction) 和一个 反射部分(reflection)。反射部分的光线会被直接反射,不会进入表面,这就是 镜面光 ;折射部分的光线会进入表面且被吸收,这就是 漫反射光

实际上,折射光并不会被表面直接吸收。在物理上,光线是一种没有耗尽就不停向前运动的能量,能量通过碰撞消耗。每一种材质都由无数微小粒子组成,这些粒子都能与折射光线发生碰撞,并让光线损失部分/全部能量:

一般的,并非全部能量都会被吸收,而光线也会沿着一个主要随机方向 散射(scatter),直至能量耗尽或离开内表面。离开内表面的光线会为漫反射颜色做贡献。考虑到这种程度的技术被称为 次表面散射(Subsurface Scattering),它显著提升了诸如皮肤、大理石、蜡质等材质的视觉效果,但代价是性能的下降。这里我们假设折射光会被完全吸收,散射的影响很小,且不会穿出表面以简化计算。

对于 金属表面(Metallic) 的折射与散射,其对光的反应和非金属材质(也被称为 介质(Dielectrics))不同。金属表面遵循相同的折射和反射定律,但所有折射光都会被表面直接吸收,没有散射。也就是说金属表面只有镜面反射光。PBR渲染管线会区别处理这两种材质。

通过上面对反射和折射光的探索,可以发现 反射光和折射光是互斥的。也就是说计算完二者的其中一种,可以根据能量守恒求出另外一种的能量。例如先求镜面反射光,再求漫反射折射光:

// 反射/镜面 部分
float kS = calculateSpecularComponent(...);
// 折射/漫反射 部分
float kD = 1.0 - kS;

这里让它们遵循能量守恒,能量百分比总和不超过1。这是我们在之前的光照计算中从未考虑到的。

反射方程

在这里引入渲染方程,使用它可以解释所有的光照。PBR基于渲染方程的特化版本(渲染方程 = 自发光项 +反射方程),即反射方程。为了正确理解PBR,需要好好理解反射方程: \[ \nonumber L_o(p,\omega_o)=\intop_\Omega f_r(p,\omega_i,\omega_o)L_i(p,\omega_i)n\cdot\omega_id\omega_i \]

辐射度量学简介

要想正确理解反射方程,还需要了解辐射度量学(Radiometry)的内容。辐射度量学是对光照的一套测量系统和单位,它能准确地描述光线的物理性质。接下来看看它的物理量:

  • 辐射通量(Radiant flux):辐射通量\(\Phi\)表示的是一个光源每单位时间发射的功率,以瓦特为单位。

    光是不同波长的能量总和,每个波长都与一种可见颜色相关。辐射通量会计算这个由不同波长构成的函数总面积,这里通过使用RGB而不是波长来简化计算。

  • 立体角(Solid Angle):用\(\omega\)​表示,它描述了单位球体上一块区域对应的球面部分的面积。

  • 辐射强度(Radiant intensity):在单位球面上,光源向每单位立体角发射的辐射通量。

    计算辐射强度的公式如下: \[ \nonumber I=\frac{d\Phi}{d\omega} \]

  • 辐射亮度(Radiance):也称辐射率,描述了一个拥有辐射通量\(\Phi\)的光源在单位面积\(A\),单位立体角\(\omega\)上辐射出的总能量: \[ \nonumber L=\frac{d^2\Phi}{dAd\omega\cos\theta} \]

    辐射亮度受入射光线与平面法线夹角\(\theta\)的余弦值影响,当直接辐射到平面上的程度越低,光线就越弱;当光线完全垂直于平面时强度最高。\(\cos\theta\)可由光线方向向量和平面法向量点积得到:

    float cosTheta = dot(lightDir, N);

    可以将立体角\(\omega\)看成方向向量,将面\(A\)看成着色点\(p\),那么就能在着色器中用辐射亮度来计算单束光线对每个片段的作用了。

  • 辐射照度(Irradiance):描述的是所有方向投射到着色点\(p\)上的光线能量总和。

理解反射方程

观察反射方程: \[ \nonumber \nonumber L_o(p,\omega_o)=\intop_\Omega f_r(p,\omega_i,\omega_o)L_i(p,\omega_i)n\cdot\omega_id\omega_i \] 它计算了着色点\(p\)在半球上的所有辐射亮度的总和\(L_o(p,\omega_o)\),也就是说,\(L_o\)表示从\(\omega_o\)方向上观察,光线投射到点\(p\)反射出来的辐射照度。

其中,一个半球可以描述为以平面法线\(n\)为轴环绕的半个球体:

需要通过积分的方式计算半球领域内所有入射方向\(d\omega_i\)的辐射亮度,由于渲染方程和反射方程没有解析解,要用离散的方法求得数值解。问题就转化为在半球领域\(\Omega\)中按一定步长将反射方程分散求解,然后按照步长大小将所得到的结果平均化。这种方法被称为黎曼和:

int steps = 100;
float sum = 0.0f;
vec3 P = ...;
vec3 Wo = ...;
vec3 N = ...;
float dW = 1.0f / steps;
for (int i = 0; i < steps; ++i)
{
    vec3 Wi = getNextIncomingLightDir(i);
    sum += Fr(P, Wi, Wo) * L(P, Wi) * dot(N, Wi) *dW;
}

通过利用dW来对所有离散部分进行缩放,最后的和就是积分的数值解。利用离散步长得到的是函数总面积的近似值,可以通过增加离散部分的数量来提高黎曼和的准确度。

反射方程概括了在半球领域\(\Omega\)内,碰撞到着色点\(p\)上所有入射方向\(\omega_i\)上的光线的辐射亮度,并受到\(f_r\)的约束,然后返回观察方向上反射光的\(L_o\)。入射光辐射亮度可由光源处获得,也能从环境光贴图中采样获取(IBL)。

BRDF

反射方程中的\(f_r\)就是BRDF,即双向反射分布函数。它接受入射方向\(\omega_i\),出射/观察方向\(\omega_o\),平面法线\(n\)和一个用来表示微表面粗糙程度的参数\(a\)作为函数的输入参数。

BRDF可以近似的求出每束光线对一个给定材质属性的平面上最终反射出来的光线所作出的贡献程度。例如有一个完全光滑的平面,对于所有的入射光线\(\omega_i\)(除了一束以外)而言BRDF函数都会返回0.0 ,只有一束与出射光线\(\omega_o\)拥有相同(被反射)角度的光线会得到1.0这个返回值。

BRDF也要遵循能量守恒定律,也就是说反射光线的总和永远不能超过入射光线的总量。

Cook-Torrance模型

常用的BRDF模型是 Cook-Torrance BRDF模型,有漫反射和镜面反射两个部分: \[ \nonumber f_r=k_df_{lambert}+k_sf_{cook-torrance} \] 其中,\(k_d\)是入射光线中被折射部分的能量所占比率,\(k_s\)是被反射部分的比率。

BRDF左侧描述的是漫反射部分,用\(f_{lambert}\)来表示,被称为 Lambertian 漫反射: \[ \nonumber f_{lambert}=\frac c\pi \] 其中\(c\)是表面颜色,除以\(\pi\)是为了对漫反射光进行标准化,因为前面含有BRDF的积分方程是受\(\pi\)影响的。

BRDF右侧的镜面反射部分如下: \[ \nonumber f_{CookTorrance}=\frac{DFG}{4(\omega_o\cdot n)(\omega_i\cdot n)} \] 包含三个函数,此外分母部分还有一个标准化因子。三个函数如下:

  • 法线分布函数:Normal Distribution Function,估算在受到表面粗糙度的影响下,朝向方向与半程向量一致的微表面的数量。
  • 菲涅尔函数Fresnel Equation,描述的是在不同的表面角下表面所反射的光线所占的比率。
  • 几何函数Geometry Function,描述了微表面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微表面有可能挡住其他的微表面从而减少表面所反射的光线。

有关DFG三个函数的实现方式很多,有的很真实,有的则性能高效。这里用UE4中所使用的函数,D项使用Trowbridge-Reitz GGX,F项使用Fresnel-Schlick近似,G项使用Smith’s Schlick-GGX。

法线分布函数

D函数从统计学上近似地表示了与某些(半程)向量\(h\)取向一致的微表面的比率。我们马上将要用到的是Trowbridge-Reitz GGX: \[ \nonumber NDF_{GGXTR}(n,h,\alpha)=\frac{\alpha^2}{\pi((n\cdot h)^2(\alpha^2-1)+1)^2} \] 其中,\(h\)是与微平面作比较用的半程向量,\(\alpha\)表示表面粗糙度。随着粗糙度的变化,反射的最终结果也不同:

使用GLSL实现的GGX法线分布函数如下:

float DistributionGGX(vec3 N, vec3 H, float a)
{
    float a2     = a*a;
    float NdotH  = max(dot(N, H), 0.0);
    float NdotH2 = NdotH*NdotH;
	
    float nom    = a2;
    float denom  = (NdotH2 * (a2 - 1.0) + 1.0);
    denom        = PI * denom * denom;
	
    return nom / denom;
}

几何函数

几何函数从统计学上近似的求得了微表面间相互遮蔽的比率,这种相互遮蔽会损耗光线的能量。

与NDF类似,几何函数采用一个材料的粗糙度参数作为输入参数,粗糙度较高的表面其微表面间相互遮蔽的概率就越高。我们将要使用的几何函数是GGX与Schlick-Beckmann近似的结合体,因此又称为Schlick-GGX: \[ \nonumber G_{SchlickGGX}(n,v,k)=\frac{n\cdot v}{(n\cdot v)(1-k)+k} \] 其中\(k\)是粗糙度\(\alpha\)的重映射,取决于光源是什么类型的(直接光还是IBL): \[ \nonumber k_{direct}=\frac{(\alpha+1)^2}8\\k_{IBL}=\frac{\alpha^2}2 \] 为了有效地估算几何部分,需要将观察方向(几何遮蔽(Geometry Obstruction))和光线方向向量(几何阴影(Geometry Shadowing))都考虑进去。可以用Smith’s method来把两者都纳入其中: \[ \nonumber G(n,v,l,k)=G_{sub}(n,v,k)G_{sub}(n,l,k) \] 使用Smith’s Method和Schlick-GGX作为\(G_{sub}\)可以得到如下不同粗糙度R的视觉效果:

使用GLSL编写的几何函数代码如下:

float GeometrySchlickGGX(float NdotV, float k)
{
    float nom   = NdotV;
    float denom = NdotV * (1.0 - k) + k;
	
    return nom / denom;
}
  
float GeometrySmith(vec3 N, vec3 V, vec3 L, float k)
{
    float NdotV = max(dot(N, V), 0.0);
    float NdotL = max(dot(N, L), 0.0);
    float ggx1 = GeometrySchlickGGX(NdotV, k);
    float ggx2 = GeometrySchlickGGX(NdotL, k);
	
    return ggx1 * ggx2;
}

菲涅尔函数

菲涅尔方程描述的是被反射的光线对比光线被折射的部分所占的比率,这个比率会随着我们观察的角度不同而不同。当光线碰撞到一个表面的时候,菲涅尔方程会根据观察角度告诉我们被反射的光线所占的百分比。利用这个反射比率和能量守恒原则,我们可以直接得出光线被折射的部分以及光线剩余的能量。

菲涅尔方程是一个相当复杂的方程式,不过幸运的是菲涅尔方程可以用Fresnel-Schlick近似法求得近似解: \[ \nonumber F_{Schlick}(h,v,F_0)=F_0+(1-F_0)(1-(h\cdot v))^5 \] 其中\(F_0\)表示平面的基础反射率,它由折射指数(Indices of Refraction)计算得出。

如图,越是朝球面掠角的方向看(实现和表面法线几乎垂直),菲涅尔现象越明显,反光越强:

需要注意的是,Fresnel-Schlick近似仅仅对电介质/非金属表面有定义。对于导体/金属表面,使用它们的折射指数计算\(F_0\)并不能得出正确的结果,这需要用一个不同的菲涅尔方程进行计算。

为了效率,可以预计算出 法向方向入射 的结果\(F_0\),(位于0度角,好像直接看向表面一样),然后基于相应观察角的Fresnel-Schlick近似对这个值进行差值,获得近似结果,且金属/非金属材质都使用相同工具。

基础反射率\(F_0\)的预计算结果可从互联网上查到,下面是一些常见数值:

对于导体或者金属表面而言基础反射率一般是带有色彩的,这也是为什么\(F_0\)要用RGB三原色来表示(法向入射的反射率可随波长不同而不同)。这种现象我们只能在金属表面观察的到。这些金属表面独有的特性引出了所谓的金属工作流的概念。也就是我们需要额外使用一个被称为金属度(Metalness)的参数来参与编写表面材质。金属度用来描述一个材质表面是金属还是非金属的,取值为[0, 1]。

接下来看看如何用GLSL实现菲涅尔项,首先注意的是,对于金属表面材质,需要为\(F_0\)添加色彩:

vec3 F0 = vec3(0.04);
F0      = mix(F0, surfaceColor.rgb, metalness);

这里为大多数电介质表面定义了一个近似的\(F_0\)。然后,基于金属表面特性,我们要么使用电介质的基础反射率要么就使用\(F_0\)来作为表面颜色。因为金属表面会吸收所有折射光线而没有漫反射,所以我们可以直接使用表面颜色纹理来作为它们的基础反射率。

代码如下:

vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
    return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}

其中cosTheta是表面法向量\(n\)与半程向量\(h\)(或者观察方向\(v\))点乘的结果。

Cook-Torrance反射方程

\(f_r\)使用Cook-Torrance BRDF,代入到反射方程中有: \[ \nonumber L_o(p,\omega_o)=\intop_\Omega(k_d\frac c\pi+k_s\frac{DFG}{4(\omega_o\cdot n)(\omega_i\cdot n)})L_i(p,\omega_i)n\cdot\omega_id\omega_i \] 然而这个方程在数学上还没完全正确。菲涅尔项F代表光线在表面上反射的比率,这说明F项里隐含包括了\(k_s\),因此最终的反射方程就是: \[ \nonumber L_o(p,\omega_o)=\intop_\Omega(k_d\frac c\pi+\frac{DFG}{4(\omega_o\cdot n)(\omega_i\cdot n)})L_i(p,\omega_i)n\cdot\omega_id\omega_i \] 这个方程现在完整的描述了一个基于物理的渲染模型,它现在可以认为就是我们一般意义上理解的基于物理的渲染也就是PBR。

编写PBR材质

了解PBR的理论知识后,接下来看看艺术家是如何创造PBR材质的。PBR渲染管线所需要的每一个表面参数都可以用纹理来定义或者建模。使用纹理可以让我们逐个片段的来控制每个表面上特定的点对于光线是如何响应的:不论那个点是不是金属,粗糙或者平滑,也不论表面对于不同波长的光会有如何的反应。

反照率:反照率(Albedo)纹理为每一个金属的纹素(Texel)(纹理像素)指定表面颜色或者基础反射率。这和我们之前使用过的漫反射纹理相当类似,不同的是所有光照信息都是由一个纹理中提取的。漫反射纹理的图像当中常常包含一些细小的阴影或者深色的裂纹,而反照率纹理中是不会有这些东西的。它应该只包含表面的颜色(或者折射吸收系数)。

法线:法线贴图纹理和我们之前在法线贴图教程中所使用的贴图是完全一样的。法线贴图使我们可以逐片段的指定独特的法线,来为表面制造出起伏不平的假象。

金属度:金属(Metallic)贴图逐个纹素的指定该纹素是不是金属质地的。根据PBR引擎设置的不同,美术师们既可以将金属度编写为灰度值又可以编写为1或0这样的二元值。

粗糙度:粗糙度(Roughness)贴图可以以纹素为单位指定某个表面有多粗糙。采样得来的粗糙度数值会影响一个表面的微平面统计学上的取向度。一个比较粗糙的表面会得到更宽阔更模糊的镜面反射(高光),而一个比较光滑的表面则会得到集中而清晰的镜面反射。某些PBR引擎预设采用的是对某些美术师来说更加直观的光滑度(Smoothness)贴图而非粗糙度贴图,不过这些数值在采样之时就马上用(1.0 – 光滑度)转换成了粗糙度。

AO:环境光遮蔽(Ambient Occlusion)贴图或者说AO贴图为表面和周围潜在的几何图形指定了一个额外的阴影因子。比如如果我们有一个砖块表面,反照率纹理上的砖块裂缝部分应该没有任何阴影信息。然而AO贴图则会把那些光线较难逃逸出来的暗色边缘指定出来。在光照的结尾阶段引入环境遮蔽可以明显的提升你场景的视觉效果。网格/表面的环境遮蔽贴图要么通过手动生成,要么由3D建模软件自动生成。

美术师们可以在纹素级别设置或调整这些基于物理的输入值,还可以以现实世界材料的表面物理性质来建立他们的材质数据。这是PBR渲染管线最大的优势之一,因为不论环境或者光照的设置如何改变这些表面的性质是不会改变的,这使得美术师们可以更便捷地获取物理可信的结果。在PBR渲染管线中编写的表面可以非常方便的在不同的PBR渲染引擎间共享使用,不论处于何种环境中它们看上去都会是正确的,因此看上去也会更自然。

参考资料

  • 理论 - LearnOpenGL CN (learnopengl-cn.github.io)
  • LearnOpenGL - Theory