jjzjj

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

KillerAery 2023-03-28 原文

探针(Probe) 或者说 光照探针(Light Probe),简单理解就是场景中的一个点然后给予这个点去往四面八方收集光照或者说探测光照的能力,并把往四面八方的入射光 radiance 记录下来(其实就是记录周围的光照,即光场)。在我们对某个 shading point 渲染的时候,就可以利用它附近 probes 的光场信息粗略估计出它所受到的光照。

这里以前我写的是环境光照信息,实际上是概念混淆了,周围的光照 ≠ 环境光照。

Precomputed Probe

在场景里预先放置若干个 probe,为每个 probe 预计算出局部(因为只能处理静态物体和静态光照,无法囊括动态物体和动态光照,所以是局部的)光场信息并存储后,在运行时就可以通过物体周围 probes 的光照信息来插值来得到 shading point 此时受到的局部光照。

预放置 probes

基于预计算的 Probe 方案往往意味着需要进行预放置,主流有三类放置方式:Tetrahedral Tessellations,ILC,VLM

四面体镶嵌(Tetrahedral Tessellations)

在场景中预先放置自定义位置的 probe ,这些 probe 将自动相连成一个个四面体。而为了增加这种 GI 方案的真实感和避免过多的 probe 带来的存储开销,一般应当把 probe 在光照发生明显变化的地方(如明暗交接处)放密集些,而在光照不怎么变化的地方可以稀疏地放置。

ps:将这些 probe 进行三角化(将空间切分成四面体)需要用到 Delaunay Triangulation 完美三角剖分算法。

  • 手动放置的方式比较费人力
  • 运行时,查找 shading point 所处四面体的运算量较高:四面体的分布并不是均匀的,最糟糕没有优化的情况下需要暴力遍历每一个四面体判断 shading point 是否在该四面体里面,无论对 CPU 还是 GPU 运算都不太理想

基于 probe 的方案是需要经常用到这种查询操作:给定一个世界坐标,查询出它周围邻近的 probes

Indirect Light Cache

Indirect Light Cache, 即 ILC(间接光缓存),是 UE4 的方案,它基本上是在静态物体表面法线向上自动地均匀放置 probe ,这是基于假设受光照影响最大的地方都是在靠近静态物体的地方。

实际上,ILC 还包含两种 probe 放置方式:通过手动放置 Lightmass Importance Volume(重要光照范围)来限制烘焙光照采样范围,或 Lightmass Character Indirect Detail Volume,来增加一段均匀放置 probe 的区域。

  • ILC 通过 CPU 寻找物体周围的 probe:所有的 probe 的数据将保存到八叉树中以方便物体找到周围的 probe

Volumetric Light Map

Volumetric Light Map, 即 VLM(体积光照贴图),也是 UE4 的方案,它使用网格(Grid)采样点保存 probe 数据,而在静态物体表面附近,会对网格进一步细分。

  • VLM 通过 GPU 来寻找物体周围的 probe :所有 probe 的数据将烘焙至贴图中,不同细分层度的网格将使用 level 不同的贴图,这样可以方便地在 GPU 中进行逐像素的三线性插值

  • VLM 比 ILC 的 probe 要多更多,从而 VLM 的 GI 效果更佳,但要存储的数据更多

  • VLM 的存储结构决定了可以通过 GPU 算法来寻找 probe,这比传统的 CPU 算法性能更加可观(即便 VLM probe 数量要多得多)

  • VLM 更适合将 probe 应用到体积雾效果中,这是因为 ILC 往往不在几乎没什么物体的空间放置 probe

UE 4.18 以后使用 VLM 代替了 ILC 方法作为 UE4 默认的 probe 方案。

ILC vs. VLM 效果图:

烘焙光照信息

基于预计算的 Probe 方案在收集光照信息的时候,也采取预计算(烘焙)的方式。这意味着只能对静态光照(静态光源、静态物体组建成的静态场景)进行收集。

SH lighting

