查看原文
其他

深度技术解析GPU光照贴图

Unity Unity官方平台 2022-05-07

Unity Lighting团队正在全力以赴改进迭代速度。我们在设计渐进式光照贴图时,始终牢记着一个目标:在对项目中的光照进行任意改动时,为用户提供快速的反馈


在Unity 2018.3中,我们加入了预览版GPU渐进式光照贴图,我们正在给它加入CPU光照贴图的对应功能和视觉质量。


我们的目标是:让GPU光照贴图的速度比CPU光照贴图提升一个数量级。这样将给艺术工作流程带来交互式光照贴图,从而提升创作团队的效率。

 

我们使用了AMD的开源光线追踪库RadeonRays。Unity和AMD在GPU光照贴图进行合作,实现了多项重要功能和优化,包括:功率采样,光线压缩和自定义BVH遍历。



GPU光照贴图的设计目标是提供与CPU光照贴图的相同功能,并且实现更高的性能:

  • 渐进式路径追踪光照贴图

  • 出色的性能

  • CPU和GPU后端拥有对应功能,包括:光照探针、混合光照、定向光照贴图、Meta Pass等。

 

我们知道,迭代时间是帮助艺术家提升视觉质量和发挥创意的关键。交互式光照贴图是我们要实现的目标,我们不仅想要出色的整体烘焙时间,还希望让用户体验提供即时反馈。


本文将介绍为了实现该目标,我们需要解决的大量问题以及所做出的决策。


渐进式反馈

为了让光照贴图给用户提供渐进式更新,我们需要做出一些设计决策。


不使用预计算或缓存数据

在使用直接光照时,我们不会缓存辐照度或可见性,直接光照可以进行缓存并重复用于间接光照。一般我们不会缓存任何数据,而是希望计算步骤足够小,不会产生任何停顿情况,并在烘焙时提供渐进式和交互式的显示效果。



下图是GPU光照贴图控制流的大致过程。这种生产者/消费者方法可以在GPU光照贴图异步运行时,允许继续编辑场景,结果会在之后准备好时显示在编辑器。



场景有可能会很大并且包含多个光照贴图。为了确保为用户提供最大效果,我们需要专注于烘焙当前可见区域。

 

为此,我们首先检测哪些光照贴图在屏幕上包含大多数未聚合的可见纹理像素,然后渲染那些光照贴图,优先处理可见纹理像素,屏幕外的纹理像素会在所有可见纹理像素聚合后烘焙。


请注意:如果纹理像素处于当前摄像机视锥体,而且没有被其它场景静态几何体遮蔽,则该纹理像素会被定义为可见纹理像素。

 

我们在GPU上执行剔除,以利用快速光线追踪功能。下面是剔除作业的流程。



剔除作业包含二个输出:

  • 一个剔除贴图缓冲区,用于存储光照贴图的每个纹理像素是否可见的信息。然后该剔除贴图缓冲区会用于渲染作业。

  • 一个表示当前光照贴图可见纹理像素数量的整数值,该整数值会由CPU进行异步回读,以调整之后的光照贴图调度过程。

 

在下面视频中,我们可以看到剔除的效果。烘焙过程为了演示而在中途停止,因此在场景视图移动时,我们可以看到未烘焙的纹理像素,即黑色纹理像素,这些纹理像素无法从摄像机的初始位置和方向看到。


 

出于性能原因,可见性信息仅在每次摄像机状态“稳定”时更新,而且不考虑超采样。


性能和效率

GPU会进行优化以获取大量数据,并对所有数据执行相同的操作,GPU也会针对吞吐量而进行优化。此外,GPU实现的加速效果会比多核CPU更加节省电量和成本。


然而,GPU在延迟方面不如CPU,这个情况是根据硬件设计而有意发生的。这就是为什么我们使用没有CPU-GPU同步点的数据驱动管线,从而充分利用GPU固有的并行计算本质。

 

原始性能还不够,用户体验才是真正重要的,我们使用了随时间变化的视觉影响即聚合速率来衡量用户体验,所以我们还需要高效的算法。


数据驱动管线

