基于 Surfel 的实时全局光照方案(Surfel-based Global Illumination)

image-20220727115731440

surfel,可以理解成贴在世界空间中某个 mesh 上的圆片(有点像贴花),在几何上定义为:顶点 + 法线 + 半径

一般的 Surfel GI 方案往往是基于 radiosity 方法:往场景里的 mesh 疯狂贴圆片(surfel),这些 surfels 用来探测光照并缓存起来,和 probe 非常相似。但是由于其是贴在物体表面上,而不是像 probe 放置在空间中,因此光照效果要更高质量些。

因为渲染的像素往往就是物体表面的像素,而这些像素更加接近于贴在表面的 surfel 而非接近于置于空气中的 probe,也因此漏光现象会少一些。

在不同的实现方案中,surfel 承担的角色可能会不一样;在 SIGGRAPH 2021 的 GIBS(Global Illumination based on Surfels)实现中,就是使用 surfels 来探测并缓存光照来实现 indirect diffuse lighting(注:不包含 direct lighting 也不包含 specular)

image-20220727115712363

传统 path tracing 中,求解一个点的间接光照,是需要多次递归发出射线的;而 surfel GI 可以看成是路径点 cache 下来,在需要求解一个点的间接光照时,只需要走一次 bounce,而后将 cache 的查询结果来代替多次递归计算的结果。

Surfel 持久化存储

我们希望 surfel 是持久化存储的,也就是说即使 surfel 不在屏幕范围内,其生命周期仍可以不结束,这样就不会白白丢掉 surfels 累积的计算。

也就是说 Surfel GI 还可以保留有屏幕外的信息。

Surfel 数据组成

  • transform ID(贴在哪个 mesh 上):可由 G-buffer(或 Visibility Buffer)获取 ID。
  • local position(相对于 mesh 的位置):可由 G-buffer 获取 depth,推出 world position 再乘 transform 的逆变换得到。
  • local normal(相对于 mesh 的朝向):可由 G-buffer 获取 world normal 再乘 transform 的逆变换得到。
  • irradiance(存储累积探测后的 irradiance):通过 tracing 探测光照并累积起来得到的 irradiance。
  • moment texture(存储半球方向上的最小深度及深度平方):用于判断 visibility。
  • ...

场景中的 mesh 可能会发生各种诸如移动变化,而 surfel 通过记录 transform id 和 local position(而非世界坐标),从而可以附着于动态的 mesh 表面上。

Surfel 数据结构

image-20231018163743549

数据结构实现:

  • Surfel Buffer 专门存储 surfel data。
  • Alive Buffer 存储 surfel indexes 来表示有哪些 surfels 存活。
  • Dead Buffer 存储 surfel indexes 来表示有哪些 surfels 被回收了,其本质是一个栈。

在具体实践中,surfel data 其实内部属性还会进一步分离:

  • 几何结构(如:normal, position, transform id)
  • accumulation data(如:short mean,mean)

首先,有些属性经常需要被查询(热数据),而另一些属性则相对没那么频繁被用到(冷数据),这会对 cache 更加友好;其次,有些属性块需要进行 double buffering 来减少共享冲突,增加效率,而不是对所有属性都进行 double buffering。

在需要生成 surfel 的时候,便可以从读取 dead buffer 栈顶,并添加到:

image-20231212130419981

但 surfels 不可能无限增多(存储空间有限),还需要有一定的回收机制,判断一个 surfel 是否应当回收取决于因素(启发式 heuristic):

  • 现存 surfels 的数量
  • 最后一次出现在屏幕的时间间距
  • 与 camera 的距离
  • tile 的覆盖数量
  • 附着的 mesh 是否被移除了
  • ....

这样在需要回收 surfel 的时候,便可以:

image-20231018163836222

Acceleration Structure

目的:希望通过一个空间加速结构,输入一个世界坐标可以快速查询到邻近的 surfels

难点:如何合理安排一个存储结构,既能保持高效查询,又能不耗费太多存储

Grid

  • 限定以 camera 为中心的一定范围内,每帧都使用新构造的 Grid
  • 每个 cell 存储一个 surfel id 列表,但因此在 surfels 比较密集的地方, grid 可能会漏掉一些 surfels

