UE5.1 Lumen Indirect Diffuse Lighting分析


Lumen学习笔记,有错误的地方欢迎各位大佬指正

由于工作内容的原因,本文章主要讲的是UE5.1的 Lumen Indirect Diffuse部分

Indirect Diffuse部分由下面五个部分组成

image

  • Mesh Card

Mesh Card是一种高精度的全局空间存储结构,它可以缓存包裹物体的材质和光照信息(Surface Cache)。当光线击中这个结构时,可以快速获取颜色信息。在近距离处,我们需要高精度的光照结果,因此需要追踪Mesh Card。

  • Voxel

Voxel是一种低精度的全局空间存储结构,它可以存储与Voxel相交的六个方向上的Mesh Card的颜色信息。对于远处的间接光照信息,我们不需要过于精确的结果,使用Voxel可以以较小的时间和空间代价获得粗糙的光照结果。

  • Direct Lighting & Indirect Lighting

有了Mesh Card这个存储结构,我们就需要为其注入光照信息。Mesh Card使用Surface Cache来记录光照信息。这里的Direct Lighting(直接光照)和Indirect Lighting(间接光照)共同计算了Surface Cache内的最终光照。

  • Screen Probe

Screen Probe Gather是一种精度较低的屏幕空间结构,用于收集周围Surface Cache和Voxel的光照信息。最后,屏幕空间的颜色是通过插值Screen Probe得到的。

概述

全局光照技术(Global Illumination, GI)旨在模拟光在环境中的传播,包括直接来自光源的光线(直接光)和经过一次或多次反射、折射或散射后到达物体表面的光线(间接光)。实时计算间接光一直是个挑战,因为在物理层面上,每个点的间接光实际上是由无数束光经过无数次的相互作用所形成。为了解决这一复杂的光线交互问题,已经提出了多种GI算法和技术。

Lumen代表了全局光照技术的一次重大飞跃,它整合了多种先进的GI方案,通过这种混合方法,不仅提高了性能,还实现了接近实时的渲染效果。Lumen以其高效的处理方式,为游戏和视觉效果行业带来了前所未有的光影表现力。

image

接下来讲一下Lumen到底是怎么做到实时渲染高质量画面的。

先从屏幕成像说起,假设我们现在的屏幕需要绘制一张图像,最直接的方法逐Pixel去Shading,每个点都计算直接光和间接光并累加,这样得到的结果是准确却十分耗时的,直接光好算,但要算间接光不可避免的就要去Trace射线,就以现在的2K屏幕为标准,如果每个Pixel都去Trace,哪怕是1spp渲染整个画面都需要2560×1440约等于360万根射线,而1spp得到的图形质量完全是不够看的,高质量的GI效果需要更多的光线,而这是目前计算机承受不起的。

image

由于间接光信息是不需要很准确的,所以完全没必要每个Pixel都去Trace,而Lumen采用Screen Probe的方式去以低分辨率收集间接光,Screen Probe会每帧像周围发射64根射线去收集附近的光照信息,我们可以把Screen Probe理解为“眼睛”,我们在物体表面每隔一定距离遍布一只“眼睛”来替我们收集周围的间接光信息,最后眼“眼睛”附近的点都从眼睛里获取颜色,具体说就是每16×16个像素的颜色从一个Screen Probe里插值处理(这里先不考虑滤波),有了这种方式,我们只需要有少量的Screen Probe就可以配合Gbuffer插值出整个屏幕的颜色了。

image

由于Screen Probe每帧都需要更新,Lumen采用了一种额外的类似结构World Probe来加快Screen Probe Trace射线收集光照的速度,World Probe是一种放在世界空间视角无关的Probe结构,使用World Probe的好处在于Screen Probe不需要再长距离步进去获取光线了,这个过程交给World Probe来做,射线超过一定距离直接去附近的World Probe借光就行了。且由于Wordl Probe位置是不会随着屏幕动的,所以每帧获取到的颜色都是非常稳定的,这样最后插值处理的屏幕颜色会更平滑。

image

而有了“眼睛”Screen Probe去收集光照信息,接下来的问题就是要如何去Trace和Trace到了要如何拿到光照信息这两个问题了。如何去Trace这个问题我们跳过,具体可以看看我全局光照加速结构那篇文章,接下来讲讲如何拿到光照信息。

假设现在我们已经有了非常完备的Ray Marching和Ray Intersection方案,接下来的就是要获取Hit Point的颜色作为间接光了,这里最容易想到的其实是SSRT(Screen Space Ray Tracing),直接近距离Trace 上一帧的颜色来作为间接光,因为GBuffer本来就要算(这里默认指延迟渲染),获取GBuffer的过程几乎免费而且上一帧和这一帧光照大部分情况不会有太大差异,所以SSRT是非常快且准确的。但是SSRT不能获取到物体背面信息以及屏幕外的信息,只用SSRT造成背面以及移动时候新场景的光照信息缺失。

image

不只是背面的光照信息,很多时候屏幕外的光照其实也会对屏幕内物体产生影响,所以Lumen采用了SDF Tracing来补充世界空间的光照信息。

image

既然要获取世界空间的光照信息,那就需要与屏幕没有关系且可存储世界空间多次弹射的间接光的结构,Lumen采用了Surface Cache(Mesh Card里存储光照的结构)以及Voxel来存储世界空间的光照信息。为什么需要用两种结构呢?首先我们要明白计算间接光的终极目标是快和带宽占用少,正是如此Lumen采用了大量的hack来达到这一目标。间接光本来就不需要非常准确,所以Lumen用了对于近处的物体采高质量间接光而远处物体采低质量间接光的方式来综合考虑间接光,也就是在近处距离Trace的时候会去找Surface Cache上的间接光信息,这保留了一定精度,而当光线Trace了一定距离以后还没有找到Hit Point,那会去Trace远处的Voxel来获取粗糙的光照信息了,距离较远的物体与物体之间间接光影响实际上非常小,所以这种双重结构带来高性能的同时也能保证最终画面的效果。

image

image

有了存储光照的结构了,下一个问题就是如何让把弹来弹去的光线收集起来,因为间接光的弹射往往不止一次,但是如果递归追踪光线就很难做到实时了,Lumen采用了一种非常巧妙的分帧累积的方法,举个例子,第0帧只计算直接光并记录到Surface Cache上,Voxel去收集Surface Cache上的光照给下一帧使用。第1帧计算直接光同时Trace光线到附近的Surface Cache 和上一帧算好的Voxel,以他们为光源去算间接光,再用Voxel去收集当前Surface Cache收集好的光照信息给下一帧用,以此类推,就可以做到每帧只算1次Bounce,多帧以后达到无数次反弹的效果。

image

有了分帧累积的思想,只需要按算法流程去Trace光线就可以获取到无数次弹射的间接光结果了。因为只有光照注入后才能Trace到光照,所以接下来需要离屏注入Surface Cache以及Voxel内的光照信息。光照注入又分为直接光和间接光两个部分,这两个部分Lumen都是在Mesh Card内做(写入到Radiance Cache),Voxel直接Trace 6个面首次命中的Mesh Card就可以得到光照信息了。

