实时渲染 -- 流明(Lumen)

首先我们需要知道Lumen需要解决哪些问题。

很多人都会问,既然已经有了硬件的Raytracing ,我们为什么还要Lumen呢。这是由于很多硬件并不支持 Realtime Raytracing,对于支持的那些硬件, N 卡还算是勉强可以,而 A 卡支持的比较糟糕。并且实际测试中,3080大概 1 秒钟能做10 个billion 的计算,这个实际的计算量用来做GI 的话,实际还是比较费力的。
比如游戏里最常见的Indoor场景,你需要每像素500 根Ray才能得到想要的效果,但是我们只能付得起每像素二分之一个Ray。因此我们需要解决如何在软件层面解决掉快速Ray Tracing 的问题。

另一个大的问题就是Sampling。之前的章节我们也讲到了Sampling,过去几十年时间离线GI都在和 Important Sampling 做殊死搏斗,直到现在也没有让我们特别满意。而我们的Realtime 的 Sampling 数量又被卡的很死,这进一步增加了我们处理问题的难度。
如上图所示,靠近窗口的Sampling 结果基本是可以接受的,虽然它有些Noisy,但是你可以通过 Filtering 解决这个问题。但是如果离窗子稍微远一点,那 Filtering 也救不了你,最终你看到的就是一个一个的大色斑。因此Lumen也需要解决如何Sampling的问题。

 

我们知道Indirect Lighting 可以在 Low Resolution 上面采样得出,Lumen的想法是在屏幕空间放一大堆的 Probes,屏幕空间 的特点是紧贴那些要被渲染的物体表面,通过 Probe获取光照。比如每16 个 pixel 我放置一个Probe,每个像素去 Shading 的时候,它的高频信息可以通过表面法线产生,最后得到右边非常逼真的效果。


因此Lumen最核心的思想是三点:

  1. 在不用硬件Ray Trace的前提下,我如何进行快速的Ray Trace。
  2. 尽可能的在 sample 里做的优化。
  3. 放置的Probe 尽可能贴着真实物体表面,使它的精度足够的高。

Phase 1 : Fast Ray Trace in Any Hardware

Lumen最核心的点就是要解决我怎么样的在任意硬件上能进行非常快速的 Raytracing ,它要解决如何我在射出一根Ray时,快速得到这个Ray到底能不能交到一个物体,并且知道和它相交的物体是谁。
这个 Raytracing 当然是可以用硬件Raytracing 去做,Lumen也提供能够开启硬件Raytracing 的功能来做更高精度Raytracing的表现,不过为了兼顾各种硬件,Lumen 最主要的是提供了基于软件的SDF(Signed Distance Field )的Tracing算法。

SDF叫做空间距离场,假设空间中有一个Mesh,我们会生成一个空间场的数据信息,对于空间上的任何一个点P,你都可以查询到P距离Mesh 上最近的距离是多少。如果P点在Mesh外部,那它的数值是正的;当P点在物体表面,那它的数值是0;如果P点在Mesh内部,那它就是负值。
在过去我们对一个形状的表达是使用 Triangle (点线面),它非常符合我们人的直觉。但是它是离散的,顶点数据之间并没有任何联系,它必须要通过 index buffer 关联起来。三角形和三角形之间的连接也不存在,因为它必须要通过顶点,查询到共用两个相同 index 的顶点,我才知道三角形两个边是连接起来的。
SDF从数学上是 Triangle的等价变换。他是连续和均匀的,是可微的,既然是可微的,就能做很多事情。

 

我们现在已经知道了SDF的定义,接下来我们来看如何生成SDF。最直接方案是,我对每个Mesh都生成自己的SDF。对于一个游戏场景来说,如果有上万个物体,实际的场景物体可能就是几百种物体通过平移旋转放缩得到。我只需要存这几百个物体的 SDF加上它的Transform ,我就可以把场景表达出来。

 

在生成SDF的时候,需要考虑Mesh特别细小于我距离场密度的情况。如上图左侧所示,如果我们采样到的是5和5中间红色点的位置,因为插值的关系,我们采样到的还是5。因此我们在这里把Mesh撑开一点。因为生成的时候会做这样的操作,因此我在做Trace 的时候也会进行一个偏移的修正。

 

