LUMEN技术要点总结

LUMEN总结

主题是动态全局光照和Lumen
Lumen更像是一个各种GI算法的集大成者。

1. 如何理解lumen及全局光照的实现机制

渲染方程

在这里插入图片描述
至今为止所有的实时光照都是按照Render Equation来进行渲染的,我们做得到只是在无限的逼近它。
我们把只进行一次反弹叫做SingleBounce,把多次反演叫做Multibounce。
比较全局光照是不是正确的,一般都是和康奈尔盒这种的成熟渲染场景进行对比,因为康奈尔盒是肯定成熟的,所以说要和它对比验证全局光照是否正确。

蒙塔卡洛积分

Global Illumination 中最复杂的问题就是积分,通常来讲,我们可以使用蒙特卡罗(Monte Carlo)积分法来解决这个积分。

蒙特卡洛是一类统计模拟的积分方法,在求解积分时,如果找不到被积函数的原函数,那么利用经典积分方法是得不到积分结果的。

Monte Carlo Ray Tracing(offline)

使用monte carlo求解GI最直接的就是monte carlo raytrcing。

思想很简单,首先从我们的眼睛出发(与现实中的光路是相反的),也就是屏幕上的每个像素,发出射线(Ray),这些射线会射中场景中的某个物体。我们从被射中的位置点出发,再往它的四面八方去投射射线(1 bounce),这些发出的射线会打中其他的物体,它又会在新的点向四周发射射线(2 bounce),依次类推,在此期间,如果某根ray击中了光源,那么我们就可以根据这条路径计算出对应的radiance。

Monte carlo ray tracing最大的问题实际上是采样,因为采样是随机的,所以没有办法保证相邻的pixelA和pixelB能够产生同样的结果,A可能采样到了光,而B则没有,因此你会在屏幕上看到很多的噪点。
在这里插入图片描述
上图所示,从左上角的图中是每次只发射一根射线,可以看到有很明显的noise,之后每一张图从左到右从上到下,采样数量都是翻倍的。因此右下角图中的射线数量是2的16次方,即64000条射线。可以看到最后渲染的结果已经很smooth了。
如何sampling是所有蒙特卡洛GI的核心。
最简单的方法是均匀采样(uniform sampling)。但是绝大部分情况下信号的分布是不均匀的。大部分地方是没有什么贡献的,所以说均匀采样会浪费大量的性能。

因此引入概率密度函数进行重要性采样。
在这里插入图片描述

我们引入一个概率密度函数,我们按照PDF的概率去选取我们的采样点,而不是按区间A到B均匀分布。我们只要在积分中除以选取去采样点的概率,最后算出的积分是等价的。

重要性采样(importance sampling)

在这里插入图片描述

如上图所示,蓝色函数是我们要求解的函数,我们选取与被积函数f(x)相似的概率密度函数p(x)。根据p(x)在f(x)越大的地方,采样点选取的概率越大,也就是信号强烈的地方,我们的采样点会比其他地方更多,在f(x)值较小的地方,采样点的选取的概率越小。在数学上可以证明,这样的采样方法,可以用尽可能少的采样来获取更为逼近的真实积分的结果。

这个概念其实很简单,我们尽可能朝着光比较亮的地方,或者尽可能沿着我的法线正对的地方去多投射一些采样的射线,那这样的话我就可以用更少量的射线来获得我想要的结果。
在这里插入图片描述

这里是大重点,对重要性采样来说,pdf选择cos或者GGX法线分布函数的公式就可以了。

我们在做rendering的时候,如果假设大部分反射是diffuse的,对光的这个敏感度就是一个cosine lobe,靠近法线(normal)的方向会敏感,然后对其他的角度衰减扩散。就算你的光线很强,但是你是从很侧面设来,我对这个光的感应度其实也不是很高。

重要性采样中比较重要的地方是要选取一个正确的pdf,也就是概率密度函数。
在这里插入图片描述

如上图,左边是uniform sampling,右边是cosine lobe,也就是采样点的分布会稍微幂集的靠近天顶的地方,可以发现同样是256 spp(sampling per pixel),使用uniform sampling会有很多噪点,而cosine lobe噪点会明显下降。
在这里插入图片描述
如上图所示,GGX材质高频足够高,低频足够宽。这种情况下,我们使用GGX的PDF会比还是用cosine lobe更为sharp。
总而言之,GI核心是你去需要去找到一个好的sampling方法,用尽可能少的射线去获取一个来自四面八方的光场提供给你的信息。