在离线阶段,每个 probe 会通过比较高质量但耗时的渲染方式(一般可以使用 path tracing 等方式)来收集静态光照,说白话也就是在 probe 上往四面八方去探测光照。同时,探测到的静态光照信息信息将以 SH 的方式(而不是 irradiance map)的形式记录到该 probe 中。

由于场景中有很多个 probe,要是每个 probe 都存储一张 irradiance map 的存储和带宽开销会非常大,因此往往采用 SH lighting 方式(每个 probe 仅需要存储十几个或者几个 SH 系数)会更加可行。

重建 shading point 光照信息

有了预先放置好的 probes ,又有它们各自预先烘焙的光照信息(SH 系数),那么我们就可以在运行时对 shading point 着色:

  1. 根据 shading point 的世界坐标找到其邻近的 probes
  2. 对这些 probes 的 SH 系数以距离为权重进行混合,得到 shading point 的 SH 系数(比较粗糙的光照信息)
  3. 根据 SH 系数就可以重建 shading point 的光照信息信息

优缺点

优点:

  • 预计算能够节省大量的计算来实现还可以的 GI 效果

缺点:

  • 由于是预计算,因此不支持动态光照
  • 对于大场景需要很多的存储空间,并需要设计一定复杂程度的空间结构来管理查询

DDGI(or RTXGI)

Dynamic Diffuse Global Illumination(DDGI),则提供了一种支持动态光照的 probe 方案。

NVIDIA 的 DDGI 实现版本则一般称之为 RTXGI 技术,因此基本上说起 RTXGI 就是相当于在说 DDGI

动态/预放置 probes

DDGI volume

DDGI volume:一个立方体积区域,里面均匀放置着 probes(类似于 grid),在 DDGI Volume 内的物体就能被间接光照亮,实现 GI 效果。虽然 volume 也需要手动放置,但是粒度更大,比起手动放置 probe 工作量要少得多。

不过,与前面预放置的固定 probes 不同:

  • volume 既可以预先固定在某个位置(静态放置),也可以动态改变位置(动态放置)
  • 动态放置 volume:需要实时探测几何信息 + 光照信息
  • 静态放置 volume:需要实时探测光照信息,而对于几何信息则可以选择预计算或者实时探测

在实践中:

  • 对于静态放置:会先放置一个大号的静态 volume 囊括整个场景(常用于表现室外间接光),然后再在场景中各个房间都放置一个静态 volume(更加高清晰度表现室内间接光)

  • 对于动态放置:往往会多个大小不一的 volumes 绑定到 camera 上,这样就可以跟随 camera 移动对视野内的一定范围的物体应用间接光照,并且具有质量上的 LOD 效果(近处采用高精度 volume,远处采用低精度 volume)

  • 在嵌套多层的 volume 之间会做一个线性淡出从而避免明显的边界

探测动态光照信息

DDGI probe 采取 ray tracing 的方式去实时探测动态光照信息,并且使用了 八面体映射(octahedral mapping) 技术来缓存收集到的光照信息:将球面方向映射成一张 2D texture,一个 texel 对应一个立体角方向,texel 的值即代表了其对应立体角方向的值。

RTXGI 实现中,ray tracing 就需要使用到硬件光追 API(需要 RTX 系列显卡)

DDGI probe 的数据组成:

  • 6×6 irradiance texture:存储每个 texel 方向的 irradiance \(E(\omega)\)
  • 14×14 moment texture:存储每个 texel 方向的深度 \(r(w)\) 及深度的平方 \(r^2(w)\),以便进行 variance depth test

variance depth test 在下文 light leaking 问题中再详细介绍。

计算 radiance

  1. probe 均匀向球面上射出若干个 rays,并记录每个 hit point 的 position,normal,albedo 并将结果存入分别存入三张 texture

如果采取预计算的 volume ,那么这一步操作就会提前到预计算阶段并保存好,从而运行时让每个 probe 无需射出 ray,直接访问保存好的那三张 texture