接下来我们要解决如何使用SDF 进行Ray Tracing 找到交点。我们这里使用Raymarching的方法,不过我们这里是在世界空间中去做,而不是之前在屏幕空间。Raymarching的主要问题就是步长如何选择。
如右图所示,从出发点P0出发,你的第一个Distance 距离就是P0点SDF的值,因为SDF是你距离最近的Mesh的距离,小于这个值的范围内根本不会有任何物体。这时你就会从 P0 点跳到 P1 点,从 P1 点再去找它的SDF,可以找到 P2 点,这样以此类推,我们就可以非常快的hit 到物体的表面。
实际上这是比较安全的,就算你穿进物体内部了,因为 SDF 它是有符号的,进去之后它会给你一个负数,那个负数就告诉你说你的表面应该在哪,你还可以弹回来。所以使用SDF 的Raymarching既快又鲁棒,这是 SDF非常大的好处。

 

SDF第二个好处是做Cone Tracing 。比如说我们要做 Soft shadow ,从需要计算阴影强度的着色点o出发,向光源发射一条Shadow Ray,沿着该方向进行marching ,每marching 到一个点,我们可以得到一个圆心为当前点,半径为当前点的SDF值的球,从出发点向该圆作一条切线,可以得到一个夹角,取所有这些夹角中的最小值,根据这个值来确定半影大小。 

由于SDF的显存占用过高,我们其实可以对SDF 进行稀疏化处理,使用间接存储的结构。如上图所示,我们可以把那些 Voxel的 distance 大于某个阈值(体外)和小于某个值(体内)的Voxel全部干掉,用一个简单的 index 就可以把SDF的存储减少很多。

 

但是我个人一直在怀疑一件事情,确实只要大于一个阈值,你把那些 Voxel 全部标为空,你这样只要存那些有用的区域,但它会导致你marching 迭代的步长变长。因为以前一次性可以跳一大步,但现在如果是空的话,我只能一步一步的试,但是这个里面工程上肯定有办法可以解决,我们就不展开了。

 

SDF 还可以做LOD,而LOD 还有一个很有意思的属性,因为它是空间上连续的可导的,你可以用它反向求梯度,实际就是它的法线。也就是说我们用了一个 Uniform 的表达,可以表达出一个无限精度的一个 Mesh ,我既能得到它的面积,又能对它进行快速的求交运算,还能够迅速的求出它连续的这个法线方向。

 

如果你使用 LOD 和Sparse Mesh,可以节省40%到 60% 的空间,这在硬件上还是蛮可观的。对于远处的物体,可以先用这个 Low level 的SDF,当你切换到近处的时候再去使用密度高的SDF,这样也可以很好的控制内存消耗。

 

如果你用Per-Mesh SDF去做Raytracing,对于单个 mesh 来讲,你做 Raytracing速度会很快。但是你架不住场景的物体数量特别的多,比如说我一根Ray打过去的话,沿途所有的 Object 我都得问一遍。如果场景全是物体,那你的计算复杂度就会越来越高。如上图所示,越亮代表Step的次数越多,会发现越靠近 Mesh 的边界上的像素,它需要的Step 就越多。

 

一个非常简单的想法就是,既然你们都是分散的,那我把你这些SDF 合成一个大的低精度的 Global SDF ,这是对整个场景的表达。当然这里面的细节比较复杂,物体的移动,消失,增加,都需要提供一套 Update 的算法。今天就不展开了,如果有兴趣的同学可以自己研究。
不管怎么说,我们能形成一整个场景的 SDF 。如果有了整个场景的SDF,Raytracing 的速度就会非常快,因为它不再依赖于一个一个的物体。当然它的缺点是受制于存储空间,不能像 Per-Mesh SDF 那么精细,但它是一个非常好的加速的方法。

 

在 Lumen 里面,Global SDF 和Per-Mesh SDF 都使用到了。如果一开始你就使用Per-Mesh SDF,你可能要二百多次的物体测试,一旦有了Global SDF,物体的测试数量就会极大的下降,因为它可以快速的找到一些近处的点,然后再根据周边的Per-Mesh SDF去做。

 