要收集Mesh Card表面的直接光加间接光其实就可以理解为我在Mesh Card上面放了一个正交摄像机去着色,最后再把颜色存入Surface Cache里面,而为了快速着色就可以用上文提到的Screen Probe Gather,Lumen的具体做法是在世界空间内的Mesh Card表面放置一堆的Probe去Trace光线,用少量的Probe插值出整个表面的颜色,略有不同的是Mesh Card上的Probe更密集且发射的射线更少,具体是每4×4个像素上放一个Probe,每个Probe只发射16根射线最后通过16个Trace的结果再去插值得到64个方向的颜色。
image

Lumen有一套复杂的排队机制,每帧只更新最重要最需要更新的部分Mesh Card内的Surface Cache和Voxel且它们都会被持久化存储,这样可以大大的加快收集光照的速度。

同时光照质量也是非常重要的,Lumen做了很多工作来减少误差的传递,后面主要说说这些细节部分。

Mesh Card

SDF加速在我全局光照加速结构那篇文章有介绍。

为了方便讲述,下文把Surface Cache拆成两个部分,Surface Cache存储材质信息,Radiance Cache存储光照信息,实际上材质和光照都是存在Surface Cache这一大结构里的。

为什么需要用Mesh Card

  • 存储世界空间的Materil和Irradiance

SDF已经大大加速了Ray Marching,但是SDF只存空间中的点到物体的最近距离,也就是这时只有HIt Point的Positon,没办法获取到Material信息,没有材质自然也就无法计算光照,因此我们需要一种结构,在SDF获取到Hit Point的同时也能找到对应的Material。

MeshCard便是这种数据结构,他不仅能够在获取到Hit Positon,还能通过Hit Positon映射到Mesh Card对应的位置获取Surface Cache和Radiance Cache。

  • 补全屏幕空间缺失的信息

Mesh Card还有一个重要的作用,就是它是与视角无关的世界空间的结构,世界空间内的光照信息会被持久存储到Mesh Card里,这种在世界空间的结构体可以很有效的补全屏幕空间的缺失的信息。

MeshCard原理

Mesh Card是一种结构的统称,在Lumens里面,我们每个导入的物体都会离线生成一套Mesh Card。Mesh Card 是Mesh的一种属性结构,用于记录数据。要注意的是,MeshCard同时存储材质属性和光照信息,Material属性虽然一般都不会改变,但是离线生成Mesh Card的时候只会生成表面对应的捕获光线的位置,其中的Material属性一般会在加载时捕获,这是因为不同的Mesh之间的重叠遮挡会使Mesh Card表面信息没办法在离线时就确定。Radiance会每帧变化,所以每帧都会选择部分需要更新的Mesh Card更新Radiance Cache。

UE里面捕获Mesh Card的方式是通过对6个轴对齐方向(上、下、左、右、前、后)进行光栅化,获取Mesh对应的材质中的Material Attributes(Albedo、Opacity、Normal、Emissive)并存储到Surface Cache上,同时需要捕获的还有对应观察角度的Hit Point,每个面的Hit Point数据需要进行物理地址转换到Surface Cache图集空间中执行采样。

image

每个Mesh至少有6个Card,复杂物体可能会生成多个。

image

Mesh Card用于记录Material的Attribute和Lighting的Radiance,我们把这两种数据分为Surface Cache和Radiance Cache,而我们最后把数据存入Mesh Card里面。

Card LOD

大世界中往往存在很多的Mesh,对每个Mesh都进行细致的光栅化是不切实际的,所以对于远距离的Mesh一般采用Card LOD,也就是远处物体不需要生成那么多Card,往往6-8个足够了,这样可以减少光栅化的次数。

image

每个 Mesh 的每个 LOD 最多可以有 32 个 Card。

Physical Page Atlas

由于经常会有一会用低分辨率Card 一会用高分辨Card的需求,Lumen把大于128×128的Card Page分为多个Page存储,而小于128×128的Card Page会用Sub-allocate的方式存在大的Card Page里

在这里插入图片描述

Surface Cache

Surface Cache内的Material属性一般都会经过硬件压缩来减少显存的占用

在这里插入图片描述

在这里插入图片描述

Surface Cache属性通常类似于GBuffer的属性,不同Mesh之间会有交叉和遮挡,所以Material属性通常在运行时捕获。

在这里插入图片描述

对于较远的物体,在光栅化记录Surface Cache中Material Attribute的时候,可以采用更低分辨率进一步减少光栅化和存储,这就是Surface Mipmap。与传统 Mipmap 存储方式不同,Surface Mipmap 在 Surface Cache 平铺展开存储,因此在采样时需要额外的地址转换。

下面左右分别是同一个Mesh的不同Mipmap

在这里插入图片描述

Surface Cache资源管理

Virtual Surface Cache

由于Surface Cache需要同时满足GI和Reflection的计算,因此需要同时使用高分辨率和低分辨率的Surface Cache。

对于GI,通常根据相机距离(一般在周围)与LOD来分配低分辨率的Surface Cache。

对于Reflection,通常会根据反射光线命中率来选择使用高分辨率的Surface Cache。

要实现这种需求,Lumen采用的物理页管理方式是类似与虚拟纹理的

在这里插入图片描述

Radiance Cache

Mesh Card不只记录Material(Surface )的属性,还需要记录光照的Radiance属性,这是为了间接光的复用,我们不可能在相交点发射无数光线去不停的递归计算间接光,所以我们把每一次的光照结果固定下来,Radiance Cache算的是Irradiance。

要注意的是,计算Radiance Cache的方式并不是直接获取Hit Position的Material直接算光照,而是通过下面用Probe做分帧计算,最后结果再存进Radiance Cache,Ray打到Hit Point的时候,我们直接取Radiance Cache里的值就行了。这么做的原因是提高光线追踪的并行效率,因为每条光线碰到的Hit Point的材质可能相差很大,很难保证局部性,所以Lumen将Surface的光照解耦,将其分摊到多帧中计算,而这些Radiance Cache可以被后续帧复用。

分帧算法

image

第一帧先把直接光记录到Radiance Cache里面

第二帧以第一帧记录了直接光的Mesh Card作为光源,发射射线去Trace,得到的结果当作下一次的光源

每次得到的结果加上上一帧的结果得到Final Lighting,以此递归,相当于把递归算法拆解到每一帧

image

如果对每一个Pexel都去Trace光线收集Radiance是不现实的,哪怕是分帧,每个Pexel都去发射光线还是会造成很大能耗,因为间接光材质默认是Diffues的,所以我们以每4呈4为一个单位生成Probe去Trace拿到光照信息,每个Probe做16次Cone Tracing再通过插值的方式得到64个结果

image

最后转换成SH来降低存储,这样可以大幅度加快实时Trace效率,又因为是低频信号,所以结果也很不错。

image

最后就是直接光和间接光的Combine了,这里的DirectLighting和IndirectLing涉及的内容非常多,会再下面VoxelLighting讲完后一起说。

image

Sort Mesh Cards

Mesh Cards里面的Lighting Cache需要实时更新,但如果对整个Surface Cache都更新的话是十分耗时的,因此Lumen里面规定,每帧最多不超过 1024 x 1024 个texels更新。Lumen用桶排序去筛选出最需要更新的Card Page,具体规则是遍历所有的Card Page并构建优先级直方图,通过优先级来更新对应的Card,优先级的计算综合考虑了视点距离和更新时间这两个因素,最终大概只有10%的Mesh Cards可以被更新。

在这里插入图片描述