Reflective Shadow Map(RSM)

蒙特卡洛光线追踪的GI算法,是一种离线的方案,在电影当中使用。
Reflective Shadow Map(RSM),它核心解决的问题是我怎么把光注入到场景中去。

图形学中,有两类大致的GI方法,一类就是之前最经典蒙特卡洛光线追踪,还有一种叫做光子映射。
Raytracing是从相机(人眼)为原点发出射线向整个世界,photon mapping是从光源出发。

(其实就是光路的路线,光线追踪是从眼睛到光源,光子映射是从光源到人眼)。

光子映射 的思想是:我们之所以看到世界中的物体,原因是因为从光源射出来的光子打到了物体表面,进而不停的反弹,人眼收集到了这些光子,进而你看到了整个世界。
简单的讲光子映射就是说我射出无数的光子,然后这些光子在物体的表面来回bounce。(从光的角度看待整个问题)
当达到一定条件后,那些光子就会停留在物体表面。我们在做shading时,收集这些光子然后进行插值,最后得出shading的结果。这就是光子映射的一个核心思想。
RSM的方法,其实是从shadow map的定义出发的。何为shadow map?
在这里插入图片描述
核心思想:shadow map是干啥的,是记录光源能直接照射到的物体表面的。所以说shadow map没有记录的地方都是收到的间接光照,shadow map上的位置都是收到直接光照的。
渲染阴影的时候,需要渲染一帧shadow map,shadow map的渲染和正常的渲染不同,正常的渲染是从当前相机的视角去渲染,而shadow map是从光源的位置和视角去渲染。
因此,如果我们从光源的位置和视角,把整个场景正常渲染一遍,当只有直接光照的时候,所有被光照照亮的物体表面都会渲染到我的map里面。换句话说,从光的位置,你所能看到所有被它直接照亮的表面。所有被第一次被灯光照亮的表面将被我渲染进去,一个像素不会多,一个像素也不会少。
当然这里我们除了存储光照通量之外,还需要存储worldPos和Normal,以提供给后续shading读取。那么在做shading的时候,就提前拥有了RSM,我们就能知道所有在空间中被光源第一次照亮的点和它对应的亮度信息。
对应上图,假设我们要渲染x点的光照值,x点因为被桌子挡住,光源无法直接照亮这个点,因此x点并没有直接光照。
根据之前渲染的那种Reflective shadow map,得知光源会射到xp点,xp点会沿着它的法线方向进行散射,我就可以把xp点的radiance给接过来进行shading,这就是RSM的一个核心思想。

最简单直接的方法就是把RSM中的每个像素都当成一个小光源,把RSM上的每个点都进行一次渲染,但这个方法有个问题就是过于粗暴,比如RSM分辨率大小是512x512,那也是将近有几十万个点。。。

所以说我们对RSM进行一番改造。距离我们较近的间接光照对我们影响最大,比较远的那些采样点贡献度则更低,因此我们不去对RSM每个点采样,而是在世界空间中去不同方向发射cone tracing,并且各个方向的方向采样密度不相同。如果某个方向上采样点距离比较远,我们采样的密度低一些,如果比较近,就高一些。(其实就是重要性采样的思想)。

间接光照其实是非常低频的数据,所以我们可以使用更低分辨率来计算间接光照,也就是每隔两个或者每隔四个pixel来做一次间接光计算,旁边像素可以根共用周围像素计算的结果。
在这里插入图片描述
当然这也会产生一些问题,如果采样点和我当前渲染像素空间位置相差很大,或者法线朝向不共面,会产生很多的artifact,当然这些渲染错误的pixel不会很多,对于整个屏幕来讲可能不到1%甚至千分之一。因此对于这种情况,我们认为这是个无效的差值,重新对它进行一次完整的采样。

游戏中的很多手电筒就是使用的RSM算法来进行的实现的,其实思想是和光子映射一样的,都是把光子注入到世界中这种方法。并且提供了可以在更低分辨率的屏幕空间里面去采集这些间接光照思路,还给我们提供了出现error时,处理这些错误像素的方法。

但是RSM是早期的算法,有很多的局限性。首先是只能解决singleBounce,而且它也不检测Visibility,并且没有考虑间接光照的遮挡。但是毫无疑问RSM是一个非常有启发性的工作。

Light Propagation Volumes(LPV)