Global SDF 在实际上的运算中它会做成四层 mip, 在 Camera 近处 SDF 精度高一点。远处我 SDF 精度低一点。因为SDF跟 Texture 一样是一个非常 Uniform 的表达,所以它天然的支持 Clip Map。
SDF虽然并不能直接用作渲染,但它可以把很多渲染的计算进行迅速的优化。使用 SDF我们可以快速的提供场景的表达,在这个表达到上面做 Raytracing的效率非常高,并且不依赖于硬件的Raytracing。

Phase 2 : Radiance Injection and Caching 

那接下来我们要解决如何把光子注入到世界。

在Lumen里面,他搞了一套非常特殊的东西叫做 Mesh Card 。当我们计算全局光照的时候,从光的视角我们去照亮整个世界,其实整个世界里面无论你看得见的还是看不见的 pixel 它实际上都会被这个光给照亮。每一个被照亮的 pixel ,实际上也成为GI 的一个贡献者,就是我的小光源之一。
我们的Mesh SDF Trace虽然可以拿到对应的相交点,但是对应物体的材质信息是拿不到的,我们还需要知道物体表面的材质属性,但SDF 并不包含 Material 的Attribute。并且仅使用RSM也是不够的,因为它只有灯光看到的那些表面的信息,其余的很多的角度依旧看不见。
Lumen想到了一个方案,首先就是每个Mesh导入的时候,如上图所示,会生成Mesh Card,这样在使用Per-Mesh SDF的时候,就可以找到对应的MeshCard。

 


这是我们在 UE 5 里面搭的一个场景,你可以看到生成的 Card ,其主要作用是提供光照采样的位置和方向。

 

Cards只是Lumen捕获光线的位置,其可以离线生成,而存储其中的数据则必须实时捕获,因为不同的Mesh之间会有交叉和遮挡,因此就算是同样的材质渲染出的Cache结果也是不一样的。
所以我们没有办法在离线的时候把这个信息Cache住,而是在运行时给每个Card渲染出它的表面Cache,里面包含有Albedo,Opacity,Depth等各种信息。如果你带有自发光的话,也会把你的 Emissive也存储进去。
可以看到这里的Surface Cache的数据与 Deferred Shading 的 G-Buffer 很像,我们在计算光照的时候直接就可以通过这里的Cache数据来计算。

 

当离我的相机比较近的物体,Surface Cache的分辨率会高一点,相机远离的物体Surface Cache分辨率低一点。比如说我侧面有一个石头,这个石头可能只有两米高,但是它可能占据了我平面 1/3 的地方。那我平面近处的东西很可能会受这个石头的影响。远处可能有个雕像,它有 5 米高,但它距离我 100 米远,它的精度就可以低一点。
我们是以Atlas的方式去排布所有的Surface Cache在对应的4096x4096 的空间里。这里需要注意的是 Surface Cache并是一个单张的 Texture ,而是一系列的 Texture的集合。

 

Surface Cache内部的贴图都会进行一遍硬件支持压缩,这样可以减少显存的占用。

 

Surface Cache存储着Gbuffer 的信息,不过这还不够,因为我们不能再在相交点重新发射大量的光线然后不停的递归去计算间接光,我们希望把Radiance 固化在Surface Cache上面就如同Photon Mapping 的思路一样。
因此我们希望它Surface Cache上还能存储对应的Radiance信息,那就是Radiance Cache,有了 Radiance Cache,就可以在 Trace 时直接进行采样作为 Trace 方向对应的 Radiance。当然这里面会有两个重要的问题。

  1. 第一个就是 Surface Cache的某个点,这个光的照耀下它到底有多亮,它有可能会被 Shadow 挡住,那么我们怎么去知道这件事情呢。
  2. 第二件事情是,我既然知道你的GBuffer信息,直接光照我可以得到,但如果你是 Multi Bouncing 我怎么办?

 