Voxel Lighting(UE5.1后已砍)

为什么需要Voxel Lighting

  • 适配Global SDF

由于Lumen加速结构的划分问题,对于近处的物体,其采用的是遍历附近物体的Mesh Distance Field找到到最近距离的点来作为步进距离,虽然要遍历多次,但是这可以直接获取到对应物体Mesh Card上的交点,有了交点就可以获取到对应的材质和光照信息了,但是对于远处的物体,Lumen采用的是Global Distance Field,由于是全局的距离场不需要遍历对应Mesh Card我们没办法跟Per-Mesh SDF一样直接获取到对应的Mesh Card的材质和光照,所以Lumen构建了专门针对于Global SDF Tracing的结构,也就是Voxel,Hit到直接可以获取到光照信息。

  • 作为间接结构

Lumen里采用逐帧累积的方式来表示不断反弹的光线,Voxel其实是一种间接结构,第一帧的Voxel的Radiance其实是从Surface Cache中的Final Lighting中采样的Irradiance。第一帧需要从Surface Cache里面去获取直接光照并注入Voxel里,这时Voxel里的光照数据在第二帧用,第二帧Surface Cache以上一帧算出来的Voxel为二次光源算间接光,更新Voxel下一帧用,以此类推。

在这里插入图片描述

Voxel Lighting原理

Voxel Lighting是采用Voxel(体素)来存储光照,光照分帧累计,最终得到全局光照效果的一种GI方案。

Lumen在运行时将Camera周围一定范围内的Lumen Scene体素化,然后通过对Voxel六个方向分别Trace射线找到与Voxel相交的最近Mesh Card,通过获取其交点的Surface Cache的Final Lighting作为二次光源,对Voxel对应的那个面做光照注入。

Voxel Lighting分为三步

  1. 场景体素化
  2. 发射ray采样
  3. 光照注入以及更新

复杂的场景往往伴随着大量的Voxel,为了加速Voxel的构建和更新,先介绍下面一些Voxel相关的优化方法。

Voxel的存储方式

Lumen采用3D Texture的手段存储Voxel各个方向的光照Irradiance,这张3D纹理上的每一个Texel都代表一个Voxel在某个方向上的Lighting。

要注意这边的3D Texture内xyz轴并不是存储对应轴向的光照信息。

在3D Texture内,X轴存的是不同的Voxel,Y轴存的是同一个Voxel不同Clipmap级别的Lighting信息,Z轴存的是同一个Voxel不同的方向,需要3维的信息(Voxel,Direction,Clipmap)才能表示某一个Tiexl的Lighting信息。
image

Voxel生成与更新

直接对所有场景和物体进行体素化是不合理的,通常我们只对摄像机视野内可见的物体做体素化,在Lumen里面我们对所有视野范围内的场景进行Clipmap分层,再体素化并用3Dtexture来存储Voxel里的光照,当相机移出视野后,Voxel光照不变,可被下次trace复用。

当摄像机位置改变,Clipmap也会改变,所以Lumen对ClipMap进行了再分层,用Tile的形式存储Voxel,这样更新等操作都是通过对Tile进行检测并进行,就不需要每帧检测所有体素了,这种方式类似于BVH等的加速结构。

Tile

Lumen通过网格化Clipmap的形式生成Tile,一个Tile包含4×4×4的Voxel,又因为每个Clipmap有 64x64x64 个 Voxels,所以每个Clipmap可划分为 16x16x16 个 Tiles。

划分为Tile的目的有三个

  1. 在Camera移动的时候可以离散化的更新Clipmap
  2. 提供层次化更新机制,有利于提高GPU并行度,获得更好的性能。
  3. 用Tile的方式去构建Voxel

划分Tile的方式给Lumen提供了更极致的性能,相比于用Voxel与记录了空间信息的PrimitiveModifiedBounds做相交测试来筛选变化Voxel,用更粗粒度的Tile去做相交测试可以更快选出相交的Tile,筛选速度大大加快了,同时每个Tile里面有64个Voxel,而GPU每个warp有64个通道,每个warp计算一个Tile这样充分发挥了GPU的并行性。

ClipMap

ClipMap是一种比MipMap存储量更小的存储结构,所有ClipMap 具有相同的分辨率,所以ClipMap也能很好的保留场景的细节,Voxel的生成一般就通过ClipMap生成。

image

用ClipMap生成Voxel意味着,越远的物体的Voxel就越大,Voxel里面的信息也就越粗糙,但结果是可以接受的,因为远处的物体对近处我们看到的东西影响确实很小。

下面是ClipMap和MipMap的存储对比。

image

Lumen将Voxel分为多个Clipmap,默认每个Clipmap里面有64×64×64个Voxel,每4×4×4个Voxel又分为一个Tile,更新一般是按Tile更新的。

第一级Clipmap 0覆盖的区域一般为2500cm,覆盖范围是(25×2)^3立方米,Clipmap一般有四级,每级覆盖的区域都是上一级的两倍。

每个 Clipmap 对应的 Voxel 大小为 Clipmap Size / Clipmap Resolution,例如最细级别的 Clipmap 的 Voxel 大小为:(2500/64) x 2~= 78,即最小可覆盖 0.78^3 立方米的空间。

image

不同的ClipMap内Voxel更新规则也不一样,越靠近摄像机越精细的ClipMap更新频率就高,而远处没那么精细的ClipMap内的Voxel更新频率就低,这是因为人眼对近处变化远比对远处变化要敏感。

ClipMap可以以更小的存储代价带来不比MipMap差的效果。

Voxel Visibility Buffer

为了完成采样 Surface Cache 的 Final Lighting,需要知道每个 Voxel 在每个方向上 Trace 到的 Mesh DF 信息,这个信息存储在 Voxel Visibility Buffer 中。与我们熟知的 Visibility Buffer 不同的是,这里存储的内容是 Hit Object ID 以及归一化的 Hit Distance,在 Injecting Lighting 时就根据这些数据对 Final Lighting 采样。

Lumen 将所有 Clipmap 的 Voxel 的 Visibility 都存储在同一个 Buffer 中,并且 Visibility Buffer 是跨帧持久化的,因此为了性能每帧会按需更新部分内容。

但有一个问题,每个Clipmap里面有64×64×64个Voxel,每个Voxel有6个方向的光照需要算,如果每帧都对每个Voxel更新就需要做64×64×64×6~=157万次计算,Lumen采用了只用变化了的Voxel更新Visibility Buffer的策略。

Lumen会对记录变化物体的AABB包围盒,然后计算这些AABB包围盒和哪些Tile相交,先做一个粗粒度检测,找出变化的Tile,再逐Tile做Voxel的相交测试,找出所有变化的Voxel再更新他们。
在这里插入图片描述

Visibility Buffer 位移更新

我们都知道,Visiblity跟ClipMap有关,但是有一个问题,ClipMap是以相机为中心的,当相机移动的时候ClipMap也会更新。如果不做处理这就会导致如果摄像机一动那Voxel Visibility Buffer就得更新,这会导致性能降低且光照闪烁不稳定,Lumen做出的处理不以摄像机位置为ClipMap原点,而用Tile为最小移动粒度,ClipMap的原点只会对齐Tile的中心,这样只有移动超出一个Tile的范围的时候才会更新ClipMap中心。

Scrolling Visibility Buffer

