草庐IT

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

KillerAery 2023-03-28 原文

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

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

因为渲染的像素往往就是物体表面的像素,而这些像素更加接近于贴在表面的 surfel 而非接近于置于空气中的 probe。

Global Illumination based on Surfels [SIGGRAPH 2021]

在 SIGGRAPH 2021 的 GIBS(Global Illumination based on Surfels)实现中,就是使用 surfels 来探测并缓存光照来实现 indirect diffuse lighting(注:不包含 direct lighting 也不包含 specular)

传统 path tracing 就是让每个 pixel 去做 ray tracing,而用 surfel 做 ray tracing 的开销会大大减少:因为 surfel 比 pixel 的数量远远要少;并且通过控制 surfels 的数量,还可以弹性控制性能和效果的平衡。

本文将主要基于 SIGGRAPH 2021 GIBS 方案来讲解 Surfel,中间也结合一些 wicked engine 的实现方式,以供扩宽思路。

Surfel 持久化存储

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

也就是说相对于 SSGI 方法,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
  • depth buffer(存储半球方向上的最小深度):用于判断 visibility
  • ...

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

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

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

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

surfel 回收机制

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

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

数据结构实现:

  • Alive Buffer 专门存储 surfel data,这样就可以通过 index 访问到对应的 surfel 数据
  • Alive List 存储 surfel index 来表示有哪些 surfels 存活
  • Dead Buffer 存储 surfel index 来表示有哪些 surfels 被回收了,其本质是一个栈

Surfelization

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

screen-based placement

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 半径会很小。

Acceleration Structure

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

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

grid

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

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

理想情况下,1 个 surfel 的半径大小不应该超过 1 个 grid 的大小,否则可能会覆盖到过多的 grid,因此为了避免场景远处生成的 surfel 半径过大覆盖了太多 grids,SIGGRAPH 2021 GIBS 采用了 grid 的变种方案:

  • 近处的 grids 均匀分布,远处则采用梯形 grids (类似视锥体指数分割那样,越远的 cell 越大块)

Final Gathering

ray generation : MSME

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

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

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

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

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

ray guiding : importance sampling

重要性采样,即让 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)\)

ray tracing : indirect lighting

每个 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)\)

\[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} \]

temporal filtering

比较粗暴的方法就是使用固定的历史 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 方案总结

GIBS 方案流程总览

生成 surfels:基于屏幕空间

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

更新 surfels:对 surfels 容器进行遍历

  • 根据 transform id ,local position, local normal 更新 world position,world normal
  • 回收 surfel:通过启发式判断当前 surfel 是否需要移除,若需要则进行回收
  • ray generation:根据 surfel 的 irradiance 收敛程度等属性来确定要生成的 rays 数量(假设为 n 个),提前占好 ray buffer 的 n 个位置(通过 ray offset + ray count 表示)

ray tracing:对 ray buffer 遍历

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

integrate:对 surfels 容器遍历

  • 访问 surfel 在 ray buffer 对应的 n 个 rays,将它们的 irradiance 结果平均一下即为本帧 surfel 的 irradiance
  • estimator:根据长期平均和短期平均的相差值来评估 irradiance 的收敛程度
  • temporal filtering:根据收敛程度来决定 temporal 混合权重
  • irradiance sharing:根据收敛程度来决定是否启用;若启用则查询周围的 surfels,混合它们的 irradiance

pixel 着色:基于屏幕空间

  • 重建 pixel 的世界坐标位置,找到其所在 cell ,获取该 cell 的 surfel 列表(意味着这些 surfel 都离该 pixel 很近)
  • 遍历这些 surfels 并根据它们的 normal 和 position 来衡量权值,最后加权和混合起来得到该 pixel 的 irradiance
  • 有了 irradiance 就可以做 shading 计算了

GIBS 方案优缺点

Surfel GI 的优势:

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

Surfel GI 的缺点:

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

改进思路

参考

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

