前言
游戏引擎中的直接光照一般分为两大类:光源和环境光。光源一般有:平行光、点光源、聚光源、面光源等,而作为直接光照的环境光一般是指基于图片的光照(IBL-Image Based Lighting),这里的环境光要跟环境光遮蔽(AO-Ambient Occlusion) 中的环境光区分一下,AO 概念中的环境光一般指的是间接光照,更多差异可以看看这个知乎问答:Environment Light 和 Ambient Light 有什么区别?[1]。
3D 实时全局光照技术(一)——开篇中提到,实时全局光照考虑的是直接光照和一次弹射的间接光照,本文会重点介绍各类直接光照的着色计算方法。
光源
图1: UE5 中的四种光源
上图分别展示了 UE5 中的平行光(DirectionalLight)、点光源(PointLight)、面光源(ReactLight)和聚光源(SpotLight)的直接光照(在后期处理设置里关掉了间接光照)的渲染效果。
半影
通过观察我们发现,面光源的阴影是有半影区的,而其他三个光源都是全影,产生半影的原因是部分光能照到,部分光照不到。这就说明面光源在同一个方向发射的光线不只一条,而对于平行光、点光源、聚光源来说它们在同一个方向发射的光线只有一条。
光照单位
除了上面面光源和其他光源的差异,平行光和另外三个光源也有差异,这个我们需要先来看看他们对光强的配置项:
图2: 四种光源的光照单位
可以看到平行光的单位是 lux(勒克斯),其他三种光源的单位是 cd(candela 坎德拉),对应到我们上篇文章中提到的辐射度量学的概念里,平行光提供的是辐照度(Irradiance),可表示为,其他三种光源提供的是辐射强度(Radiant Intensity)可表示为,上篇我们提到渲染方程中需要的入射光是辐射率(Radiance),那么 UE 提供的数据要怎么带入到方程中来计算着色结果呢?且看下文分解。
点光源和聚光源
在介绍光照计算前还有个小插曲,点光源和聚光源看起来很相似,但还是有些差异需要明确。点光源由中心向整个球面均匀发光,不同方向的辐照度都是以距离平方反比衰减;而聚光源是有个锥形的发射范围,且聚光源是有朝向的,辐照度衰减除了跟距离相关还跟光源朝向相关。不过他们向同一方向只有一条光线这点还是一致的,所以本文不会单独介绍聚光源的衰减方式,想要了解细节的话可以看看这篇文章[2] 。
光照计算
平行光和点光源
前面提到平行光和点光源的一个重要特性:在同一个方向上不会有多条光线,而光源到着色点连线的方向是确定的,也就是说平行光和点光源到着色点只会有一条光线有贡献,其他光线与着色点不相交贡献为 0。那我们也就不用积分了,直接通过 BRDF ()就能算出算出出射光辐照度():。我们前面提到 UE 提供的平行光就是辐照度的形式,这个公式对平行光也太友好了吧,直接带入计算就可以了。
UE 中的点光源是以辐射强度的形式提供,设点光源里着色点的距离为,那么根据开篇辐射度量学的定义可以算得辐照度,而我们又知道点光源在一个方向只有一条光线,也就是说着色点在点光源与着色点连线这个方向的单位立体角中获得的点光源贡献只有一个,即,有了就可以跟平行光一样的方式计算着色结果了。
面光源
面光源的计算会比其他光源麻烦一些,因为对于一个着色点会有多条光线与其相交,我们避不开渲染方程中积分计算的部分。在离线渲染中有路径追踪这个大一统的解决方案,面光源自然也不在话下,但我们前面也讲过路径追踪的消耗是实时渲染吃不消的,所以面光源在实时渲染中一直是一个老大难的问题。直到 2016 年,由分别来自Unity,Ubisoft 和 Ready At Dawn Studios 的四位作者提出的 LTC(Linearly Transformed Cosines)[4] 横空出世,基于 LTC 的面光源渲染结果精确且快速,一举解决了实时渲染领域多边形面光源(polygonal lights)的问题。
论文中的公式推导笔者还没完全掌握,所以本文只是按照笔者的理解简单介绍下 LTC 的解法思路和最终的执行步骤,如果不对欢迎指正,感兴趣的可以直接去看看原论文。
准备
还是先祭基于微表面模型的反射方程:,渲染方程要考虑着色点半球面上的所有光照贡献,对于面光源这种直接光照来说反射方程可以写成,积分域由半球面球面缩小为多边形(光源) P。遗憾的是这一步并没有解决什么问题,我们假设入射光辐射率不变,用表示,方程的中的变量还有材质、入射光、出射光、法线,材质中又包含金属度 metallic、粗糙度 roughness、材质基础反射率,这可是一个 6 维函数的积分啊,要怎么解呢?
有没有被吓到,别怕其实没有这么复杂,我们来回忆下着色步骤,在像素着色的时候其实材质和视角是已经确定了的,对应的 metallic、roughness、、、都定下来了,只有是变量,所以我们还需要找一找方程中还包含变量的项。其实 F、G、D 项都跟相关,但是 LTC 论文中直接用一个 D 表示了整个 BRDF,我看着有点懵,后来看到有一些大佬的文章里讲在对论文在对 BRDF 做线性变换的时候没有考虑 F 和 G,但是在积分计算完之后会采样范式做拟合,这部分我还是不太明白,后面继续研究,本文还是按照论文的思路继续往下讲。
按照论文,令,则反射方程可以表示为。这样我们就只需要计算一个 1 维的球面分布函数的积分了,遗憾的是,这个积分还是不好算,那该怎么办呢?
上面写了这么多都只是前菜,接下来才是 LTC 高能的部分。
线性变换球面分布 LTSD(Linearly Transformed Spherical Distributions)
既然球面分布函数的积分不好算,那能不能把它转换为一个好计算的球面分布函数呢?下面就是论文作者非常聪明的发现,对于任何一个球面分布函数,一定可以通过一个线性变换矩阵将其变换成另一个球面分布函数,这就是 LTSD — 线性变换球面分布。
我们知道也是一个球面分布函数(余弦分布函数),并且它的积分是可以计算出来的,所以我们自然想到将余弦分布函数作为原函数来进行变换,所以我们称此方法为 LTC(Linearly Transformed Cosines)。
线性变换球面分布有一个很好的性质,假设原函数为,线性变换矩阵为,变化后的球面分布函数为,对于原函数和变换函数衍生到球面外部的多边形和有如下等式:
,其中。
近似 Approximating
有了这个性质,我们就只需要把 M 给算出来就好了,我们前面把反射方程简化为,在 GGX 模型中 D 表示的是物体微表面的法线与入射光的夹角根据粗糙度不同而变化的函数,微表面模型又假设所有微表面都会发生完美的镜面反射,所以如果我们确定了视角方向和粗糙度,就可以计算出余弦分布函数经过变换成为来近似 GGX 模型所需要的矩阵 M 。
图3:LTC 拟合 GGX 分布 from https://eheitzresearch.wordpress.com/415-2/
论文中给出 M 的形式如下:
那么我们可以枚举 α 和,计算出对应的 M 并将它逆矩阵存储到一个 64x64 分辨率的 2D 纹理中,这个纹理也称 LUT(Lookup table),将渲染方程的某一些项预计算存入 LUT 这个思想很常用,我们后面还会遇到。
着色 Shading
本文只介绍常量面光源,也就面光源各部分亮度一致,与之对应的是基于纹理的面光源,想了解此类光源的着色可以去看原论文。
我们上一步已经预计算好了并存到了纹理中,那么在着色环节,我们只需要根据材质的粗糙度和当前视角方向去读对应的,原函数为余弦分布函数,我们计算得出原函数对应的多边形,那么我们就能计算出对应的积分值,根据 LTSD 的性质,这个值也就是 BRDF 的积分值,再乘上 L 就得到着色结果了。
最后再放一张论文的 teaser image,效果非常好:
环境光
前文介绍了光源的着色计算,接下来我们来看看环境光要怎么进行着色计算。
图4: UE5 中的环境光
环境光也被称为基于图片的光照(IBL-Image Based Lighting),一般是用一个立方体贴图(CubeMap)来表示,如下所示:
图5: 对应图 4 中使用的 CubeMap
注:可以看到上图 CubeMap 的分辨率是 1024x512*6,所以 UE 中展示的 CubeMap 看起来是一张,但确实是由 6 长图组合而成。
在介绍环境光照的着色之前,我们要先明确一点,我们认为环境光照是来自无限远的,也就是说场景中的物体之间的距离相对它们离环境光的距离可以忽略不计,这样的话我们对于场景中的不同物体都可以使用同一个环境光数据。
要计算环境光着色,我们还是先把反射方程(渲染方程不考虑自发光项)给写出来:
这个我们很熟悉了,不就是要求着色点法线对应的半球面积分吗,现在 CubeMap 有了,入射光直接通过纹理查找就能取到。是的,可以这么做,不过这是离线渲染的做法,要使用蒙特卡洛方法的路径追踪来采样,实时渲染可等不了。我们一般认为如果一个算法里有采样,那它基本上就不能用于实时渲染领域(temporal sampling 除外)。那有什么办法能够高效的解引入 IBL 的渲染方程呢?
来自 Epic Games 的 Brian Karis 提出了一个巧妙的方案:Split Sum Approximation,我们来一层一层揭开它的面纱。
我们先把反射方程写成微表面模型的形式:
着色结果为漫反射 Diffuse 项和镜面反射 Specular 项相加,我们分别处理。
漫反射项
Diffuse 项我们用表示,则,其中是漫反射 BRDF,我们通常用 lambert 漫反射模型则,这项是常数项可以移到积分前面。为漫反射系数,UE(UnrealEngine 虚幻引擎) 中使用的是“金属度-粗糙度”的材质表示,即,F 是菲涅尔项跟入射角度相关,不能移到积分前,这就不好办了。不过我们分析发现,对于金属来说我们不需要考虑漫反射,而对于非金属来说,只有在入射角度很大的时候 F 的值才比较大,这个时候多在物体边缘对整体影响不大所以我们可以把它忽略,这样的话就可以写成,也就可以提到积分前了。综上,漫反射项可以写为:
这样积分内就变得简单很多了,我们前面说过 IBL 被认为是来自无限远的光照,那么对于场景中的任一点都可认为是在场景中心,那么上述方程可以进一步简化为:
至此,积分项就只跟Li和着色点的法线方向相关了,我们完全可以把积分项预计算出来,预计算结果可以构造成另一个 CubeMap,我们知道 BRDF 是 Radiance 和 Irradiance 的比值,所以上式中的积分结果其实就是入射光 Irradiance ,而存储这个积分结果的 CubeMap 也被称为 IrradianceMap:
图6:IrradianceMap from https://learnopengl.com/PBR/IBL/Diffuse-irradiance
我们在着色的时候,只需要根据着色点法线方向去做纹理查询获取入射光 Irradiance ,然后在乘以就可以获得着色结果了。
镜面反射项
镜面反射项我们用表示,则,我们把它写成 cook-torrance 的形式:
对于这个式子没有能像漫反射项那样能够直接提到积分前的常数项,怎么办呢?
这里我们就需要用到 Epic Games 提出的 Split Sum Approximation 方法了,提出这个方法的论文中用的是蒙特卡洛求和的形式,我们这里跟 Games202 中保持一致还是使用积分的方式来推导,结果是一样的。在 Games 202 中闫令琪老师说过,对于一个积分,如果的积分域很小或者变化比较平滑(smooth),那么就有约等式:
我们来分析一下镜面反射项的积分是否满足上述条件,对于 BRDF 我们考虑 glossy 和 diffuse 两种情况:
注:这里考虑了 glossy 和 diffuse 不局限于本章节镜面反射这个范围,而是对于通用的 BRDF 进行考虑。
图7:glossy 和 diffuse 的波瓣范围 from games 202
如果 BRDF 是 glossy 的,那么入射光的波瓣就很小,如果 BRDF 是 diffuse 的话,那么 BRDF 本身变化就很平滑,这不就刚好满足上面的条件吗,所以我们可以将镜面反射项简化为:
Split Sum: 1st Stage
上式将反射方程的镜面反射项拆成了两个积分相乘,对于第一个积分直白解释是将半球面中 BRDF 覆盖的区域中的光积分起来再进行归一化,这表示的其实就是对光源做模糊处理(filtering),而模糊的滤波刚好跟 BRDF 覆盖的区域相关。那我们完全可以根据不同 BRDF 覆盖的范围不同来提前生成多个模糊过后的环境光 CubeMap,然后在着色的时候我们根据 BRDF 的覆盖范围去取对应的 CubeMap ,如果没有对应的 CubeMap,我们就可以在 BRDF 覆盖范围最近的两个 CubeMap 之间取差值,跟纹理查找中使用到 Mipmap 的方法类似。
图8:Prefiltering 环境光贴图 from games 202
我们再来分析下这些预计算的 CubeMap 要怎么使用,来看看下面这张图:
图9:对入射光波瓣范围采样可以近似为对 prefiltering 后的环境光贴图直接查询 from games 202
先看左边,左边的部分表示当我们通过光线追踪的方法去计算着色结果的时候,我们会向视线方向基于法线的波瓣范围去采样,每一个采样光照都做一遍着色计算,然后在采样范围内对着色结果做加权平均就可以得到着色结果;我们想一下,上面这个操作是不是很像对于球面方向的光提前做好加权平均(filtering),然后我们在视线方向的镜面反射方向做一次查询,这个查询结果就是它周围光照加权平均后的值。
至此我们就解决了 Split Sum 前半部分的计算,接下来我们再来解决后半部分。
Split Sum: 2nd Stage
先把后半部分的积分写出来:,这是对整个 BRDF 做积分,我们发现这个积分里的参数太多了,按照前面预计算的思路,我们很难对这么多参数的不同组合做多维的预计算,就算做出来了这个存储空间也是巨大的,暴力预计算不可取,我们需要更聪明的方式。
我们在前面分析过,F、D、G 中的变量分别有材质基础反射率、粗糙度 roughness 和入射角,这样就只需要对这三个维度做预计算了。但是三维的预计算还是太麻烦,Split Sum 的作者显然有更好的办法,注意看,高能部分来了。
对于 F 项我们一般用的是它的 Schlick 近似:
接下来,我们把它带入到上面的积分中可得:
这样就可以把提到积分外:
这个式子虽然看起来比之前复杂了很多,但是分解成的两个积分中都只有粗糙度 roughness 和入射角两个变量了,所以我们可以根据入射角和粗糙度 roughness 预计算出一个二维的 LUT 并存储到一个 2D 纹理中:
Split Sum: 结果
我们通过 1st Stage 中一次 CubeMap 纹理查询(可能会有差值计算)的结果乘以 2nd Stage 中一次 2D 纹理查询的结果就能够快速计算出着色点的近似着色结果了,来看看效果:
图10:Split Sum 渲染与真实渲染对比,from Games 202
可以看到 Split Sum 的渲染结果已经非常毕竟 Reference 了,UE yes !
小结
可以看到本文介绍的算法中有很多近似,实时渲染要求一帧的渲染耗时不能超过 30 毫秒,在如此苛刻的条件下近似是不可避免的,如何能够高效地做到更接近真实的近似是我们努力的方向,我们可以把一些好的近似方法以及他们对应的要求好好记下来,说不定再别处就会用到,例如今天的 Split Sum 中的积分近似在阴影处理中也有用到,面光源 LTC 算法里通过 LUT 简化积分计算的方法在 Split Sum 的第二步也有用到,这些通用的解决方案是我们更应该掌握的,加油,好好学习。
参考
-
Environment Light 和 Ambient Light 有什么区别? https://www.zhihu.com/question/581738793
-
图形学基础 - 着色 - 光源 https://zhuanlan.zhihu.com/p/360821314
-
Real-Time Polygonal-Light with LTC https://zhuanlan.zhihu.com/p/84714602
-
Real-Time Polygonal-Light Shading with Linearly Transformed Cosines https://eheitzresearch.wordpress.com/415-2/
-
GAMES202: 高质量实时渲染 https://sites.cs.ucsb.edu/~lingqi/teaching/games202.html