既然光子已经注进去了,那我们就要让光在这个世界里面流动起来。也就是LPV
LPV最早是CryEngine3提出的一种实时的、无需任何预计算的全局光照技术。

在这里插入图片描述

在RSM中,我们找到并定义了一系列的虚拟点光源,LPV第一步仍然是如此。它在找到虚拟点光源后,把整个场景分成的一个个小格子叫做voxel(像素)。
在这里插入图片描述
我们需要把在RSM得到的虚拟点光源 ”注入” 到它对应的Voxel中。

由于在Voxel里可能有多个虚拟光源存在,存储时使用多个Cubemap来暴力存储,但是这会造成巨大的存储开销,通常来讲我们可以用二阶球谐来拟合这些光照结果,因为球谐函数SH能够将光照加权累积在一起。
最后得到了在小格子里面radiance在空间上的分布场。
在这里插入图片描述

当我们有了Voxel之后,还需要对场景进行扩散。对于一个voxel,在传播时可以传播上下左右前后共六个格子。它从每个voxel发出对应方向的射线。
在这里插入图片描述

这种扩散叫做Propagation,LPV的主要问题是当Radiance扩散出去之后,Voxel内部还有没有Radiance,如果内部还有的话,那么能量是不守恒的;如果radiance不在了,那么那个地方就是黑的,周围为什么还会被照亮呢?
我们会发现这个光的扩散的速度扩散的范围跟你做了多少次 Iteration 有关,这种感觉就像是说光是有限速度在各个这个 Radiance 里面去传递的。因此LPV 的方法不太符合物理学原理。(迭代一次,光线扩散一个格)。

LPV不符合物理规律:一.能量不守恒,二.光以有限速度传播
LPV最有价值的点是,它是第一个把空间按照体素进行划分的,在每个voxel里面去保存radiance的分布情况,并且更有意义的是他提出了把radiance的分布用SH存储的方案。

2. 虚幻5最强技术Lumen站在了哪些巨人的肩膀上

SVOGI(不是重点,现在几乎没有几个人使用了,重点在VXGI)

下面是SVOGI,SVOGI是对LPV的空间的升级,之前的LPV虽然对空间进行Voxel划分,但是每个voxel如果你分的太粗,就不能够准确地表达这个世界里面的光的分布,如果分的太细,那我就拥有极其多的voxel。而且物体内的空间是中空的,没有voxel存在的必要,因为voxel只需要存物体里面的光照信息。
在这里插入图片描述

因此SVOGI使用的是保守光栅化方法,只要覆盖到像素的任何区域我都会进行voxel,比如很小很薄的三角形只占有像素的很小的部分,依旧会进行voxel。
先把表面的Voxel全部收集起来。
在这里插入图片描述

如果我们要表达的空间很大,自然而然可以使用树来存储。空间上每一个维度都进行二分,三维空间上八分,因此我们使用八叉树。
原文十分复杂,因为对于每一个节点,他不仅存自己的数据,还要存了周围的数据。因为它要做filtering的时候需要这些数据来做差值,因此所需要的数据结构惊人的复杂。

SVOGI一个启发性的思路是,通常采样的时候需要采样几百根ray才能达到你想要的效果,它的想法是我想要重点采样的地方,并不是使用ray而是cone,cone的英文是圆锥,当圆锥体展开的时候,对应的面积就会越来越大。
在这里插入图片描述
没事,实际上没有人使用这个算法,大部分都用VXGI。

VXGI

一个新的概念clipmap和mipmap的区别
在这里插入图片描述
关于介绍clipmap和mipmap区别的文章

VXGI 完美取代了SVOGI。

VXGI的思路是,我们其实并不需要整个场景的表达,对于GI来说,我们最重要的工作是把我眼睛看到的地方GI做好,并且离我比较近的区域的GI更为重要,远处区域的GI虽然也重要,但是我不需要对它进行很高精度的采样。

这时候我们里传统的clip map的思想,离我相机近的地方,我用更密的voxel去表达;离我远一点精度就下降一倍,以此类推。