用Tile来粗粒度移动ClipMap中心的方法固然能够减少Visibility Buffer的更新次数,但是ClipMap始终会移动,每次移动都更新全部VBuffer是很费的,这里Lumen用了滚动更新的思想避免更新整个VBuffer,具体来说就是只更新移动后多出来的VBuffer,其余直接复用已有的。
在这里插入图片描述

场景体素化

传统做法

要体素化场景,我们先找都每个面元在哪个Clipmap上(因为ClipMap上的Voxel大小不同,所以要分开做),然后把整个像素的WorldPosition转换成屏幕UV,查询该面元在哪个Voxel范围内,一个Voxel往往包括了非常多的面元,我们要做的就是把Voxel里面所有面元的影响都通过某种方式累积到对应的Voxel上。
在这里插入图片描述

我们在Shader里可以算出这个Pixel属于哪个Voxel,但无法确定这个Pixel是否会对Voxel产生影响,因为多个Pixel重叠的时候我们只保留深度最小的,为了能在Voxel累积尽可能多的信息,我们要选一个能生成最多Pixel的投影方向进行计算。
image

如上图采用跟法线最接近垂直的面进行投影,可以获得最多的Pixel,这样也能累积最多的信息来减少Voxel的误差。

需要注意的是,上面的投影用保守光栅化的方式可以保留更多的信息,可以在对ClipMap内全部物体做光栅化时用完整的几何信息做MSAA来实现保守光栅化,VXGI就是这么做的。

Ray cast构建

上面已经说过Voxel更新的方式了,体素化的时候其实可以理解为Voxel第一次更新。

而Lumen体素化Voxel的方式不是保守光栅化,而是用发射射线的方式。

image

这里补充一下Visibility Buffer的更新,由于需要对Tile内的物体做相交测试来得到需要更新的物体,Lumen会先用列表记录每个Tile内的物体,这样在Tile内部做Voxel求交的时候只需要遍历一小部分物体。

体素化的构建规则是对Tile内每个Voxel每条边上随机发射一条Ray,如果能打中任意一个Mesh,就说明这个Voxel不能为空,我们把这个Voxel标记为需要更新。

后续对需要更新的Voxel每个面都做采样,把求交结果也就是Hit Object ID 以及归一化的 Hit Distance信息记录到Visibility Buffer内,方便后续的光照注入。

image

采样

Cone Tracing

Lumen里采样Voxel用的方法是Cone Tracing,Cone Tracing其实就是根据圆锥体来采样,一般来说知道BRDF可以通过求Lobe(包含有贡献的光源的锥体)范围内光源来计算光照。

image

在Voxel里,可以理解这个圆锥体包含的光源是由不同级别的Voxel组合而成的,因为Voxel记录的其实是对应范围内的光源的综合影响,所以锥体采样(Cone Tracing)也就可以转换为采样不同级别的Voxel按一定权重带来的影响。

image

这种方式根据距离来决定要采样哪张ClipMap上的Voxel,再通过UV Scale 和UV Bias计算3D里的UV,最终可以从对应的ClipMap里采样到对应的方向Voxel,而这个方向一般是由对应方向附近的3条法线(可以想象成坐标轴基底)按一定权重混合得到的。

image

Voxel的权重一般根据遮挡程度来计算

在这里插入图片描述

光照注入

在构建体素的时候就会朝各个面朝内发射射线,射线命中的Hit Object ID 以及 Hit Distance会被记录到Visibility Buffer里,有了这两个信息就可以从对应的Surface Cache里面拿光照信息了。
在这里插入图片描述

Voxel优化点

面剔除

在生成Voxel的时候忽略看不见的面,对被Voxel与Voxel遮蔽的面做剔除

在这里插入图片描述

三角形合并

对于同一材质的物体比如一面墙体,我们完全可以只用一个Voxel来表达,这样可以有效减少三角形的数量

在这里插入图片描述

数据压缩

由于Voxel的特殊性,其内的数据可以无损压缩和解包,所以一般情况可以把Position、Normal和TextureID压缩成32位的uint存储。

在这里插入图片描述

解包

在这里插入图片描述

Position

Voxel Position一般采用3个float表示,一个float占4byte,但如果世界是规则划分且分块的,我们假设每个Tile包含32×32个Voxel。

在这里插入图片描述

因为Voxel在世界空间是排列整齐的,所以这个时候只需要6位(2^6-1=63)就可以存储Voxel在Tile内某个轴的相对位置。

在这里插入图片描述

后续再顶点着色器只需要传入Tile的世界位置就可以还原Tile内Voxel的世界位置了

在这里插入图片描述

Normal

Voxel的Normal只有6个方向,因此可以用3个bit来存6个数字,在顶点着色器再还原方向。

在这里插入图片描述

纹理ID

一般情况只有70种纹理是独特的,只需要7位就可以存储对应的纹理ID。

在这里插入图片描述

Scene Dirct Lighting

Lumen里对直接光的处理分为三步

  1. 对多光源的处理
  2. 对阴影的处理
  3. 计算Final Gather
    在这里插入图片描述

多光源处理

Card Page Tile Culling

Lumen对多光源的处理用的是Card Page Tile的方式,这种方式不同于传统的Tile Base和Cluster Base是基于屏幕空间的,因为Surface Cache的计算和屏幕空间无关,所以Card Page Tile是在世界空间划分Tile。

具体来说Mesh Card生成好以后会储存在一张Card Page里面,Lumen在Card Page上进行划分,找到对应区域的Card,把他们拼接成8×8的Tiles组成的Page,可以理解为拼接完后每个Tile表示的是某个区域内的Mesh Card,只有在对应范围内的光源才会对这片区域的Mesh Card造成影响。
在这里插入图片描述
Lumen会在对应的Card Page Tile范围生成包围盒,通过包围盒判断光源是否能造成影响,如果有影响,将Light影响的所有Tile连续存储进RWLightTilesPerCardTile这个Buffer中。

对于每个Tile最多选取8盏灯,遍历每个光源计算光源对Card的影响,但只选八盏灯显然是不合理的,后续需要更智能的灯光选择策略。

Tile Base&Cluster Culling(拓展)

在Deferred Rendering中,为了进一步优化性能,一般都会做多光源优化,比较常见的方式是Screen Space Tile Culling和Cluster Culling。

Screen Space Tile Culling 是基于屏幕空间对光源进行划分,把屏幕分成一定数量的Tile,每个Tile通过光源索引找出对这个Tile里面的Mesh有影响的光源,这个方法适合处理有着大量光源的场景,但对于某些特殊情况,如地铁隧道里面由于透视的关系大部分的光源都在一个Tile里面,这就会出现很远处的光源会对很近的物体造成影响的问题。
在这里插入图片描述

而Cluster Culling的做法就是在Tile Base的基础上根据距离在横向再划分一次Tile,这个Tile的划分由距离决定,这样一块Tile表示的不再是一整个锥体,而是锥体里的一小块空间,用这种办法筛选的光源就准确很多了。

image

阴影处理

Lumen采用Shadow Map+Offscreen Shadows的方法对阴影做处理。

这是因为通常情况下,Dense SM和VSM对阴影的计算都是Camera Visibility的,我们只要算可见场景的阴影,而且一般Deferred Rendering的Gbuffer只有视野范围内最表层的信息,不可见的Pixel都被剔除了。所以Lumen额外补充了Off Screen Shadow来获取离屏阴影。

