图形笔记 - 初级PBR

对整个PBR框架的个人理解

Posted Kongouuu's Blog on November 5, 2021

前言

一般学习图形学的时候第一个着色模型会是布林冯着色模型

Blinn-Phong

Blinn-Phong2

这是一个比较简单的光照模型,跟上图所示分为三个部分,也就是说我们要为每个物体提供以下数据:

1. ka: 环境光强度,来形容物体的一个底色,一般我们会拿物体的diffuse map 输入,然后乘以一个较小的值来代表这个变量 2. kd: 这个就是Lambert模型的漫反射,我们一样是拿diffuse map的输入,不过考虑到光线在不同角度接触到面的时候,面接收到的能量是不一样的,所有后面要加一个(n·l),也就是cos(光线和法线的夹角) 3. kd: 这个是高光权值,我们可以使用一个全白的自定义值也可以采样一个高光贴图来完成这个。要注意的是这里在强度后面乘了(n·h),这里我们想看到的是视角方向跟反射方向越接近,高光越强大,于是我们把视角和入射角度加一起来点乘法线取计算这么个强度。

这个模型可以做出一个不错的效果,但在“真实性”上却有着不少的问题。首先环境光照全靠参数猜,过于“不真实”。再来三者的加权并没有任何物理意义,高光的设计也完全偏离了物理(尤其是带高光贴图的情况下)。而且在我们考虑一个多光源光照的时候,很容易因为设置问题而让物体过量,主要还是因为我们在这个模型并不会去过多考虑一个光照的物理性问题。

一个比较典型的例子就是布林冯没有办法去做一个“Glossy”材质的物体,关键问题就处在默认了我们的材质表面是光滑的。所以为了达到一个更真实的渲染效果,我们需要步入PBR

PBR

一般来说我们在进行一个PBR光照模型的搭建的时候,我们会先把整个模型分为两个部分

  1. 直接光照
  2. 环境光照

这个小节会主要参考直接光照。

PBR理论与直接光照

BRDF

首先我们提到真实性,那我们会考虑到BRDF,也就是形容一个表面他面对来自什么方向的光,会朝着我们摄像机方向反射多少能量的一个方程。具体是这样的一个感觉。我们需要使用他的主要原因是想要在一个基于物理且能量守恒的情况下,去计算我们这个像素他到底应该是什么颜色。

我们考虑到不同的材质他不一定是会对光有一个无损反射的效果,很可能入射的光会被吸收或打散到周围,所以我们的PBR一般会考虑一个微表面的模型。微表面本身意思就是我们在一个材质上采样的任何一个点他都可能是一个不平整的表面,所以光不应该按照单个法线【光滑平面】去处理,这里后面会提到。

一般来说我们把BRDF考虑成两个部分, 漫反射和高光。

Fr = Fdiffuse + Fspecular

高光

先说下高光,一般使用这个Microfacet Cook-Torrance BRDF的高光计算。

  1. F: Fresnel (高光反射),如果是金属的,那么输入F0会在0.7~1.0, diffuse的物体差不多都在0.02~0.04。 我们看一下公式

    \[F(\theta) = F_0 + (1-F_0)(1-cos\theta)^5\]

    可以很简单的了解到,如果我们的材质是金属,那么在任何角度都会有大量的高光反射。 如果材质偏绝缘,高光越吃角度。

    除此之外还有一点就是这里的输入F0不完全是一个物体的金属度。每个金属都有自己对不同光的反射程度,高光的部分他不一定是白色的。所以这里F0我们除了考虑Metallic以外还需要去采样物体本身的Albedo Map。 \(F = metallic \\ F_{0} = F * albedo + (1-F) * (0.04,0.04,0.04)\)

​ 一般来说会写成上面这种格式,意思是如果物体是金属,那么我们会参考材质的Albedo,如果物体是非金属,那么高光就是白色。

  1. D: Distribution of Normal, 法线分布可以使用GGX光照模型实现。这里我们一般会考虑到一个输入α,也就是粗糙度roughness2。需要这个分布也是因为我们考虑的是为表面的模型,如果表面粗糙的话表示采样的点他是非平整的,有多个不同的法线。H为光线和视角的半程向量 \(D_{GGX}(H) = \frac{\alpha^2}{\pi((N\cdot H)^2(\alpha^2-1)+1)^2}\)

  2. G: Geometry Function, 也是一样在考虑一个微表面模型凹凸不平的光照情况。假设我们采样的点他在一个像素内是有着凹凸不平的几何形状,那么我们认为这个凹凸可能会造成一定程度上的阴影(光被几何遮蔽)。这里有很多的算法,可以套Smith/GGX的式子去算。

  3. 分母 4(n·l)(n·v):校正因子(correctionfactor),作为微观几何的局部空间和整个宏观表面的局部空间之间变换的微平面量的校正。根据毛星云大佬的文章,部分引擎会把我们Fspecular里面的分母4(n·l)(n·v)一起放进G里头,把整体当作visibility,虽然不是什么大事。

    https://zhuanlan.zhihu.com/p/53086060

总的来说除了输入的比如法线光线视角方向以外,只需要metallic (F0) 和 roughness就能构建这样的brdf

漫反射

漫反射比较常用的有两个模型, Lambert 和 Disney

Lambert

Lambert 是最简单的漫反射模型,如下:

Lambert

我们考虑到兰伯特漫反射的时候,我们会直接取 ρi, ρ是albedo map里面的固有色。 我们的固有色为了pbr的能量守恒必须除以π,这是因为我们假设固有色的漫反射会平均的反射到半球内的每个立体角,那么我们视角方向的立体角就最多只能得到1/π的能量。

Disney