如上图,最核心是要生成Surface Cache Final Lighting。

  1. 第一步,对于 Surface Cache 上的每个像素,我们可以很简单的计算 Direct Lighting 。对于刚才第一个问题,如果在Shadow里面,我们可以通过Shadowmap来做。
  2. 第二步,其实比较复杂,为了能够计算Indirect Lighting,我需要在 WorldSpace里面其实建了一批对光的 VoxelLighting的表达,这一步的 VoxelLighting并不是给当前帧用的,而是给下一帧使用。为什么要做这一步,并且VoxelLighting的具体细节将会在接下来的章节中讲述。
  3. 第三步,我们使用上一帧的第二步建立的Voxel Lighting,采样出对应的 Indirect Lighting ,把它和Direct Lighting 这两个合到一起,就变成了我的这一帧的这个 Final Lighting。

随着时间的积累,第一帧F0的时候只有一次 Bouncing , 第二帧F1 的时候,其实我就有这个两次 Bouncing 的值了。在F2时就具有三次Bouncing 的值。

第一步是计算Surface Cache上的直接光照,这是比较简单的,Surface Cache每一个 pixel我去找对应的Lighting,只要采样Shadow map 我就知道是否在阴影里。并且我也不用真的渲染Shadow map,实际上我只要用我的 SDF 查询一下灯光的可见性,就知道我这个点和灯光可不可见。 

 

 如果你有多光源,我就对于每一个Page的每个光源都算一遍,最后累加在一起。
World Space Voxel lighting


当我们解决了Surface Cache直接光照后,我们需要处理间接光照,间接光照如果在很近的地方,是可以使用Per-Mesh SDF来找到对应的Surface Cache进行更新,但是如果采样点很远,我们对于远处的物体并不会使用Per-Mesh SDF ,而是会使用 Global SDF Tracing,因此我们没有办法同 Per-Mesh SDF 一样从 Surface Cache 上获取 Material Attribute。
因此我们对于远处的物体需要构建一个针对 Global SDF tracing的结构。我们把整个场景以相机为中心,做了一个 Voxel的表达。所有需要 Globa SDF Ray tracing 的功能都需要采样 Voxel Lighting。

 与传统的 Voxel Lighting 不同,Lumen 并不是体素化全部场景,而是体素化相机周围一定范围内的空间做一个Clip Map , Clip Map 里面有 4 层,每层 64x64x64 个Voxel,把它存储到一个 3D 的 texture 里面。这样,我们将 Lighting 注入到 Voxel 中,这样就以更粗的粒度记录了空间中的光照信息。

在之前我们讲VXGI 里面,它一般都是用保守光栅化的方法去构建Voxel。但是在 Lumen 里面,他的方法就更为巧妙,Lumen 将 Clipmap 又进行了网格化,将 4x4x4 的 Voxels 合并为一个 Tile,Clipmap 有 64x64x64 个 Voxels,因此每个 Clipmap 可划分为 16x16x16 个 Tiles。从每个Voxel的边上,随机的射一根Ray进去,如果我能打中任意一个 Mesh在我的这个Voxel里面,就说明这个 Voxel 不为空的。

 每个Tile 顶多五六个物体,这样我们只需要跟大概四五个物体进行求交,运算效率会非常高的。

 

 当我们有了整个空间的Voxel表达,我们现在要知道Voxel 的lighting。需要注意的是,每帧我们会重新的体素化更新所有的Voxel 的lighting。
实际Voxel 的数据是根据从 6 个方向分别采样与 Voxel 相交的 SDF,根据它采样的 Mesh Card 信息再从 Surface Cache 中的 Final Lighting 中采样 Irradiance。
第一帧F0的时候,Voxel是全黑的,这个时候没有Indirect Lighting,只有 Direct Lighting ,因此我们 Surface Cache 的 Final Lighting 实际上就是直接光照的结果,这时我们把Final Lighting 的结果注入到对应的Voxel Lighting中。
下一帧里,我就有了Voxel 的信息,虽然Voxel只有直接光的信息,不过没关系,我就用第一帧Voxel 的信息再去更新我的Final Lighting 。然后我再把Final Lighting的信息写到Voxel 里面。经过多帧的跌断,我取到的信息天然的就具有 Multi Bouncing 的结果,这个方法非常的巧妙。
有些比较复杂的东西,比如说 terrain 肯定不能用 Mesh Card 去表达;再比如中间有半透明的雾怎么去做处理。 Lumen 里面有无数的细节,我们作为 10 系列的课程也就不展开了。
Surface Cache Indirect Lighting