接着我们需要计算每个 hit point 的着色结果,这样才能计算出对应 ray 的 radiance

  1. one-bounce lighting:通过 position,normal,albedo, rayDir 进行 hit point 的直接光照着色

\[L_{one} = \sum_{lights} (L_{light} * f * \cos \theta) \]

  1. multi-bounce lighting:通过 position,normal 并利用周围 probes 的上帧 irradiance texture 来重建 hit point 的光照信息,并进行间接光照着色

\[L_{multi} = \frac{E(position, normal)* albedo}{\pi} \]

间接光照着色必须使用 diffuse BRDF,即乘 albedo 除 PI。因为 irradiance 丢失了入射角相关的光照强度信息,无法使用 specular/glossy BRDF。

  1. 将结果写入进 radiance texture

\[L(rayDir) = L_{one} + L_{multi} \]

更新 probe 的 irradiance

  1. 使用 radiance texture 来更新 probe 中的 irradiance texture

\[E(texelDir) = \frac{\sum_{\text {rays}} L(rayDir)\cdot \max (0, \cos \theta )}{\sum_{\text {rays}}\max (0, \cos \theta )} \]

  1. 使用 position texture 来更新 probe 中的 moment texture

\[\begin{aligned} r(texelDir) &=\frac{\sum d(rayDir) \cos ^s \theta_i}{\sum \cos ^s \theta_i} \\ r^2(texelDir) &=\frac{\sum d(rayDir)^2 \cos ^s \theta_i}{\sum \cos ^s \theta_i} \end{aligned} \]

position texture 代表了某个方向的光线交点的世界空间坐标位置,通过变换可以计算成该方向的最近 depth,这将在后面用于 probe 的 depth test,去解决一些漏光问题。

  1. temporal filtering:

\[E_{new}(texelDir)=\operatorname{lerp}(E_{old}(texelDir),E(texelDir), historyWeight ) \]

重建 shading point 光照信息

在最后我们需要对 shading point 进行着色的时候,就可以通过更新好 irradiance 的 probes 来重建 shading point 的光照信息:

  1. 根据 shading point 的世界坐标找到其邻近的 8 个 probes

  2. 对每个相邻的 probe:

    • 计算出 shading point 到 probe 的 \(dir\)

    • 测试 \(dir\) 是否与 \(n\) 面向同一侧:若 \(dot(dir, n)\le 0\),则跳过该 probe

    • 根据 \(dir\) 索引到 texture 中对应的 texel ,得到 \(E(dir)\) , \(r(dir)\)\(r^2(dir)\)

    • 计算 shadow 系数:根据 \(r(dir)\)\(r^2(dir)\) 来对 shading point 进行类似 shadow map 的测试

    • 该 shading point 受到的光照信息 irradiance 即为 \(E(dir)*shadow\)

  3. 对这些通过测试的 probes 算得的 irradiance,以距离为权重进行混合便能得到 shading point 的光照信息

重建光照信息实际上在 DDGI 的流程走了两次,一次在计算 ray radiance 那块需要计算 hit points 的着色,另一次便是在这里需要计算 shading points 的着色。

优缺点

优点:

  • 支持动态光照信息
  • 在预计算模式下,还能避免运行时 ray tracing 的开销
  • 若将 volume 绑定在 camera 上,则存储空间的开销增长与场景大小无关

缺点:

  • 硬件 ray tracing 的开销可能仍然很大
  • 在预计算模式下,对于大量动态物体的场景可能会很容易穿帮,因为几何信息只包含静态物体
  • 不能放置太多 probe,因为每个 probe 需要更多存储空间了(textures 相比于 SH 系数)

有关于优化和改进 DDGI 的方案,可以详见于 动态漫反射全局光照Dynamic Diffuse Global Illumination | 知乎 [宇亓],这方面个人觉得宇亓的文章会更加全面。

Screen Space Probe

动态放置 probes

uniform placement

Uniform:在屏幕上均匀放置 probe;一般使用 2d texture 存放 uniform probes。

adaptive placement

