本文的手绘图片可以随意使用,我自己画的~
SDF是一个函数,描述了空间中的每个点到某个表面的距离。这个“距离”带有一个符号,通常表示点是在表面的内部还是外部。
如果点在表面上,SDF的值为0。
如果点在表面外部,SDF的值为正,表示该点到最近表面的距离。
如果点在表面内部,SDF的值为负,表示该点到最近表面的距离。
在程序上,SDF通常表示为一个函数,它接受一个点(一般是三维向量)作为输入,并返回一个标量值,代表该点到最近表面的有符号距离。
例如,一个圆的SDF函数(在二维空间中):
point
是你想要查询的位置。
center
是圆的中心。
radius
是圆的半径。
length
函数计算两点之间的距离。
具体来说,给一个点 Point_1
,通过SDF函数,就可以求出 length
。
Ray Marching(尤其是Sphere Tracing)是SDF(Signed Distance Field)的最知名的应用之一。Ray Marching 使用SDF来逐步沿光线方向推进,直到找到或者接近场景中的表面。
概述Ray Marching如何使用SDF的:
初始化:从观察者的位置开始,为每个像素发射一条光线。
查询距离:对于光线上的当前点,使用SDF查询该点到最近物体表面的距离。
前进:沿光线方向移动距离,即你从SDF中获得的值。因为SDF给出的是到最近表面的距离,你可以安全地沿光线移动这个距离而不会错过任何表面。
迭代:重复查询距离和前进的步骤,直到你足够接近一个表面(SDF值接近0)或超出了最大步数/最大距离。
着色:一旦找到表面,可以计算光照、纹理等来着色该像素。
通常,在Ray Marching中,「安全距离」被用来快速找到场景中的表面。但是,这些距离也可以用来估算光源的遮挡程度,从而生成软阴影。
怎么做呢?
首先,使用常规的Ray Marching技术找到场景中的表面。这意味着你会沿光线迭代,使用SDF来确定每一步的距离,直到你接近或达到一个表面。
一旦找到表面,你可以从该点发射一条射线到光源,这条射线被称为阴影射线。
与Ray Marching相似,你会沿阴影射线迭代前进,再次使用SDF来确定每一步的距离。
一旦阴影射线的前进距离超过了从视点到光源的距离,或者超过了预定义的最大步数,步进过程就会停止。
上图是没有遮挡物的情况,如果有遮挡物,那么就需要进行「估计遮挡」步骤。也就是说,如果在前进到光源之前,SDF的值变得非常小(接近于0),那么这意味着我们碰到了另一个物体,因此原始的视点是在阴影中的。
但为了得到软阴影,我们不仅检查是否有遮挡,还考虑遮挡的程度。这通常通过考虑当前点到SDF表面的距离与到光源的距离之比来估计。较大的表面距离意味着较淡的阴影,而较小的表面距离意味着较深的阴影。
在实际光照中,当一个光源被一个物体部分遮挡时,它会产生一个阴影区域,这个区域分为两部分:全影区和半影区。
接下来引入一个概念,"Safe Angle"(安全角)。这个的安全角的概念与半影的产生直接相关。首先从一个Shading Point出发射向光源,在某一点处(Current Point)可以得到对应的SDF值SDF(Current Point)。如果这个SDF值比较大,那么Safe Angle越大,说明阴影并不硬(黑)。
上图中我只画出了其中某个Current Point,实际上是有好几趟SDF查询的,就像上面Ray Marching那样。实际来说,每一步进都会有一个Safe Angle,这里只需要取最小的即可。
那么问题来了,我们要怎么计算Safe Angle?直接计算一个反三角函数就可以了。
但是,在实际计算中arcsin的计算是非常缓慢的,因此我们不会在shader中使用反三角函数。我们使用一个更简单的公式:
其中,k
是一个常数,用于控制阴影的软硬程度。增大 k
会得到更软的阴影,而减小 k
会得到更硬的阴影。
当然,计算真正的“Safe Angle”需要使用反三角函数(如arcsin)。为了性能考虑,在shader计算中通常会避免这样做。所以,代替直接计算“Safe Angle”,人们通常会使用一个与安全距离相关的比值来估计它,然后使用这个估计值来决定阴影的软硬程度。
优点就是,一旦SDF完成了计算,那么渲染效率是非常高的,计算出的阴影边缘也非常平滑。
但是缺点也很显著,SDF的存储空间占用很大,复杂的场景预计算时间也很长,动态场景就需要重新计算SDF。
为什么我们要先讲SDF呢?因为SDF提供了一种高效、精确的方式来描述场景中的物体。这在计算环境光照时尤其有用,特别是当考虑到遮挡、反射或间接光照等因素时。
接下来我们进入正题,讲讲环境光照。
当使用Signed Distance Fields进行渲染时,Ray Marching可以高效地模拟环境光照,特别是环境遮挡和间接光照。通过在SDF上进行多次Ray Marching,可以模拟光在场景中的反弹,从而得到全局光照的效果。
从渲染方程(没有写visibility项)的角度来看,环境光照可以被视为全局光照的一个简化。
当我们只关心环境光照时,这个方程可以大大简化。
目前的渲染方程就只有积分里面的两项了,分别是BRDF项和Lighting项。
其中BRDF项包含了两个部分,分别是diffuse和specular。
其中,假设这里的BRDF的漫反射项使用的是 $f_\text{Lambert}$ (即Lambertian模型),specular项使用的是Cook-Torrance模型。
漫反射项:
高光项:
其中,DGF是非常经典的三项。
首先是D项,描述的是微表面法线的分布,主流使用的是GGX模型。
GGX模型相比于Phong模型和Beckmann模型而言,高光反射过渡更加柔和。Roughness是由 $\alpha$ 控制的。
接下来是F项,大多数都是Schlick近似模型。
最后是G项。可以使用Schlick的几何遮挡近似。
BRDF项示意图。
目前为止,渲染方程的计算已经明确,我们可以用蒙特卡洛方法计算。
但是,蒙特卡洛方法的准确性通常与使用的样本数量成正比。一个简单的像素可能需要数百到数千次样本来减少噪声,而在实时渲染中,但凡涉及到采样的方法都会非常慢,因此我们需要避免。并且,每次光线与物体交互时,都需要计算该物体的BRDF,非常复杂。
接下来,我们需要化简这个积分,也就是对环境光照和BRDF项都做化简。
下面这个公式想必大家都非常熟悉,用于估算两个函数的乘积在一个域上的积分。
当 $g(x)$ 在 $\Omega_G$ 内近似为常数或者变化不大的时候,结果会比较准确。而此处的 $g(x)$ 正好可看作BRDF。因此,我们将渲染方程化简为下式。
现在这个式子相当牛逼啊,把环境光项和BRDF分开了。注意看,拆出来的这一部分(下式),$\Omega_G$ 表示表面粗糙度对应的微小的角度范围或方向范围。
就目前而言,我们可以通过预计算得到一张完整的环境贴图(也可以实时渲染)。为了计算此处的积分,我们需要在 $\Omega_{f_r}$ 的区域内做积分。此处我们还可以进一步化简,即「预先对纹理进行滤波操作(模糊),我们只需要对滤波后的环境贴图采样一次光线方向就能得到积分」。
通过预计算的方式,将上面的公式化简为了一个更为简单的函数,假设滤波后的环境贴图是$L_{i}'$:
不再需要计算积分,因为这已经在预滤波过程中完成了。您只需要从已滤波的环境贴图中进行简单的采样。
对于不同的BRDF,会有不同的Lobe。因此,决定Lobe的因素有很多,包括:粗糙度、金属度、不同的BRDF模型等等。
其中,粗糙度是决定Lobe宽度的主要因素。平滑的表面(如镜子)具有非常尖锐的Lobe。粗糙的表面会使光线散射到更广的角度范围,导致更宽的Lobe。在实际开发中,我们可以直接根据粗糙度来使用不同的Lobe,像下面代码所示。而不同的Lobe就需要采用不同层级的Mipmap进行采样。
当考虑环境光和BRDF的交互时,我们实际上是在对两者进行卷积。对于一个平滑的表面,其BRDF的反射Lobe非常尖锐,相对地,一个粗糙的表面有一个宽广的BRDF Lobe。对于每种粗糙度,环境光照都被预先模糊,并存储在特定的mipmap级别中。为了得到更平滑的结果,可以用三线性插值。
目前的渲染方程,已经被我们整成下面这样了,我们已经高效解决了左边这个部分,接下来就专心处理BRDF的部分即可(蓝色框框的内容)。
在分析渲染方程的时候我们详细探讨了基于微表面材质的BRDF项。
其实最重要的,
菲涅尔项(F):垂直看向物体有多少能量被反射,不垂直又会反射多少能量
微表面的法线分布(D):如果法线大多数都垂直于平面,那么就比较接近镜面
渲染方程中,我们要对上面这一个方程在半球面上积分,相当的麻烦。并且我们发现,这个方程接收的参数非常多,是一个五维的函数(Roughness、入射角度的极角和方位角、出射角度的极角和方位角)。因此,我们需要降维。
这里使用Schlick近似,代入到渲染方程中。
Schlick近似:
那么至此,BRDF就变成了一个二维的函数了。输入参数是 粗糙度(Roughness) 和 法线与半向量之间的夹角 $cos(\theta )$ 。这样就可以愉快的预计算了,最终得到specular的IBL结果。这也就是我们平时所说的二维LUT(Look-Up Table)。
最终,我们避免了采样,完成了渲染方程的计算。而这个方法叫做 「Split Sum方法」。
很多复杂的函数可以通过一组更简单的基函数来逼近或表示。傅立叶变换就是其中典型的例子。下图是一系列基函数,上图是表示不同数量基函数逼近一个复杂的方波函数。
这种思想还运用在各种领域,其中一个就是现在要讲的球谐函数Spherical Harmonics。这个球谐函数是定义在球面空间上的一系列的二维的基函数的集合。
每一个球谐函数的基函数都是由「勒让德多项式」构成的。这里买弄悬虚一下,贴出公式。以下内容参考Wiki百科。
球谐函数可以表示为:
其中:
$\theta$ 是从正 $z$ 轴到点的方向的夹角(即天顶角或与极轴的夹角),它的范围是 $0, \pi$ 。
$\phi$ 是从正 $x$ 轴到点在 $x y$ 平面上的投影的方向的夹角(即方位角),它的范围是 $[0, 2 / \pi)$。
$P_l^m$ 是关联的勒让德多项式。
$e^{i m \phi}$ 是复指数函数, 为球谐函数引入方位依赖性。
球谐函数之间具有正交性质。这意味着当对单位球面上的任意两个不同的球谐函数进行积分时, 它们的积分为零:
其中 $\delta$ 是Kronecker delta函数, 只有当 $l=l^{\prime}$ 和 $m=m^{\prime}$ 时它才等于 1 , 否则 等于0。
接下来再介绍一个非常有用的性质,投影性质。
假设我们有一个定义在单位球面上的函数 $f(\theta, \phi)$ ,我们可以将其展开为球谐函数的线性组合:
其中系数 $a_l^m$ 可以通过与相应的球谐函数进行内积来得到:
这里的 $*$ 表示复共轭。因为球谐函数是正交的,所以这种投影会得到与 $Y_l^m(\theta, \phi)$ 关联的 $f(\theta, \phi)$ 的“成分”或“权重”。
上面这一部分看不懂也没关系,但是我们需要知道的是,对于一个环境光照,我们用一个球面函数来表示。而这个球面函数,可以用一系列的球谐函数逼近/近似。更高的阶数对应更高的频率,更高的频率意味着对环境光照更精确的模拟程度。
而刚刚说的投影性质,正好可以帮助我们求得给定一个球面函数(环境光照)对应阶数的所有对应球谐函数的基函数的系数。这里还有一个理解「投影」的方式,「得到系数的过程就叫做投影」。就像我们投影一个向量,得到对应系数那样,是一样的道理。上面提到的Kronecker delta的性质也恰恰体现了正交的性质。举个例子,当只有x轴投影到同一个x轴的时候,他才不可能是0,否则结果都是0。
接下来,当环境光照照亮一个diffuse的BRDF物体时,可以将散射BRDF视为低通滤波器。因为漫反射BRDF没有对特定方向的偏好,它不会增强或衰减任何特定方向的光照变化,这正是低通滤波器的特性。
使用低阶的球谐函数(例如只使用L=2或L=3)来近似环境光照时,我们主要捕捉到的是光照的低频部分。因此,在渲染漫反射BRDF的材质时,可以使用球谐函数。
我们将环境光照分别乘上各个基函数然后在球面上做积分,就可以得到各个基函数对应的系数了。
计算球谐函数的系数,可以利用球谐函数的正交性,对两边进行积分。至于背后的数学原理在此处不展开,下面直接给出结论。
这里, $\Omega$ 表示整个球面。这个积分实际上是环境光照函数和球谐基函数之间的点积。
如果想恢复原始的环境光照图,就将所有的系数乘上对应基函数然后累加即可。
其中, $L_{l m}$ 是球谐函数的系数, $Y_{l m}(\omega)$ 是球谐基函数。
换句话说,在做Shading的时候,环境光照在整个半球上都有一定的分布,给定Shading Point的法线之后,将环境光照的球谐系数与Shading Point的法线方向上的球谐函数值相乘,最终就可以得到需要的Shading了。下图回顾整个思路。
我们已经学习了基函数的使用:
使用足够多的基函数可以表示任何函数。
通过使用较少的基函数,我们可以保持特定的频率内容(低频)。
通过某种方式(如何实现仍待明确),我们将积分简化为点积。
目前为止,我们仍然只是从环境光照中进行着色,而没有考虑阴影的效果。
下一步:预计算辐射度传输(PRT)
这种方法可以处理阴影和全局照明问题!但是这种方法的代价是什么呢?
这里就要考虑visibility项了。对于没有遮挡的方向,该值为1,否则为0。这样,当我们计算环境光影响时,遮挡项可以通过逐元素乘法与光照项相乘,从而考虑到阴影效果。
因此我们利用基函数的基本原理把一些东西先预计算出来,从而节省开销。否则,计算量是非常大的。
PRT的基础思想是把渲染方程分为两个部分,会动的和不会动的。且两个部分都是球面函数,可以用球谐函数化简。
当BRDF是diffuse时,BRDF就是一个常数,因此我们把BRDF提到外面。
然后 $L_i$ 写成基函数的形式,并且把 $l_i$ 常数系数放在积分号外面,求和符号也可以近似放出来。
这样,就可以把积分部分预计算了,最终用的时候就是几个点积的操作。
但是,这是有代价的,即「场景不能动了」。
Glossy物体的BRDF不仅与入射角度有关而且还和观察角度有关,不同视角观察物体表面同一点会有不同的光照。
因此,右边那个积分就不能简单的化简为一个常数 $T_i$ 了,而是一个包含观察方向 $o$ 的函数 $T_i(o)$ 。
但是不慌,我们延续之前的思路继续SH近似它!
基本思路是将 $T_i(o)$ 展开为球谐函数的线性组合, 即 :
其中, 系数 $a_l^m$ 可以通过类似之前的方法来计算 :
也就是说,我们采样其中一些观察角度做预计算。
如果选取16个基函数,在Glossy中,每一个Shading Point的计算:长度16的向量 乘上 16*16的光传输矩阵。
如果我们要求更多的基函数才能达到满意的效果,那么PRT恐怕就没办法解决了,也就是不能用SH来做基函数。
另外值得一说的是,我们可以在控制好基函数数量的情况下,将预计算的质量做得相对较高。计算多次反射、甚至于Path Tracing这种脏活累活也自然可以交给预计算处理。反正最终到实时渲染的时候也只是做基函数数量的点乘而已。
上一节中,我们将渲染方程看作 Lighting 和 Light Transport 。如下图,Light Transport 中横线部分是BRDF,则绿色圈圈我们可以理解为某种类似光照的基函数。
预计算辐射度传输(PRT)很牛逼,在预计算阶段,时间通常不是限制因素。因此,可以使用高质量的算法,例如路径追踪、考虑多次反射,来捕获场景的细节和复杂的光-物体相互作用。
球谐函数和基函数也是很神奇的工具。即使使用了较多的基函数,实时阶段的操作也是线性的,并且与基函数数量成正比,因此速度非常快。
局限也非常明显。PRT尤其适用于那些几何和材料属性不频繁变化的场景。而动态物体(如玩家和敌人)可能需要其他实时光照技术。
首先全局光照技术可以分为以下三类:
基于画面空间的(Image Space):Reflective Shadow Maps
基于3D空间的:Light Propagation Volumes、Voxel Global Illumination
基于屏幕空间的(Screen Space):Screen Space Ambient Occlusion、Screen Space Directional Occlusion
为了得到全局光照,我们就需要看渲染方程中的间接光照。
RSM一般会直接用于的场景是:手电筒。
直观地说,为了得到间接光照,我们将直接光照能够照射到的各个地方(一般会选取关键的位置)视作一个新的光源,这个新的光源仅为了计算间接光照。
RSM其实是Shadow Mapping的拓展。了解RSM之前先回顾一下什么是SM。
Shadow Mapping 是用于生成物体阴影的一种方法,通过对灯光源视角的场景进行渲染并创建深度图(也称为阴影图)来实现阴影效果。
而RSM就是在SM生成的这张深度图中添加更多的信息,每个像素的法线和辐射。
也就是说,RSM由三项内容构成:深度值(depth)、法线(world space normals)和辐射(flux)。
每一个Shadow Map的像素,都对应到场景里面的一个小的纹素(Texel)。那么我们会注意到一个问题,就是一张Shadow Map一般是 $512512$ 的分辨率。也就是有 $512512$ 个次级光源,照亮一个shading point,考虑当前摄像机的观察方向,这个计算量是非常大的。
我们如何从一个特定的观察方向获取的信息中计算出从光源出发的所有其他方向的辐射度(radiances),这是极其困难的。因此,为了化简,这里做了一个大胆的假设,将所有的的次级光源照射的Shading Point的材质当作Diffuse。
Games202-Lecture5-7