GPU旨在用于处理大型数据集,并且能够以延迟为代价实现高吞吐量。GPU可以由CPU提前填充的命令队列驱动。大量命令连续流的目标是确保我们可以通过工作内容让GPU饱和。


下面将介绍用于最大化吞吐量和原始性能的关键部分。


我们的管线

我们基于以下原则使用GPU光照贴图数据管线:


  • 仅准备一次数据

    此时,CPU和GPU可能处于同步状态,以减少内存分配。

     

  • 烘焙开始后,不允许使用CPU-GPU同步点

    CPU会向GPU发送预定义的工作量。在某些情况下,这种工作量会过于保守。


    例如:使用4次反弹过程的时候,所有间接光线会在第二次反弹过程完成,但是我们仍有要执行的队列中的程序核心,实际上它们会提前完成。


  • GPU无法生成光线和内核

    GPU可能会被要求处理空白作业或非常小的作业。为了高效处理这些情况,内核会通过最大化数据和指令的一致性的方法编写,我们会通过数据压缩处理这个问题。

     

  • 一旦烘焙开始,不要有任何CPU-GPU同步点和GPU Bubbles

    例如,一些OpenCL命令可以产生小型GPU Bubbles,即GPU没有内容要处理的时候。像clEnqueueFillBuffer和clEnqueueReadBuffer命令,即使是异步版本也会如此,因此我们要尽可能避免使用这些命令。


    数据处理需要尽可能长时间留在GPU上,即:在GPU上进行渲染和合成,直到完成整个过程。

     

    在需要把数据传回CPU进行额外处理时,我们将异步执行操作,并且不会再将数据发回到GPU,例如:缝隙拼接是目前要在CPU上进行的后期处理过程。

     

  • CPU将以异步方法调整GPU负载

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



对GPU友好的作业大小

GPU架构的关键功能是广泛的SIMD指令支持。SIMD全称是Single Instruction Multiple Data单指令多数据流,指令集会在Warp/Wavefront中以固定顺序对特定数量的数据执行。


Warp/Wavefront的大小为64,32或16个数值,具体取决于GPU架构。因此,一个指令会对多个数据会应用相同的转换过程,这就是单指令多数据的处理过程。

 

为了实现更高的灵活性,GPU也能够在SIMD实现中支持不同的代码路径。为此,它可以禁用一些线程,同时在重新加入线程前处理部分线程,这种过程被称为SIMT,即Single Instruction Multiple Threads单指令多线程。但代价是Wavefront/Warp中的不同代码路径仅从SIMD单位的一部分受益。

 

SIMT方法的简单扩展是:GPU能够对每个SIMD核心保持多个Warps/Wavefronts


如果一个Wavefront/Warp在等待缓慢的内存访问,只要有足够的待处理工作,调度器就可以切换为另一个Wavefront/Warp,并在同时继续处理该Wavefront/Warp。为了使它生效,必须降低每个情况所需的资源量,从而可以提高占有量即待处理工作数量。

 

综上所述,我们的目标是:

  • 同时使用多个线程

  • 避免不同分支

  • 保持好的占有量

 

保持好的占有量是由内核代码决定的,我们不打算在本文中介绍,下面提供了一些相关的学习资源:

  • NVIDIA的Vasily Volkov编写的的《理解GPU上延迟隐藏机制》

    https://www2.eecs.berkeley.edu/Pubs/TechRpts/2016/EECS-2016-143.pdf

 

  • Unity的Francesco Cifariello编写的《GPU Scalarization介绍》

    https://flashypixels.wordpress.com/2018/11/10/intro-to-gpu-scalarization-part-1/

 

我们的目标是较少的使用本地资源,特别是向量寄存器和本地共享内存。

 

下图是在GPU上烘焙直接光照的流程。该部分主要包含光照贴图,但是光照探针也会以类似方式工作,只不过光照探针没有可见性或占用数据。




小提示:BVH表示Bounding Volume Hierarchy包围盒层次,它是光线/三角形相交的加速结构。