有关基于 Surfel 的实时全局光照方案(Surfel-based Global Illumination)的更多相关文章

  1. ruby - 在 jRuby 中使用 'fork' 生成进程的替代方案? - 2

    在MRIRuby中我可以这样做:deftransferinternal_server=self.init_serverpid=forkdointernal_server.runend#Maketheserverprocessrunindependently.Process.detach(pid)internal_client=self.init_client#Dootherstuffwithconnectingtointernal_server...internal_client.post('somedata')ensure#KillserverProcess.kill('KILL',

  2. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  3. 基于C#实现简易绘图工具【100010177】 - 2

    C#实现简易绘图工具一.引言实验目的:通过制作窗体应用程序(C#画图软件),熟悉基本的窗体设计过程以及控件设计,事件处理等,熟悉使用C#的winform窗体进行绘图的基本步骤,对于面向对象编程有更加深刻的体会.Tutorial任务设计一个具有基本功能的画图软件**·包括简单的新建文件,保存,重新绘图等功能**·实现一些基本图形的绘制,包括铅笔和基本形状等,学习橡皮工具的创建**·设计一个合理舒适的UI界面**注明:你可能需要先了解一些关于winform窗体应用程序绘图的基本知识,以及关于GDI+类和结构的知识二.实验环境Windows系统下的visualstudio2017C#窗体应用程序三.

  4. kvm虚拟机安装centos7基于ubuntu20.04系统 - 2

    需求:要创建虚拟机,就需要给他提供一个虚拟的磁盘,我们就在/opt目录下创建一个10G大小的raw格式的虚拟磁盘CentOS-7-x86_64.raw命令格式:qemu-imgcreate-f磁盘格式磁盘名称磁盘大小qemu-imgcreate-f磁盘格式-o?1.创建磁盘qemu-imgcreate-fraw/opt/CentOS-7-x86_64.raw10G执行效果#ls/opt/CentOS-7-x86_64.raw2.安装虚拟机使用virt-install命令,基于我们提供的系统镜像和虚拟磁盘来创建一个虚拟机,另外在创建虚拟机之前,提前打开vnc客户端,在创建虚拟机的时候,通过vnc

  5. ruby - 在 RSpec 中 stub /模拟全局常量 - 2

    我有一个gem,它有一个根据Rails.env的不同行为的方法:defself.envifdefined?(Rails)Rails.envelsif...现在我想编写一个规范来测试这个代码路径。目前我是这样做的:Kernel.const_set(:Rails,nil)Rails.should_receive(:env).and_return('production')...没关系,只是感觉很丑。另一种方法是在spec_helper中声明:moduleRails;end而且效果也很好。但也许有更好的方法?理想情况下,这应该有效:rails=double('Rails')rails.sho

  6. ruby - 将全局 $stdout 重新分配给控制台 - ruby - 2

    我正在尝试将$stdout设置为临时写入一个文件,然后返回到一个文件。test.rb:old_stdout=$stdout$stdout.reopen("mytestfile.out",'w+')puts"thisgoesinmytestfile"$stdout=old_stdoutputs"thisshouldbeontheconsole"$stdout.reopen("mytestfile1.out",'w+')puts"thisgoesinmytestfile1:"$stdout=old_stdoutputs"thisshouldbebackontheconsole"这是输出。r

  7. ruby - 在模块/类之间共享全局记录器 - 2

    在许多ruby​​类之间共享记录器实例的最佳(正确)方法是什么?现在我只是将记录器创建为全局$logger=Logger.new变量,但我觉得有更好的方法可以在不使用全局变量的情况下执行此操作。如果我有以下内容:moduleFooclassAclassBclassC...classZend在所有类之间共享记录器实例的最佳方式是什么?我是以某种方式在Foo模块中声明/创建记录器还是只是使用全局$logger没问题? 最佳答案 在模块中添加常量:moduleFooLogger=Logger.newclassAclassBclassC..

  8. Ruby 守护进程和 JRuby - 备选方案 - 2

    我有一个应用程序正在从Ruby迁移到JRuby(由于需要通过Java提供更好的Web服务安全支持)。我使用的gem之一是daemons创建后台作业。问题在于它使用fork+exec来创建后台进程,但这对JRuby来说是禁忌。那么-是否有用于创建后台作业的替代gem/wrapper?我目前的想法是只从shell脚本调用rake并让rake任务永远运行......提前致谢,克里斯。更新我们目前正在使用几个与Java线程相关的包装器,即https://github.com/jmettraux/rufus-scheduler和https://github.com/philostler/acts

  9. ruby - Sinatra 中的全局救援和日志记录异常 - 2

    如何在出现异常时指定全局救援,如果您将Sinatra用于API或应用程序,您将如何处理日志记录? 最佳答案 404可以在not_found方法的帮助下处理,例如:not_founddo'Sitedoesnotexist.'end500s可以通过调用带有block的错误方法来处理,例如:errordo"Applicationerror.Plstrylater."end错误的详细信息可以通过request.env中的sinatra.error访问,如下所示:errordo'Anerroroccured:'+request.env['si

  10. ruby-on-rails - (Ruby,Rails) 基于角色的身份验证和用户管理...? - 2

    我正在寻找用于Rails的优质管理插件。似乎大多数现有的插件/gem(例如“restful_authentication”、“acts_as_authenticated”)都围绕着self注册等展开。但是,我正在寻找一种功能齐全的基于管理/管理角色的解决方案——但不是简单地附加到另一个非基于角色的解决方案。如果我找不到,我想我会自己动手......只是不想重新发明轮子。 最佳答案 RyanBates最近做了两个关于授权的railscast(注意身份验证和授权之间的区别;身份验证检查用户是否如她所说的那样,授权检查用户是否有权访问资源

随机推荐