这样就可以构建一个稀疏八叉树结构,依旧还是构建了树状结构,并且这个树状结构基于view的密度分布,对GPU更加友好,并且实现起来更加的清晰明确。
在这里插入图片描述
由于场景中的你的相机总是在动,那么对应的ClipMap也要跟随着进行更新。VXGI提出一种可循环的寻址系统。当你的相机移动变化时,我们只需要进行增量更新,也就是GPU中,你每次只需要更新它边上那圈数据,而不需要把中间那些没有必要更新的数据重新生成和赋值。
在这里插入图片描述
这样我们对整个空间就有一个近处密、远处稀疏的Voxel表达。如上图所示,远处的Voxel也没有特别明显,但实际上它已经离得很远了。
在这里插入图片描述
VXGI还需要考虑透明度问题,每个voxel并不是只有透光和不透光两个状态。实际上每个voxel有一个opacity。假设voxel里面有一个mesh,这个mesh把55%的光挡住,而45%的 光透过,并且各个方向的透光率不一样,因此我们要沿着他各个方向算出它的不透明度。

我们在采样时,不像大家想的那样hit到了一个voxel我就停在那里,而是有一个半透明效果。
在这里插入图片描述
如上图所示,对一个场景而言里面的voxel会有很多半透(灰色)的区域。
在这里插入图片描述
对于VXGI,我们同样使用RSM,注入到我们每一个表面的Voxel上去。
在这里插入图片描述
大家可以看到只有一些voxel被照亮,因为只有这些voxel接受到了直接光照。(利用RSM)
在这里插入图片描述
接下来,我们对于每个屏幕上的像素,进行Cone Tracing。对于非常粗糙的表面,我就以Cosine Lobe四面八方的采样;如果表面是比较光滑我就沿着大致的反射方向采样;如果非常光滑的表面,我不仅只沿着反射方向去取那个间接光照,Cone还可以变得更细。(其实也是重要性采样的思想)。
在这里插入图片描述
根据之前所讲,如果沿着这个方向的Opacity不是完全屏蔽,那么光能透漏过来。那时候我再继续往后走,越往上走,我的采样面积就越大。我就去clip map里面更高的mip的那个voxel,这样我一次性就能覆盖更大的区域。
我光线的透过alpha会随着我不断地透射而变低,当Alpha小于某阈值,很接近于零的时候,我们就说,就当做不透明算了,我们就不往后再走了,这样能让我们的算法更快一点。
在这里插入图片描述
VXGI里面的问题:

不准确的遮挡(透明度)
漏光,当遮挡墙比体素的size小很多

在这里插入图片描述
VXGI其实也有很多的问题了,比如实际的Cone Tracingopacity累积是一个估计值。

如上图中右上所示,绿色三角形在Voxel 里面阻挡了一部分光照,黄色的方块也阻挡了部分光。它两个Alpha如果按照乘法累积肯定不为零。但是在实际上光路上其实已经完全阻挡了光照。

实际的渲染中的表现就是 Light Leaking漏光,特别是对于那种一些很薄的物体,或者本身不是很薄但是距离比较远的物体尤其严重。

SSGI(又称SSR)

SSGI是寒霜引擎2015年在siggraph上发表的,又称为SSR。
在这里插入图片描述
SSGI原始思想很简单,如上图,红框内的东西是直接渲染出来的东西。下面白色的部分,如果我要我去渲染它的间接光,因为它的表面非常的mirror,所以我只要在屏幕空间里面把红色部分反一下,这些数据我就可以用了。因此,SSGI的思路是:在屏幕空间中把这些渲染好的像素点作为我全局光照的小光源,即复用屏幕空间的数据。
在这里插入图片描述
如上图,假设整个屏幕中的像素已经渲染好了,当我要shading图中黄色的点的时候,我已经知道了normal和相机方向,我就可以沿着反射方向射一些ray。这些ray就在我的屏幕空间里面去找空间上位置正确的点,如果这个点是红色我就获得了一些红色的radiance,如果这个点是绿色我就收到了一些绿色的radiance,这样的话就可以直接用屏幕空间的数据进行indirect lighting

光线步进

在这里插入图片描述
在屏幕空间找到对应采样点的方法叫做raymarching,假设我们从一个点出发射出一根ray,这根ray使用均匀间隔一直往前走,如果它当前位置的深度值比我这根ray的深度更靠前,这说明这根ray被挡住了。这时候认为找到了一个交点,这就是均匀的remarching,它要求步进间距非常的密,速度也就会很慢。
在这里插入图片描述
我们需要另一种方法来加速raymarching。GPU硬件提供了生成一个特殊buffer的方法,当然我们可以自己实现,叫做HZB,也叫做Hi-Z。可以把Zbuffer做成一层层的Mip,每个上层Mip里面的一个像素,都对应下层mip里面四个点的像素。上层mip像素点的值,是下层mip四个像素点中深度的最小值,也就是离我们最近的那个值

