1 - 理论知识
接下来我们将接触简单的 PBR,首先看看有关 PBR 的理论知识。
简介
PBR,即基于物理的渲染(Physically Based Rendering),是一个基于物理 / 现实的渲染技术的集合。PBR 的目的是用符合物理规律的方式来模拟光线,这会使得渲染结果比 Phong 光照模型更加真实。
此外,由于它和实际物理很接近,艺术家可以使用基于物理的参数去调整表面材质,而不是用粗劣修改和调整让材质看起来正确。使用基于 PBR 的材质会使不论光照条件如何,该材质均正确。
PBR 实际上仍是对现实的近似,PBR 光照模型需要满足下列三个条件:
- 基于微表面模型;
- 满足能量守恒;
- 使用一个基于物理的 BRDF。
这里会学习 PBR 中的 Metallic Roughness 工作流,这是由迪士尼提出探讨并被 Epic 首先应用于实时渲染的 PBR 方案:
在学习 PBR 之前,需要有帧缓冲、立方体贴图、Gamma 校正、HDR 和法线贴图基础。
微表面模型
所有 PBR 的技术都基于微表面理论。这个理论认为,达到微观尺度之后的任何平面都可以用被称为微表面(Microfacets)的细小镜面来进行描绘。根据表面的粗糙度,这些细小镜面的排列方式可以相当不一致:
如果表面越粗糙,那么微表面的排列方式也就越混乱,会使反射光线更加发散,产生更宽广的镜面反射;反之,在光滑表面上的反射光线几乎朝同一方向,产生范围小并锐利的反射:
在微表面层次上,没有表面是完全光滑的。由于微表面已经微小到无法通过像素区分,假设一个粗糙度 Roughness
参数,然后用统计学方法来估计微表面的粗糙程度。可以通过平面的粗糙度计算出有多少比率的微表面朝向一些向量
可以发现粗糙度越高就越像漫反射。
能量守恒
微表面近似模型遵循能量守恒(Energy Conservation):出射光的能量不应该超过入射光的能量(除了自发光材质)。例如上图中,随着镜面反射区域的增大,亮度也减少了。
为了遵循能量守恒,我们需要对漫反射光和镜面反射光作区分。当光线打到表面上时,它就会分成一个 折射部分 (refraction) 和一个 反射部分 (reflection)。反射部分的光线会被直接反射,不会进入表面,这就是 镜面光 ;折射部分的光线会进入表面且被吸收,这就是 漫反射光。
实际上,折射光并不会被表面直接吸收。在物理上,光线是一种没有耗尽就不停向前运动的能量,能量通过碰撞消耗。每一种材质都由无数微小粒子组成,这些粒子都能与折射光线发生碰撞,并让光线损失部分 / 全部能量:
一般的,并非全部能量都会被吸收,而光线也会沿着一个主要随机方向 散射 (scatter),直至能量耗尽或离开内表面。离开内表面的光线会为漫反射颜色做贡献。考虑到这种程度的技术被称为 次表面散射 (Subsurface Scattering),它显著提升了诸如皮肤、大理石、蜡质等材质的视觉效果,但代价是性能的下降。这里我们假设折射光会被完全吸收,散射的影响很小,且不会穿出表面以简化计算。
对于 金属表面 (Metallic) 的折射与散射,其对光的反应和非金属材质(也被称为 介质 (Dielectrics))不同。金属表面遵循相同的折射和反射定律,但所有折射光都会被表面直接吸收,没有散射。也就是说金属表面只有镜面反射光。PBR 渲染管线会区别处理这两种材质。
通过上面对反射和折射光的探索,可以发现 反射光和折射光是互斥的。也就是说计算完二者的其中一种,可以根据能量守恒求出另外一种的能量。例如先求镜面反射光,再求漫反射折射光:
// 反射/镜面 部分 float kS = calculateSpecularComponent(...); // 折射/漫反射 部分 float kD = 1.0 - kS;
这里让它们遵循能量守恒,能量百分比总和不超过 1。这是我们在之前的光照计算中从未考虑到的。
反射方程
在这里引入渲染方程,使用它可以解释所有的光照。PBR 基于渲染方程的特化版本(渲染方程 = 自发光项 + 反射方程),即反射方程。为了正确理解 PBR,需要好好理解反射方程:
辐射度量学简介
要想正确理解反射方程,还需要了解辐射度量学 (Radiometry) 的内容。辐射度量学是对光照的一套测量系统和单位,它能准确地描述光线的物理性质。接下来看看它的物理量:
辐射通量 (Radiant flux):辐射通量
表示的是一个光源每单位时间发射的功率,以瓦特为单位。光是不同波长的能量总和,每个波长都与一种可见颜色相关。辐射通量会计算这个由不同波长构成的函数总面积,这里通过使用 RGB 而不是波长来简化计算。
立体角 (Solid Angle):用
表示,它描述了单位球体上一块区域对应的球面部分的面积。辐射强度 (Radiant intensity):在单位球面上,光源向每单位立体角发射的辐射通量。
计算辐射强度的公式如下:
辐射亮度 (Radiance):也称辐射率,描述了一个拥有辐射通量
的光源在单位面积 ,单位立体角 上辐射出的总能量:辐射亮度受入射光线与平面法线夹角
的余弦值影响,当直接辐射到平面上的程度越低,光线就越弱;当光线完全垂直于平面时强度最高。 可由光线方向向量和平面法向量点积得到:float cosTheta = dot(lightDir, N);
可以将立体角
看成方向向量,将面 看成着色点 ,那么就能在着色器中用辐射亮度来计算单束光线对每个片段的作用了。辐射照度 (Irradiance):描述的是所有方向投射到着色点
上的光线能量总和。
理解反射方程
观察反射方程:
其中,一个半球可以描述为以平面法线
需要通过积分的方式计算半球领域内所有入射方向
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
来对所有离散部分进行缩放,最后的和就是积分的数值解。利用离散步长得到的是函数总面积的近似值,可以通过增加离散部分的数量来提高黎曼和的准确度。
反射方程概括了在半球领域
BRDF
反射方程中的
BRDF 可以近似的求出每束光线对一个给定材质属性的平面上最终反射出来的光线所作出的贡献程度。例如有一个完全光滑的平面,对于所有的入射光线
BRDF 也要遵循能量守恒定律,也就是说反射光线的总和永远不能超过入射光线的总量。
Cook-Torrance 模型
常用的 BRDF 模型是 Cook-Torrance BRDF 模型,有漫反射和镜面反射两个部分:
BRDF 左侧描述的是漫反射部分,用
BRDF 右侧的镜面反射部分如下:
- 法线分布函数:Normal Distribution Function,估算在受到表面粗糙度的影响下,朝向方向与半程向量一致的微表面的数量。
- 菲涅尔函数:Fresnel Equation,描述的是在不同的表面角下表面所反射的光线所占的比率。
- 几何函数:Geometry Function,描述了微表面自成阴影的属性。当一个平面相对比较粗糙的时候,平面表面上的微表面有可能挡住其他的微表面从而减少表面所反射的光线。
有关 DFG 三个函数的实现方式很多,有的很真实,有的则性能高效。这里用 UE4 中所使用的函数,D 项使用 Trowbridge-Reitz GGX,F 项使用 Fresnel-Schlick 近似,G 项使用 Smith’s Schlick-GGX。
法线分布函数
D 函数从统计学上近似地表示了与某些(半程)向量
使用 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:
使用 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 近似法求得近似解:
如图,越是朝球面掠角的方向看(实现和表面法线几乎垂直),菲涅尔现象越明显,反光越强:
需要注意的是,Fresnel-Schlick 近似仅仅对电介质 / 非金属表面有定义。对于导体 / 金属表面,使用它们的折射指数计算
为了效率,可以预计算出 法向方向入射 的结果
基础反射率
对于导体或者金属表面而言基础反射率一般是带有色彩的,这也是为什么
接下来看看如何用 GLSL 实现菲涅尔项,首先注意的是,对于金属表面材质,需要为
vec3 F0 = vec3(0.04); F0 = mix(F0, surfaceColor.rgb, metalness);
这里为大多数电介质表面定义了一个近似的
代码如下:
vec3 fresnelSchlick(float cosTheta, vec3 F0) { return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0); }
其中 cosTheta
是表面法向量
Cook-Torrance 反射方程
即
编写 PBR 材质
了解 PBR 的理论知识后,接下来看看艺术家是如何创造 PBR 材质的。PBR 渲染管线所需要的每一个表面参数都可以用纹理来定义或者建模。使用纹理可以让我们逐个片段的来控制每个表面上特定的点对于光线是如何响应的:不论那个点是不是金属,粗糙或者平滑,也不论表面对于不同波长的光会有如何的反应。
反照率:反照率 (Albedo) 纹理为每一个金属的纹素 (Texel)(纹理像素)指定表面颜色或者基础反射率。这和我们之前使用过的漫反射纹理相当类似,不同的是所有光照信息都是由一个纹理中提取的。漫反射纹理的图像当中常常包含一些细小的阴影或者深色的裂纹,而反照率纹理中是不会有这些东西的。它应该只包含表面的颜色(或者折射吸收系数)。
法线:法线贴图纹理和我们之前在法线贴图教程中所使用的贴图是完全一样的。法线贴图使我们可以逐片段的指定独特的法线,来为表面制造出起伏不平的假象。
金属度:金属 (Metallic) 贴图逐个纹素的指定该纹素是不是金属质地的。根据 PBR 引擎设置的不同,美术师们既可以将金属度编写为灰度值又可以编写为 1 或 0 这样的二元值。
粗糙度:粗糙度 (Roughness) 贴图可以以纹素为单位指定某个表面有多粗糙。采样得来的粗糙度数值会影响一个表面的微平面统计学上的取向度。一个比较粗糙的表面会得到更宽阔更模糊的镜面反射(高光),而一个比较光滑的表面则会得到集中而清晰的镜面反射。某些 PBR 引擎预设采用的是对某些美术师来说更加直观的光滑度 (Smoothness) 贴图而非粗糙度贴图,不过这些数值在采样之时就马上用(1.0 – 光滑度)转换成了粗糙度。
AO:环境光遮蔽 (Ambient Occlusion) 贴图或者说 AO 贴图为表面和周围潜在的几何图形指定了一个额外的阴影因子。比如如果我们有一个砖块表面,反照率纹理上的砖块裂缝部分应该没有任何阴影信息。然而 AO 贴图则会把那些光线较难逃逸出来的暗色边缘指定出来。在光照的结尾阶段引入环境遮蔽可以明显的提升你场景的视觉效果。网格 / 表面的环境遮蔽贴图要么通过手动生成,要么由 3D 建模软件自动生成。
美术师们可以在纹素级别设置或调整这些基于物理的输入值,还可以以现实世界材料的表面物理性质来建立他们的材质数据。这是 PBR 渲染管线最大的优势之一,因为不论环境或者光照的设置如何改变这些表面的性质是不会改变的,这使得美术师们可以更便捷地获取物理可信的结果。在 PBR 渲染管线中编写的表面可以非常方便的在不同的 PBR 渲染引擎间共享使用,不论处于何种环境中它们看上去都会是正确的,因此看上去也会更自然。