这里存在一些问题:

  • 示例中的光照贴图占用率为44%,因此仅有44%的GPU线程会产生可用的工作内容。


    此外,内存中的使用数据很少,即使对于未使用的纹理像素,我们也要为带宽付出成本。实际使用时,光照贴图占用率通常在50%~70%之间,因此可以有很大的效果提升。


  • 数据集太小。为了简单起见,该示例展示了3×3的光照贴图,但即使是常见的512×512光照贴图,对于最近的CPU来说数据集依旧太小,无法实现最高效率。


  • 在前面部分中,我们介绍了视图优先级和剔除作业。由于一些占用的纹理像素不被烘焙,前面的二个问题会更加明显,因为它们目前在场景视图中不可见,所以会进一步减少占用率和整体数据集

 

如何解决这些问题呢?通过和AMD进行合作,我们添加了光线压缩功能,该功能大幅提升了光线追踪和着色的性能。简单来说,该功能会把所有光线定义创建在连续内存中,从而允许Warp/Wavefront内的所有线程处理常用数据。

 

在实际情况中,我们需要让每个光线知道关联纹理像素的索引,我会将这项数据保存在光线的Payload载荷中。此外,我们会保存全局压缩光线数量。

 

下面是压缩的流程图。



用于着色和追踪光线的内核都可以只运行在常用内存上,对代码路径使用最小分支。

 

我们还没解决数据集对GPU来说太小的问题,特别是在启用视图优先级的情况。我们的办法是去除Gbuffer表示中光线生成的相关性


通过使用本地方法,我们仅对每个纹理像素生成一个光线。由于最后要生成更多的光线,因此我们预先会对每个纹理像素生成多个光线。

 

通过这种方法,我们可以为GPU创建更多要处理的工作内容。下面是流程图。



在进行压缩前,我们会对每个纹理像素生成多个光线,我们把它称为扩展。我们还会生成用于聚集阶段的元数据,从而积累到正确的目标纹理像素。


扩展和聚集内核都不会频繁执行。实际情况中,我们会为直接光照展开和着色每个光线,为间接光照处理所有反弹效果,从而只在最后聚集一次。

 

通过这些方法,我们实现了目标:生成了足够的工作内容来使GPU饱和,而且仅在重要的纹理像素上使用带宽。


下面是怼每个纹理像素发射多个光线的优点:

 

  • 即使在视图优先级模式中,活跃的光线集也总是大型数据集

     

  • 准备,追踪和着色过程会处理一致性很高的数据,扩展内核会在连续内存中针对相同纹理像素创建光线。

     

  • 扩展内核会处理占用率和可见性,使准备内核更为简单和快速。

     

  • 扩展/处理的数据集缓冲区大小会和光照贴图大小分离

 

  • 对每个纹理像素发射的光线可以由任意算法驱动,自然的扩展会变成适应性采样。

 

间接光照使用了非常类似的方法,尽管相较而言它的方法更加复杂,如下图所示。


 

小提示:首次反弹的环境光线会视为直接光线。

 

对于间接光线,我们必须执行多次反弹,每次反弹都可能会丢弃随机光线,因此我们可以迭代地进行压缩,以保持在常用数据上工作。

 

我们当前使用的启发式方法有利于在每个纹理像素上得到等量的光线,目标是取得渐进式的结果。然而,这样的自然扩展是通过使用适应性采样来改进启发式方法,从而发射更多光线到当前结果较混乱的位置。


启发式方法可以通过了解硬件的Wavefront/Warp,来实现内存和线程组执行中的更高一致性。


透明和半透明效果

下图是使用GPU光照贴图烘焙的资源ArchVizPRO的效果。

 


透明和半透明效果有很多用例。处理透明和半透明效果的常用方法是:投射光线,检测相交情况,获取材质,如果遇到的材质是半透明或透明的,则调度新的光线

 

但在我们的示例中,出于性能原因,GPU无法生成光线。我们无法要求CPU预先调度足够的光线,所以这是要处理的最糟糕情况,它会对性能产生较大影响。

 

因此,我们使用混合的解决方案,通过不同的方法处理半透明效果和透明效果,解决了上述问题:

 

  • 透明效果:材质因为有洞而不透明的情况

    在这种情况下,光线可以穿过材质,或者基于可能性分布而在材质上反弹。因此,CPU预先准备的工作量不需要变化,我们的内容依旧独立于场景。

     

  • 半透明效果:材质会过滤穿透的光线。

    在这种情况下,我们会进行近似处理,而且不会考虑折射效果。也就是我们会让材质为光线着色,但不会改变光线的方向。


    这让我们可以在遍历BVH时处理半透明效果,这意味着我们可以轻松处理大量镂空材质,并很好的调整场景中的半透明复杂度。