列表可采用 index + offset 表示,而非使用固定长度的列表,这样可以进一步节省存储空间

理想情况下,1 个 surfel 的半径大小不应该超过 1 个 grid 的大小,否则可能会覆盖到过多的 grid,因此为了避免场景远处生成的 surfel 半径过大覆盖了太多 grids。

SIGGRAPH 2021 GIBS 则采用了 grid 的变种方案: 近处的 grids 均匀分布,远处则采用梯形 grids (类似视锥体指数分割那样,越远的 cell 越大块)

image-20220728111257887

另一种 grid 的变种方案则是 clipmap,不多展开。

Hash Grid

给定任意 position,通过 hash 来索引到某个 cell,从而可以支持无限大的场景。

一般来说,hash grid 是需要解决 hash 冲突;但在 surfel GI 中,可以无视冲突,可以直接遍历 cell 的 surfel 列表,并根据每个 surfel 的具体距离剔除掉范围过远的 surfels。

Grid + 稀疏存储

由于 surfels 的分布往往是在地表的,这也就意味着场景中地表以上大部分的空间是没有 surfel 的,因为 surfel 往往集中在小部分的地表空间。

采用稀疏存储,可进一步减少存储:对空间进行分成较大的块(brick),使其包含多个gird(例如 4*4*4 ),每个 grid 存储一个 surfel id 列表;若某个 brick 内部任一 grid 存在至少一个 surfel,则称该 brick 为有效

  • Brick Pool:对象池只分配空间给有效 brick
  • Pool Index 3D Texture:输入 brick position,输出该 brick 在 Brick Pool 中的存储位置;若 brick 无效,则输出 none(例如特定为-1的值)

2022092101065710

Surfelization

接下来就是如何生成 surfel 的问题。

基于屏幕的生成 [2021]

SIGGRAPH 2021 GIBS 方案采用了基于屏幕的 surfel 放置方案:将屏幕划分为 16*16 个 tiles,每个 tile 覆盖的 surfels 数量如果太少了,则在该 tile 中最少 surfel 的屏幕像素点来生成新的 surfel。

image-20220727172101523

为了生成 surfel,我们需要根据访问屏幕像素对应的 G-Buffer 属性(transform ID,world normal,depth),并以此来初始化 surfel 的几何结构:

此外,对于 Skinned Mesh 情况则需要额外处理,因为它和一般的 rigid geometry 不同,它的表面是会形变的,不方便贴 surfel 上去。

解决方式:贴在权重最大的骨骼上而不是 mesh 上,因此针对 Skinned Mesh 情况,要使用 bone id 而不是 transform id;尽管这种解决方式不准确,但是实际表现出的效果是可接受的。

image-20220729144737802

此外,我们还希望生成的 surfel 符合 LOD 思想(即远处的 surfel 没必要那么高质量那么精确),规定其在屏幕空间的投影大小需要大致相同,也就是说场景远处的 surfel 半径会很大,而近处的 surfel 半径会很小。

然而 screen-based placement 的 surfels 可能存在一些问题:它会漏掉屏幕外的放置点;例如有一些屏幕外的地方贡献了很多次级光照,但是这些地方 camera 又从来没去观察过(即使 surfel 可以持久化存储,但如果连见都没见过,就根本不存在该 surfel)

基于 RSM 的生成

间接光照最大的贡献是一次间接光照,其来源于被直接光照到的 surfels,但是这些被直接光照找到的地方可能不会被屏幕看到。可以借鉴 RSM 的思想,在 screen-based 方案的基础上额外增加一个从 light camera 出发的 placement,而且同时还能直接注入直接光照,节省掉大量的 shadow rays。

placement 条件:在 light camera 屏幕可见且在 main camera 附近一定范围内(可以用长宽高的 box 来定义该范围);这样就可以只在光源可见且离 camera 较近的那些地方生成 surfels,而非整个光源可见范围都生成 surfels(数量太多)。

基于 Triangle 的预生成 [2016]

由于该放置方式计算量比较大(需要遍历 triangles),因此必须是预计算的,因此往往会预先对每个 mesh 进行处理(放置 surfels)。