当我们有了整套对世界的表达,我们将要去做Indirect Lighting 的计算。Lumen将屏幕空间的 Surface Cache 划分为8x8像素的 Tile,在 Tile 上放置 Probe,每 Tile 可放置 2x2 个 Probe。每个 Probe 在半球方向上进行16次的 Cone Tracing,但我会存入8x8Trace 的结果,这个结果是需要16次的 Cone Tracing差值得到。

 

我们将会得到的的数据存成了SH, 因为我要根据采样点把16个点进行差值为64个点,所以使用SH来进行。

 

如图所示,直接光照其实是 HDR 所以你看上去它会偏亮,实际上的话它之间亮度差得很大。

 这样,两个Lighting 就可以结合在一起,形成我们想要的这样的一个 Multiple lighting。
使用 Surface Cache还把一个很难的问题给解决掉了,就是自发光。大家在做游戏的时候,自发光其实很难处理。如果你只考虑它本身变量的话,那很简单,在最终的颜色上硬生生的加上去一个光。但是如果我们想自发光能够影响GI的话,这其实非常难。
虽然在前面我们讲了很多方法能解决 Multi Light source 问题,但是我这是一条光带,我怎么去渲染呢?如果使用Surface Cache,即使是一个光带,我实际上也能够把它整体Cache 到 Lighting 里面去。

对于整个 Surface Cache的更新还是非常耗时的。因此Lumen里规定,每帧最多不超过 1024 x 1024 个texels更新,对于间接光照因为它要在很多Voxels 上采样,因此最多更新512 x512 个texels。对于每一个 Mesh Card来讲,他都要排队请求自身更新。这里面其实有一整套的比较复杂的排队算法。

Phase 3 : Build a lot of Probes with Different Kinds 

前面讲的内容依旧无法直接给我们渲染提供直接帮助,这一节我们将来看如何能够拿到对应像素点的间接光数据。

对于最终的渲染来讲,我们需要拿到当前像素点法线半球面的所有Radiance,也就是对应 Radiance的一个 Probe 的表达。所以我们首先需要去分布这些Probe,最自然的一个想法是在空间上均匀分布,因为我既然已经有 Surface Cache 我又得到了一个 Voxel,我这样简单去构建整个 Probe 分布是可以的,比如说每隔一米放一个 Probe 。
但它并不能保证你能准确地表达光场的变化,如果这个变化如果你表达不了的话,实际上你渲染出来的东西看上去就很怪。就如我们经常用一句行话:这看上去很平。所有的预计算生成 Probe 的 方案都会产生类似的问题。
而Lumen就比较大胆,他说我就在 Screen Space 去洒 Probe 。如果我够狠一点的话,我屏幕上的每个像素点,我都对它进行整个Probe的 采样,这当然在结果上没什么问题。
但是Lumen还没有那么粗暴,它每隔 16x16 个 pixel ,去采样一个 Screen Space 的这个Probe 。因为实际上,近处位置16x16 个pixel 的范围在空间上相距不会太远,而间接光照非常低频,在这么近的距离里面,它的变化不会很大。

 

对于屏幕空间 Probe ,我们使用八面体映射(Octchedron mapping) 来进行球面坐标到2D纹理空间坐标的转换。虽然在球面上撒采样点最简单的方法就是按照经纬度采样,但是经纬度采样如果映射到一个 2d 的 texture 会出一个问题:极点附近的采样密度特别高,赤道附近的采样密度过低。而我们希望一个分布相对均匀的采样,并且这个采样需要满足,我给你任何一个方向,能迅速的知道它的 UV 空间的位置。因此我们选择使用Octchedron mapping来进行映射。

 

虽然Lumen每隔 16 个 Screen Pixel 放置一个Probe,但是我们放置完之后,还需要检查当前像素平面和对应Probe彼此之间是否在一个平面上。我们可以根据深度和Normal很容易得到平面信息。
如上图黄色部分就是那些对应Probe无法满足需求的像素。这时说明当前16 x16 的精度对他们来说不够,对于这些像素,我们会自适应地细化为8x8 像素的Probe,如果还不够的话,我们会继续细化为4x4。

 

