背景
前段时间面试被问过几次 PBR 的问题,遂写个博客梳理下,下次面试就不用再找资料了(不是x。也是对基础光照计算的一个梳理吧。
在传统的光照模型中,光会被分为环境光、漫反射和镜面反射三个部分。环境光是在没有明确光照但并非伸手不见五指的环境下,物体接收到的微弱光照的总和。漫反射是粗糙表面无规则向各个方向反射的光,而镜面反射是光滑表面向入射光沿法线对称方向反射的光。每个部分会进行单独的计算,并最终累加获得最终的光照结果。
PBR
基于物理的渲染算法(Physically Based Rendering),是当前光栅化渲染架构下广泛使用的基础渲染算法,足够支持基本材质的渲染需求。常用商业引擎如 unreal 和 unity 的默认渲染效果都是 PBR 渲染或基于此的扩展。当然不同的引擎、不同的硬件情况、不同的效果需要,PBR 的具体实现算法会有一些不同,但核心算法是相通的。这里的 PBR 使用的是 Learn OpenGL 和 Unreal Engine 中的算法和实现。
核心条件
一个 PBR 模型必须满足三个条件:基于微平面的表面模型、能量守恒和基于物理的 BRDF。我们接下来会逐一拆解这些条件。
微平面模型
微平面模型的核心就是一个非绝对光滑的平面在微观上由很多方向不同的微小镜面组成,每个微平面都会基于自身法线向不同方向反射光线。一个平面越粗糙,微平面的排列就越乱。在计算中我们使用 roughness 粗糙度来表达微表面朝向表面方向的比例。微表面的朝向和表面朝向越接近,镜面反射范围越小越锐利,漫反射影响越不明显。
能量守恒
基于能量守恒,在 PBR 的计算中我们可以,出射光的能量需要小于等于入射光的能量,反映在效果就是镜面反射越强,镜面反射影响范围、漫反射效果就越弱。在更加复杂的渲染如次表面散射(可以实现皮肤、玉、蜡等效果)中,我们会考虑光折射进入物体再射出的情况,这就需要考虑折射和反射分别的能量占比,且折射光颜色会受物体颜色影响等情况。但在简单的 PBR 材质中,我们假设折射光会被全部吸收,即只考虑反射光的情况。基于这个理论,我们可以用 漫反射比例 = 1 - 镜面反射比例 来简化一些计算。此外,金属表面没有漫反射而只有镜面反射,也需要特殊考虑。镜面反射系数将在后文中使用菲涅尔方程计算。
反射方程
反射率方程是描述光是如何被反射的。它是一种渲染方程,即具体的 PBR 效果计算就是基于这个方程进行的。这个方程主要涉及到点 $p$, 入射角 $\omega_i$,出射角 $ \omega_0 $ 和法线 $n$。一个点在指定方向的反射等于这个点收到的所有入射光在视角方向的反射光,所以我们使用入射光的角度的半球积分来计算。当然计算机是无法处理积分的。具体的计算方法会在辐射度量学相关文章中讲述。具体方程如下:
其中 L 代表的是辐射率(Radiance),具体的计算方式和物理逻辑太长了值得单开一篇,这里就不放了,可以看辐射度量学相关文章。总而言之,这里的辐射率可以从光源或者环境光贴图获取。$n*\omega_i$ 则是光照在法线方向的分量,是 这里只讲讲 $F_r$ 即双向反射分布函数(Bidirectional Reflective Distribution Function,BRDF)的具体计算方法。
BRDF
BRDF 可以说是 PBR 的核心实现,它近似的描述了$\omega_i$ 的入射光对 $\omega_0$ 方向的反射光有怎样的贡献。BRDF 基于表面材质属性来对入射辐射率进行缩放或者加权,使不同材质实现不同的反射效果。上面的两个理论对渲染效果产生的影响会在这个Learn OpenGL 和 Unreal 里使用的是 Cook-Torrance BRDF 模型,其他引擎使用的模型可能略有不同但总体而言不会相差非常多。
Cook-Torrance BRDF 包含漫反射和镜面反射两个部分。$ fr = k_d * f{lambert} + ks * f{cook-torrance} $。$ k_d $ 是入射光中漫反射部分,$ k_s $ 则是镜面反射部分。
漫反射 (diffuse reflection)
$f{lambert}$ 是漫反射部分,这套算法使用的是 Lambertian 漫反射,是一种理想化的、入射方向与出射方向无关且反射过程中没有能量损耗的漫反射。$f{lambert} = c/ \pi $,$c$ 是表面颜色,$\pi$ 是为了对漫反射进行标准化,因为光照积分计算的是整个半球的入射光,而入射光均匀的反射到整个半球上。
### 镜面反射 (specular reflection)
$f_{cook-torrance}$ 则是整个算法的核心,会基于各种物理逻辑和经验科学实现镜面反射效果。它的完整方程如下所示:
分母部分是标准化因子,而分子部分则由三个函数组成,分别是法线分布函数(Normal Distribution Function),几何函数(Geometry Function)和菲涅尔方程(Fresnel Rquation)。
法线分布函数
法线分布函数(Normal Distribution Function) 主要是基于微平面理论,估算在受到表面粗糙度的影响下,表面朝向与当前半程向量方向相同的微平面数量。
半程向量 $h$: 入射光和视角的中间方向。如果使用反射光和观察方向计算镜面反射,当观察方向和反射方向夹角大于90度时,反射光在观察方向的投影为负,镜面反射直接为 0,所以改用半程向量和法线的夹角来计算镜面反射,这样可以使反射光更加自然。
这里使用的是 Trowbridge-Reitz GGX 法线分布函数算法,其具体方程如下:
其中 $ \alpha $ 是一个和粗糙度成正比的数,不同引擎有不同的算法,在unreal 中是粗糙度的平方。可以看出糙度越大,分子越大,分母中法线和半程向量的乘积带来的影响越小但分母越大。即粗糙度越大,光的反射越分散,范围越大但最亮处越暗。具体代码实现如下。
1 | float D_GGX_TR(vec3 N, vec3 H, float a) |
几何函数
几何函数(Geometry Function)是用于模拟计算微平面之间相互遮挡的函数。因为微平面方向不确定,所以有概率出现一个平面的反射光被另一个平面遮挡的情况。这里我们使用的算法是GGX与Schlick-Beckmann近似的结合体,因此又称为Schlick-GGX算法。具体公式如下:
其中 k 是粗糙度 $\alpha$ 的重映射,在直接光照和简介光照下有不同的算法。直接光照下 $k{direct} = (\alpha + 1)^2/8$,间接光照下 $k{IBL} = \alpha ^2 / 2$。
考虑到入射光和出射光都有可能出现微平面自遮蔽,即观察方向(v)的几何遮蔽(Geometry Obstruction) 和光线方向的几何阴影(Geometry Shadowing)。即最终计算时需要将两个方向都考虑进去,得到 $G(n,v,l,k) = G(n, v, k) * G(n, l, k)$。具体代码如下1
2
3
4
5
6
7
8
9
10
11
12
13
14
15float 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 Rquation)描述的是不同角度下反射光线所占比率。在现实生活中,如果我们以一定角度观察物体时,反光会变得非常明显,甚至如果从90度观察几乎所有平面都能完全反光。这个现象叫做菲涅尔现象。在清澈的水边我们可以轻易观察到这个现象。因此我们在计算时可以用简化适配过的菲涅尔方程作为镜面反射比率并参与效果的计算。这里我们使用Fresnel-Schlick近似法计算菲涅尔效果,具体公式如下。(注意分母部分 cosTheta 有 $n·lightDir$、$n·v$、$v·h$ 等好几种说法,不过这些在实时渲染中通常被认为是差不多的,Learn OpenGL 中采用的是的 $v·h$)
$F_0$ 是物体的基础反射率(Base Reflectivity),即我们从法线方向垂直观察时物体时物体的反射率。对于非金属(dielectric or non-metal surfaces,差不多在这个意思,很难完全准确翻译),基础反射率可以用折射率计算。考虑到金属折射率为负并不适配,所以我们预先计算出金属的反射率。金属的 $ F_0 $ 我们可以通过查表获得。且金属表面而言反射一般带有色彩,所以 $F_0$ 有 RGB 三个值。
在实际的计算中,考虑到非金属的基础反射率比较接近且几乎都不会高于0.17,我们可以直接使用它的平均反射率 0.04 作为基础反射率。金属的反射率大多在 0.5 - 1 且会反射表面颜色。因此我们可以直接使用金属颜色作为反射率。为了支持更多好看的视觉效果,我们引入金属度(Metalness)对 {F_0} 进行差值估算。具体代码如下。1
2
3
4
5
6
7vec3 F0 = vec3(0.04);
F0 = mix(F0, surfaceColor.rgb, metalness); // lerp
vec3 fresnelSchlick(float cosTheta, vec3 F0)
{
return F0 + (1.0 - F0) * pow(1.0 - cosTheta, 5.0);
}
总结
当我们把 BRDF 放到整个反射方程中,我们可以得到
可以看到除了光照以外的部分都已经可以计算了。光照部分将在辐射度量学和基于图像的光照(Image based lighting, IBL)中仔细拆解。此外,在具体的项目中,我们一般会使用贴图而非单一数值来控制不同物体不同的辐射效果。这个部分也会进行单独的拆解。
参考资料
- LearnOpenGL - Theory 中文,英文
- TraceYang的笔记 - 基于物理渲染的基础理论
- TraceYang的笔记 - UnrealEngine4 PBR Shading Model 概述
- 知乎 - 基本光照模型系列——1.Lambert光照模型
- 知乎 - 菲涅尔方程(Fresnel Equation)