具体步骤:对每个 triangle,根据面积确定 surfels 数量,若 triangle 面积太小(不足以生成一个 surfel),就累积起来再去叠加下一个相邻 triangle;其放置还需要基于 Halton-sequence ,这样可以得到更紧凑的 surfels 分布,近似满足泊松分布(相同数量的 surfels 效果更好)

image-20220803151448953

预计算的 surfel 放置是有一定挑战的:每个 mesh 自带的 surfels 就会是静态的,不能根据距离等因素动态调整 surfel 的生命周期及数量。或者可以考虑下 surfel lod 方案,难点在于运行时如何让 lod 不同的 surfels 相互传承 irradiance 等信息(因为运行时随时都可能切换 lod)。

基于 Ray Tracing Pipeline 的生成 [2022]

如果显卡支持硬件光追 ray tracing pipeline 特性,那么最理想的生成方式便是基于此。

这部分借鉴于 D5 渲染器 surfel GI 的思路。

在生成初始 surfel 后,surfel 需要发出若干个射线来收集 irradiance,然而必定存在一些 hit point 附近是没有 surfel(或者 surfel 质量不够满足),那么可以利用 payload 在 hit point 上生成一个新的 surfel;在下一帧,旧的 surfel 和新的 surfel 又会重复上述操作,如此多帧后,场景就会慢慢地被 surfels 所覆盖。

image-20231210014101718

Radiosity

每个 surfel 对应生成了若干个 rays(根据 importance sampling 来生成方向和 pdf)后,我们就需要开始收集光照并计算出 surfel 的 irradiance。

注:因为我们要做的是 indirect lighting 效果,因此 surfel 的 irradiance 只包含间接光照,而并不包含直接光照。

image-20221123133622286

对每个生成的 ray,从 surfel 出发,射出后得到第一个 hit point:

  • 一次间接光照:从 hit point 出发,再往光源发射 shadow ray(若到达光源前命中物体,则说明可见性 \(V\) 为 0,即该光源没有贡献光照),计算出 hit point 的 direct lighting

    \[E_{dir}(x) = \sum_{j = 0}^{lightNum}{E_{light[j]}(x)* V(x \to {light[j]})} \]

wicked engine 在实现一次间接光照的时候,并没有对所有光源计算光照贡献之和;而是采用了多光源俄罗斯轮盘赌的方式,随机从光源列表抽一个光源出来计算贡献,并将这个光源的光照贡献乘于光源数量来作为结果。

  • 二至无限次间接光照:通过 grid 获取 hit point 附近 surfels,并将它们的 irradiance 混合(根据距离权重)得到 hit point 的 irradiance \(E_{indir}(x)\)

\[E_{indir}(x) = \frac{\sum_{k=0}^{nearSurfelNum} \mathrm{DistWeight}(x_{nearSurfels[k]},x) * E_{nearSurfels[k]}}{ \sum_{k=0}^{nearSurfelNum}\mathrm{DistWeight}(x_{nearSurfels[k]},x) } \]

接着就能计算出本次 ray 的 radiance:

\[L(i) = \frac{(E_{dir}(x_i)+E_{indir}(x_i)) * \mathrm{albedo}(x_i)}{\pi} \]

其中,\(i\) 代表了第几个 ray,\(x_i\) 代表了第 \(i\) 个 ray 的 hit point 位置。

当所有的 ray 都计算出 radiance 后,就可以计算出 surfel 在本帧的 irradiance:

\[E_{surfel} = \sum_{i=0}^{rayCount} \frac{L(i) \cos(\theta)/pdf(i)}{rayCount} \]

Adaptive Ray Count [2019]

一般来说在每帧下,每个 surfel 需要生成相同次数的 ray,但是我们可以对比较重要的 surfel 多生成些 rays。

Modified Exponential Moving Average Estimator, MSME :如果 surfel 的 irradiance 变化很小,那么说明基本收敛,则少生成些 ray;如果 irradiance 变化很大,那么应该在本次加多些 ray 的次数。这样在同等性能下,可以实现更快适应场景变化。

image-20220728163506766

GIBS 采用了 MSME 的实现方式,引入长期平均值(mean)和短期平均值(short mean),用这两者的相差体现 irradiance 的变化程度。

实际上在当前帧,一个 surfel 要发出 ray 的数量应综合取决于以下因素:

  • local variance(irradiance 变化程度)
  • 最后一次出现在屏幕的总计帧数
  • surfel 生成后的总计帧数
  • 全局所生成的 rays 数量