每个pixel 渲染时,都要从临近的4个 Probe 之间去取它的插值,这时我需要通过法线和位置计算出自己的空间平面,把四个 Probe 的中心点投影到我的平面上,通过获得的投影距离来确认对应的光照权重。

 

我们根据投影权重会计算出一个 Error值,如果Error值累计大于某一个 threshold 我就认为这个Probe不能用了。如果我这些采样点很多都不能用的话,我认为就是你这四个采样点对我是无效的。这时,我就会进行自适应使用更细化的Probe来进行我的间接光插值计算。

 

Screen Space 的所有 Probe都放在一个 Atlas里面。由于我们的贴图都是正方形,但是我们的屏幕是长方形,因此天然就会一些贴图空间的冗余。 它会把你这些需要自适应的 Probe packing 在它的下面。因此它没用用到额外的存储空间就做到了Adaptive 的采样。

如上图,我们是把Lumen的Screen Space Probe 打印了出来,暗红色的点是 16x16 ,黄色 8x8 或者 4x4 的。

 

我们在进行屏幕空间 Probe采样的时候,用了 jitter 来防止过于重复。 多次jitter的结果在时序上又变成了一次Multi Bouncing 的采样。
Importance Sampling
我们如今已经知道如何去分布我们的 Probe ,接下来我们就要去采样。之前讲过,我们如何使用Uniform 的采样会导致一些问题,在一个房间里,如果不知道窗户在哪里,采样如果不是使劲的朝着窗户方向去采的话,结果就会像秃头般一样的,黑一块白一块非常丑。

如果不进行important Sampling 我们看到了 lighting 的结果大概如上图所示。

 

因此我们最重要的一件事情是找窗户,我要尽可能往窗户的那个方向多射一些Ray,也就是说你需要环境感知。

如之前的章节所说,我要尽可能让我的这个概率函数P符合上面函数的分布。上面这个函数是两个函数的积,其中一个是光,另一个是我表面的BRDF。所以首先你需要知道光在哪儿,第二个你需要知道法线在哪,因为背面的光对我来说毫无意义。 

如何得到光的方向呢,虽然并不知道这一帧的光在哪,但是我做一个假设,就是光的变化没有那么快。因此我们可以把上一帧的 Probe 采一遍,重投影上一帧周围四个的Probe的值,把对应SH的信息画在一个 8x8 的图里,这个图中亮的地方就是光比较亮的地方。这样我就大概知道哪个地方亮,哪个地方暗。当然如果上一帧这里不可见的时候,会出现有失败的情况。我们会Fallback到世界空间的Radiance Cache里面

 

第二点对于BRDF的部分,大家天然的能够想到,沿着 Normal 做一个 Cosine Lobe 。这听上去非常合理。但是这里面有一个很大的错误,那就是像素的Normal 非常非常高频。比如一个小区域中可能有一千多个pixel ,那一千多个 pixel 加权的法线朝向并不能由我这单独采样点的法线所代表。

 如果真的把 1024 个 Pixel 的 Normal 全部加在一起也太夸张了,因此我们使用64 个采样点去估计,并且不使用随机采样,它会根据Depth的权重,确保这些采样点和当前像素点的Depth彼此相差不要太大。然后我把这些 Normal 的Consin lobe 全部积分在一起。

Lumen 采用了一种称为结构化重要性采样(Structured Importance Sampling)的机制,其核心思想是把不重要的采样分给重要的采样部分。

 
当我们知道了Lighting和BRDF的贡献值,我们可以对所有采样点进行排序,排列出哪些点是重要的,哪些点是不重要的。这个时候我们设置一个阈值,找出排名靠后的三个最不重要的方向,假设他们的这个 PDF 值都小于我设定的阈值的时候,我们就不去采样这三个方向,把这三个采样留给我最需要采样的方向,我就可以对我最需要采样的方向进行一次 Super Sampling。
依此类推,通过这个 PDF 的值,把最不重要的方向全部过滤掉,然后让我的采样尽量集中在重要的方向,这个方向可能来自于你的法线方向也可能来自于光源。这样,就有了固定开销的Adaptive Sampling。

