基于 Surfel 的实时全局光照(Surfel-based Global Illumination)
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)
传统 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 数据结构
数据结构实现:
- 用 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 栈顶,并添加到:
但 surfels 不可能无限增多(存储空间有限),还需要有一定的回收机制,判断一个 surfel 是否应当回收取决于因素(启发式 heuristic):
- 现存 surfels 的数量
- 最后一次出现在屏幕的时间间距
- 与 camera 的距离
- tile 的覆盖数量
- 附着的 mesh 是否被移除了
- ....
这样在需要回收 surfel 的时候,便可以:
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 越大块)
另一种 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的值)
Surfelization
接下来就是如何生成 surfel 的问题。
基于屏幕的生成 [2021]
SIGGRAPH 2021 GIBS 方案采用了基于屏幕的 surfel 放置方案:将屏幕划分为 16*16 个 tiles,每个 tile 覆盖的 surfels 数量如果太少了,则在该 tile 中最少 surfel 的屏幕像素点来生成新的 surfel。
为了生成 surfel,我们需要根据访问屏幕像素对应的 G-Buffer 属性(transform ID,world normal,depth),并以此来初始化 surfel 的几何结构:
此外,对于 Skinned Mesh 情况则需要额外处理,因为它和一般的 rigid geometry 不同,它的表面是会形变的,不方便贴 surfel 上去。
解决方式:贴在权重最大的骨骼上而不是 mesh 上,因此针对 Skinned Mesh 情况,要使用 bone id 而不是 transform id;尽管这种解决方式不准确,但是实际表现出的效果是可接受的。
此外,我们还希望生成的 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 效果更好)
预计算的 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 所覆盖。
Radiosity
每个 surfel 对应生成了若干个 rays(根据 importance sampling 来生成方向和 pdf)后,我们就需要开始收集光照并计算出 surfel 的 irradiance。
注:因为我们要做的是 indirect lighting 效果,因此 surfel 的 irradiance 只包含间接光照,而并不包含直接光照。
对每个生成的 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)\)
接着就能计算出本次 ray 的 radiance:
其中,\(i\) 代表了第几个 ray,\(x_i\) 代表了第 \(i\) 个 ray 的 hit point 位置。
当所有的 ray 都计算出 radiance 后,就可以计算出 surfel 在本帧的 irradiance:
Adaptive Ray Count [2019]
一般来说在每帧下,每个 surfel 需要生成相同次数的 ray,但是我们可以对比较重要的 surfel 多生成些 rays。
Modified Exponential Moving Average Estimator, MSME :如果 surfel 的 irradiance 变化很小,那么说明基本收敛,则少生成些 ray;如果 irradiance 变化很大,那么应该在本次加多些 ray 的次数。这样在同等性能下,可以实现更快适应场景变化。
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 到有效光
根据 pdf 选取 texel 时,可使用层次化积分优化(即将 irradiance map 做成 mipmap,先遍历大步再遍历越来越小的步),原本遍历所有 texels 的复杂度 \(O(n)\) 便可以降低为 \(O(logn)\):
基于 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。
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
总结
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。搭配两者可能会比较好。
参考
- [1] SIGGRAPH 2021 | Global Illumination based on Surfel
- [2] Github | Wicked Engine | Janos Turanszki
- [3] I3D 2016 | Precomputed Illuminance Composition for Real-Time Global Illumination
- [4] Ray Tracing Gems | 2019
- [5] D5 GI光照解决方案-GAMES 2022技术分享 (d5render.cn)
ps:PathTracing和自己的实现对比图(仅对比间接光照效果,无直接光)
作者:KillerAery
出处:http://www.cnblogs.com/KillerAery/
本文版权归作者和博客园共有,未经作者同意不可擅自转载,否则保留追究法律责任的权利。