Ray Guiding

重要性采样,即让 ray 更大概率地指向 pdf 比较大的方向,除于 pdf(归一化)后得到更快收敛的积分值。pdf 越接近原分布函数,则越快收敛。

BRDF pdf:使用 cosine PDF 来做 diffuse 的 importance sampling

Lighting pdf:利用球面方向映射八面体的方式,来记录各立体角方向的历史累积 irradiance,做成一张 radial irradiance map

  • 一个 texel 代表一个方向,texel 的值则存储了代表该方向的 irradiance
  • irradiance 越大说明该方向更容易 trace 到有效光

image-20220728165145081

根据 pdf 选取 texel 时,可使用层次化积分优化(即将 irradiance map 做成 mipmap,先遍历大步再遍历越来越小的步),原本遍历所有 texels 的复杂度 \(O(n)\) 便可以降低为 \(O(logn)\)

image-20220728170226162

基于 texel 方向的 ray guiding 仍然太粗粒度(除非增加 map 分辨率),可以将 Restir 思路进一步推广到 indirect lighting:将 surfel 也视为一种光源(只不过是次级光源),这样就可以做 reservoir sampling。

Direct Lighting(可选)

GIBS 中,surfel 分配的每个 ray 为了收集 1 次间接光照,还得从 hit point 出发额外往光源打多 1 个 shadow ray,即总开销 2*n 个 rays。

而另一种可选的做法:surfel 先自己收集直接光照(1 个 shadow ray)并 cache 起来,然后分配的每个 ray 直接从 hit point 获取 surfels 的直接光照+间接光照,并以此更新自己的间接光照,总开销 1+n 个 rays。

  • 可以极大减少计算直接光照的次数,因为 surfel 不需要每帧注入光照(可以 cache 下来),等光源发生变化才需要重新注入。
  • 会降低一些直接光照的质量,因为从 hit point 获取 surfels 的直接光照+间接光照就意味着需要进行 blending。
mIHZmHkq6Y

Filtering

Temporal Blending

比较粗暴的方法就是使用固定的历史 irradiance 混合权重(一般为 0.8~0.9),但可以考虑根据 estimator 的评估来作为 temporal 的混合权重参考:

  • local variance(irradiance 变化程度)

例如 irradiance 变化程度很大时,我们认为历史帧参考意义不大,因为历史混合权重应当小些;相反,就证明基本收敛,就调大些历史混合权重。

Irradiance Sharing

当 surfel irradiance 变化过大时(远远没有收敛,例如发生在刚生成新的 surfel 时),我们可以多多参考附近的 surfel irradiance,相当于做了一次空间滤波。

实际操作就是利用空间加速结构来快速查询该 surfel 周围的其它 surfels,将它们的 irradiance 按以下因素来加权混合:

  • distance
  • normal
  • depth function

image-20220728182404474

总结

GIBS 方案流程总览 [2021]

生成 surfels:基于屏幕空间(1 thread <=> 1 pixel)

  • 生成 surfel:屏幕空间分成 16×16 个 tiles,每个 tile 检查内部 surfels 的数量,若太少则根据 G-Buffer 属性来生成新的 surfel。
  • 更新 acceleration structure:每当有一个 surfel 生成时,找到其所覆盖的 cells,对这些 cells 的 surfel 列表都添加该 surfel。

更新 surfels:1 thread <=> 1 surfel index <=> 1 surfel

  • 根据 transform id ,local position, local normal 更新 world position,world normal。
  • 回收 surfel:通过启发式判断当前 surfel 是否需要移除,若需要则进行回收。
  • ray generation:根据 estimator(surfel 的 irradiance 收敛程度)来确定要生成的 rays 数量(假设为 n 个),提前占好 ray buffer 的 n 个位置(通过 ray offset + ray count 表示)。
  • 增加对应 cell 的 surfel 计数:通过当前 surfel 的 world position 找到对应 cell 的 surfel count ,并使用原子加法(加1)。

更新加速结构:1 thread <=> 1 cell

  • 更新 cell 的 offset:上一个 pass 结束后,每个 cell 都有确定的 surfel count,这时候需要对一个全局的 cell-surfelindex count 使用原子加法(加 surfel count)并得到一个 offset(原子加法前 cell-surfelindex count 的原值)。