Disney 模型效果看起来会比Lambert的好一些,不过Disney系列的光照模型并没有特别严格的能量守恒,或者说他们的主要目标并不是让计算贴近物理,而是单纯的想得到一个更好看的效果。

Disney

BRDF融合和计算

就像前面提到的,我们一个较为基础的颜色结果就是漫反射和高光反射的叠加:

Fr = Fdiffuse + Fspecular

我们在计算各自的光照值的时候都是考虑了光源的全部能量,这样计算出的结果会造成我们输出的能量会比输入的多,这样是不正确的,也就是说图像会过量。所以一般来说我们肯定是会给两个BRDF各一个权重去决定他们的值。

这地方属于一个比较民科的地方,比较通用的办法是Cook Torrance BRDF里面的计算:

高光的部分在这个模型里是直接取的菲涅尔系数的值,表示在这个像素有多少光被反射。那理所当然的没有被高光反射的能量来处理就可以被当作漫反射。不过这里漫反射同时也要看材质的金属度,因为完全的金属是没有漫反射的。 \(k_s = F(i,h) \\ k_d = (1-k_s)*(1-metallic)\)

直接光照

我第一次看到这个计算公式的时候就开始思考积分。当时并没有特别明白这个整个模型的意义,实际上积分考虑的更多是大量光源的情况下的能量反射(尤其是能环境光照)。正常的直接光照可以完全无视掉积分,拿一个光源来说,我们只需要计算积分内的公式一次,两个光源就两次,没有什么特殊需要注意的。环境光照整体上是比直接光照复杂许多的。

PBR理论与环境光照

IBL

考虑环境光照的时候首先会想到的是,光源是什么。一个比较简单的模型就是我们拿形容背景的Cube-Map当作光源输入来形容环境光照。这一种使用贴图来进行光照的手法叫IBL(Image based rendering),当然,这只是IBL里面的期中一环罢了。

之前在学习整个IBL的时候并没有过多的去在意其在整个渲染框架中的作用,不过这里也只是简单的介绍一下简单的环境光照的计算以及在整个PBR模型中的作用。

积分计算

相较于直接光照,我们的环境光照要考虑的则是大量的光在打进我们采样的平面的半球。因此我们就需要用到上面讨论过的带积分的渲染方程了。

BRDF在理想状态下计算的是半球范围内所有的入射光对我们的视角方向的一个影响。当然我们完全没有办法去记录整个半球的信息,在某种意义上几乎等于我们对每个像素需要直接采样半个cube map的点。因此我们需要用到一个很关键的技术,那就是蒙特卡洛积分。

一般来说我们想到如果要近似一个积分,我们可以使用黎曼积分(就是把整个积分曲线分成同等大小的多个长方形去算面积),蒙特卡洛也是差不多的办法。比起去计算所有的长方形的面积,蒙特卡洛进一步的优化了这个方法,就是去进行随机采样。

蒙特卡洛算法如下: \(Sum = \frac{1}{N}\sum\frac{f(x)}{pdf}\) 我们每一次采样的时候都是选取了一个立体角的入射光,所以我们可以得到这个立体角在整个球面上占的面积(要注意不同立体角其实面积不一样的)。这里的pdf其实就是我们采样的范围在整个半球面积上的百分比。当我们使用f(x)除以pdf的时候,我们实际得出来的值会从入射立体角的光照值,变成整个半圆获得的光照值,所以在所有的计算加完了后,我们得到的光照值会是N个半球上得到的光照,所以最后要除以一个N。

计算流程

基础的流程是下面这样:

1
2
3
4
5
6
7
8
9
10
11
for(int i=0; i < SAMPLE_NUM; i++)
{
	float pdf;
	vec3 lightDir = RandomVectorHemisphere(normal, &pdf);
	vec3 lightColor = GetEnvironmentLight(environmentTexture, lightDir);
	vec3 localLighting = lightColor * BRDF(lightDir, normal, view, roughness, mettalic) * dot(normal, lightDir);
	totalLighting += (localLighting / pdf);
}
totalLighting /= SAMPLE_NUM


简单来说就是选取一堆采样,依次计算渲染方程,最后就能得到环境光照项。

加速

最简单的加速就是对我们采样的环境贴图进行预处理。如果按照我们上面的方式去计算,那么针对每个像素我们需要对环境贴图采样大量的点,这完全是没必要的。 我们知道我们在针对每个像素是在对朝向的半球进行比较平均的采样,那么我们完全可以对整个cube map进行一个预处理。 使用split-sum预处理后的cube map,在每个像素都会表示: 采样光线方向为中心的半球内提供的光照, 这样我们在计算环境光照的时候只需要对环境贴图进行一次采样就可以了。

不过split sum预计算的一个小问题就是不太方便去考虑光源旋转和遮挡问题,加上采样cube map还是稍微比较慢一些的,这时候可以考虑使用Spherical Harmonics来处理,这里我有写过另一篇文章是讲SH的。

混合进光照框架

我们的环境光照只是我们整体渲染流程里面的一个附加项,他完全可以和点光源/平行光这种一起使用。

回顾一下我们在布林冯里面形容的模型,这其实是差不多的。

我们计算出来的直接光照 加上 这一阶段计算出来的环境光照就可以得到我们像素应该有的光照值。

总结

其实直接光照和环境光照的模型远远比这里提到的复杂多了。我并不是想深探各种技术,比如说diffuse能量补偿、阴影贴图、全局光照等。只是想用最简单的情况来描述一下不同的知识点在实战中的定位。

有些常识在最开始学习的时候我并没有很注重,比如说BRDF之间的混合,渲染方程的积分与直接光照,环境光照和IBL在渲染中的定位等。

参考

https://blog.csdn.net/weixin_44176696/article/details/119791772

https://zhuanlan.zhihu.com/p/386611011