GPU Lightmapper:技术深度剖析

FLORENT GUINIER / UNITY TECHNOLOGIESContributor
May 20, 2019|15 Min
GPU Lightmapper:技术深度剖析
为方便起见,此网页已进行机器翻译。我们无法保证翻译内容的准确性或可靠性。如果您对翻译内容的准确性有疑问,请参阅此网页的官方英文版本。

照明团队正在全力加快迭代速度。我们在设计渐进式 Lightmapper 时就考虑到了这一目标。我们的目标是就您对项目中的照明所做的任何改动提供快速反馈。在 2018.3 版中,我们推出了 GPU 版本的渐进式光源映射器预览版。现在,我们正朝着在功能和视觉质量上与 CPU 同类产品看齐的方向迈进。我们的目标是使 GPU 版本比 CPU 版本快一个数量级。这为艺术工作流程带来了交互式光绘,极大地提高了团队的工作效率。

有鉴于此,我们选择使用RadeonRays:一个来自 AMD 的开源光线追踪库。Unity 和 AMD合作开发了GPU Lightmapper,实现了多项关键功能和优化。即:功率采样、射线压缩和自定义 BVH 遍历。

GPU Lightmapper 的设计目标是提供与CPU Lightmapper相同的功能,同时实现更高的性能:

  • 无偏见的交互式灯光制图
  • CPU 和 GPU 后端功能均等
  • 基于计算的解决方案
  • 波前路径跟踪,实现最高性能

我们知道,迭代时间是艺术家提高视觉质量和释放创造力的关键。交互式光线映射是这里的目标。不仅仅是整体烘烤时间令人印象深刻,我们还希望用户体验能提供即时反馈。

为此,我们需要解决一系列有趣的问题。在本篇文章中,我们将探讨我们做出的一些决定。

渐进式反馈

为了让 Lightmapper 向用户提供渐进式更新,我们需要做出一些设计决定。

无预计算或缓存数据

在进行直接照明时,我们不会缓存辐照度或可见度(直接照明可以缓存并重新用于间接照明)。一般来说,我们不会缓存任何数据,而是倾向于选择足够小的计算步骤,以避免产生停滞,并在烘焙时提供渐进的交互式显示。

图片

场景可能非常大,包含许多光照图。为确保工作用在能为用户带来最大利益的地方,必须将工作重点放在当前可见的区域。为此,我们首先会检测屏幕上哪些光贴图包含了最多的未收敛可见色块,然后渲染这些光贴图,并优先处理可见色块(一旦所有可见色块收敛,屏幕外的色块将被烘焙)。

如果一个色点位于当前的摄像机光锥内,并且没有被任何场景静态几何体遮挡,则该色点被定义为可见。

我们在 GPU 上进行这种删减(以利用快速光线追踪)。以下是剔除工作的流程。

图片

剔除工作有两个输出:

  • 剔除贴图缓冲区,存储光照贴图的每个像素是否可见。然后,渲染工作就会使用这个剔除映射缓冲区。
  • 一个整数,代表当前光贴图的可见色块数。CPU 将异步读回这个整数,以便在未来调整光绘调度。

在下面的视频中,我们可以看到删减的效果。烘烤中途停止,以便演示。因此,当 "场景 "视图移动时,我们可以看到从初始摄像机位置和方向看不到的尚未烘焙的色块(即黑色)。

出于性能考虑,可见度信息只在摄像机状态 "稳定 "时更新。此外,超采样也没有考虑在内。

性能和效率

GPU 经过优化,可以处理大批量数据,并对所有数据执行相同的操作;它们针对吞吐量进行了优化。更重要的是,与多核 CPU 相比,GPU 在实现这种加速的同时,还具有更高的功耗和成本效益。不过,GPU 在延迟方面不如 CPU(硬件设计故意如此)。这就是为什么我们使用数据驱动的流水线,没有 CPU 和 GPU 的同步点,以充分利用 GPU 本身的并行计算特性。