这表示当前 cell 已经占领了 cell-surfelindex buffer 中 [offset, offset+surfel count) 的范围,并且这段范围不可能被其它 cell 占领(因为原子操作)。

  • 将 cell 的 surfel 计数清零:将 cell 的 surfel count 清 0,便于后续更新 cell-surfelindex buffer。

更新加速结构:1 thread <=> 1 surfel index <=> 1 surfel

  • 通过当前 surfel 的 world position 找到对应 cell,并获取其中的 offset 属性。
  • 更新 cell-surfelindex buffer:对 cell 的 surfel count 进行原子加法(加1)并得到一个 index(原子加法前 surfel count 的原值), 将 surfel index 写入到 cell-surfelindex buffer[offset + index] 的位置上。

ray tracing:1 thread <=> 1 ray

  • ray guiding:根据 surfel id 获取 pos, normal,并根据 importance sampling 生成该 ray 的方向和 pdf。
  • ray tracing:通过 ray tracing 收集一次间接光照和二至无限次间接光照,得出 radiance 结果除于 pdf 后即为计算出的 irradiance。

integrate:32 threads <=> 1 surfel index <=> 1 surfel

  • 每个 thread 共需要访问 surfel 在 ray buffer 对应的 n/32 个 rays,并通过 group shared memory 来将这些 rays 的结果合并在一起,相当于访问了 n 个 ray;在访问 rays 的同时,将它们的 irradiance 结果平均一下即为本帧 surfel 的 irradiance,同时也根据它们的 depth 来更新 surfel 的 moment texture。
  • 更新 estimator(MSME 算法):根据长期平均和短期平均的相差值来评估 irradiance 的收敛程度。
  • temporal filtering:根据收敛程度来决定 temporal 混合权重,不仅要用于更新 irradiance。
  • irradiance sharing:根据收敛程度来决定是否启用;若启用则查询周围的 surfels,混合它们的 irradiance。

final gather:基于屏幕空间(1 thread <=> 1 pixel)

  • 重建 pixel 的世界坐标位置,通过加速结构找到其所在的 cell ,获取该 cell 的 surfel 列表。
  • 遍历这些 surfels 并根据它们的 normal 和 position 来衡量权值,并且同时利用 moment texture 对应方向的 texel 进行切比雪夫测试得到 visibility 权值。
  • 最后加权和混合起来得到该 pixel 的 indirect irradiance,即我们想要的间接光照。

GIBS 方案优缺点

Surfel GI 的优势:

  • 比起 DDGI 有更高质量的 irradiance 混合方式。
  • 比起 SSGI 可以有屏幕外的信息(虽然生成还是基于屏幕)。
  • 整个流程完全动态,都是 runtime 计算的。
  • 持久化存储,避免浪费掉累计的 irradiance 计算。

Surfel GI 的缺点:

  • 仍然有一定 light leaking 问题:即使有 moment texture,由于分辨率过低还是很容易出问题,性能与 artifacts 的取舍。
  • 无法实现半透明物体的 GI 效果:surfel 的生成依赖于 G-Buffer,因此贴不上半透明物体,不过可以将 Surfel GI 的思路套用在 probe-based GI 的方案上。
  • 只对 diffuse 效果有比较好支持(毕竟 surfel 主要提供的是 irradiance),可能不那么适用于 specular 效果。

改进 [TODO]

  • async compute pipeline:surfel GI 基本上流程只有 compute shaders,因此很适合做 async compute,但最好跑在 depth buffer & visibility buffer 完成之后。
  • 各种 LDS(local data share)优化:surfel GI 一堆 CS,很多都可以利用 LDS 技巧进行深度优化。
  • 搭配其它 GI 方案,例如:surfel 比较适合做 diffuse GI,而对于 specular GI 来说则表现得不太好;相反,SSGI 不适合做 diffuse GI,而更适合做 specular GI。搭配两者可能会比较好。

参考

ps:PathTracing和自己的实现对比图(仅对比间接光照效果,无直接光)

image-20221011153244282
posted @ 2022-10-02 01:40  KillerAery  阅读(4561)  评论(0编辑  收藏  举报