Off Screen Shadow

由于Mesh Card在收集直接光照的时候往往需要从屏幕外的光源里采样(用的是整个Lumen Scenen的Shadow),如果不考虑屏幕外的遮挡情况很容易出现漏光现象,举个例子,下图里光源在屏幕外,如果不考虑屏幕外的遮挡情况,下面的阴影区域就可能被照亮,从而导致漏光。

在这里插入图片描述

Off Screen Shadow 主要是通过距离场来做的,主要原因有两个

  1. 离屏阴影不需要太准确
  2. SDF算阴影速度非常快

Lumen对Off Screen Shadow的实现分为两步

  1. CullMeshObjectsForLightCards
  2. Distance Field Shadow Trace Pass

第一步是CullMeshObjectsForLightCards,计算阴影也需要做多光源处理,把光源影响之外的Mesh先剔除掉再计算Shadow,第二步就是Distance Field Shadow Trace Pass,通过SDF直接算阴影

VSM(拓展)

常规的ShadowMap存在精度问题,如果精度很低,阴影会出现严重的锯齿,如果精度高,这存储量对显存就很不友好。

Virtual Shadow Map 思想很简单,就是用同样的空间去存储更多的细节,而传统的Shadow Map存在一个问题,就是存在冗余的情况,因为传统Shadow Map是在光源视角去看形成的一张深度图,而光源看到的很多位置其实摄像机视角是看不到的,所以其实我们如果能做到把摄像机看不到的部分的Shadow Map给不要了,就可以节省很多的空间,VSM就是利用节省下来的空间放更高精度的Shadow Map。

VSM通过分Tile的思想来做高精度的Shadow Map,通过世界空间Pixel投影到光源视角下再判断深度来标记该Pixel位于哪块Virtual Tile上,再选择是否要保留这块Tile。

在这里插入图片描述
最终Shadow Map上只保留和摄像机视角下的Pixel重合的部分,看不见的部分全部剔除掉。

在这里插入图片描述

尽管Shadow Map的精度很高,但是最终占用的内存可能跟完整的低精度Shadow Map占用内存一样,生成高精度Shadow Map代价不算高,但显存却很珍贵,所以VSM是一种时间换空间的方案。

Final Gather

直接光的Final Gather依赖上两个步骤的结果,再CPU端会先执行Batched Light这个Pass,先遍历收集所有有效光源,使用前面CullLightingTiles阶段生成IndirectArgs以GPU Driven的方式计算Radiance并输出到Tile所处Card Page对应的区域上,依次对每个光源进行绘制,对应光源会根据阴影处理生成的Shadow Mask去计算并混合光源的结果,最终的Final Gather会被存进RWDirectLightingAtlas中。

Scene Indirect Lighting [TODO]

Screen Probe Gather

为什么要用Screen Probe Gather

  • 间接光不需要非常准确

间接光弹射的次数越多高频信息就越少,我们要模拟光线的无数次反弹就意味着间接光最终是低频的,低频的光照信息往往是非常“平滑”的,只要足够平滑就可以用很少的参数去近似处理。

Lumen对间接光的处理也是类似的,最终Pixel的间接光往往是通过中间多次的Filter和插值得到的,但由于多次弹射已经丢失了大部分高频信息了,所以哪怕存在这么多中间的过程也能得到和真实间接光非常接近的结果

在这里插入图片描述

  • 降低采样分辨率,减少Trace的次数

如果不用Probe,计算的粒度将变为屏幕上的Pixel,这样每帧都需要Trace成百上千万根射线,更别说后面还需要做全分辨率的Filter,目前的计算机如果这样做根本达不到实时的要求。

Lumen为了达到实时渲染的标准(30FPS),对于每个像素每帧的预算只有1/2的Ray,所以Lumen默认每16×16个Pixel一个Screen Probe,每个Probe只发射64根射线,这样256个Pixel只需要发射64次Trace,同样的后续的Filter以Probe为粒度,大幅度缩减了总耗时。

在这里插入图片描述

Screen Probe Gather原理

Screen Probe Gather的流程可以分为5步

  1. 确定Screen Probe在屏幕空间的位置
  2. 每个Probe以生成位置为中心向外发射射线去获得颜色
  3. 获取到颜色后先在Probe与Probe之间做时序滤波和空间滤波,再通过球面谐波函数压缩成SH存储
  4. 根据最终得到的Probe信息去插值出每个Pixel的颜色
  5. 再整个屏幕空间做时序滤波

在这里插入图片描述

Screen Probe Placement

Lumen采用两种方法来决定Probe的放置位置

  • Uniform Placement 均匀放置
  • Adaptive Placement 自适应放置

在这里插入图片描述

具体来说Screen Probe的生成是基于屏幕空间的,需要保证每个Screen Probe至少覆盖16×16个Pixel,这里需要注意,Probe的间隔由屏幕间距决定,但是最终我们需要找的是屏幕中Probe的WorldPosition、WorldNormal、Depth等信息。

但只均匀的放置Probe会出现问题,比如上图可见树枝的位置并没有Probe,而附近的Probe由于和树枝位置的几何差异过大无法使用,这就导致确实树枝部分的信息。

Adaptive Probe就是用来解决这个问题的,原本放置的过程中每隔以16个像素放置一个Probe,但如果这样放间距太大,间距之间如果存在几何差异大的地方(存在突变),那这一部分信息就会丢失,那我们就在这些几何差异较大的表面加大放置密度,直到有Probe覆盖这些树枝为止。

Adaptive Probe相当于是高分辨率的Uniform Probe,上图中黄色部分像素颜色就无法通过正常放置的Uniform Probe插值得到,整个时候需要在这些几何差异过大的表面去放置Probe来补充信息,Lumen采用的方案是先放置覆盖16×16像素的Probe,如果存在插值失败的像素则在这些区域自适应的放置覆盖8×8像素的Probe,如果还是失败则再放置覆盖4×4像素的Probe。

在这里插入图片描述

具体说下算法实现,Placement Adaptive Probe的Pass会在Placement Uniform Probe后执行,且Adaptive Probe的Pass会通过循环执行多次,每次循环Screen Position间距都会减半,在循环中会计算当前Tile内线程对应的Probe以及其右方、下方和右下方共4个Probe的插值权重(如果附近Probe差异太大权重会很低),如果最终权重总和低于一定阈值则执行Adaptive Probe的放置,Tile(8×8个Probe)内用GroupShare队列记录插值失败的Probe的位置,在线程同步以后,从队列中取出这些位置,在这些位置上继续插值算权重,如果失败则重复上面过程(下一次Placement Adaptive Probe循环再算),如果成功则把新生成的Probe的Depth和Normal等信息写在和Uniform Probe同一张Texture上。

在这里插入图片描述

Jitter

Screen Probe Jitter是一种时序超分思想,每一帧生成的Probe都会有不同的抖动,这样多帧以后,尽管Probe都是由间距的,但是由于抖动的原因,多帧插值得到的结果近似于更高分辨率(更小间距)放置的Probe得到的结果,也就是用更小的代价得到了更平滑的结果。

在这里插入图片描述

具体来说每帧都有一个不同的FrameIndex,Lumen通过生成一个和FramenIndex相关的随机数和限定范围来决定Jitter的值。