Adaptive:在 uniform probes 之间,每隔一小段距离尝试进行插值测试,对插值失败的地方额外放置 probe;一般使用动态数组存放 adaptive probes。

所谓插值测试,一般是指根据 uniform probes 的世界坐标进行插值得出当前屏幕点的插值世界坐标,如果与当前屏幕点的实际世界坐标相差太大,那就意味着该位置出现几何上的突变,从而导致插值失败。

UE5 Lumen 就是采取了 screen space probe 的方案。

探测动态光照信息

基本流程和 DDGI 差不多,只不过由于 screen space probe 是贴在表面上的,因此它只需要探测表面法线为中心的半球面(而不是探测整个球面)。

并且,它在八面体映射(octahedral mapping) 的基础上再做一步转换就能映射到仅含四个面的 texture 上(即存储空间可以节省一半)。

spatial filtering

由于 screen space probe 是贴在屏幕上的,因此也可以充分利用屏幕信息(一般是G-Buffer)来作为 probe 层级 spatial filtering 的指导。

优缺点

相比于空间中均匀放置 probe 的方案,screen space probe 具有

优点:

  • 对性能友好:基于屏幕的方案所需要的 probes 数量明显要少很多
  • probe 的利用率更高:贴合在物体表面上的 probes 离物体表面的距离更近,因此上 irradiance 的表示更加贴切和准确;而在空间中均匀放置的 probes 可能会有一些压根没怎么用到,利用率更低

缺点:

  • 能量损失更严重:ray 命中的地方很可能不存在 screen space probe,即丢失了屏幕可见几何表面以外的光照信息。因此,一般来说 screen space probe 会搭配别的 GI 方法补上这块的空缺。
  • 很难支持体积渲染:丢失了空间中非物体表面的 irradiance 信息,几乎不能支持体积渲染

Light Leaking 问题

不过 probe 经常会遇到错误的 Light Leaking (漏光)问题:受到几何上不应该存在光照关系的 probe 影响,常见于墙壁遮挡的内外侧。

例如,下面的一个 probe 生成在室内,我们预期室外的墙体是亮黄色的,室内的墙体是暗灰色的:

结果由于 probe 方法没有考虑遮挡,而是直接且错误地进行了对相邻的 probe 插值,从而室外本该明亮的一侧变暗,室内本该黑暗的一侧发生 light leaking:

标记法

这个方法很直观,就是给 probe 额外一个标记属性:是否在室内(例如1为在室内,0为在室外);这样我们要拿 shading point 周围的 probes 来插值时,根据 shading point 在室内外的情况来选择用哪种标记属性的 probes 来进行计算。

虽然粗暴直接,但是开销低廉,实际效果很 work,很多游戏工业界都会采用的方法(例如原神)。在一些方案中,这些标记也可以由自动化生成,而无需人工生成。

shadow-map-like

一些 probe 方案将半球方向上每个 texel 方向的深度存入到基于八面体映射的 depth texture 上。

那么在重建 shading point 光照信息时,我们就可以使用类似 shadow map 的思想来检测 shading point 到该 probe 的光路会不会被遮挡,从而计算出该 probe 对 shading point 的贡献:

\[d_x = x_{probe} - x \\dir = normalize(x-probe) \]

\(x_{probe}\) 为 probe 的位置,\(x\) 为 shading point 的位置。

\[shadow = d_x > d(dir)?0:1 \]

也可以,可使用类似 VSSM 的思想,基于切克比夫不等式(Chebyshev’s Inequality) 去做光滑的遮挡测试(避免硬边缘) :

\[\mu = r(dir)\\ \sigma = r^2({dir})-r(dir)*r(dir) \]

\[shadow = P(X>d_x) \approx \frac{\sigma^{2}}{\sigma^{2}+(d_x-\mu)^{2}} \]

参考

有关基于 Probe 的实时全局光照方案(Probe-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(注意身份验证和授权之间的区别;身份验证检查用户是否如她所说的那样,授权检查用户是否有权访问资源

随机推荐