如图所示,右边使用Structured Importance Sampling,它的光线会集中在墙上相对比较亮的地方,整个 Rendering 的结果就会好很多。

 

如图所示,左边是没有做important Sampling,右面是做了这个important Sampling。

 

这张图同样如此。
Denoising and Spatial Probe Filtering
接下来我们需要进行降噪,做 GI 的话Filtering 就是你逃不掉的东西。

 

按照16x16 Pixel的 Probe 得到的信息是非常不稳定的。因此我们使用周围3x3的 Probe 来做Filtering。

 

每一个Probe都射出去了 64 根Ray,如果我把临近Probe同方向的Ray的光照结果,直接加到一起其实是不对的。
因为Neighbor Probe 和Current Probe存在一定距离,Neighbor Probe相同方向射到的物体在Current Probe 看过去的射线角度可能完全不一样。如上图所示,有两个Probe,他们相同方向上有两根Ray,一个蓝色一个灰色,蓝色射中的物体在Current Probe视角下并不是一个方向。
所以当我们去加权这些 Ray 的时候,对于所有的 Neighbor Probe 的Ray 都要做一次可用性检测。如果这个夹角超过了10 度,我就不用了。

 

如图所示,如果这个东西不处理,这个墙上会有很多的 noise 。

 

仅仅处理角度还不够,假设我Neighbor Probe的Ray交点非常的远,但Current Probe射到的距离很近的,虽然角度是对的,但不好意思,我认为这种情况也是无效的,我还是只用我自己的数据。

 

这个问题不解决的话,它就会出现这种漏光的问题。如上图左边,你发现那个毛巾的这个内侧面,和它靠近墙的地方。如果你不考虑这个差值,很多光就会漏进来。
World Space Probes and Ray Connecting
Screen Space Probe如果它跑的太远,效率会很低。因此Lumen希望Screen Space Probe 你只处理周边的东西。

对于远处的物体,我们会在 World Space 里面预先放好一些 Probe ,我们把远处的lighting 都Cache在里面,这样当 Screen Space Probe 取一个方向的Ray的时候,就可以在沿途的 World Space Probe 里面的光线给你取出来。
如果你的场景是一个相对静态的场景,光源也基本固定,但你的相机仍会走来走去,因此你 Screen Space Probe 一直是不稳定的,每一帧都要去更新。而World Space Probe 使用 Clip Map 的方法去部署,我在运动的时候只需要在边缘处增加几个 Probe 后面删掉几个Probe 就可以了。


如上图,我们可以看到World Space Probe的分布。Screen Space 在整个球面上的采样是 8x8 ,而World Space Probe作为最后的救赎者,采样数是32 乘32,差不多 1000 多根Ray。有了World Space Probe,很多时候Screen Space Probe 就不用跑得很远,它只要跑到附近的 World Space Probe里面就去借他的光就可以了。


那么我们如何我把光怎么接起来呢。 Screen Space Probe的Ray只会在近处采样,当我走到临近的World Space Probe 的时候,我就罢工了,远处剩下的内容我只需要去World Scape Probe取就可以了。

World Space Probe的覆盖范围形成一个 Bounding,Screen Space Probe 做 Raytracing的时候,只会走Bounding对角线长度的两倍.
距离相机较近的地方的Screen Space Probe周围的World Space Probe密度会高一点,如果你到了远处,World Space Probe的范围其实已经比较大了,所以那个时候Screen Space Probe Ray 跑的距离就会比较远。
而World Space Probe的Ray在采样的时候也会 Skip 掉自己的对角线长的距离,因为它没有必要采样近处的内容,近处的只需要交给 Screen Space Probe就可以了。


这里面讲一个很有意思的 Artifact , Screen Space Probe 射出一根Ray,World Space Probe如果沿着 Screen Space Probe 那个同样的方向去找,就会产生一个很有意思的问题。


World Space Probe很可能会跳过靠近我的一个阻挡物,这个时候就会出现漏光的问题。因此我们需要让光线弯曲一下。


做渲染的时候我们其实不是那么特别 Care 物理的完全正确,需要光线拐弯的时候,它光线就给我拐弯。我们求Screen Space Probe发出的射线到World Space Probe范围边缘的交点,我用交点和Screen Space Probe中心构造一个新的方向,然后我用这个方向当做World Space Probe采样的方向,虽然光转弯了,但它确实能解决一部分的漏光的问题。