但是这里存在一个问题:BVH遍历的顺序会出错

 

在遮蔽光线的情况中,因为我们只对沿着光线的每个相交三角形的半透明衰减感兴趣,所以这个问题的影响不大。由于乘法计算的顺序是可以互相交换的,所以乱序的BVH遍历不是一个问题。

 

但对于相交光线而言,我们想让相交光线可以在三角形上停下,当三角形是透明的时候,让光线以概率性的方法停下,并且收集从光线起始点到投射位置的每个三角形的半透明衰减情况。

 

由于BVH遍历的顺序乱了,我们的解决方案首先只会运行相交部分以找到投射位置,然后会在光线射到半透明部分时标记光线。因此,对于每个被标记的光线,我们会从相交光线起始点到相交光线投射位置生成额外的遮蔽光线。


为了高效地完成这一步,我们在生成遮蔽光线时使用压缩部分,这意味着仅在相交光线被标记为需要半透明处理时,才会需要付出额外的成本。

 

所有这些方法的实现都归功于RadeonRays的开源性质,作为我们和AMD合作的一部分,我们复制代码库,并根据需要对代码库进行了定制。


高效的算法

我们看到了这些方法对原始性能的效果,结果非常好。但是这仅仅整体的第一部分。每秒有大量采样数量是很好的,但真正重要的是烘焙时间。也意味着:我们想对投射的每条光线取得最大的作用效果。

 

Unity GPU光照贴图是纯粹的漫反射光照贴图。它简化了光线和材质的相交效果,同时有助于减弱Fireflies和噪声。但是,我们仍然可以做很多事情来提升聚合率。


下面是我们使用的一些方法。

  • 俄式轮盘方法每次反弹时,我们会基于积累的反照率有概率地消除路径。


  • 环境多重采样(MIS环境)方法具有很大变化的HDR环境可能在输出内容产生大量噪声,并需要大量采样数量来取得合适的结果。因此我们应用了特别调整的采样策略组合。


    首先进行分析,识别重要区域,再相应地进行采样,从而评估环境。这种方法叫作Multiple Importance Sampling多重要性采样,它不只可以用来处理环境采样,该方法是和Unity Labs Grenoble团队合作实现的。

     

  • 多光线方法每次反弹时,我们会按照概率选择一个直接光线,然后使用空间网格结构限制影响表面的光线数量。


    因为光线选择采样对质量至关重要,所以目前我们正在深入调查多光线方法的问题,该方法是我们和AMD合作实现的。


    下图是使用GPU光照贴图和HDRP渲染的Unity伦敦办公室。该场景有很多光线。



  • 降噪处理通过使用由路径跟踪器输出训练的AI降噪器来消除噪声。


小结

本文,我们介绍了如何把数据驱动管线,对原始性能的关注和高效算法结合在一起,通过GPU光照贴图提供交互式光照贴图体验。

 

GPU光照贴图仍处于开发状态,它会不断进行改进,如果你有反馈意见或建议,请访问:

https://forum.unity.com/threads/gpu-lightmapper-preview.561103/


更多Unity教程,尽在Unity Connect平台(Connect.unity.com)。下载Unity Connect APP,请点击此处。 观看部分Unity官方视频,请关注B站帐户:Unity官方


推荐阅读

AMD Radeon Rays将集成到Unity的GPU渐进式光照贴图系统

Unity技术分享|渐进光照贴图

Unity中的批处理优化与GPU Instancing

通过快捷灵活的导入功能加速BIM工作流程

Project Tiny C# 预览版现已推出

如何使用Unity创建随机关卡


直播课程

6月12日晚8点,Unity技术经理成亮将为你解析轻量级渲染管线LWRP最新功能及案例。[了解详情...


直播课程:轻量级渲染管线LWRP最新功能及案例解析

直播地址:

https://connect.unity.com/events/2019_lwrp_new_features



喜欢本文,请点击“在看”

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存