20400 字
102 分钟
《光线追踪精粹》笔记(个人整理后)

光线追踪精粹:原书来源

一、光线追踪基础#

1. 引入#

重要性采样#

指使用不均匀分布的PDF(概率密度函数)采样来减少误差。

准蒙特卡洛采样#

使用数论方法的样本低差异模式代替传统的伪随机数生成器来创建随机样本的方法。

2. 光线的表示方式#

P(t)=O+tdP(t) = O + td ,其中OO是空间中的一个点(光线射线的原点),dd是光线的方向。一般dd是归一化后的,这样tt就是距离了。

tt是距离带来什么好处?

答:这样可以用tmintmintmaxtmax表示光线前进的最近最远的值,方便光线停止。

DXR中的光鲜数据结构:

struct RayDesc {
    float3 origin;
    float3 Direction;
    float tmin;
    float tmax
}

3. 光线追踪器中的必要shader(DX12为例)#

1

  1. 光线生成着色器,启动整个管线,允许开发人员使用内置TraceRay来指定要启动哪些光线

  2. 相交着色器,

  3. 任意命中着色器,可以丢弃无用的相交(例如忽略透明的物体)

  4. 最近命中着色器(主要是计算颜色)

  5. 未命中着色器

    光线追踪的伪代码如下:

2

一般使用normal(法线分布函数决定下一个光线往哪里迭代)

BLAS(底层加速结构)和TLAS(顶层加速结构)#

底层加速结构包括几何土元和程序化生成的图元。而TLAS包含一个或者多个BLAS,BLAS构建慢,但是求交快,TLAS构建快但是过度使用会影响性能。在动态场景中,如果只是节点包围盒发生了变化,refit就可以。但如果一直refit而不rebuild又会降低求交效率。所以要平衡refit和rebuild(rebuild慢)

DX12只需要输入VB和IB就可以调用接口直接构建加速结构,包括BLAS和TLAS

着色器表#

着色器表是GPU内存中按照64位对齐的连续块,用来存储光追着色器数据和场景资源绑定。下面是其中一种布局方式。

3

在实际使用的过程当中,我们需要使用map来对shader table里面的内容赋值,包括不同着色器的identifier等,需要自己设置不同shader的offset。

4. 球幕相机#

第4章介绍的是一种特殊的camera,这种camera可以渲染全景(类似手机相机里面的全景拍照)和环形立体投影(类似VR的双眼)。这一章最重要的是如何根据屏幕像素点的位置计算半球面的仰角和方向角(如下图的屏幕空间)。具体方法是用弧度除以像素数,得到每像素弧度,然后再根据像素点到中心点(摄像机的位置)计算方向角,最后计算高度,从而得到光线的方向。

例如对于4096*4096分辨率的图片,球幕总弧度为180度(π),则每像素弧度是π/4096,那么I像素点的方向就是p = (I-M)*π/4096,其中M是摄像机位置。

4

球幕相机还可以做左右眼的偏移,具体可以看该书的代码。

5. 避免自相交的快速可靠的方法#

本章给了一个问题“给定数组A,他由N个数字(Ai)组成,如何快速的查询数组任意区间内的最小值和最大值,例如第8个元素和第23个元素之间的最小值和最大值”。

这种方法在ray marching的时候会用到。

方法一:暴力法:预计算一个NxN的矩阵,每一个元素(i, j)表示第i个元素到第j个元素之间的最大值和最小值。这种方法的存储空间复杂度是o(N2),查询复杂度是O(1),修改复杂度是O(N2)

方法二:稀疏表查询法,是一种对暴力法的优化,他的想法是认为所有的序列(i-j)其实都是两个(2的整数倍长度)序列的并集,描述比较麻烦,可以看下图所示:L1是只存储长度为2的序列的最小值,L2只存储从该位置开始长度为4的最小值,以此类推,那么A2-A8总共7个数,相当于两个4个数序列的并集,即最后一张图的3和4,那么A2-A8的最小值就是3。这种方法的空间复杂度是O(NlogN),查询复杂度是O(1),修改复杂度是O(logN)

5

方法三:区间树递归查询法。区间树如下所示,每个节点存储所有子节点的最大值和最小值和对应的index范围。查询的时候需要递归的向下查询,本方法的空间复杂度是O(N),查询的复杂度是O(logN),修改的复杂度也是O(N)

6

方法四:区间树迭代查询法,这是一组二叉树:

7

其查询伪代码为:8

本质就是左边如果是奇数,右边index如果是偶数,那么就直接merge到result上面(因为这两个地方再往上一个level就包含了另一个数了)。

该方法的时间和空间复杂度都和递归法相同,但是因为不需要递归,常数节省非常多,所以效率很高。是最常用的方法。

二、相交和效率#

6. 避免自相交的快速可靠的方法#

传统方法:使用光线命中距离代入光线方程(当光线传输距离过长时,这种方法会因为精度误差问题导致交点不在平面上,不利于解决自相交问题(添加bias))

采用质心坐标的参数化方法:用光线方程和三角形相交时点的质心坐标来表示相交点,因为质心坐标也有精度问题,导致算出的交点可能不在原光线上,但是仍然在相交平面上,在解决自相交问题时会好一点。