在这里插入图片描述

Ray Tracing

重要性采样

重要性采样主要目的是加快蒙特卡洛积分的收敛速度,由于发射的光线有限,如果不用重要性采样,每个Probe只均匀发射64根射线得到的结果很难收敛,因为采样数太少了且没有合适的PDF值,重要性采样可以算出合理的采样方向和PDF值能够只用少量样本就拟合原积分,能大大加快收敛速度。

下面是没用重要性采样和重要性采样的对比图,可以看出重要性采样后得到的结果更平滑噪点更少了

在这里插入图片描述

在蒙特卡洛积分中,为了加快收敛,我们需要计算合理的PDF值,PDF和函数体的形状越拟合收敛速度越快,所以最准确的方式其实是算出Li * fs * cos的归一化方程来作为PDF,这样可以做到一次采样就收敛,但是这显然是不切实际的, 所以Lumen采用了分别对Li和分别对fs做重要性采样来尽可能拟合方程,这种hack虽然强行把方程拆开了,但是综合考虑BRDF和对光源算出的PDF仍然可以大幅加快收敛速度。

在这里插入图片描述

BRDF重要性采样

BRDF重要性采样的其实就是根据求BRDF和Cos项的归一化方程来作为PDF值,而由于此处的Screen Probe是用来计算间接光的,而Lumen计算间接光的过程中不考虑材质信息,所以这里的BRDF默认为常数1,第一时间想到的方法其实是找到Screen Probe所在表面,沿着Normal方向做一个Cosine Lobe,但这种方法其实是不合理的,因为一个Probe往往覆盖16×16个像素,而在远处这16×16个像素范围其实覆盖了非常多的物体,而不同物体之间的法线差异可能很大,这样其中部分Pixel的Normal其实是非常高频的,极端一点此Pixel和Probe位置的Normal方向可能相反,这样重要性采样反而会减慢收敛速度。

在这里插入图片描述

所以Probe位置的法线并不能代表BRDF的Lobe值,简单来说就是信息太少了。Lumen的做法每个Probe采64次附近的Pixel的Normal,对比附近点的Normal和Probe中心的Normal,如果他们处于同一平面且方向相近,则累积贡献,否则说明该Pixel和Probe位置差异过大,直接不计算。

下图中绿色点是累积贡献的Pixel,红色点位置差异太大直接舍弃掉。

在这里插入图片描述

Lumen使用了球面谐波函数压缩和非球面谐波函数压缩两种方式来算BRDF的PDF。

球面谐波函数压缩法

每个Probe会采样64次,计算过程中,如果算出Pixel位置的几何差异不大,则从Gbuffer中找到该Pixel对应的法线,因为BRDF*Cos是常数,所以直接计算该Pixel法线方向的9个SH(球谐系数)累加到一起最后再归一化处理,算出所有有效的Pixel的BRDF的SH均值,该值作为最后的PDF使用。

在这里插入图片描述

详细来讲,Lumen为每个Probe开64个线程,每个线程对应Probe上面的一个Pixel,先获取到该Probe对应的位置信息如Screen Position和Scene Depth,再从GBuffer获取每个线程对应的Pixel的World Normal和World Position来计算Pixel Plane,计算Probe位置到Pixel Plane距离,该距离作为差异因子筛选出可以使用的Pixel Normal,如可使用则Index加1且存入到GroupShare变量里面,最后再计算Pixel 的World Normal方向的9个SH系数存入GroupshareSH里。

在线程同步后开始求SH的均值,由于线程的并行执行,要在一个Pass内计算不定数量SH值是比较麻烦的,Lumen这里采用的是一个线程计算4个系数和的办法,具体来说就是第1次循环015线程将64个SH每4个一组求和写入GroupShare变量(LDS)中,第2次循环03线程将16个SH每4个一组求和写入LDS,以此类推最终完成SH并行求和

在这里插入图片描述

非球面谐波函数法

第二个方式与球面谐波函数法不同的是,他不是求均值,而是每个Pixel都计算一个PDF,这种方法有一定的存储代价,存9个SH只需要9×16bit,而存64个PDF浮点数则需要64×16bit的空间。而且存SH计算出来的结果在减少噪点、漏光方面会比非SH方法做的要好,所以该方法在UE里默认不开。

Lighting 重要性采样

Lighting重要性采样其实就是找光源,Trace的方向对应光源的话PDF大一点,否则PDF小一点。而要算PDF要知道光源的方向就要Trace,而你要少量Trace就得到收敛结果就需要PDF,这就像先有鸡还是先有蛋的问题。

Lumen通过采样上一帧的Probe的Radiance图来解决这个问题,因为帧与帧之间光照变化可能不快,这种方式在大多数情况下都是准确的。我们知道Probe的Radiance Texeture其实存的是64套SH系数,而SH系数算出来的结果其实就是亮度,因此我们只需要比较SH信息的大小,就能大概知道光源在哪了,如下图,越亮的地方越可能出现光源,因此越亮的地方PDF越大

在这里插入图片描述

详细来说,同样的每个Group64个线程对应的是Probe的64个Pixel,这里会先获取该Probe的深度和位置信息,根据深度以及Jitter偏移去重建出上一帧Probe的位置,这时还会找到上一帧对应Probe附近的9个Probe选4个(其实是3个,自己必须算计去)来做插值确保Radiance的可靠性,这里的插值规则是先对比附近3个Probe和目前Probe的差异性,对比规则和BRDF那里说的类似,区别是这里不会舍弃掉结果,而是根据差异度算一个权重,差异越小权重越大。同时找到附近Probe的对应Pixel的颜色再乘以权重,累加起来就是最终的Radiance值。因为之前的Radiance是单位面积单位方向的,所以最后需要乘上一个Solid Angle消除方向的影响才能得到单位面积的Irradiance的值。

在这里插入图片描述

PDF

上面已经算好了BRDF和Lighting的PDF,Lumen直接把这两个值乘起来作为最终的PDF,当然这里也有阈值设置来规避不合理的PDF。

结构化重要性采样

Lumen采用结构化重要性采样的方式来优化Trace过程,这里不只是算PDF了,而是真正改变Ray的发射。主要思想把不那么重要的Ray发射机会让给PDF高的方向去发射,这样可以进一步加快收敛速度。

结构化重要性采样可以理解为重要性采样的升级版, 同样也综合考虑了BRDF和Light的PDF值。

在这里插入图片描述

下图中部分边缘命中不了光源的黑色射线被分配到可以击中亮处的高PDF的方向。

在这里插入图片描述

如下图所示,左边是PDF,右边是代表Probe方向的八面体映射贴图,PDF大的地方进行了细化(原本发射一次Ray现在发射4次),也就是PDF极小(左边接近黑色的格子)的Pixel对应的Ray不发射了,把这次发射机会让给PDF大的格子去发射多跟Ray。

在这里插入图片描述

PDF排序

这里的PDF排序实在ComputeShader里并行操作的,因此为了避免竞争写入和覆盖问题,Lumen开了两倍线程大小的LDS来避免越界,这样每个线程按照索引进行降序排序,在索引一致的情况下不会原地写入,而是往后偏移,这样保证了每个PDF都能按照降序找到自己的位置。

细化