如果我们拥有了Hi-Z,一个Ray如果跟Hi-Z的某个Mip不相交,那我跟你下层的Mip一定不相交。如果我跟你某个Mip相交,那我一定跟你下层某个Mip的像素相交。所以实际上把这个Depth buffer做成了hierarchy的结构。
在这里插入图片描述
当我有了Hi-Z,想做Raymarching的时候就比较简单了。比如初始状态下,我在Mip0层,我先在Mip0层往外走一格。
在这里插入图片描述
如果法线在Mip0层没有交点,我需要接着走,不过这时我往上跑一层跑到Mip1走一格,这就相当于走了两格。
在这里插入图片描述
这个时候如果还是没有发现交点,胆子就开始更大了,再往上走一层在mip2走一格,再做一次深度测是的话,这一次相当于做到了四格。
在这里插入图片描述
这个时候假设我不小心发现我检测到了物体。如今我是在Mip2级别,我就知道他可能交到了mip1或者mip0的某个点,但是具体那个点不知道。这时候,回退到mip1往前走一格。
在这里插入图片描述
我发现好像跟mip1也有个交点,那我再回退到mip0继续走。
在这里插入图片描述
这时候我们就能找到交点。虽然听上去hi-z比均匀采样更为复杂,但是算法复杂度是log2的,所以说就算1024分辨率,最多也就10步就到了。如果你用uniform的raymarching可能要走到五六十步才能走到。
在这里插入图片描述
SSGI还有一个很有意思的思想就是复用采样。当我对一个像素求出了采样点,我周围的点在也要进行球面采样的时候,如果不考虑visibility,他采样到的那个小灯泡实际上也是你的小灯泡,相当于帮你做了一次采样。这个思想其实也是非常重要的。因为在我们真正做GI的时候,你不可能对每一个采样点的位置射那么多的ray,不然整个计算就会爆炸。
在这里插入图片描述
和之前一样,我们提供采样的
Scene Color
也会做Mipmap,对于远处的Cone Tracing会采样到分辨率更低的贴图,这相当于对整个光照进行了一次Filtering
在这里插入图片描述
当然SSGI也会有一些问题,如果在屏幕空间没有的东西,我就看不到。比如像上面例子中框出的部分因为反射的数据拿不到,所以下面的整个都变白了,但是这个不影响SSGI作为一个非常有用的算法。
在这里插入图片描述
SSGI有很多优点:

  1. 因为Hi-Z精度非常高,所以两个物体的交接面,非常细腻的几个pixel的内容误差,它都能把你给算出来,因此SSGI能够处理非常近的contact shadow。而以前voxel的方法对它的处理很糟糕。
  2. 对于Hit的计算,因为用了Hi-Z的方法非常准确,遮蔽voxel去估计要准确的多
  3. 无论场景有多复杂,SSGI对场景的复杂度是无感的。
  4. SSGI能够处理动态的物体。
    这几个优势相当重要,这就是为什么在这么复杂的lumen里面,还使用到了SSGI。

3.已经有了硬件的Raytracing,为什么我们还需要Lumen

光线追踪超级慢,每个像素只能负担的起二分之一根光线,但是高质量的GI的话要求每像素几百根光线。
很多人会问,既然已经有了硬件的raytracing了,为什么还要lumen呢?这是由于很多的硬件并不支持realtime raytracing,对于支持的那些硬件,N卡还算是勉强可以的,而A卡支持的比较糟糕。并且实际测试中,N卡的计算量也是比较费劲的。
比如游戏中最常见的indoor场景,需要每像素500根ray才能得到想要的效果,但是我们只能付得起每像素二分之一ray。因此我们需要解决如何在软件层面解决掉快速ray tracing的问题。
在这里插入图片描述
另一个大的问题就是sampling,过去几十年时间离线GI都在和important sampling做殊死搏斗,直到现在也没有让我们特别满意。而我们的realtime的sampling数量又被卡的很死,这进一步增加了我们处理问题的难度。
如上图所示,靠近窗口的sampling结果基本上是可以接受的,虽然有些noisy,但是你可以通过filtering解决这个问题。但是如果离窗子稍微远一点,那filtering也救不了你,最终看到的就是一个个的大色斑。因此lumen也需要解决如何sampling的问题。
在这里插入图片描述
我们知道间接光照(indirect lighting)可以在低分辨率图像(low resolution)上面采样得出,lumen的想法是在屏幕空间放一大堆的probes,屏幕空间的特点是紧贴那些要被渲染的物体表面,通过probe获取光照。比如每16个pixel放置一个探针,每个像素去shading的时候,它的高频信息可以通过表面法线产生,最后得到右边非常逼真的效果。