然而,光有原始性能是不够的。用户体验才是最重要的,我们用视觉效果随时间的变化来衡量,又称趋同率。因此,我们还需要高效的算法。

数据驱动管道

GPU 可用于大型数据集,它们能够以延迟为代价实现高吞吐量。此外,它们通常由 CPU 提前填满的命令队列驱动。持续不断的大型命令流的目的是确保 GPU 能够饱和工作。让我们来看看我们用来最大限度提高吞吐量和原始性能的关键配方。

我们的管道

我们处理 GPU 光映射数据管道的方法基于以下原则:

1.我们只需准备一次数据。

此时,CPU 和 GPU 可能会同步,以减少内存分配。

2.一旦开始烘烤,就不允许 CPU 与 GPU 同步。

CPU 向 GPU 发送预定义的工作负载。这种工作负载在某些情况下会过于保守(例如,使用 4 个弹跳,但所有间接射线都在第 2 个弹跳后完成,那么我们仍有排队的内核将被执行,但会提前退出)。

3.GPU 不能生成射线或内核。

相反,它可能被要求处理空作业(或非常小的作业)。为了高效处理这些情况,内核的编写方式是最大限度地提高数据和指令一致性。我们通过数据 "压实 "来处理这个问题,稍后将详细介绍。

4.我们不希望出现任何 CPU 和 GPU 同步点,也不希望烘烤开始后出现任何 GPU 气泡。

例如,一些 OpenCL 命令会产生较小的 GPU 气泡(即 GPU 没有东西可处理的时刻),如 clEnqueueFillBuffer 或 clEnqueueReadBuffer(即使是异步版本),因此我们尽可能避免使用这些命令。此外,数据处理需要尽可能长时间地在 GPU 上进行(即渲染和合成直至完成)。当我们需要将数据传回 CPU 进行额外处理时,我们将以异步方式进行,而不会再次将数据传回 GPU。例如,缝合目前是 CPU 的 PostProcessing

5.CPU 将以异步方式调整 GPU 负载

当摄像机视图发生变化或光照图完全聚合时,更改正在渲染的光照图会产生一定的延迟。CPU 线程使用无锁队列生成并处理这些回读事件,以避免互斥竞争。

图片
适合 GPU 的作业大小

GPU 架构的主要特点之一是支持广泛的 SIMD 指令。SIMD 代表单指令多数据。在所谓的 warp/wavefront 内,一组指令将在给定的数据量上按顺序执行。这些波面/warps 的大小为 64、32 或 16 个值(取决于 GPU 架构)。因此,一条指令将对多个数据进行相同的转换--单指令多数据。不过,为了获得更大的灵活性,GPU 还能在其 SIMD 实现中支持不同的代码路径。为此,它可以在处理子集时禁用一些线程,然后再重新加入。这就是 SIMT:单指令多线程然而,这样做是有代价的,因为波阵面/warp 中的不同代码路径只能从 SIMD 单元的一小部分中获益。阅读这篇出色的博文,了解更多信息。

最后,SIMT 理念的一个巧妙延伸是,GPU 能够在每个 SIMD 内核上保留多个翘曲/波阵面。如果一个波阵面/warp 正在等待缓慢的内存访问,调度器可以切换到另一个波阵面/warp,并在此期间继续工作(前提是有足够的待处理工作)。不过,要想真正做到这一点,每个上下文所需的资源量必须较少,这样占用率(待处理的工作量)才会较高。

总结一下,我们的目标应该是

  • 多线飞行
  • 避免分歧分支
  • 良好的占用率

良好的占用率与内核代码息息相关,这个话题过于宽泛,不适合在本博文中讨论。这里有一些很好的资源:

一般来说,目标是稀疏使用 Localization 资源,尤其是矢量寄存器和本地共享内存。

让我们来看看在 GPU 上烘焙直接照明的流程。本节主要介绍光照图,但光照探针的工作方式与光照图非常相似,只是它们没有可见度或占用率数据。

图片