细化操作主要就是分配射线的发射,每个Probe64个Pixel对应64个方向,64个Pixel也记录着64个PDF,前面我们已经对PDF进行了排序,接下来以每4个PDF为一组,找出每组中最大那个PDF,如果最大的PDF也小于某个阈值,说明这一组射线PDF都极小,此时可以减少发射射线的数量,但完全不发射又是不对的,所以就用MipMap的方式4合1,原本这四个Pixle发射4根射线,现在只在MipMap降一级的大格子上发射一根光线。PDF最大的Pixel则细化成4个Pixel,也就是在升一级的MipMap对应格子发射4根射线。

Composite

由于改变了Ray的方向,因此需要把Ray的方向整合,也就是用八面体贴图集成表示,因为数量不变,所以最终对应的也是64个Pixel的值。

在这里插入图片描述

Radiance Cache[TODO]

Radiance Cache部分源码没详细看

在这里插入图片描述

屏幕空间Ray Cast(Trace Scene)

在这里插入图片描述

Trace Scene其实就是屏幕空间的SSGI,整个过程相对于在世界空间Trace以及存储信息是十分轻量化的,同时SSGI能不经过仍和额外处理直接获取到上一帧的高频信息,因此SSGI带来的效果和性能提升都是非常大的。

SSGI分为两个问题,Trace和计算Hit Point的颜色。Lumen Trace Scene用的是Hi-Z的方法来加速Ray Marching,这个可以看我另一篇文章,而以及得到了Hit Point,我们就可以直接从Hit Point去采颜色了,这里需要注意的是我们虽然有了GBuffer信息,但是这里我们取的并不是直接光的颜色,所以我们需要利用的是上一帧渲染出来的颜色信息而不是用GBuffer算直接光照,这里需要把Hit Point 重投影到上一帧去取上一帧对应Pixel的Color来作为间接光源计算。

在这里插入图片描述

Lumen的Scene Trace是通过Probe来做,每个Probe内的一个Texel对应的是获取到的Irradiance数据,因此每个线程会获取线程所在Texel对应的球面方向也就是RayWorldDirection,这个时候还需要根据是否双面材质和背面射线来决定是否return,如果可以继续进行则会在Ray发射的方向沿着Noram进行偏移避免自相交的问题,Lumen通过一个宏来决定是走Hi-Z还是固定步长步进(Linear Ray Marching),HI-Z的伪代码如下,会通过一个While循环来找到屏幕空间的Hit Point在回退到未命中前的第一个位置,用该位置的数据进行后续的计算。

在这里插入图片描述

上一帧颜色

在这里插入图片描述

Hit到的颜色用八面体贴图存储

在这里插入图片描述

同时这里还需要计算光线命中点到光线起始点的距离Trace Hit用于后续CompactTrace的光线重建

在这里插入图片描述

世界空间Ray Cast(Trace Mesh Card&Voxel)

Trace Mesh Card是用来补充屏幕空间没有Trace到的信息的,但这后面其实还有一步,近距离的Trace是Trace Mesh Card的,只要Ray超出一定距离就会Trace Voxel。从Lumen Software Ray Tracing Pipeling的Ray Tracing顺序我们就可以知道整个Trace过程,Global SDF Trace就是用来Trace Voxel的

在这里插入图片描述

和Trace Scene类似,最终的结果都是需要获取到Trace Hit和对应的MeshVoxel的光照信息的。

Trace Mesh Card

在这里插入图片描述

Lumen通过bRenderDirectLighting来控制执行TraceMeshCard,这里推测应该是以Mesh Card为光源去算光照信息所以才叫这个名字,这里第一步会先设置TraceInput,Ray的方向、起点、最大步进距离等信息都会存在这个结构里面,接着会通过Position计算当前所在区域对应的CardGrid同时获取到当前Grid的所有的MDF信息,ConeTraceSceneCard就是遍历各个Card去Trace的。

Trace Voxel

在这里插入图片描述

当光线超出一定距离以后就不在Trace Mesh Card而是采Voxel来补充信息,这里UE5源码貌似是只有在使用硬件光追的情况下才会激活RADIANCE_CACHE的宏去采样Radiance Cache,而在软件光追的情况下是直接Trace的Voxel。

Probe Filter

用Screen Probe的方式去存储光照信息最后直接插值出来的结果就一定会有噪点,因为每个Screen Probe发射64根射线,综合下了每个Pixel只分到了1/4根射线,这是远远不够的,采样数少就意味着收敛慢,只能用其他方式去加速收敛。这里说一说个人理解,做滤波的其实就是为了加速收敛,因为滤波就是根据权重去综合考虑附近的采样结果,其本质就是增加采样数,收敛后的结果直接是平滑的,这也就达到了降噪的目的。

Lumen通过时序滤波和空间滤波两种方式来在Probe层级降噪。

Temporal

Probe的Temporal Filter目的是让颜色在帧与帧之间的平滑过渡,也就是时域上的平滑(突变减少更加的连续了),这会让画面更稳定能大幅度提高画面效果,跟像素级的Temporal原理类似,是通过逆矩阵重投影回上一帧,找到上一帧的对应以及附近3个Probe来综合计算颜色值,需要注意的是Temporal过程中如果出现几何差异过大的Probe,Lumen做的是直接弃用而不是乘一个很小的权重。

如果场景内同时有动态物体和静态物体,Probe的Temporal Filter需要分为两种情况讨论,对Static 物体和对Dynamic 物体。

对于静态物体的Temporal Filter

直接通过ClipToPreClip也就是投影矩阵逆变换的方式找到上一帧位置对应的Probe进行计算。
在这里插入图片描述

对于动态物体的Temporal Filter

在这里插入图片描述
当Probe重投影到上一帧后,Lumen会同时采样附近的Probe,如下图,黄色的为Current Probe,左上角为重投影后的History Probe,此时线程计算Current Probe黄色点对应方向的Irradiance,同时采样History Probe附近3个Probe相对位置的Irradiance,判断这些History Probe和Current的位置差异计算一个权重值,如果位置差异太大则直接舍弃此次计算,累加各个History Probe的颜色值和权重的乘积再标准化后再与Current的颜色Lerp,Temporal的lerp系数一般为0.8.
在这里插入图片描述

Lumen只会对Uniform的Probe做Temporal Filter,Adaptive Probe当前帧位置和历史帧存在差异较大一般只会做Spatial Filter。

Spatial

Probe的Spatial Filter的目的是使颜色在空间上平滑,这会让物体与物体之间或者物体本身的颜色过渡变得更加均匀,Lumen的做法是先找到Screen Probe附近的几个Probe,通常为3个或者8个(取决于Temporal的强度,如果不进行Temporal的时候则需要对3×3的区域做Spatial Filter),根据附近Probe的Normal、Depth等权重来累积影响,这里和Temporal Filter一样同样需要舍弃一些Probe,不然会出现漏光现象。

漏光是因为Probe的权重一般只取决于几何差异,用几何差异来计算权重可以尽可能的让光线来自于合理的方向,如下图假设要算B的颜色,那他的颜色贡献大概率不可能来自于ACDE。
在这里插入图片描述

当几何差异不大光照差异过大的时候,如果只考虑几何差异就会出现漏光的现象,但是通过均值来评估光照是很费的,同时也是不合理的,因为并不是所有的光都不可以“漏”的,比如说SkyLight造成的明暗交界等,真正需要评估的应该是发射光线的目标是否合理,因此Lumen分以下两种情况进行Clamp。