因此lumen最核心的思想是三点:
1. 在不用硬件ray trace的前提下,我如何进行快速的ray trace
2. 尽可能的在sample里做的优化
3. 放置的probe尽可能贴着真实物体的表面,使它的精度足够高。

在任意硬件上的光线追踪

Lumen最核心的点就是要解决我怎么样的在任意硬件上能进行非常快速的raytracing,它要解决如何我在射出一根ray时,快速的得到这个ray到底能不能交到一个物体,并且知道和它相交的物体是谁。
这个raytracing当然是可以用硬件raytacing去做,lumen也提供能够开启硬件raytracing的功能来做更高精度raytracing的表现,不过为了兼容各种硬件,lumen最主要的是提供了基于软件的SDF(signed distance filed)的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。比如说我们要做软阴影,从需要计算阴影强度的着色点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,RayMarching的速度就会非常快,因为它不再依赖一个个的物体。当然它的缺点是受制于存储空间,不能像per-mesh SDF那样精细,但是它是一个很好的加速方法。
在这里插入图片描述
全局SDF是一个不准确的近表面。
对cone起始处的物体使用Permesh SDF进行采样,对于远处的可以使用globa 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。

Radiance Injection and Caching

接下来解决如何把光子注入到世界。
在这里插入图片描述
在Lumen里面,搞了一套非常特殊的东西叫做mesh card。当我们计算全局光照的时候,从光的视角我们去照亮整个世界,其实整个世界里面无论你看的见还是看不见的pixel他实际上都会被这个光给照亮。每一个被照亮的pixel,实际上也是GI的一个贡献者,就是我们的小光源之一。
我们的mesh SDF trace虽然可以拿到对应的相交点,但是对应物体的材质信息是拿不到的,我们还需要知道物体表面的材质属性,但SDF并不包含Material的Attribute。并且仅使用RSM也是不够的,因为他只有灯光看到的那些表面的信息,其余的很多的角度依旧看不见。
Lumen想到了一个方案,首先就是每个mesh导入的时候,如上图所示,会生成mesh card,这样在使用per-mesh SDF的时候,就可以找到对应的mesh card。
在这里插入图片描述
这是我们在UE5里面搭的一个场景,你可以看到生成的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的集合。
在这里插入图片描述
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信息,直接光照我可以得到,但如果你是multiBouncing我怎么办?

在这里插入图片描述

如上图,最核心是要生成Surface Cache Final Lighting,
第一步,对于Surface Cache上的每个像素,我们可以很简单的计算Direct Lighting。对于刚才第一个问题,如果在Shadow里面,我们可以通过Shadow Map来做。
第二步,其实比较复杂,为了能够计算Indirect Lighting,我们需要在WorldSpace里面去实现了一批对光的Voxel Lighting的表达,这一步的VoxelLighting并不会是给当前帧用的,而是给下一帧用的。为什么要做这一步,并且VoxelLighting的具体细节会在接下来的章节中讲述。
第三步,我们使用上一帧的第二步建立的Voxel Lighting,采样出对应的Indiredt Lighting,把它和Direct Lighting这两个合到一起,就变成了我的这一帧的这个Final Lighting。随着时间的积累,第一帧F0的时候只有一帧Bounce,第二帧F1的时候,其实我就有两次的Bounce值了。在F2时就具有三次Bouncing的值。

Surface CacheDirect Lighting

SSGI的工作最早是2015年寒霜引擎在Siggraph上发表的工作。
在这里插入图片描述
第一步是计算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的表达。所有需要Global 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 Ligting实际上就是直接光照的结果,这是我们把Final Lighting的结果注入到对应的Voxel Lighting中。
下一帧里,我就有了Voxel的信息,虽然Voxel只有直接光的信息,不过没关系,我就用第一帧Voxel的信息再去更新我的Final Lighting。然后我再把Final Lighting的信息写到Voxel里面。经过多帧的跌断,我取到的信息天然的就具有Multi Bouncing的结果,这个方法非常的巧妙。
有些比较复杂的东西,比如说terrain肯定不能用Mesh Card去表达,再比如中间有半透明的雾怎么去做处理。

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里规定,每帧最多不超过1024x1024个Texels更新,对于间接光照因为它要在很多Voxels上采样,因此最多更新512x512个Texels。对于每一个Mesh Card来讲,他都要排队请求自身更新。这里面其实有一整套复杂的排队算法。