避免自相交:即使把新光线的起点“精确”放到表面上,仍然会产生自相交,因为起点到表面的距离可能不是0,下面是一些常用方法:

  1. 图片ID排除法:显式的排除以橡胶的图元,问题在于如果交点在共同的边上,或者新光线和表面夹角比较小,仍然会自相交;(2,无法处理重复或者重叠的几何体。(3,只适合平面的图元。如果图片不是平面的,则可能会产生有效的自相交。
  2. 限制光线区间:设置光线相交距离的最小值ε>0ε > 0,这种方法需要根据不同场景调整εε 的值,不够可靠和通用。也会出现小夹角下(距离足够长)时的自相交,或者错过了一个临近的表面的有效交点(例如交点旁边有个垂直的面)如下图所示。

9

  1. 沿着色法向量或者原光线方向偏移,和方法2的问题类似。而且因为插值和法线贴图的原因,着色法向量可能不垂直于表面。
  2. 沿几何法向量做自适应的偏移。这个方法是书中推荐的方法,他认为误差的大小和交点距离原点(0,0,0)的距离成正比,距离越远,误差越大,εε 应该根据这个距离动态调整。该算法实际上是设置了一个阈值origin(),比这个阈值小的距离则直接加上normal的偏移,比这个阈值大的距离,则转换到整数空间做偏移后再转换到浮点数,以减小不同距离下的浮点数误差。如下所示 1011

7. 光线和球体相交检测的精度提升#

光线球体相交的通用解法:设光线为R(t)=O+tdR(t) = O + td,球的方程为(PG)(PG)=rr(P-G)\cdot(P-G) = r \cdot r,(其中G是球体的中心点),直接代入公式可以判断是否有交点以及交点位置P0。并且能够知道在交点处的单位法向量是(P0G)r\frac{(P_0-G)}{r}

因为浮点误差,这种方法在球距离光源很远的时候会出现问题,如下图所示,从左到右是单位球距离相机100,2000,4100和8000时的效果。

12

同样因为浮点误差,在光线靠近一个巨大的球体时,也会出现问题,如下图所示:

13

为什么会出现浮点误差:浮点数是spow(2,e)s\cdot pow(2, e)的表示形式,当加减法时,会把ssee做对齐,较小的浮点数尾数就会被右移,这样精度就会降低。这种问题在计算c=ffrrc=f\cdot f - r \cdot r时很明显例如b24acb^2 - 4ac,平方后导致可用精度减半,再相减就会以更小的精度为保留。

15

14

16

更好的解法:书中给了一个更好的解法,如上图所示,f=OGf = O-G。这种方法的基本思想是做点乘之前前先做减法。

巨量消失:当两个非常接近的浮点数做减法时,会保留非常小量的有效位数。当光线和一个巨大球体的交点落在光线起点附近时就会出现这种情况。(bb24acb ≈ b^2 - 4ac时)

解决巨量消失的方法:利用二次方程两个解 t0t1=cat_0 \cdot t_1 = \frac{c}{a},使用以下方程求解其中一个解,避免两个相近的数相减(让bb翻倍):

17

8. 计算光线和双线性曲面相交的几何方法#

双线性曲面:是支持但不计算光线相交的最简单的曲面,其定义如下图所示:

18

这种双线性曲面其实是有四个控制点,这可以看成是一个四边形,或者是用两个三角形来表示。而如果想要把三角形网格转换成参数曲面网格,则需要有一种专门的方法做这个事情。

可以把三角形看成退化的四边形,这样就可以用(1u)(1v)(1-u)(1-v)uu(1u)v(1-u)v来表示三角形了。

GARP:这个方法是一种把三角形组装成四边形后求交的方法。他把四边形组成的3维曲面,通过和光线求公式,得到一个或者两个(参数化曲面可能有自重叠的情况)解,每一个解都用uvuvtt来表示。该方法需要较好的数学基础和3维空间的想象能力,在求叉乘和abcabc的时候有些没搞明白,代码如下:

19

20

21

9. DXR中的多重命中光线追踪#

多重命中:指的是在命中一个面以后光线继续前进,一根光线命中多个面并返回多个面的信息的情况,通常用来模拟弹道穿透、射频广播等领域。多重命中仍然需要高效进行。

多重命中的暴力遍历法:暴力遍历就是按照正常情况,在anyhitshader里面ignorehit持续遍历,并且记录下来经过的相交点的信息(包括漫反射颜色、距离和法线等),这种方法比较灵活,但是比较慢,每一次相交基本都要遍历所有的BVH节点。

节点剔除多重命中BVH遍历:其实就是当已经收集了N>=NqueryN >= Nquery的时候,判断当前交点是否比已知的最远节点还要远,如果还要远,则直接剔除掉。如果,如下所示:

22

实验数据显示,anyhit shader和intersection shader实现暴力和节点剔除的效率完全不同,这是为什么???????

答:书最后给了解释,认为在相交着色器中有很多“区间更新”的操作,会做更频繁的剔除工作,这个工作比节省的开销还要大。

10. 一种具有高扩展效率的简单负载均衡方案#

简单来说就是把大量像素均匀的分配给各个处理单元。

一个简单的例子:一根光线直接打到环境球上,另一根光线在汽车的前灯处反复反弹,这两根光线产生的开销就完全不同。且这种开销无法预知。 负载均衡应该适时的考虑不同CPU或者GPU计算单元的计算能力,给计算能力强的单元更多的任务。

简单分块的负载均衡方法:如果有4个CPU,那么把整块图像均匀的分成四块是比较简单的方法,这种方法的缺点在于可能某一块的场景十分复杂,计算比较慢。其他三块都在等他

按任务大小的负载均衡方法:把简单分块的块分的足够小(每个像素),这种方法对缓存不太友好。

Task Distribution的方法:nn个像素的图像被划分成mm个区域,,每个区域有ss个像素,且mm是2的幂次m=b2m=b^2mm需要保证每个区域都至少有ss个像素(例如128)且mm越大越好。假设处理器有PP个,那么{0,1,2,…,m-1}个区域应该被划分成pp个连续的区间。区间的长度应正比于处理器的相对性能。最后把每个区域都做一个重映射(例如把每个下标的最低bb比特位左右反转)从而是使区域均匀分布。代码如下所示:

23

24

因为“例如把每个下标的最低bb比特位左右反转”这个方法自己是自己的逆函数,所以想要重新找到绘制块然后将所有的块组装起来也是很简单的,如下图所示。

25

三、反射、折射和阴影#

11.自动处理相邻Volumes的材质#

当两个不同材质的物体相邻时,会出现材质相邻的情况(例如装着水的水杯,水杯和水是不同的材质,也是不同的mesh)在光追的时候,这两个mesh 的关系影响到最终的效果。可以把两个mesh中间留一些缝隙,也可以让两个mesh 稍微重叠一点(这个时候需要设置不同mesh材质的优先级),(但是不能合并两个mesh,因为合并两个mesh会让一个mesh有不同的材质)。这一章就讲了如何处理这种问题

处理相邻材质的算法:维护一个栈,这个栈上表示光线进入材质的材质index,当光线进入一个新的材质,就push进去新材质的index,当光线逃出一个材质时,就把逃出材质的index pop出来。材质被引用的奇偶性表示是否在某个材质内部。这有下面三种情况:

  1. 对于反射,需要 pop top元素
  2. 对于折射,pop top元素以后还需要删除以前对该材质的引用
  3. 对于相同的材质边界,保持堆栈不便。
  4. 如果相机本身就在一个介质内,则初始情况就要有一个初始堆栈。
  5. 下面是代码

26

27

28

29

30

12.基于微表面阴影函数来解决凹凸贴图中的阴影边界问题#

在使用凹凸贴图时,因为对法线的扰动会导致出现阴影硬边,这篇文章就是解决这个问题的。

阴影硬边出现的原因在于凹凸贴图对法线的扰动是不均匀的:12-32

12-31

这篇文章主要的贡献在于,对于这种被扰动的法线,他会使用GGX法线分布函数对这个扰动做修正,从而达到平滑的效果。在他的方法里面,最重要的两个公式分别为,G1就是阴影因子(当前法线阴影项的合理概率),αggx则是GGX金丝来的粗糙度,根据这两个值,文章给了代码的实现:θi是入射光方向和真实表面法线的夹角,θd是表面法线和凹凸发现的夹角。

12-33

12-34

12-35

13.光线追踪实时阴影#

光线追踪相比较shadow map的优势:

  1. 避免了因为shadow map分辨率不足导致的锯齿状阴影
  2. 避免了peter panning
  3. 可以处理半影(软阴影)
  4. 可以对半透明物体产生的阴影做处理。

本文的一些加速方法:

  1. 只对半影区域做密集采样,对完全阴影和完全光照的地方做稀疏采样。文章中存储前四帧所有光源的可见性到一个四通道纹理中,一个通道存储一帧所有光源的可见性,半影通常发生在可见性发生变化的区域,(在这些区域需要做密集采样)。除此以外,文章还使用了一个5x5的最大值滤波器和一个13x13的低通滤波器,最大值滤波器保证周围的像素点也会受到一个超大变化量的影响(一个超大变化量会影响多个像素),而低通滤波器则可以防止快速运动时的闪烁
  2. 时域采样复用,使用reprojection技术,
  3. 如何增加或减少采样数量:如果可见性变化大于一个阈值,S=min(Smax,S+1)S = min(S_{max}, S+1)如果可见性小于一个阈值,且前四帧的采样数恒定,S=max(0,S1)S = max(0, S-1)。对于reprojection失败的像素,直接按照SmaxS_{max}做采样,当屏幕上大部分像素都重投影失败时,为了防止性能下降,可以将SmaxS_{max}的值降低。
  4. 采样mask:当采样数降到0时,表示这个像素一直没有变化,可以直接复用之前的shading结果,单这样可能会发生误差累积,所以文中的策略是把屏幕分成多个4x4的块,每次要强制更新这4x4pixel里面的像素,把2x2=4和块看成一组,四帧里面每组更新其中一个块,如下所示: 13-4
  5. 计算可见性:空间滤波+时间滤波,类似SVGF的方式。最后输出的是降噪后的全图的可见性buffer,最后则会根据这个可见性buffer来做阴影着色。
  6. 在计算灯光的可见性之前会先做light culling,把过远的灯光都cull掉。

14.用DXR实现的Ray-Guided的单散射介质体积水焦散#

焦散的传统绘制方式:首先确定水面的position和normal,从光源处绘制一个水表面的pos和normal的图,这个叫做焦散图。从这些位置出发,一部分光线发生折射并且和水下的纹理相交,相交的位置存储在折射焦散图里面,一部分光线发生反射,和墙壁等水上场景相交,相交的结果存储在反射焦散图里面。这些焦散图在后面体积光切片时会被用到。

与场景求交的方法(base):

  1. 对shadow map和depth buffer做raymarching,来找到交点,但是可能会有一部分场景同时被shadow map和depth buffer挡住

  2. 对shadow map和depth buffer做多layer,在多个视角做shadow map和depth buffer,从而产生多张图,这种方法复杂度很高,代价也很高。

  3. 把水下场景体素化,对体素做raymarching。但是这样做非常慢。

    本文的方法(使用DXR):

    下图公式描述了从眼睛E向右看时,射入眼睛里的辐照度公式。

    其中PP是射向EE的线的中点,ΩΩ是所有折射进来的光线,

    ττ : 是水体积的消光系数,

    l(ω)+PEl(ω)+|P-E| :光线到达P点之前沿着水下传播的消光,PEP-EPP点到EE点的长度(消光(or吸光),值的是光强度发生了衰减)

    σs(P)σs(P):点PP出的散射系数

    p(EP,ω)p(E − P, ω):相位函数,决定了有多少从折射光方向散射到PP

    LinL_{in} : 沿折射光方向照射到PP点处的辐照度

    vv : 沿折射光方向的可见度,例如折射光线是否到达了PP点?有两种可能的近似方案来对所有的散射事件做积分计算:

    1. 使用3D grid去累积每个网格中心的离散值,这里需要使用足够高的分辨率防止漏光。

      (1)水面上的点到场景交点需要有足够多的光线,对于每一个到达grid cell的折射光线,计算距离grid cell中心最近的点P

      (2)计算从P点到达眼睛的相位函数和透射辐照度

      (3)对网格单元中透射的辐照度进行离散积分。

    2. 创建一个足够密集的三角形光束体,来近似使用渲染管线和additive blending计算的散射积分。下一章提供了一个避免三角形和水面三角形因折射光线方向的快速变化产生非凸包的情况。

    14-3

    14-4

计算光束压缩比:所谓三角形凸包,其实就是水面三角形和水底下折射三角形(每个水面三角形发出的折射光线和水底相交)形成的几何体,如图所示:

14-7

这两个三角形会形成一个光束压缩比,计算方式为,水面三角形面积除以水底三角形面积,这个压缩比用来描述三角形光束形成非凸体积的可能性,或者控制把每个凸包细分为更小光束的密度。

渲染焦散图:将所有水面三角形用PS写到两个texture上,一个是water surface的3D position,另一个是这个water surface的surface normal

光线追踪折射焦散图和累积表面焦散:使用DXR对上一步得到的焦散图中的有效像素做光线追踪,与场景的交点存到折射焦散图里面,此外交点位置会被变换到screenspace,并且会被用来累积表面焦散散射。

  1. 对于焦散图中每个pixel做trace
  2. 计算光线和水下场景几何体的交点,有些情况下可以把这个光线cull掉,例如一个shadow map test发现这个光线被水表面上方的某个几何体遮挡住的时候。
  3. 将交点的位置写到折射焦散图中。
  4. 可选,沿着折射光线与场景交点的反射方向二次trace一个光线,并且把交点存到一个反弹的焦散图中
  5. 在一个offscreen buffer里面对焦散做累加(离散积分)。其步骤是,把交点转换到screenspace上,并且用InterlockedAdd在该屏幕位置做累积。累积的辐照度值可以用之前的压缩比来做缩放,也可以根据距离和吸光系数来做缩放。

自适应水表面三角形的曲面细分:这一个步骤主要是为了

  1. 防止三角形光束变成非凸包

  2. 尽量接近散射积分的理想结果而提供足够多的slice

  3. 确保没有体积光穿透场景中的小物体而造成漏光

    曲面细分和非凸包如下所示:

    14-1214-5

构建三角形光束体

使用geometry shader 对经过曲面细分的三角形构建三角形光束体对应的triangulated hull

  1. 把输入顶点映射到焦散图或者折射焦散图空间
  2. 从焦散图中读取光束顶部三角形的position
  3. 从焦散图中读取光束底部三角形的position
  4. 构建八个三角形。
  5. 计算每个输出顶点处的三角形光束的估计厚度,这样厚度就会通过插值传递给VS
  6. 计算每个输出顶点的光线方向。

使用 addivice blending渲染体积焦散

使用PS做additive blending,根据当前3D position和茶之后的光线方向,计算相位函数(phase function),把散射项乘以插值后的厚度。

结合表面焦散和体积焦散

  1. 对表面焦散做降噪和模糊

  2. 用降噪后的表面焦散照亮场景,例如乘上GBuffer中的albedo

  3. 对体积焦散结果做模糊并把它添加到经过了光照的GBuffer,从而实现两个焦散的结合。

    整个步骤如下所示

    14-6

四、采样#

15.重要性采样#

普通的蒙特卡洛采样(这里面XX是一组nn维的随机数,其实就是均匀的对函数做采样,得到的期望值):

15-1

例如算AO的时候,一个PP点的环境光遮蔽函数aa如下式定义,f(x)f(x)这个时候就是可见性:

15-2

用蒙特卡洛采样上式函数就如下所示:

15-3

重要性采样其实就是给每一个采样点一个权值,权值越高,则为了保证贡献相同,这个地方的采样点的概率就应该越低,如下式所示。为了保证下式是无偏的,P(xi)P(x_i)越高,则f(xi)f(x_i)也应该越高

15-4

则基于重要性采样的环境光遮蔽函数就如下所示(按照正比于cosθcosθ的概率生成光线(θθ是相对于表面法线的夹角,因为接近水平的光线不需要很多)p(x)p(x)越接近于被积函数f(x)f(x),效果越好,但是f(x)f(x)通常未知):

15-5

蒙特卡洛积分使用方差来衡量误差,随机变量XX的方差定义如下:

15-6

方差是用来衡量随机变量和期望值(均值)之间的差距,如果方差小,则说明和均值是接近的。而样本数越多,方差越小,误差越小。一般来说方差是误差的平方。光线数翻倍,则方差减半,所以从1条光线到2条光线误差降低非常快,但是1000条光线到2000条光线误差就显得没那么快了。(所以好的采样点,降噪算法非常重要)

与此同时,把光线传播距离(距离近的光源采样光线数量多)和光源能量(光强度大的光源采样光线数量多)可以减小方差,从而提升采样效率。

16.采样变换#

本章主要是将如何按照期望的概率密度分布函数生成样本的方法,主要是在特定域中做变换。

中间涉及到如何在一维空间做均匀采样,如何把一个均匀分布的函数转换成另一个函数,使另一个函数的采样点也是均匀分布的,包括正态分布,离散采样,二维双线性采样,二维纹理分布采样,利用mipmap的树状结构纹理采样,均匀表面采样,三角形到正方形的变换,三角形网格采样,球型采样。PHONE模型采样,GGX采样等技术。

在本章中,我们可以学到在不同几何表示上如何做均匀采样,我认为是非常重要的,但是内容太多,每一种采样类型都可以写好多笔记,这一章还是当成字典多看书吧。

17.消除光线追踪中的亮光点#

光线追踪经常会出现图像中有一些非常明亮的噪点,这些噪点是因为当场景中有一些完全反射的物体时,光线追踪时会有一些光线恰好达到这个完全反射的物体上,然后又trace到光源上面,因为光源通常是(500, 500, 500)这样的亮度(为了保证整个场景的亮度),所以这些采样点就会出现过爆的现象,如下图所示:

17-2

解决这个办法有两个,一个是在最后着色时,把超过1的像素亮度clamp掉,这样会导致能量损失,最后可能光球下面的焦散就没了。

另一种方法叫path regularization,就是直接对具有反射材质的BSDF公式做blur(是blur BSDF,不是blur噪点),这样每一个点就都不会过爆,但是大部分射到反射材质的点亮度都会比较高,这样相当于拥有多个不那么亮的亮点了

18.GPU实现的多光源重要性采样#

这个和RTXDI的采样基本一致,就是认为采样的时候,应该按照每个光源的贡献,成比例的概率去采样,这就需要一个全局的光源概率PDF。

本章中把所有光源都用一个分层的树状加速结构来知道采样,每个节点代表一组光源,在每一层中估计每组光源的贡献,并且在每层随机选择向下遍历的路径,如下所示:

给一个着色点XX,每一层按照概率估计每个相邻子节点对XX的重要性,最后,用一个服从均匀分布的随机数来决定树上的路径,越重要的光源采样概率越高。

重要性指标:

  1. 辐射通量,光源越强,贡献越大
  2. 到着色点的距离,距离越近,贡献越大
  3. 光源的朝向
  4. 光源的可见性,不可见的光源没有贡献
  5. 着色点的BRDF,在BRDF主方向上贡献更大。

18-1

为了处理含有大量光源的场景,避免采样数量过多,有一些比较经典的传统方法来加速渲染,例如建立空间加速架构,光源剔除,重要性采样光源等方法。

  1. 实时光源剔除:人为的限制每个光源的影响范围,使光源强度在某个距离变为00,从而限制影响给定点的光源数量,再加上tile base shading,把光源放在tile上。使用per tile的light tree来提高剔除率。
  2. VPL:VPL的想法是追踪从光源发出的光,并且在路径上存储VPL,利用这些VPL来近似间接光照,VPL的概念有点像对多光源做重要性采样,把多光源聚类成cluster并且作为树的一个节点,在遍历的时候估计每个节点的贡献。不过VPL和多光源重要性采样也是有区别的,VPL是直接把估计直接用来光照上,而多光源重要性采样则是把估计值用来选择哪一个光源。
  3. 光源的重要性采样:主要是按照贡献对光源排序,只对大于一定阈值的光源做可见性检测,然后基于可见性的统计去添加剩余光的贡献。有的人用八叉树划分光源,通过八叉树节点上所有光源的贡献就是这个八叉树节点的贡献;有的人把空间做均匀细分;有的人使用kd树或者BVH,并且限制光源的影响范围,通过随机选择光源范围来缓解因为限制光源范围导致的能量损失偏差;Iray使用了hierarchical light importance sampling ,只使用三角形,并且给每个三角形一个辐射通量(功率),给每个三角形建立BVH,节点的贡献使用一个辐射通量值和一个方向信息。

基础知识回顾:

  1. 光照积分:从物体表面点XX离开,沿着观察方向v的辐射度LoL_o,是emitted和reflected 的辐射度之和,如下所示,其中ff是BRDF,LiL_i是沿着方向ll的入射辐射度,L(XY)L(X \leftarrow Y)表示从YYXX发射的辐射度:

    18-2

    半球上的积分可以重写为光源上一点在半球整个表面上的积分,如下图所示,dAdA是光源上的一小块,XYX-Y表示平方衰减:

    18-3

    因此,如果场景中有mm个光源,则XX上反射的辐射度公式可以写为,其中vv代表可见性,max(nl,0)max(n \cdot l,0)表示光源只有一个面发光而不是双面发光的:

    18-4

  2. 光源选择的重要性采样:直接看公式吧,答案就是只有光源PDF和光源实际的辐照度成比例时,最后蒙特卡洛估计中的求和项就会变成常数项的求和,方差就为零,这也是为什么PDF尽量要和采样点的辐照度贡献成比例的原因:

    18-5

  3. 光源的光线追踪:光强度指的是单位表面上的辐射度,要用光强度除以光的几何形状的表面积。而通常情况下光的几何形状和实际发光体是两个东西。为了防止光照被光源本身的mesh遮挡住,DX12的API可以设置一个参数来控制这个额。

本书的算法

  1. 光源预处理:预先计算每个三角形光源的辐射通量值,通常是三角形发出的总辐射功率,漫反射光源的通量:

    18-6

    Li(X)L_i(X)XX位置的发射辐射度,AiA_i是三角形的面积。

    三角形的辐射度是需要在纹理空间把所有的emit 三角形光栅化,使每个像素都表示最大mipmap level中的一个纹素,然后需要在PS里面对对应texel中的辐照度取出来,然后对其做原子操作的累加。最后再除以纹素的个数,从而得到辐射度的平均值,这个 操作主要是原子操作费时间。因为这个操作里面PS没有render target,所以viewport可以无限大,我们可以用VS从内存中取出UV,同时吧三角形放在纹理空间合适的位置,保证其一直在视口里面。最后还要把辐射通量是0的三角形剔除掉。

  2. 构建加速结构:从上到下建立二叉BVH,要平衡树的质量和构建速度。为了把不同光源的方向考虑进来,每一个节点还要存一个方向锥体,包括一个轴和两个角度(主要是限制在发现周围的发射方向。还要定义一个分割平面,把所有的光源分成两个子节点。SAH和SAOH是两种分割方法,他们的代价计算方式都不同,如下所示,n(C)n(C)a(C)a(C)分别返回节点CC的光源个数和表面积,:

    18-7

  3. 光源重要性采样:采样时就要考虑各种参数带来的权重了,其中

    (1)距离:距离是通过着色点和当前BVH节点的AABB中心点的距离

    (2)光源的通量:节点的通量是带节点内包含的所有光源发出的通量之和,这个是之前与计算好的,如果BVH变化了,那也需要重新预处理这些值

    (3) 光源方向:通过光源的法线和节点的AABB中心到渲染点方向之间的夹角

    (4)节点重要性,其中XCX-C是着色点XX距离CC的AABB中心的距离,θθ是来自节点CC的光源方向锥:

18-8

  1. 随机数的使用:使用均匀随机数,选择两个子节点,节点的重要性除以总重要性
  2. 对叶子节点采样,最后在光源上生成光源样本。

最后结果表明,确实使用的信息越多,收敛越快,应该多重考虑距离,光照通量,光源方向等信息。

五、降噪#

19.在UE4中利用光追和降噪做电影级渲染#

本章主要介绍了将光追集成到UE4中遇到了一些问题,以及对应的解决方法,包括:

  1. 把DXR集成到UE4中,并且能够重用现有的材质shader(这一步主要是工程上的东西,包括如何编译大量的光线追踪渲染器,如何增加对BLAS和TLAS做抽象,如何在引擎层更新BVH树并且还能做到跨平台可扩展,如何修改shader parameters,如何做保留渲染,如何为光线追踪定制一套新的着色器,怎么批量提交多种光线的着色器参数,怎么样才能完全发挥光线追踪的特性)
  2. 利用NVIDIA中的RT core,做硬件加速的BVH遍历和光线/三角形相交测试
  3. 做了一个新的reconstruction filters,可以用在高质量的随机渲染效果上,包括软阴影,glossy 反射,diffuse GI,AO和半透明,每个像素只需要很少的sample。

UE4在集成光追的时候,想要把不同光路做拆分,阴影,反射和diffuse光线都不同,然后每个效果只用很少的光线,并且努力做降噪从而弥补样本数量的不足,他们用局部属性去提高降噪 质量(例如利用光照的大小,或者BRDF lobe的形状)并且把这些局部属性和最终图像结果结合,他们把这个新技术成为分区光路滤波(partitioned light path filtering)

光追的阴影:

  1. 光追阴影在半影方面优势很大,这种半影在车展上效果很好。

  2. lighting evaluation:使用LTC的方法,然后使用光追来收集可见性项的估计,然后再对结果做composite,这里面使用了split sum近似把可见性项拆出来:

    19-1

  3. 阴影降噪:分为时间和空间两个部分,空间降噪器主要是局部遮挡的频率分析,例如为软阴影做的轴对齐滤波器,降噪器知道光源的信息,例如大小,形状,方向,距离receiver的距离等。而时间降噪器则增加了每个像素的有效样本数。

光追的反射:

  1. SSR只能局限于屏幕空间,light probe不能处理动态场景

  2. 反射物体的材质shader做简化(因为光追每次求交都会跑材质shader)

  3. 反射的降噪:只依赖于反射光线的入射幅度项的降噪算法,再次使用split sum把入射幅度项L拆出来,单独对他做降噪,剩下的BRDF做预积分,如下所示:

    19-2

    反射降噪也分为时间和空间两个部分。对于其中的空间滤波器,他们在屏幕空间中推导了一个各向异性形状的滤波核,这个滤波核考虑了局部阴影点的BRDF分布,并基于命中距离,表面粗糙度和法线把BRDF投影回屏幕空间做估计。时间降噪器则使用了反射的motion vector(这种方法在曲面上不理想,需要用32章中的技术)

  4. 基于光追的高光着色:LTC是一种对任意粗糙度进行分析产生逼真面积光的技术,但是他不能处理遮挡。光追有可见性信息,所以可以用来评估材质着色的镜面分量。在文章中,他们简单的把面积光当成emissive object,shade them at the reflection hit point。

光追的diffuse GI:

  1. AO:原来是用SSAO做的
  2. 基于光追的暴力方法:我们从一个候选的GBuffer采样点中发射一个cosine-hemispherical distribution光线。然后记录emitter的BRDF加权结果(不是可见性),这种方法很费时,但是可以作为base。UE4的做法则是用一个light map 来提供近似的间接光照分布。他们使用了volumetric light map来作为求交的emission,这样开销就跟计算可见性相似了。
  3. 使用path tracing 代替light map。
  4. 使用降噪:

(混合)光追的半透明(延迟渲染管线):

使用单独一个半透明光追pass来渲染半透明物体,不用GBuffer 了。当然有一些技巧,例如当光线的透光性越等与0时提前终止掉光线防止不必要的场景穿透等。

20. 实时光线追踪的Texture LOD#

光线追踪也需要用texture的mipmap,但是和光栅化自动选择mipmap不同,光追的mipmap level需要自己计算,光栅化的做法是在PS中计算一个quad的difference。但是在迭代的光追里面就不再适用于一个quad去计算了(因为本来就不是屏幕空间的)。本章中介绍了两个计算MipMap Level的方法,一个叫做 ray differentials,通过chain rule去计算texture footprint,这种方法需要大量的计算,每条光线需要大量的数据,但是可以提供高质量的texture filtering。第二种方法叫做ray cones,计算相对简单,他用锥体来表示ray footprints。

mipmap值λλ的计算方法如下,这个公式很有用,后面两个方法基本都在围绕这两个公式。你会回来看的

20-0

Texture LOD —— ray differentials算法(扩展了ray differentials)

序:光线可以直接访问mipmap的level 0,但是一方面需要对每个像素使用很多光线,另一方面重复访问level会导致cache不友好,而且当物体离相机很远的时候,会产生模糊

射线的数学表达式,OO是原点,射线微分(ray differential)由四个向量组成,其中xxyy是屏幕坐标,相邻像素之间有一个单位:

20-1

整个核心思想是光线在场景做bounce时,对每条路径做ray differential。

  1. 初始化相机光线:在whw \cdot h的分辨率下,坐标xxyy的像素的非归一化射线表示为,pp是[0, 1],cc是[-1, 1],{r,u,v}是右手坐标系中正交相机经过FOV缩放后的基向量:

    20-2

    射线微分如下所示(其实就是xyxy方向的偏微分),其中rr是一个像素到下一个像素的right vector,uu是up vector:

    20-3

  2. 优化后的质心坐标微分计算,三角形的任何点都可以用质心坐标(u, v)表示,当光线和三角形相交于点PP以后,需要计算四个偏微分ux\frac{∂u}{∂x}uy\frac{∂u}{∂y}vx\frac{∂v}{∂x}vy\frac{∂v}{∂y}:假设PP是空间中任意一点,则点PP可以表示为,其中ss为投影距离,gg为与三角形表面不平行的投影向量:

    20-4

    这个方程有点像射线和三角形求交的公式,可以用克莱姆法则变成线性方程组,如下所示(u和v的分子分母上下同时乘上e2×ge_2\times g,sg项和ve2项都为0了):

    20-5

    这里面直接求出

    20-6

    认为P=O+tdP = O + td,然后再经过链式法则推导计算ux\frac{∂u}{∂x}uy\frac{∂u}{∂y}vx\frac{∂v}{∂x}vy\frac{∂v}{∂y},并通过这几个偏微分算出纹素(s,t)(s,t)的偏微分:

    20-7

    问:为什么要计算纹素的偏微分?????

    答:最后算出来的射线微分,代入到文章一开始的公式当中,就可以得到当前点的mipmap值了

  3. 把GBuffer里面的值代入到第二步推导出的公式当中,可以得到每个像素点的射线微分

    Texture LOD —— ray Cone算法

  4. 利用ray cone做texture LOD:当一个像素的纹理LOD λλ计算出来后,在GPU中使用三线性mipmap的纹理采样,如下图所示:

    20-8

    所谓射线锥,其实就是一个从眼睛到像素再到物体形成的锥体,然后再从与物体的交点开始往前做锥体,其实可以发现,射线锥最主要是计算锥的方向(已知),宽度(未知,也是最主要的)和角度(已知)。其实宽度就是锥体和物体相交地方的宽度,只不过锥体两个边在不同相交地方拥有不同的法线,最后角度就不一样(具体的角度由原来扩大或缩小β2\frac{β}{2}个角度)。如下所示:

    20-9

    20-10

    在计算宽度的时候,直接使用w0=2d0tan(α2)αd0w_0 = 2‖d_0‖tan(\frac{α}{2}) ≈ α‖d_0‖, 即可,宽度越大,LOD的mipmap层级应该越高

  5. 更快速的计算ββ:直接通过发现计算ββ:如下图所示,只需要通过变动法线nn,就可以得到只要法线移动β2\frac{β}{2}个角度,反射光线就会移动ββ,从而实现快速计算。

  6. 最后归纳出mipmap level的计算公式如下,其中光线第一次和物体相交记为i=0i=0nin_i表示第ii个相交点的发现,did_i是上一个交点到当前交点的距离,i△_i是第i个交点的基础的三角形LOD:

    20-11

书中最后还展示了Ray Cone的伪代码,具体看书即可(非常建议看一下,看完之后会对Ray Cone印象深刻的)

21. 使用Ray Cone和Ray Diff 做环境贴图的过滤#

这章一看标题就是把上一章的内容拿来用了,公式做了非常多的简化,也没什么好记得笔记

22. 通过自适应光追改进TAA#

文章提出了一种新的实用算法叫做ATAA(自适应时域抗锯齿),通过自适应光线追踪超采样的方式扩展TAA。具体来说就是在TAA的过程当中,输出一个segmentation mask用来保存失败的TAA pixel以及失败的原因,然后利用sparse 光线追踪替换这些失败的点。如下图所示。这种方法可以对失败的像素采用8x数量的光线做超采样,得到的效果相当于8xTAA,但是如果失败的像素点只占6%的话,平均每个像素还不到0.5根光线。

22-1

算法细节:

segmentation 的策略

需要一个motion vector,判断当前像素是否在上一帧中找不到像素,如果找不到的话就用FXAA(因为很快)。对于那些运动的物体,虽然上一帧中能找到对应的像素,但是颜色值完全不同,这个时候就可以标记为需要用光追矫正。

实际上可以使用类似SVGF的方法,对深度,法线,网格ID,亮度做一个组合,最后生成一个segmentation mask

光追的策略:

其实也没什么特殊操作,就是时空复用采样点以增加采样点数量的一系列优化方式,以加速收敛

局限性:

  1. 当有些物体是subpixel级别的,就可能会被忽略掉。
  2. 因为DXR无法准确识别Mipmap level,所以会和SSAA的结果相比要差一些。
  3. 因为光线稀疏的分布在整个屏幕中,所以无法保证后处理pass的必要数据是在像素里面的。

六、混合管线系统#

23.寒霜(frostbite)引擎(预览系统)中的可交互lightmap和Irradiance Volume#

依赖light map和probe的静态GI烘焙一次就能得到好的效果,而且还很快,有移动光源的动态GI则又耗时,运行时也只是个近似。light map的生成可以并行化,因为每一个texel都可以独立生成。

一.输入与输出#

输入:

  1. 几何数据:每一个三角形都有一个UV2,以便将光照贴到模型上,不过这些模型都是proxy meshes,都是经过减面的模型,这样可以减轻因light map分辨率不够导致的自相交,同时还要保证UV尽量避免texture sample时产生的拉伸而变形。UV也可能会被分为很多块,以防止块与块之间距离太近导致漏光。

  2. 材质:每个几何模型都有一些材质属性,例如diffuse albedo, emissive color等

  3. 光源信息:点光源,面光源,方向光和天光等,天光存储在一个分辨率比较小的cubemap里面

  4. irradiance volumes:要对动态物体的光照做预览,动态物体的光照会被层次irradiance volumes照亮,每一个irradiance volume存储了一个三维网格的SH。

  5. 场景的几何体都会做预处理,为light map的texel产生出一种叫做sample location的东西,作为光追时的起始点,每一个样本点的位置都落在每一个光照贴图上,然后这些样本点将会与没有展开UV的场景几何体求交,求交成功的点会参与到稍后进行的光追计算中(如下图所示)。计算采样位置会用Halton低差异序列在proxy mesh的UV空间内。场景的几何体都用BVH存起来。

    23-1

输出:

输出数据会存到lightmap或者irradiance volume中,

  1. irradiance:主要是lightmap或者irradiance volume中的directional irradiance。irradiance的表示方式有很多种,例如平均值,主成分方向(principal direction)或者SH。
  2. sky visibility:主要描述了给定一个lightmap texel或者irradiance volume point,他的天空可见度,这个数值会参与到后面的材质效果当中。
  3. AO:这个主要记录了lightmap texel或者irradiance volume point的AO,实时计算时会参与到reflection occlusion中。
二.GI pipeline总览#

如下图所示,总共分为几个步骤:

  1. 更新场景,例如模型的移动或者灯光的变化
  2. 更新缓存,如果辐照度缓存无效或者还没计算完成,就要用光追计算缓存中入射光的辐照度,这个缓存会用于加速管线中光线追踪的计算。
  3. schedule texels:基于摄像机角度,把大部分相关的light map texel或者可见的irradiance volumes都找出来
  4. trace texels:对每一个待计算的texel和辐照度采样点做光追,,这些光路可以用来计算入射光辐照度,也可以用来计算天空可见度以及AO
  5. merge texels,把钢计算的irradiance samples加到持久化的输出里面
  6. 后处理,包括dilation和降噪

23-2

三.光照计算和光路构建(pipeline总览的第4步):#

计算lightmap每个texel的辐照度E,就需要对上半球内的入射光的光强度L使用投影立体角的值做带权积分,这个方程不好解,采用蒙特卡洛积分法来做。简单来说就是构建若干条由lightmap texel到光源的光路,然后用他们做光线追踪,这些点都依附在几何体表面上,如下伪代码所示:

23-3

23-4

但是上面的方法相对较慢,有很多优化方法,例如:

使用重要性采样:使用光照法线上半球内的投影值作为权重做重要性采样,因为垂直入射光的贡献应该更大。

使用随机数构建光路:使用低差异序列选择采样点位置,再用低差异序列生成样本的方向,可以使用四维低差异序列代替两个二维低差异序列。这样可以保证足够均匀,减少采样方差。

next event estimation:当场景只有一个光源且体积很小时,光线几乎不可能找到光源,那么就把一条光路上的所有点都和光源链接起来,然后计算他们的贡献。这样可以重用很多已经够早好的子路径,来构建更多的光路。

四.光源:#

局部点光源:辐照度强度由光源到着色表面的距离和光线入射的角度决定。

面光源:面光源的辐照度是对面光源可见的表面积分得到的。这里就使用next event estimation来做就行,如下图所示

23-5

方向光:方向光的辐照度在光路路径中的每一个反射点上被采样。

天光:当一个光线没有与场景任何一个点相交时使用天光。

五.特殊材质(除漫反射材质外)#

自放光表面:带有自发光的三角形会直接被放到类似BVH的加速结构里面

半透明物体:光的透过量由物体表面的漫反射率和透光率决定,基于这两个值,会随机决定一调光线是透过物体还是反射。如果穿透物体的话,则光源不会和半透物体的交点连接。

透明物体:透明物体会影响光照的可见度,这个效果可以在anyhit shader里面将透射度乘以一个可见度来完成。

六.Scheduling texels(pipeline中的第五条)#

在做光追之前,系统需要决定哪些texel是需要计算的,这里使用了一个启发式的算法,例如(视图优先算法)和(质量收敛剔除法,convergence culling)

convergence culling会去遍历所有的texel,看是否已经收敛了,在没收敛的texel上取一个sample放在buffer里面

当所有需要计算的sample都放入到buffer中以后,开始做scheduling texel的第二部分,每一个sample 可能会被evaluated很多次,次数和系统性能相关,下图表述了texel计算时候的调度策略,ns表示纹素对应的样本数,ntn_t表示纹素的数量,nin_i表示运算迭代数,最后总的采样数是nsntnin_s\cdot n_t\cdot n_i,

23-6

七.性能开销:#

实现了一套代码去评估光追的时间,系统会根据光追消耗的时间动态调整sample的数量,代码没啥好说的,如下所示:

23-7

八.后处理:#

为了“所见即所得”,需要解决三个问题:

(1) texel可能会变黑,因为可能某个纹素还没来得及计算,这个需要对输出的light map做一个dilation filter,保证所有的texel在运行时线性UV采样不会出现黑色。

(2)texel有噪点,噪点会随着时间的增加而减弱,寒霜采用了一种预测收敛期望值的降噪算法对结果做处理,核心思想是追踪纹素的方差,并且通过这个方差去控制做邻域滤波的大小,最后这个filter会应用到light map 空间,并且用几何体ID去识别边界,这样可以避免对边界做filter,如下图所示,这个方法叫做A-Trous denoiser:

23-8

上面这个方法,其实就是根据不同的geometryID和亮度方差做filter,只选择跟当前像素点相似的pixel做filter,例如上图中绿色的点是在被filter降噪的点,距离最近的方框只有左下角因为亮度差距太大被cull掉了,稍微大一点的框左下角的红色因为ID不同被cull掉了,这样就只filter附近的像素。文章计算方差的代码如下,认为置信区间大于95%就算收敛:

23-9

(3) 接缝问题(seams:lightmap里面不同UVblock之间会有接缝的问题,当两个texel在世界空间中对应的几何体相连接,但是UV空间不连接时就会有这个问题,寒霜使用了基于CPU的接缝修补方案。

九.加速技术#
  1. view prioritization,view的优先级技术:编辑场景的时候,camera可能只观察到场景的一小部分,那就对需要渲染的texel做一个排序,只渲染可见的texel加速收敛,如下伪代码 23-10
  2. light 加速结构:有点像light culling,只计算有可能对着色点产生光照的光源。这个加速结构是一个spatial hash function,有点像是一个3d的k-d tree(或者是不均匀的voxel),不过是空间包围盒。每一个包围盒都被映射到一个具有nen_e个元素的一维哈希函数上。这个哈希表会在场景加载时,在GPU上创建一次,存了一个灯光都够影响到的所有采样点样本点的索引。
  3. 辐照度缓存:计算每个反射点到每个可见光的开销很大,要对这个做缓存,具体策略是根据不同的光照成分缓存不同的光照贴图,例如局部光照、阳光和天光都有自己的一块缓存,这样某一个光照更新的时候只用更新对应的缓存就好了,不用更新所有。有这个缓存,则计算的时候就不需要做很多光线追踪,而是做很多次贴图采样就可以了。
十.性能效果#

23-11

24.基于光子映射的全局光照#

光子映射主要用于动态光源和物体,能够和静态的GI一起合作。具体的pipeline如下所示

24-1

这一章有几个关键的步骤,分别是生成RSM,并且基于RSM做第一次bounce,使用小波变换构建重要性采样点,使用俄罗斯轮盘做提前termination, 存储光子(photon),光子splatting(溅射?大致意思就是影响到周围的像素点),定义splatting的kernel(大小),构建kernel的形状,光子发射,时间滤波和空间滤波。

  1. 生成RSM,并且基于RSM做第一次Bounce:确定所有光源要发射的光子总数,然后把这些光子按照光强度成比例分配给各个光源,对每一个光源生成RSM。
  2. 使用小波变换构建重要性采样点:我们并不是根据RSM均匀的生成采样点,而是要做重要性采样,小波变换有两个步骤,首先要将离散Haar小波变换应用到概率图上,生成图像的金字塔表示,之后使用低差异序列重建每个样本的位置(第16章的采样变换),并且基于小波变换中每次迭代的缩放系数来矫正采样位置。(注意这里是对整个RSM level使用小波变换,每步的分辨率减半,直到分辨率变为2×22\times 2,这么小的分辨率使用一个pass消耗很大,所以最后一级单独使用CS计算)如下图所示:

24-2

  1. 对光子做trace:对每一个RSM采样点使用重要性采样生成RSM光线的方向,采样生成的方向应该和RSM点的BRDF类似。然后再对这个反射的方向使用俄罗斯轮盘做提前termination,概率是BRDF和采样方向概率之间的比例。而存活的光子则会相应的调整其输出保证最后结果正确,这种方式可以在光线遇到反射很少光的表面时,几乎没有光子会继续下去了。

    直接看代码会清楚一点:

    生成光线:

    24-3

    24-4

    最近命中着色器从payload中解压所需要的值,然后确定接下来反射的光线(包括俄罗斯轮盘)

    24-5

    存储的光子被添加到线性缓冲区,使用原子操作。

    24-6

    24-7

  2. 定义splatting kernel的大小:

    kernel的大小很重要,如果kernel太大光线就会很模糊,如果太小就会有很多噪点。因为很大的kernel会使光子覆盖很多像素,导致raster,shading和blending的工作更多。。光子kernel的大小和光线长度、光子密度分布有关系,光线长度的缩放因子如下所示,,其中ll是光线长度,lmaxl_{max}是定义的最大光线长度的常数:

    24-8

    光子密度越大,kernel应该越小,光子密度和缩放如下所示:

    24-9

    其中αxα_xαyα_y是相机视锥体的apertures(光圈),ZviewZ_view是和相机的距离,txt_xtyt_y是tile dimensions in pixels,rxr_xryr_y表示图像分辨率

    kernel的大小用代码表示如下所示:

    24-10

    24-11

  3. 定义splatting kernel的形状

    在光子相交的表面法线方向应该减小kernel的半径,在光的传播方向上应该放大kernel,。代码如下所示

    24-12

    24-13

  4. 光子splatting

    整个splatting使用上面定义的nernel来做就可以了,没什么特别的,如下所示:

    24-14

  5. 时间滤波:

    时间和空间滤波算法都是基于两个像素之间的深度差异和他们表面差异的edge-stopping函数,(就是双边滤波吧)。防止跨越边界做滤波,深度差权重定义如下:

    24-15

    其中z(P)z(P)是像素位置PP处的屏幕空间深度,并且z(P)△z(P)是深度梯度矢量。

    时间滤波就是把这一帧和上一帧通过reprojection算出的mv放在一起,然后两个平均一下,如下所示:

    24-16

  6. 空间滤波:这里使用A-Trous小波变换,是一种multi-pass的滤波算法, 每一个passi都会把kernel增大到ΩiΩ_i,每一个stage,filter都会增大一倍,并且中间的sample会被忽略掉(不会增加sample点,从而增大开销),这种算法对GPU比较友好,因为这样group shared memory就可以被高效利用了,如下所示:

    24-17

    但这种滤波方式会让图像过渡模糊,文中提出了一种基于内容的图像滤波方式,即利用静态小波变换解决离散小波变换位移不变的缺点。文章利用静态小波变换,他在每次迭代时保存每个像素的detail coefficients,detail coefficient如下定义:

    24-18

    24-19

    第14和15个式子指的是像素P在A-Trous小波变换的第i次迭代的辐照度值记为Si(P)S_i(P)did_i是detail coefficient,有了这么一个detail coefficient,那么最原始的辐照度值S0(P)S_0(P)就能够很好的计算出来, 如下式所示:

    24-20

但是其实如果这么做的话,只是相当于把原图s0s_0饶了一大圈子由得到了S0S_0,我们想得到的其实是做完滤波的图像, 这里就用到了variance clipping的方法,如下式所示:这里面bib_i就是在时域滤波时候用的颜色空间BBox。

24-21

最后再应用这个已经做过滤波的辐照度texture

24-22

25.基于实时光线追踪的混合渲染器:#

这个感觉很平常,直接把步骤写一下好了: 一. 对象空间渲染

(1)纹理空间对象参数化

(2)透明度和半透明的光线追踪

二.全局光照(diffuse)

三.生成GBuffer

四.直接阴影

(1)用Gbuffer辅助生成阴影

(2)阴影去噪

五.反射

(1)使用Gbuffer辅助生成反射

(2)用光线追踪在反射交界处生成阴影

(3)反射去噪

六.直接光照

七.反射和辐射度合并

八.后处理

26.deferred 混合path tracing#

渲染pipeline如下,基本就是混合渲染管线,就是多用了一个reprojection,然后分开计算specular和diffuse,也没什么特别的:

26-1

27.基于光线追踪的高品质科学可视化#

这里主要是做科学可视化中复杂的数据、概念和物理现象时用的光追,要求所见即所得的工具,介绍了一个叫做VMD的分子(化学)可视化工具,里面有一些关键技术,不详细说了

  1. 使用正确的几何图元(球体可以用半径+原点)
  2. 冗余消除,压缩及量化(可视化流体流动,如果组成的所有段都有相同的半径,那就可以把半径这个公共的参数提取出来)。里面提到了一个八面体法向量编码实现的法线压缩算法。
  3. 关于加速结构的思考:BVH加速结构的选择很重要,因为BVH本身也有开销,特别是rebuild和refit的时间需要重新衡量,最好可以拆线程拆出来
  4. 科学可视化中的AO:最好能够调一两个关键参数就能修改AO的效果,不需要美术那么高的门槛就能做,
  5. 强化透明表面的边缘
  6. 剔除过多的透明表面
  7. 轮廓描边
  8. 裁剪平面和球体

七.全局光照#

28.对不均匀的介质做光线追踪#

一、光在介质中的传输公式:

光在介质中传输会有一部分被吸收,一部分被散射,这两个值分别用散射系数(scattering) σσ和吸收系数(absorbing) αα来描述。消光系数是两者之和。Beer-Lambert的透光率公式如下,其中光线起点为oo,方向为ddTT是透光率:

28-1

上面这个式子在光传播时非常重要,例如一根光线传播ss距离的散射值就需要通过对TT加权积分才能得到,如下式所示:

28-2

一个蒙特卡洛光线追踪器也希望在T上做重要性采样,具体做法是随机一个距离,通过这个距离来判断一个光子发生碰撞时到底是应该吸收还是散射(距离越远吸收的概率越高)。这样光线追踪器就可以知道到底应该是散射还是吸收。如下式所示:

28-3

二.Woodcock tracking(追踪)

基本思想是:我们处理不了不均匀的介质,但是均匀的介质相对还是比较好处理的。那就先假想一个消光系数(fictitious extinction coefficient),让这个假想的消光系数和真实的消光系数之和等于所有位置的最大值kmax。人工介质(artificial volume)就可以看成真实粒子和虚拟粒子的混合体,真实粒子可以发生吸收和散射,而虚构粒子不会有吸收和散射。这样在光线传播过程当中,碰撞的粒子可能是虚构的也可能是真实的,比例就是真实/虚构的比例,这样就相当于是重要性采样了。如下图所示:

28-4

woodcock tracking的代码如下所示:

28-5

三.实现的代码(这部分得看书,太多了)

代码虽然多,但其实基本上就是围绕上面sample_distance来的,不同的介质get_extinction函数的实现不同,

29.光线追踪中的高效particle volume splatting#

本章主要讲了如何渲染大量用光线追踪照射的粒子。可以实现粒子反射、大型透明物体等很牛逼的效果。具体做法其实就是对粒子做BVH等加速结构,然后对这些加速结构做排序。

一、算法: 主要思想就是沿着视角光线去采样所有附近的粒子,然后沿着光线对做过深度排序的samples做积分。

所有的primitive 都是一个拥有一个半径为rr,中心PP2r2r大小的bound box的radial basis function(RBF)。我们通过原点OO,方向dd的光线和距离Bbox 中心的距离PP来确定采样点的位置XX,如下式所示:

29-1

然后再去估算这个采样点的高斯径向基函数(Gaussian radial basis function)(这个东西的解释在这里高斯径向基函数-wiki百科,还有这里核函数和径向基函数,整体看下来就是某种沿径向对称的标量函数,例如空间中任一点xx到某一中心xcx_c之间欧氏距离的单调函数)

29-2

然后沿着每束光线对所有sample的深度做排序,对他们用一下的表达式做合并,:

29-3

α=ϕ(Xi)α = ϕ(X_i)是sample 的透明度,c=c(ϕ(Xi))c = c(ϕ(X_i))是采样点的颜色,ffbb分别是混合操作前和操作后的值。

二、实现:

概述:直接按照常理生成光线会生成大量不相关的光线,导致性能低下,文章的方法会尽可能的增加遍历的相关性,使用尽可能少的光线覆盖有效的空间区域。如下图所示:

29-4

光线生成:

生成光线的时候,会先和Volume Bounding Box求交,把结果分成一组长条(slabs)用slab_spacing表示,每一个slab都有一个ray.tmin和ray.tmax来限制加速结构的遍历。然后使用rtTrace做求交,然后对每一个slab内的相交采样点保存到PerRayData里面。之后对sample list做排序并且做积分,

具体需要看代码,有点长,简单来说就是记录一个光线和bbox交点的tenter和texit,然后把tenter和texit之间的部分(bbox内部)分成一个一个的slab,每一个slab都有一个tmin和tmax,对每一个slab做trace,其实每一个slab trace的时候就可以看成是一段普通的光线了,然后对结果做sort,最后对排序后的粒子求和

相交着色器和any-hit shader:

如下所示:简单来说就是如下灵魂画图:

29-6

29-5

排序和优化:大小为31的冒泡排序?可能是因为数量比较少,所以性能还不错

30. 使用屏幕空间光子映射技术渲染焦散#

光子映射是一个很耗时的工作,本章就是通过在屏幕空间的光子映射技术来渲染焦散,从而把他应用到实时渲染里面。该方法把光子存储为屏幕空间的一个texel(正常来说会存成一个voxel或者surfel),这种方法被称为SSPM(screen space photon mapping)

一.概述:

本章把光子映射分为三个阶段,分别为:

(1)光子发射和光子追踪(散射):光线生成器的每一个光线对应一个光子,当一个光子从光源出发到达一个不透明的表面上时,将被存储在屏幕空间中,有点像SSR。屏幕空间就是一个texture,每一个纹素都存储了带噪点的焦散。

(2)光子收集(降噪):其实就是降噪,做filter

(3)使用光子图来做照明:这是基于延迟渲染管线,有一个depth buffer和roughness buffer

二.光子发射:

在世界空间,发射的光子拥有color、intensity、direction。而当光子在世界空间停下来时(照射到屏幕空间)只保留光子的颜色和强度,因为一个像素会保存很多光子,压缩空间。光子的辐射通量可以同时保存光子的颜色和强度,下式所示为光子发射的辐射通量,其中pwp_wphp_h是光子的尺寸,lel_e是来自光源的光线的辐射率(Radiance):

30-1

30-2

为了减少光子的浪费,需要使用projection map(其实就是从光源观察的图)把光子集中在重要区域。文章使用了一种叫做投影体(projection volume)的方式,就是一个大的包含了所有能生成焦散物体(半透明物体)的大盒子,如下图所示。将这个盒子投影到方向光的反方向,可以得到一个矩形的光区域(light area)

30-3

light area A的分辨率为pwp_w php_h,每个光子对应光区域a1a_1的面积为1pwph\frac{1}{p_w p_h},方向光发射的辐射通量为:

30-4

三、光子追踪

在世界空间做tracing,光子不被存储的条件有四种:

(1)光子的强度很小(例如穿过透明物体时能量衰减到一定值)

(2)要存储的位置在屏幕之外

(3)光子深度超过了存储的深度缓冲(一直没有物体被击中)

(4)光子是直接光照的一部分(没有打到projection volume上)

四、存储光子

把一个光子压缩成一个像素,之后降噪的时候会把像素中的光子散射到其他像素中(从临近像素中收集光子)。世界空间下,像素的面积apa_p为:

30-5

这里w和h是屏幕的宽度和高度,θxθ_xθyθ_y是FOV的xxyy方向角。dd是从观察点到像素在世界坐标系下的距离。因为ΦeΦ_e不是radiance,是辐射通量flux,所以一个光子必须被转换成radiance,所以存储在像素中的radiance lpl_p如下式所示:

30-6

五、光子收集

直接用NVIDIA 的reflection denosizer,使用camera matrix, depth buffer, roughness buffer,normal buffer 作为输入,收集临近像素的反射值

六、lighting

和正常的屏幕空间lighting一样,photon map里面的photon是像素的附加光。

七、效果(说实话还是有点慢的)

30-7

八、(伪)代码:

30-8

30-9

31.通过路径重用估计(footprint estimation)来减少方差#

多重重要性采样能加权不同采样方法的结果,使方差最小,本章就是使用子路径的footprint estimates去预测通过光子复用产生的真实的variance reduction(The trick is to use footprint estimates of sub-paths to predict the true variance reduction that is introduced by reusing all the photons.)没看懂- -

一.介绍

在做光线追踪的时候,有很多种采样策略,包括路径追踪,双向路径追踪,光子映射和VCM,如下图所示:

31-1

其中路径追踪(PT)就是从眼睛出发,随机tracing寻找光源,当然也可以直接和已知的光源连起来(主要是为了解决光源较小的问题)

而双向路径追踪(BPT)就是同时从光源和眼睛出发,把所有可能的链接都提供一个采样点,这种方法可以找到焦散的路径,而PT是不行的。Veach 引入了一个把所有采样方式放在一起加权的方式,这种方式几乎是最优的,叫做平衡启发式(balance heuristic),如下式:

31-2

其中a、b是可能的samplers集合SS中的sampler的index,概率密度是P{a,b}.n{a.b}表示这种采样方式用了多少次,PT和BPT中nn始终是1。所有不同启发式算法的权重ww的和为1,例如有两种不同方式去寻找同一个路径,这两种方法给的概率居然不同,那最终的结果是两种方法的加权平均值。

光子映射(PM)上一章学过了,就是沿着light sub-paths做trace,结果以光子形式存储,第二个pass中,生成view的sub-pass,并且周围的光子会被合并到当前路径中。PM可以和Veach的加权方式兼容,因为可以在每一个view sub pass vertex找到所有nΦn_Φ个光子,因此可以做大量的重用。由BPT+PM == VCM。但是Veach会引入误差。

二、为什么会引入误差(不能完全复用)

BPT的方差由从摄像机发出的view path和从光源发出的light path组成,这个方差由两个随机变量X和Y产生的,如下式所示

31-3

因为使用nΦn_Φ个光子,light sub-pass合并时的方差会减少nΦn_Φ倍,但是如果大部分的方差来自于view sub-pass,那么实际上的方差就会比nΦn_Φ倍小得多,使用nΦn_Φ个光子会减小V[Y](光源子路径),因此我们把第三第四项除以一个值。

三、有效的reuse factor

公式换成:

31-4

四、一个近似的方案

其实上面那个公式最主要的是要知道V和E都是多少。文章做了几个假设,

(1)假设采样密度函数pp和目标函数是成比例的,那么蒙特卡洛积分器中计算的比率fp\frac{f}{p} 对于任何样本都是一个恒定值,那么方差的估计就是00.

(2)把入射辐射度看成来自确定性直接光计算一个随机变量的期望值(E[Y] = LiL_i, V[Y] = 00),以及一个E[X] = 1的摄像机采样sub-pass,和V[X] = εε(像素抖动)的采样,那么最终方差就为E[Y]*E[Y]*V[X]。为了近似辐射度和重要性,我们需要每平方米的密度和每个子路径的立体角,或者也可以密度的倒数,即footprint A[X]和A[Y],如下图所示:

31-5

然后式子就变成了了下面这个(假设εε很小)

31-6

(3)然后我们就需要算AA是多少了,这里假设对AA的形状不感兴趣,只算AA的面积,那么就只需要两个值就可以表示,一个是搜索面积AA,另一个是用于推到面积变化的立体角ΩΩ

(4)假设从初始区域开始,每一段路径都有自己的AAΩΩ,假设每一段路径都是一个具有实心角ΩΩ的圆锥体,那么就可以计算每段路径的AA,如下图所示A=Ωd2A=Ωd^2

31-7

因为A的面积会越来越大,所以上图中两个面积的combination是用一个卷积组合在一起的。

(5)如何获得这两个区域:1. 光路上第一个vertex的光源区域,2.真实相机模型中的传感器区域,3.输入区域朝向输出方向的投影。

(6)估计单个路径段的footpoint,应用了立体角的卷积,如下式所示:

31-8

(7)最后把所有式子合并在一起,得到:

31-9

五、结果

结果其实一般,基本和VCM的方差差不多,但是可以解决之前提出的问题。

32.使用辐射度缓存实现精确的实时specular 反射#

本章介绍了一种利用辐射度探针和屏幕空间反射,向渲染管线添加光追来减少视觉错误,以达到一种实时高光照明效果的算法

一、介绍

渲染引擎通常把全局光照分为diffuse GI和specular GI,diffuse GI和入射光无关,只需要存储一个总入射辐射度就行,例如lightmap或者辐照度探针。而specular GI和入射光方向强相关,通常出射光都是一个分布。因为没办法存所有的信息,所以通常都是预计算好的。

二、算法概览

算法结合了以前方法的经验

(1)SSR可以快速近似局部specular项并且得到很真实的结果,但如果有不连续性时,就会出现问题,例如从radiance cube中采样(radiance is computed by other means)。自认为意思就是不能从已经计算过的radiance中再来采样。

(2)只有类似镜子这样非常光滑的表明才需要高分辨率。稍微不那么光滑的(glossy)的表面用低分辨率就足够了。

(3)从一组表面上trace的光线可能会击中非常相似的点,这个可能性会随着每像素样本数的增加而增加

(4)游戏中经常会有少量动态物体。

本文的方法就是把上面观察的优点组合在了一起,整个pipeline如下图所示:

31-10

因为上图不够清楚,文章还给了个例子,这个例子就很清楚了,如下图所示:

32-1

图中摄像机发射出了4条光线,R1,R2,R3,R4,其中R1,R2,R3可以直接从辐射度探针1(缓存)中采样,R1可以从辐射度探针(缓存)2中采样,光线R2的交叉点在屏幕空间是可见的,可以从屏幕空间中获取数据,但是R4在两个探针和屏幕空间都不可见,就必须做光追,

三、辐射度缓存(探针、probe)

正常的cubemap,可以使用八面体投影,这一步是预计算的。每一个probe都是由一个包含了albedo、normal、roughness、metalness和luminance的GBuffer组成,

四、光线追踪

根据specular BRDF生成样本方向,追踪光线并存储命中信息,在XX点处,镜面上的入射光,几何法向量为 ωgω_g ,视线方向是 ωoω_oΩiΩ_iXX点上的正半球,ωiω_i是半球上的方向,fsf_s是BRDF,渲染方程为:

32-4

使用cook-torrance模型,使用GGX法线分布,使用蒙特卡洛积分和重要性采样计算,就变成了了:

32-5

其中fΩiΩvf_{Ω_i|Ω_v}是采样的概率密度函数,使用Stachowiak提出的方案(这里就有点看不懂了。。不过是分子分母同时乘上了一项),使用schlick近似代替菲涅尔项。

32-6

但不管怎样,上面这个式子可以写成,其中LtotalL_{total}是加权辐射度样本的和,WtotalW_{total}是单个像素的样本权重的和,反正是两个采样加权起来除一下,后面那项可以用另一个式子查表得到:

32-7

32-8

利用重要性采样出的方向生成光线,以Gbuffer深度重建的表面位置为起始点。代码如下所示:

32-2

32-3

五、光线辐射度计算

这一部分就是把上一部分的LtotalL_{total}WtotalW_{total}分别存成纹理,最后要对这两个纹理做滤波以后才能组合在一起。

这一部分的详细信息看书上的代码就行,基本都是工程上的事情了

六、空间滤波

和第24章的空间滤波一模一样,不多说了

七、时间滤波

和第24章的时间滤波也一模一样、、、、

八、反射运动向量

反射的motion vector和屏幕空间的motion vector还不一样,例如下图就是眼睛运动时候,motion vector的示意:

32-9

文章介绍了一种通过曲面微分来解决这一问题的解析方法,这里就不多说了(才不是我懒得看了)

九、结果

文章给出了每一个步骤的性能,下面两张表是sample为1和1-4得到性能:如果缓存都没办法用,该方法会退化成光追。正常情况下比光追块两倍,如果完全复用的话,比光追快15倍(基本上就是SSR比光追的速度)

32-10

32-11

《光线追踪精粹》笔记(个人整理后)
https://monsterstation.netlify.app/posts/cg/光线追踪精粹/raytracinggem/
作者
Furry Monster
发布于
2026-02-07
许可协议
CC BY-NC-SA 4.0