最终渲染时,我们还是只使用 Screen Space Probe去渲染, World Space Probe只是帮助我们去快速的获取远处的光线。 因此如果World Space Probe 对应的空间内没有物体,也不在我的 Screen Space 里面,这些地方的 Probe 是不需要采样的。
也就是说,只有那些对 Screen Space Probe 有差值需求的World Space Probe才会去更新。我们会把Screen Space Probe周围八个 World Space Probe标记为 marked 。那只有这些 marked 的 World Space Probe 才有必要进行采样。

如上图,如果你只是用Screen Space Probe 的话,它如果只有两米,你看到的结果大概是这个样子的。


但是如果你有World Space Probe,你可以看到这个光看上去就准确的多了。

Phase 4 : Shading Full Pixels with Screen Space Probes

虽然我们做了 Important Sampling ,但实际上 Indirect Lighting还是很不稳定,因此我们把这些光全部投影到 SH 上面去。 SH 本质上相当于对我们的整个 Indirect Lighting进行了一个低通滤波,把它变成了一个低频信号,用它来做 Shading 的时候看上去就柔和了非常多。

这就是我们最终能够得到的结果。


Overall, Performance and Result

对于不同的 Raytracing方案,硬件上的成本是不一样的。最快的tracing就是基于 Global SDF 。其次就是在屏幕空间我进行 linear Screen 去插值。那么比它稍微慢一点的就是 Per-Mesh SDF 。 HZB 它稍微比 linear Screen 要慢,但是它的准确度其实会更高。硬件Raytracing的准确度肯定是最高的,但是它开销会更高。

我们通过这张图可以看到Lumen使用了混合的Tracing方法。红色区我们使用 Screen Space 的Tracing;绿色的区域用 Per-Mesh SDF ;蓝图的部分也就是说更远的地方,我只能用 Global SDF 。
如果每一个 Pixel Tracing使用的方法是单一的,我们应该看到的是纯色图,但现在我们看到的图是渐变图。这其实也说明我们每个Probe采样中,混合使用了各种方法 。

我们希望越靠前的方法,准确性越高。

  1. 我们首先是使用Screen Space Trace 。它基于HZB走50 步,如果能 Trace 到,我就把这个结果拿过来。
  2. 如果 Screen Space Trace失败,我们就会使用最主要的Per-Mesh SDF的方案。 Per-Mesh SDF Trace 的距离非常近,只有 1.8 米。这个时候我可以返回Mesh ID,可以直接拿到Surface Cache。
  3. 再远一点,我们只用 Global SDF, 而Global SDF只能拿到Voxel Lighting 。
  4. 如果 Global SDF 也失败了,你就采到CubeMap上去。

这里我们需要重点提一下SSGI,如果没有SSGI只有Lumen的话,可以看到下面那个倒影其实很粗糙。因此对于近处高频的物体,SSGI 还是蛮重要的。

Lumen最了不起的地方,还是在工程上真的完成了交付。在 PS5 的这个硬件平台上,能做到 3.74 毫秒。如果你愿意降低采样分辨率,你的效率可以更高,可以从将近3.74 毫秒下降到 2.15 毫秒。

所以16x16 的 pixel 的选择,我相信作者自己肯定也做了大量的尝试,得出的一个兼顾性能和效果的参数。

Lumen对整个动画电影行业,都有着非常巨大的影响,将室内设计师,效果图公司所梦寐以求的离线渲染的效果,做到了可以实时产生,非常的了不起。。
Lumen也奠定了未来 10 年下一代的游戏引擎的渲染标杆,我们认为 GI是下一代顶级游戏引擎的标配,你只要做下一代游戏引擎,你的 GI 必须是实时的。
Lumen 只是这一系列伟大征程的开始,你可以看到Lumen 基于现有的硬件, 还是做了大量的妥协。未来十年随着硬件的发展, 实时GI 将会变得更加成熟,也会变得更加简洁。而Lumen 是一方向的开山鼻祖,他的创作者必然会载入史册。

  • 12
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

想做后端的前端

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值