4.虚幻5是如何让渲染效果如此逼真的

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

1.Screen Space Probe

对于最终渲染来讲,我们需要拿到当前像素点法线半球面的所有Radiance,也就是对应Radiance的一个Probe的表达。所以我们首先需要去分布这些Probe,最自然的一个想法是在空间上均匀分布,因为我既然已经有Surface Cache我又得到了一个Voxel,我这样简单的去构建整个Probe分布是可以的,比如说每隔一米放一个Probe。

但它并不能保证你能准确地表达光场的变化,如果这个变化你表达不了的话,实际上你渲染出来的东西就会看上去很怪异。就如我们经常用一句行话:这看上去很平。所有的预计算生成Probe的方案都会产生类似的问题。

而Lumen比较大胆,他说我就在Screen Space 去Probe。如果够狠一点的话,我屏幕空间上的每个像素点,我都对它进行整个Probe的采样,这当然在结果上是没有问题的。
但是Lumen还没有那么粗暴,它每隔16x16个Pixel的范围在空间上相聚不会太远,而间接光照非常低频,在这么近的距离里面,它的变化不大。

对于屏幕空间Probe,我们使用八面体映射(Octchedron Mapping)来进行球面坐标到2D纹理空间坐标的转换。虽然在球面上撒采样点最简单的方法就是按照经纬度采样,但是经纬度采样如果映射到一个2d的texture会出现一个问题:极点附近的采样密度特别高,赤道附近的采样密度过低。而我们希望一个分布相对均匀的采样,并且这个采样需要满足,我给你任何一个方向,能迅速的知道它的UV空间的位置。因此我们选择使用Octchedron mapping(八面体映射)来进行映射。
在这里插入图片描述
虽然Lumen每隔16个Screen Pixel放置一个Probe,但是我们放置完之后,还需要检查当前像素平面和对应Probe彼此之间是否在一个平面上。我们可以根据深度和Normal很容易得到平面信息。
如上图黄色部分就是那些对应Probe无法满足需求的像素。这时说明当前16x16的精度对他们来说不够,对于这些像素,我们会自适应地细化为8x8像素的Probe,如果还不够的话,我们会继续细化为4x4.
在这里插入图片描述
每个Pixel渲染时,都要从临近的4个Probe之间去取它的插值,这时我需要通过法线和位置计算出自己的空间平面,把四个Probe的中心点投影到我的平面上,通过获得的投影距离来确认对应的光照权重。
在这里插入图片描述
我们根据投影权重会计算出一个Error值,如果Error值累计大于某一个Threshold我就认为这个Probe不能用了。如果我这些采样点很多都不能用的话,我认为就是你这四个采样点对我是无效的。这时,我就会进行自适应使用更细化的Probe来进行我的间接光插值计算。
在这里插入图片描述
Screen Space的所有Probe都放在一个Atlas里面。由于我们的贴图都是正方形的,但是我们的屏幕是长方形的,因此天然就会一些贴图空间的冗余。它会把你这些需要自适应的ProbePacking在它的下面。因此他没用到额外的存储空间就做到了Adaptive的采样。
在这里插入图片描述
如上图,我们是把Lumen的Screen Space Probe打印了出来,暗红色的点是16x16,黄色的8x8或者4x4的。
在这里插入图片描述
我们在进行屏幕空间Probe采样点时候,用了Jitter来防止过于重复。多次Jitter的结果在时序上又变成了Multi Bouncing的采样。

2.Radiance Injection and Caching