这里有几个问题:

  • 该示例中的光贴图占用率为 44%(9 个像素中占用了 4 个像素),因此只有 44% 的 GPU 线程会实际产生可用的工作!此外,内存中的有用数据非常稀少,因此即使是未被占用的像素,我们也需要支付带宽费用。在实际应用中,灯光贴图的占用率通常在 50% 到 70% 之间,因此潜在收益巨大。
  • 数据集太小。为简单起见,示例中显示的是 3x3 光图,但即使是常见的 512x512 光图,对于最近的 GPU 来说也是一个太小的数据集,无法达到最高效率。
  • 在前面的章节中,我们谈到了视图优先级和筛选工作。上述两点更加真实,因为一些被占用的色元不会被烘焙,因为它们在 "场景 "视图中是不可见的,这就进一步降低了占用率和整体数据集。

如何解决这个问题?作为与 AMD 合作的一部分,还增加了光线压实功能。这个想法大大提高了光线跟踪和着色性能。简而言之,我们的想法是在连续内存中创建所有射线定义,从而使翘曲/波阵面中的所有线程都能处理热数据。

在实际操作中,每条射线还需要知道与之相关的像素的索引,我们将其存储在射线有效载荷中。此外,我们还存储了全局压缩光线计数。

下面是压实后的流动情况:

图片

现在,对光线进行着色和跟踪的内核都只能在热内存上运行,而且代码路径的偏差最小。

未来计划我们还没有解决对 GPU 来说数据集可能太小的问题,尤其是在启用视图优先级的情况下。下一个想法是从 gbuffer 表示法中对射线的生成进行去相关化。采用最简单的方法时,我们只能为每个像素生成一条射线。既然我们最终还是要生成更多的射线,那么我们不妨先为每个图元生成几条射线。这样,我们就能为 GPU 创造出更多有意义的工作,让 GPU 得以咀嚼。流程是这样的

图片

在压缩之前,我们会为每个像素生成许多射线,我们称之为扩展。我们还生成元信息,用于收集步骤,将其累积到正确的目标文本格式中。

扩展内核和收集内核的执行频率都不高。在实践中,我们会对每一束光(直接光)进行扩展和遮光,或对所有反弹光(间接光)进行处理,最后只收集一次光。

有了这些技术,我们就能实现目标:生成足够多的工作,使 GPU 达到饱和,并且只将带宽用在重要的像素上。

这就是每个色素拍摄多条射线的好处:

  • 即使在视图优先模式下,活动射线集也始终是一个庞大的数据集。
  • 准备、跟踪和着色都是在非常连贯的数据上进行的,因为扩展内核将在连续内存中创建针对同一色元的光线。
  • 扩展内核处理占用率和可见性,使准备内核更简单,从而更快。
  • 扩展/工作数据集缓冲区的大小与光线贴图的大小无关。
  • 我们拍摄每个像素的光线数量可以由任何算法驱动,自适应采样就是一种自然扩展。

间接照明采用了非常相似的理念,尽管更为复杂:

图片

对于间接光,我们必须进行多次反弹,而每次反弹都可能丢弃随机光线。因此,我们进行迭代压缩,以继续处理热数据。

我们目前使用的启发式方法倾向于在每个像素上使用等量的射线。目标是获得非常先进的输出。不过,其自然延伸是通过使用自适应采样来改进这些启发式方法,从而在当前结果存在噪声的地方拍摄更多射线。此外,启发式还可以通过了解硬件的波阵面/warp 大小,提高内存和线程组执行的一致性。

透明度/透明度

使用 GPU Lightmapper 烤制ArchVizPRO中的资产。

透明/半透明有很多使用案例。处理透明度和半透明的常用方法是:投射光线、检测交集、获取材质,如果遇到的材质是半透明或透明的,则安排新的光线。不过,在我们的案例中,由于性能原因,GPU 无法产生射线(请参阅上文 "数据驱动流水线 "部分)。此外,我们无法合理地要求 CPU 提前安排足够多的射线,以确保我们能处理最坏的情况,因为这将对性能造成重大影响。