Angle Error

当有物体离Probe较近的时候有可能出现一种问题,Current Probe同向射线命中的是远处物体而Neighbor Probe同向射线却被附近的物体挡住了,这时候如果直接使用几何差异来做权重会导致出现非常多的噪点,主要原因是因为远处物体间接光更低频而如果采样近处就会有高低频信息混合的噪点问题。

解决方案就是根据角度评估是否可以累积光照,因为如果被近处物体挡住,那其实Hit Point与Curren Probe和Neighbor Probe的角度就会很大,而如果命中点如果在远处他们之间的角度也不会很大,也就代表可以使用,需要注意的是Lumen还原的是Neighbor Probe的Hit Point,从Current Probe与HIt Point连线来判断角度。

在这里插入图片描述

Distance Mismatching

还有一种情况就是Current Probe命中近处的物体而Neighbor Probe命中远处的物体了,这种情况如果只按照Angle来判断会发现他们的Angle相近,但这种情况也是不可能用的,Lumen增加了距离判断的方式,通过判断Current Probe和Neighbor Probe各自的Hit Distance,Distance相近则使用否则舍弃。

在这里插入图片描述

Hit Point&Distance

这里的Hit Distance会在之前Trace的时候记录在一张Texture里面,知道了Distance就可以通过StratPoint+WorldDirection*Distance的方式还原Neighbor Probe和Current Probe对应方向的Hit Point,知道了Hit Point也就可以算出角度了。

ConverToIrradiance

在做完两种Filter后,Lumen通过球面谐波的方式计算出Probe的Irradiance以及它对应方向的影响,同时球谐自带滤波效果的特性也让结果能更加平滑的过渡。

球面谐波函数其实类似于二维的泰勒展开,可以通过对应的系数去拟合原方程,利用的系数越多得到方程就和原方程越接近,如果系数过少则会导致原方程丢失部分高频信息,也正是因为这个特性,球谐附带滤波的作用(减少高频噪点)。

在这里插入图片描述

ConverToIrradiance的主要思想是重建与还原,先把球面上对应方向的光照信息投影到基函数上重建出球面谐波函数的系数,在计算的时候通过球面方向找出对应的基函数再和系数相乘就能得到结果了,其原理为采样球面所有方向的光照与Diffuse_Transfer的乘积并累加,其重建出的完整的球面方程变量是方向,方程结果则是Irradiance对该方向的影响。

重建

由于每个Probe只发射64根射线,所以只需要累积64个方向的SH就可以求出总SH值,相当于只用64次采样来拟合球面谐波函数,具体流程为先找到对应方向的Radiance和该方向对应的基函数的值

在这里插入图片描述

把基函数和Radiance相乘的结果(其实就是Radiance投影到球面谐波上)存到RGB对应的系数上(RGB各对应9个系数总共27个SH值),以下为RGB的SH存储结构

在这里插入图片描述

还原

重建以及把球面上所有方向Radiance压缩成了RGB对应的27个系数了,也就意味着我们可以通过3组系数得到RGB对应的球面谐波方程,方程的变量为球面方向,那我们只需要根据方向去获得球面的基函数对应的值再乘对应的Irradiance系数就可以得到结果。

用球谐的好处在于只需要计算一次SH的累加就可以通过对应方向的球谐函数直接求解(通过9*3次点乘)出Irradiance对该方向的影响,大幅度减少采样次数和计算量。

FixupBorders[TODO]

Integrate

Intergrate的内容很多,这里主要说下Pixel插值、权重计算和Adaptive的处理。

Integrate主要做的事情就是根据Probe上的信息去还原Pixel原本的颜色,Luemn通过FilterRadianceWithBorder和IrradianceWithBorder两张Probe的八面体贴图来还原DiffuseIndirect、BackDiffuseIndirect和SpecularIndirect

Pixel插值

Pixel插值方式是先确定当前Pixel离哪个Probe最近以及找到附近的几个Probe,从Probe内与Pixel法线一致的方向获取对应的Irradiance值,再根据几何差异来计算附近Probe的Irradiance对Pixel的影响。

在这里插入图片描述

权重计算

Lumen对Integrate的权重计算同时考虑了相对位置差异和深度差异

相对位置差异

Screen Probe的放置是相对于屏幕空间的,这里的相对位置指的是Pixel在屏幕空间中的位置相对于Screen Probe的位置,这里通过Pixel Screen Position和附近四个Probe Screen Position双线性插值出对应的权重。

在这里插入图片描述

可以算出上图左上角Probe权重为(0.7,0.7),右上角Probe权重为(0.3,0.7),左下角Probe权重为(0.7,0.3),右下角Probe权重为(0.3,0.3),Lumen还通过一个BilinearExpand来稍微改变权重的大小来让最后结果更合理。

深度差异

计算出Pixel所在的平面plane,通过附近Probe的深度还原Probe的世界空间位置,用Probe的WorldPosition到Plane的距离来计算对应的位置权重,只有和在Pixel相近平面上的Probe才会做出贡献。

在这里插入图片描述

Adaptive处理

如果Pixel出现在很细长的物体上,很可能附近只有一个甚至没有Probe是可以贡献颜色的,因为每个Probe至少覆盖16×16个Pixel,但是某些尖锐细长物体可能只占据几个Pixel,这种时候可以通过用Adaptive Proeb来插值补全Uniform Probe缺失的信息。

在这里插入图片描述

具体操作为先找到这个插值失败的Probe的ScreenCoord,根据Screen Coord从ScreenTileAdaptiveProbeHeader上对应位置采样,看看该位置是否生成了Adaptive Probe,如果生成了则遍历对应Adaptive,这里有另一张ScreenTileAdaptiveProbeIndices负责存储对应ScreenCoord内多个Adaptive的Index,通过Index可以算出对应的Adaptive Probe位置颜色等信息在Texture的具体位置(AdaptiveScreenCoord),遍历Adaptive找出Weight最大的Adaptive Probe,把原Screen Coord和原来的Weight设置为该Adaptive的值,插值的时候也用该Adaptive的值。

Screen Filtering

由于Probe是每帧随机放置,如果直接使用Integrate的结果会导致闪烁问题,所以需要对全屏做一次时序降噪,这里的降噪思想是累加和插值。

在上面Integrate的过程中,bool遍历bLightingIsValid负责标记该Pixel是否能通过附近Probe插值出颜色,如果bLightingIsValid为true则说明这一帧Pixel的颜色大概率是正确的,可以用来指导时序滤波,该过程会放大这一帧颜色Lerp的权重,同时累加系数NumFramesAccumulated,当NumFramesAccumulated值为1的时候平等考虑历史信息和当前帧生成的信息。bLightingIsValid为false的时候降低当前帧权重使用更多的历史信息来指导。

参考文章

最强分析 |一文理解Lumen及全局光照的实现机制
游戏引擎随笔 0x29:UE5 Lumen 源码解析(一)原理篇
游戏引擎随笔 0x30:UE5 Lumen 源码解析(二)Surface Cache 篇
游戏引擎随笔 0x33:UE5 Lumen 源码解析(五)Voxel Lighting 篇
KillerAery博客
实时全局光照VXGI技术介绍
UE5中VirtualShadowMap的简易实现原理(一)

  • 27
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值