我们如今已经知道如何去分布我们的Probe,接下来我们就要去采样。之前讲过,我们如何使用uniform的采样会导致一些问题,在一个房间里,如果不知道窗户在哪里,采样如果不是使劲的朝着窗户方向去采的话,结果就会秃头般一样的,黑一块白一块非常丑。
在这里插入图片描述
如果不进行important sampling 我们看到了lighting的结果大概如上图所示。
在这里插入图片描述
因此我们最重要的一件事情是找窗户,我尽可能往窗户的那个方向多射一些Ray,也就是说你需要环境感知。
在这里插入图片描述
如前面的章节,我要尽可能让我这个概率函数P符合上面函数的分布。上面这个函数是两个函数的积,其中一个是光,另一个是我表面的BRDF。所以首先你需要知道光在哪儿,第二个你需要知道法线在哪,因为背面的光对我来说毫无意义。
在这里插入图片描述
第二点对于BRDF的部分,大家天然的能够想到,沿着normal 做一个cosine lobe。这听上去非常合理。但是这里面有一个很大的错误,那就是像素的normal非常非常高频。比如一个小区域中可能有一千多个pixel,那一千多个pixel加权法线朝向并不能由我这单独采样点的法线所代表。
在这里插入图片描述
如果真的把1024个pixel的normal全部加在一起也太夸张了,因此我们使用64个采样点去估计,并且不使用随机采样,它会根据Depth的权重,确保这些采样点和当前像素点的depth彼此相差不要太大。然后我把这些normal的consin lobe 全部积分在一起。
在这里插入图片描述
Lumen采用了一种叫做结构化重要性采样的机制,其核心思想是把不重要的采样分给重要的采样部分。
在这里插入图片描述
当我们知道了lighting和brdf项的贡献值,我们可以对所有采样点进行排序,排列出哪些点是重要的,哪些点是不重要的。这个时候我们设置一个阈值,找出排名靠后的三个最不重要的方向,假设他们的这个PDF值都小于我设定的阈值的时候,我们就不去采样这三个方向,把这三个采样留给我最需要采样的方向,我就可以对我最需要采样的方向进行一次super sampling。
依次类推,通过这个PDF的值,把最不重要的方向全部过滤掉,然后让我的采样尽量集中在重要的方向,这个方向可能来自于你的法线方向也可能来自于光源。这样,就有了固定开销的Adaptive Sampling。
在这里插入图片描述
如图所示,右边使用structured importance sampling,它的光线会集中在墙上相对比较亮的地方,整个rendering结果会好很多。
在这里插入图片描述
如图所示,左边是没有做important sampling,右面是做了这个important sampling。
在这里插入图片描述
这张图也是同样。

5. GI是下一代顶级游戏引擎的标配吗

1.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射到的距离很近的,虽然角度是对的,但是不好意思,我认为这种情况也是无效的,我还是只用我自己的数据。
这个问题不解决的话,他就会出现这种漏光的问题。
在这里插入图片描述
如上图左边,你发现那个毛巾的这个内侧面,和它靠近墙的地方。如果你不考虑这个插值,很多光就会漏进来。

2.World Space Probe 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最后的拯救者,采样数量是32x3,差不多1000多根Ray。有了World Space Probe,很多时候Screen Space Probe就不用跑的很远,它只要跑到附近的World Space Probe里面就去借他的光就可以了。
在这里插入图片描述
那么我们如何把光连起来呢。Screen Space Probe的Ray只会在近处采样,当我走到邻近的World Space Probe的时候,我就罢工了,远处剩下的内容我只需要去World Space 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,你可以看到这个光看上去就准确的多了。

3.Shading Full Pixels with Screen Space Probes

虽然我们做了important sampling,但实际上indirect lighting还是很不稳定,因此我们把这些光全部投影到了SH上面去。SH本质上相当于对我们的整个indirect lighting进行了一个低通滤波,把它变成了一个低频信号,用它来做shading的时候看上去就柔和了非常多。
这就是我们最终能够得到的结果。
在这里插入图片描述
对于不同的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采样中,混合使用了各种方法。
在这里插入图片描述

我们希望越靠前的方法,准确性越高。
我们首先是使用screen space trace。它基于HZB走50步,如果能trace到,我就把这个结果拿过来。
如果screen space trace失败,我们就会使用最主要的per-mesh SDF的方案。Per-Mesh SDF Trace的距离非常近,只有1.8米。这个时候我们可以返回Mesh ID,可以直接拿到surface cache。
在远一点,我们只用global SDF,而Global SDF只能拿到voxel lighting。
如果global SDF也失败了,你就采到cubemap上去。
在这里插入图片描述

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

Lumen最了不起的,还是在工程上真的完成了交付。在PS5的这个硬件平台上,能做到
3.74ms,如果你愿意降低采样分辨率,你的效率可以更高,可以从将近3.74ms下降到2.15ms。
所以16x16的pixel的选择,我相信作者自己肯定做了大量的尝试,得出的一个兼顾性能和效果的参数。

  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值