因此,我们采用了混合解决方案。我们以不同的方式处理半透明和透明,从而解决上述问题:

透明度(一种材料因有孔洞而不透明)。在这种情况下,射线可以根据概率分布穿过或弹开材料。因此,CPU 提前准备的工作负载无需改变,我们仍然是独立于场景的。

半透明性(当一种材料对穿过它的光线进行过滤时)。在这种情况下,我们采用近似值,不考虑折射。换句话说,我们让材料为光线着色,但不改变光线的方向。这使我们能够在行走 BVH 的同时处理半透明效果,这意味着我们可以轻松处理大量的剪切材料,并很好地扩展场景中的半透明复杂性。

图片

但是,有一个问题;BVH 的遍历顺序不对:

在闭塞光线的情况下,这样做实际上是没有问题的,因为我们只对光线沿线每个相交三角形的半透明衰减感兴趣。由于乘法是交换式的,因此 BVH 的无序遍历不成问题。

然而,对于交叉射线,我们希望能够在一个三角形上停止(当三角形是透明的时候,以一种概率方式),并收集从射线原点到命中点的每个三角形的半透明衰减。由于 BVH 遍历是无序的,我们选择的解决方案是首先只运行交叉点来找到命中点,如果有任何半透明点被命中,则标记该射线。因此,对于每一条标记光线,我们都会生成一条额外的闭塞光线,从交点光线原点到交点光线命中点。为了提高效率,我们在生成闭塞光线时使用了压缩技术,这意味着只有当交点光线被标记为需要进行半透明处理时,才需要支付额外的费用。

这一切都要归功于 RadeonRays 的开放源代码特性,作为与 AMD 合作的一部分,RadeonRays 被分叉并根据我们的需求进行了定制。

高效算法

我们已经看到了我们在原始性能方面所做的工作,非常好!然而,这只是谜题的第一部分。每秒高采样率固然很好,但真正重要的是烘烤时间。换句话说,,我们希望从每一束光线中获得最大收益。最后一句话实际上是数十年持续研究的结果。这里有一些很好的资源:

一个周末完成光线追踪

光线追踪下一周

光线追踪你的余生

Unity GPU Lightmapper 是一款纯粹的漫反射光源贴图。这大大简化了光线与材料的相互作用,也有助于抑制萤火虫和噪音。不过,要提高收敛速度,我们还有很多工作要做。以下是我们使用的一些技术:

俄罗斯轮盘

在每次反弹时,我们都会根据累积反照率对路径进行概率杀伤。在埃里克-维奇的论文(第 67 页)中可以找到很好的解释。

环境多重重要性取样 (MIS)

HDR 环境中的高方差会在输出中产生大量噪点,需要大量采样才能产生令人满意的效果。因此,我们采用了专门针对环境评估的综合取样策略,首先对环境进行分析,确定重要区域,然后进行相应的取样。这种方法并非环境取样所独有,一般被称为多重重要性取样,最初是在 Eric Veach 的论文中提出的(第 252 页)。这项工作是与格勒诺布尔Unity 实验室合作完成的。

许多灯光

在每次反弹时,我们都会概率性地选择一个直射光,并通过空间网格结构来限制影响表面的灯光数量。这项工作是与 AMD 合作完成的。我们目前正在深入研究多光源问题,因为光源选择取样对质量至关重要。

图片

去噪

使用根据路径追踪器输出结果训练的人工智能去噪器去除噪音。请看 Jesper Mortensen 的Unity GDC 2019 演讲

收尾工作

我们已经看到,数据驱动的管道、对原始性能的关注和高效算法是如何结合在一起,通过 GPU Lightmapper 提供交互式光绘体验的。请注意,GPU Lightmapper 正在积极开发中,并将不断改进。

请告诉我们您的想法!

照明团队

PS:如果您觉得这篇文章很有趣,并且有兴趣接受新的挑战,我们目前正在哥本哈根招聘一名照明开发人员,请与我们联系!

---

想了解如何在 Unity 中优化图形?查看本教程。