最强分析 |一文理解Lumen及全局光照的实现机制
Author: [GAMES104官方账号]
Link: [https://zhuanlan.zhihu.com/p/643337359]
Dynamic Global Illumination andLumen
我们今天的课程主题是动态全局光照和Lumen ,在讲Lumen之前,我们需要首先需要了解GI(Global Illumination),如果不把GI讲清楚,大家将会陷入Lumen复杂深邃的技术细节中。
如果不了解GI的发展演变,大家会感觉Lumen中的某些细节处理是凭空从天上掉下来的。但其实不然,Lumen中的很多算法和思想在之前的一些GI算法中都有出现和实践过,因此Lumen更像一个各种GI算法的集大成者。我们只有理解GI最宏观的体系结构,你才能理解Lumen 是怎么产生的。
一. Global Illumination
第一步我们是要了解渲染中最著名的Render Equation ,这在之前的课程中也讲到过。我们今天几乎做的所有Rendering 相关的工作都是去满足这个Render Equation 。但是至今为止,没有任何一款游戏引擎能够做到实时的完全按照Render Equation来进行渲染,我们能做到的只是在无限的逼近它。
如果只计算点光(Point Lighting)的直接光照(Direct Lighting)还是比较简单的。但 GI 并不只计算点光,而是需要计算四面八方照的光,这些光和物体表面的 BRDF方程都要进行积分,才能形成我最终的光照。
这里面复杂的积分过程非常麻烦,电脑屏幕上拥有百万甚至千万级别的 pixel 。对于每个pixel,都要去采集四面八方射过来的光线,从而计算出它的积分值。离线情况尚且困难,更何况是实时的情况下,一秒钟还要渲染几十帧甚至上百帧,这里面的计算量简直恐怖如斯。
更糟糕的是,需要计算的光线数量严格意义上是无限多的。就算你在场景里面只摆放了一个光源,但这个光源可以把周围物体无数个小面片照亮,每一个被照亮的小面片依旧可以视为一个新的光源来照亮其他物体。同时被这些小面片照亮的其他地方也会继续反弹光线。我们把只进行一次反弹叫做 SingleBounce,把多次反弹叫做 MultiBounce 。
上图右侧是著名的渲染场景康奈尔盒(Cornell Box),通过比较渲染场景和同一场景的实际照片来确定渲染的准确性。它的左边是红色,右边是绿色,上面放置一个光源,下面放有一个长方块和一个方形块。这里需要注意的是,上方放置的光源并不是点光源,而是一个面光源,因此光源本身就可以分解成无限多个。就算是Cornell Box 这么简单的场景,如果你想把它做到接近真实也是非常困难的,并且如果想要做到实时渲染更是难上加难。
我们如果仔细观察,这个简单场景中的的光学现象其实非常复杂。光源照射到红色墙反弹回来后会出现很多红光,红光会照亮这长方块的左侧面。同样,右边墙上也看到绿色。而这就是 GI 的效果。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-47f813b1694ffa609b968af4abad7446_1440w.jpg)
我们以游戏为例,如果游戏中只有直接光照,最终的效果就如上图左侧一样。而通常我们在游戏看到的效果并非如此,因为通常我们还会加上环境光。环境光其实是对 GI 的一个 hack,如上图右侧。虽然有了环境光的效果会让场景显得好了一点,但依旧达不到真正GI非常丰富且复杂效果。
阳光投进洞口的岩洞,拉开窗帘的房间,打着手电的傍晚,这些场景如果没有GI 来模拟复杂MultiBounce 的光学现象,整个画面会看上去非常塑料。因此我们需要去实现GI的这种效果。
Monte Carlo Integration
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-734cf1b5867b2d28b981af4373dcecbe_1440w.jpg)
如之前所述,Global Illumination 中最复杂的问题就是积分,通常来讲,我们可以使用蒙特卡罗(Monte Carlo)积分法来解决这个积分。蒙特卡洛是一类统计模拟的积分方法,在求解积分时,如果找不到被积函数的原函数,那么利用经典积分方法是得不到积分结果的,但是蒙特卡洛积分方法告诉我们,利用一个随机变量对被积函数进行采样(Sampling),并将采样值进行一定的处理,那么当拥有一定数量的采样时,最终估算得到的结果可以很好的近似原积分的结果。这样一来,我们就不用去求原函数的形式,就能求得积分的近似结果。
他的想法其实非常简单,假设对于一个函数f(x)区间 A 到 B ,我们在做Render 时的积分区间就是在法线朝向的半球上进行积分。
我们对于区间A到B内的点进行采样,得到每一个点的函数值,然后进行平均,就能知道你的这个积分的估算值是多少。对于 Shading 来讲,就是采样多个方向的光线计算的Radiance,最后把他们加在一起。
二.Monte Carlo Ray Tracing
使用Monte Carlo 求解GI 最直接的应用就是Monte Carlo Ray Tracing。
Monte Carlo Ray Tracing的思想非常简单,首先从我们的眼睛出发,也就是屏幕上的每个像素,发出射线(Ray),这些射线会射中场景中的某个物体。我们从被射中的位置点出发,再往它的四面八方去投射射线(1 Bounce),这些发出的射线会打中其他的物体,它又会在新的点向四周发射射线(2 Bounce),以此类推。在此期间,如果某根Ray射中了光源,那么我们就可以根据这条路径计算出对应的Radiance 。
Monte Carlo Ray Tracing通过多次反弹,每增加一层Bounce的,它的计算复杂度会进行指数级的扩张。SingleBounce如果还能勉强接受,但是MultiBounce计算量会随着迭代的次数急速膨胀。
我们需要注意的是,在我们实际应用的场景中,真正对于结果有贡献,或者说有亮度的地方是非常不均匀的。比如说一个硕大无比的房间,它真正亮的地方可能就是阳光射进窗子里的那一部分。你的整个房间都是被就是被它的MultiBounce照亮。这个时候你在进行采样的时候,你会经常发现,只有极少数的射线有机会打中窗子,绝大多数射线只会采样到了房间内黑暗的部分,根本没采到光。
因此Monte Carlo Ray Tracing 最大的问题实际上就是采样,因为采样是随机的,所以没有办法保证相邻的 pixel A 和 pixel B 能够产生同样的结果,A可能采样到了光,而B则没有,因此你会在屏幕上看到很多的噪点。
如上图所示,最左上角的图中是每次只发射一根射线,可以看到会有很明显的 Noise 。之后它每一张图从左到右从上到下采样数量都是翻倍的。
因此右下角图中的射线数量是2的16次方64,000 条射线。可以看到最后渲染的结果已经非常 Smooth 了。如果我们只需要一次Bounce 的结果 ,如果硬件够狠,几分钟你也许能渲染出来,但是如果是 MultiBounce 的话,依旧需要非常非常久的时间才能得到结果。
综上所述,你会发现基于Monte Carlo的方法,你的 Sampling数量越多效果越好,但是也会越慢。如何 Sampling是所有基于蒙特卡罗的GI 方法的核心。
Sampling最简单的方法就是均匀采样 (Uniform Sampling)。但是绝大部分情况下信号的分布是不均匀的。使用Uniform Sampling除非密度很高,否则的话得出的结果其实并不尽如人意。如同上面我们的例子中,在一个房间内只有一个窗子是亮的,渲染的时候你在半球内实施Uniform Sampling,只有很少的射线能够采样到窗户。这就是Uniform Sampling 的一个问题。
我们要引入一个很有意思的概念叫概率密度函数(probability distribution function )PDF。我们按照PDF的概率去选取我们的采样点,而不是按区间A到B均匀分布。我们只要在积分中除以选取采样点的概率,最后算出的积分是等价的。
如上图所示,蓝色函数是我们要求解的函数,我们选取与被积函数 f(x) 相似的概率密度函数 p(x) 。根据 p(x) 在f(x) 越大的地方,采样点选取的概率越大,也就是信号强烈的地方,我们的采样点会比其他地方更多,在f(x)值较小的地方,采样点的选取的概率越小。在数学上可以证明,这样的采样方法,可以用尽可能少的采样来获取更为逼近的真实积分值的结果。
这个概念其实也很简单,我们尽可能朝着光比较亮的地方,或者尽可能沿着我的法线正对的地方去多投射一些采样的射线,那这样的话我就可以用更少量的射线来获得我想要的结果。
我们在做 Rendering 的时候,如果假设大部分反射是 diffuse的 ,对光的这个敏感度就是一个Cosine Lobe,靠近法线(Normal)的方向会敏感,然后对其他的角度衰减扩散。就算你的光线很强,但你是从很侧面射来,我对这个光的感应度其实也并不高。
如上图,左边是Uniform Sampling,而右边是Cosine Lobe,也就是采样点的分布会稍微密集的靠近天顶的地方,你可以发现同样是256 spp( Sampling per pixel),使用Uniform Sampling会有很多噪点,而Cosine Lobe噪点会明显下降 。
如上图所示,GGX材质高频足够高低频足够宽。这种情况下,我们使用GGX 的PDF会比使用Cosine Lobe更为Sharp,更能表达Glossy的效果。
总而言之,GI 核心是你需要去找到一个好的 Sampling 方法,用尽可能少的射线去获取一个来自于四面八方的光场提供给你的信息。
三.Reflective Shadow Map(RSM )
前面我讲了Monte Carlo Ray Tracing的GI 算法,但它是一种离线方案,通常是在电影行业中使用,因为这种方案对于实时计算的游戏来说太过昂贵。这一章节们来讲 Reflective Shadow map(RSM ),它核心解决的问题是我怎么把光注入到场景中去。
在图形学里面,我们大致有两类的GI方法,一类就是之前最经典的Monte Carlo Ray Tracing。另一个著名的方法叫做光子映射(Photon Mapping)。
Photon Mapping 的思想非常有意思,和我们之前的Ray tracing从相机为原点发出射线向整个世界不同,Photon Mapping 是从光源出发。Photon Mapping的思想是:我们之所以能看到世界中的物体,原因是因为从光源射出来的光子打到了物体表面,进而不停地反弹,人眼收集到了这些光子,进而你看到了整个世界。因此Photon Mapping使用光学中最基础的概念,从光的角度去看待整个问题。
关于Photon Mapping具体的实现细节,我们今天就不展开了。简单的讲Photon Mapping就是说我发射出无数的光子,然后这些光子在物体的表面来回 Bounce 。当到达一定条件后,那些光子就会停留在物体表面。我们在做 Shading 时,收集这些Photon然后进行插值,最后得出Shading的结果。 这就是Photon Mapping一个核心思想,大家如果有兴趣的话可以查阅相关的资料。
接着我们来讲 RSM ,它的想法基于一个直观的观察。我们在渲染阴影的时候,需要渲染一帧 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上每个点都去采样,而是在世界空间中去不同方向发射Cone Tracing ,并且各个方向的方向采样密度不相同。如果某个方向上采样点距离比较远,我们采样的密度低一些,如果比较近,我们采样的密度会高一些。
RSM只需要 400 次采样就能得到 1 bounce 的 GI 的效果,这也说明间接光照其实是非常低频的数据,它并不像直接光照那么高频。根据这个思想,我们可以用更低分辨率来计算间接光照,也就是每隔两个或者每隔四个pixel来做一次间接光计算,旁边像素可以跟共用周围像素计算的结果。
当然这也会产生一些问题,如果采样点和我当前渲染像素空间位置相差很大,或者法线朝向不共面,会产生很多Artifact 。当然这些渲染错误的 pixel 不会很多,对于整个屏幕来讲可能不到 1% 甚至千分之一。因此对于这种情况,我们就认为这是个无效的差值,重新对它进行一次完整的采样。
很早以前的游戏中其实已经实现了RMS,所以你可以在游戏中看到手电筒照照亮周围物体的效果。
我们非常感谢RSM,首先想到了用把光子注入到世界当中这种方法。并且给我们提供了可以在更低分辨率的屏幕空间里面去采集这些间接光照的思路,还给我们提供了出现了 Error时,处理这些错误像素的方法。Lumen中很多地方都使用了这种思路。
RSM毕竟是个早期算法,有很多的局限性。首先是只能解决 SingleBounce ,而且它也不检测 Visibility ,并且没有考虑间接光照的遮挡。但是毫无疑问RMS 是一个非常具有启发性的一个工作。
四.Light Propagation Volumes(LPV)
既然光子已经注进去了,那我们就要让光在这个世界里面流动起来。这里就要介绍一个也非常具有启发性的算法 Light Propagation Volumes(LPV)。
LPV最早是 Cry Engine 3 提出的一种实时、无需任何预计算的全局光照技术。
在RSM中,我们找到并定义了一系列的虚拟点光源,LPV的第一步仍是如此。它在找到虚拟点光源后,把整个场景分成的一个个小格子叫做Voxel(体素)。
我们需要把在RSM得到的虚拟点光源“注入”到它对应的Voxel中。
由于在Voxel里可能有多个虚拟光源存在,因此存储时可以使用多个Cubemap来暴力存储,但是这会造成巨大的存储开销,通常来讲我们可以用二阶的球谐函数(Spherical Harmonics)SH来拟合这里的光照结果,因为球谐函数SH能够将光照是加权累积在一起。最后我就得到了在小格子里面Radiance 在空间上的分布场。
对于任何一个物体,在Shading 的时候,就可以拿到世界空间中每个Voxel内部光照的 SH 进行光照计算。
当我们有了Voxel 之后,还需要对场景进行扩散。对于一个Voxel,在传播时可以传播上下左右前后共六个格子。它从每个Voxel发出对应方向的射线,例如穿过了Voxel的右面就传播到右边的相邻格子。
这种扩散叫Propagation,LPV的主要问题是当Radiance 扩散出去之后,你这个Voxel内部还有没有 Radiance ,如果内部还有Radiance ,那么能量是不守恒的;如果 Radiance 不在,那个地方就是黑的,那么周围为什么还能被照亮呢?
我们会发现这个光的扩散的速度扩散的范围跟你做了多少次 Iteration 有关,这种感觉就像是说光是有限速度在各个这个 Radiance 里面去传递的。因此LPV 的方法不太符合物理学原理。
为什么要提起这个算法呢,我觉得最核心的就是他是第一个把空间进行 Voxel 的划分,在每个Voxel 里面去保存Radiance 的分布情况,并且更有意义的是他提出了把Radiance 的分布用SH存储的方案。
五. Sparse Voxel Octree for Real-time Global Illumination (SVOGI)
接下来我们要介绍 SVOGI ,SVOGI是对之前LPV的空间的升级,之前的LPV虽然对空间进行Voxel 划分,但每个Voxel如果你分得太粗,就不能够准确地表达这个世界里面的光的分布,如果分的太细,那我就将拥有极其多的Voxel。并且物体里面的空间是是中空的,没有Voxel存在的必要,因为Voxel只需要存物体里面的光照信息。
因此SVOGI提出一个叫做保守光栅化的方法。一般来讲,决定一个三角形是否占有一个像素的标准是三角形是否覆盖了像素的中心,保守光栅化则是只要覆盖到像素的任何区域我都会进行Voxel。比如很小很薄的三角形只占有像素的很小部分,依旧会进行Voxel。
如上图所示,我们把表面的 Voxel 全部收集起来。
如果我们要表达的空间非常大,自然而然我们可以使用树来存储。大家知道空间上我的每一个维度上都进行二分,三维空间上八次分,因此我们使用八叉树。如果这个区间里没有对应物体,这颗树就会分得特别粗,如果有物体则会继续细分。
实际原文的实现非常复杂的。对于每一个节点,它不仅存自己的数据,还要存了周围的数据。因为它要做 Filtering 的时候需要这些数据来做差值,因此所需要的数据结构惊人的复杂。
我们很努力的想找到它的源代码,但很遗憾由于我们的信息渠道不够充沛,并没有找到源代码。所以有一些他们没有讲到但我们很关注的一些细节无法得知。
但他首先意识到Voxel的空间分布是不均匀的,基本上只要用到空间结构表达,使用这种Octree Tree空间划分的思想就是一个很自然而然的想法。
SVOGI 另一个很启发性的思路是,通常采样时你需要采样几百根Ray才能达到你想要的效果。他的想法是我想要重点采样的地方,并不是使用Ray而是Cone,Cone的英文是圆锥,当圆锥体展开的时候,对应的面积就会越来越大,那它在远处就会扫到更大颗粒的Voxel。由于Voxel 对空间的表达是一个树状结构,越靠近树的根部,它Voxel表达的空间区域就越大。所以你远处的颗粒更大的Voxel存储的值,实际上是一个很大面积的Radiance 的集合。
SVOGI提出的这个思想非常的好,因为你只要对空间进行结构性的Voxel 表达,你就会有这样的优势。
六.Voxelization Based Global Illumination (VXGI)
SVOGI的很多细节我们查不到,但是这并不重要,因为实际上没有人再用这个算法。原因很简单,八叉树的数据结构在 GPU 上表达非常的复杂。这在VXGI发明后,一切都被变成了浮云。
VXGI的思路是,我们其实并不需要整个场景的表达,对于 GI 来讲,我们最重要的工作是把我眼睛看到的地方的GI做好,并且离我比较近的区域的GI更为重要,远处区域的 GI虽然也重要,但是我不需要对它进行很高精度的采样。
这个时候我们使用传统的Clip Map 的思想,离我相机近的地方,我用更密的 Voxel 去表达;离我远一点精度就下降一倍,以此类推。
这样就不用极其费劲的去构建一个稀疏的八叉树结构,我依旧还是构建了一个树状结构,并且这个树状结构基于View的密度分布,对 GPU 更加友好,并且实现起来更加的清晰明确。
由于场景中你的相机总是在动,那么对应的ClipMap 也要跟随着进行更新。VXGI提出一种可循环的寻址系统。当你的相机移动变化时,我们只需要进行增量更新,也就是在 GPU 中,你每次只需要更新它边上那圈数据,而不需要把中间那些没有必要更新的数据重新生成和赋值。
这样我们对整个空间就有一个近处密、远处稀疏的Voxel 表达。如上图所示,远处的 Voxel 也没有显得特别稀疏,这是因为随让远处的Voxel很大,但是因为透视投影的原因,在屏幕空间中感觉并没有没有特别明显,但实际上它已经离你很远了。
VXGI 还需要考虑透明度的问题。每个Voxel 并不是只有透光和不透光两个状态。实际每个 Voxel 有一个 Opacity 。假设Voxel里面有一个Mesh,这个Mesh把55%的光挡住,而45%的光透过,并且各个方向的透光率不一样,因此我们要沿着它各个方向算出它的不透明度。
我们在采样时,不像大家想的那样 hit 到了一个 Voxel 我就停在那里,而是有一个有半透明效果。
如上图所示,对于一个场景而言里面的Voxel 会有很多半透(灰色)的区域。
对于VXGI,我们同样使用RSM,注入到我们每一个表面的Voxel 上去。
大家可以看到只有一些 Voxel被照亮 ,因为只有这些Voxel 接受到了直接光照。
接下来,我们对于每个屏幕上的像素,进行Cone Tracing 。对于非常粗糙的表面,我就以Cosine Lobe四面八方的采样;如果表面是比较光滑我就沿着大致的反射方向采样;如果非常光滑的表面,我不仅只沿着反射方向去取那个间接光照,Cone还会变得非常的细。
根据之前所讲,如果沿着这个方向的 Opacity不是完全遮蔽,那么光能透露过来。这时候我再继续往后走,越往上走,我的采样面积就越大。我就取 Clip map 里面更高的 Mip 的那个Voxel,这样我一次性就能覆盖很大的区域。
我光线的透过的Alpha会随着我不断的透射而变低,当Alpha小于某个阈值,很接近于零的时候,我们就说,就当做不透明算了,我们就不往后再走了,这样能让我们的算法更快一点。
VXGI 实际上是我个人认为非常好的一个算法。虚幻引擎中有一代里面有一个插件就可以实现 VXGI。我对所有能够实装的算法都是比较敬畏的。
因为你真的敢把一个算法放出来,让大家在各种场景里测试的话,说明它本身的效率性能和它的鲁棒性已经达到了一定的程度。
VXGI 其实也有很多的问题了,比如实际的Cone Tracing中opacity 累积是一个估计值。
如上图中右上所示,绿色三角形在Voxel 里面阻挡了一部分光照,黄色的方块也阻挡了部分光。它两个Alpha如果按照乘法累积肯定不为零。但是在实际上光路上其实已经完全阻挡了光照。
实际的渲染中的表现就是 Light Leaking漏光,特别是对于那种一些很薄的物体,或者本身不是很薄但是距离比较远的物体尤其严重。
七.Screen Space Global Illumination (SSGI)
SSGI的工作最早是2015年寒霜在SIGGRAPH 上发表的工作。
SSGI的原始思想很简单,如上图所示,红色框的是我直接渲染出来的东西。下面白色的部分,如果我要我去渲染它的间接光,因为他的表面非常的 Mirror ,所以我只要在屏幕空间里面把红色部分反一下,这些数据我就可以用了。因此SSGI的思路是,我在屏幕空间中把这些渲染好的像素点作为我全局光照的小光源。
如上图,假设整个屏幕中的像素已经渲染好了,当我要Shading图中黄色的点时,我已经知道了Normal和相机方向,我就可以沿着反射方向射一些Ray。这些Ray就在我的屏幕空间里面去找空间上位置正确的点,如果这个点是红色我就获得了一些红色的Radiance ,如果这个点是绿色我就收到了一些绿色的 Radiance ,这样的话我就可以直接用屏幕空间的数据进行 Indirect Lighting 。
我们在屏幕空间中找到对应采样点的方法叫做Raymarching。假设我们从一个点出发射出一根Ray,这根Ray使用均匀间隔一直往前走,如果它当前位置的深度值比我这跟Ray的深度更靠前,就说明这跟Ray被挡住了,这个时候我就认为找到了一个交点。这就是均匀的 Remarching ,它要求步进间距非常的密,速度也就会很慢。
当然我们需要另一种方法加速Remarching 。GPU 硬件提供了生成一个特殊Buffer的方法,当然我们也可以自己实现,叫做HZB,也叫做Hi-Z。它可以把 Z buffer 做成一层层的 Mip,每个上层 Mip里面的一个像素,都对应它下层Mip里面四个点的像素。上层 Mip像素点的值,是下层Mip四个像素点中深度的最小值,也就是离我最近的那个值。
如果我们拥有了Hi-Z,一根Ray如果跟 Hi-Z的某个Mip不相交,那我跟你下层的Mip一定不相交。如果我跟你某个Mip相交,那我一定跟你下层某个Mip的像素相交。所以它实际上把这个Depth Buffer 做成了一个 Hierarchy 的结构。
当我有了Hi-Z,想做Raymarching的时候 就比较简单了。比如初始状态下,我在Mip0层,我先在Mip0层往外走一格。
如果发现在Mip0层没有交点,我需要接着走,不过这时我往上跑一层跑到Mip 1走一格,这就相当于走了两格。
这个时候如果我发现还没有交点,我就开始胆子更大了,我在再往上走一层在Mip2走一格,我再做一次深度测试的话,这一次我相当于做到了四格。
这个时候假设我不小心发现我检测到了物体。如今我是在 Mip2级别,我就知道他可能交到了Mip1 或者Mip0 的某个点,但是具体是哪个点我不知道。这时候,我回退到 Mip 1往前走一格。
我发现好像跟 Mip 1也有个交点。那我再回退到 Mip 0 继续走。
这时我们就能找到交点。虽然听上去Hi-Z比均匀采样更为复杂,但是它的算法复杂度是 log 2 的,也就是说你就算是 1024 或者更高的分辨率,我也最多只需要十几步就走到了。如果你用 Uniform 的 Raymarching 可能要走五六十步才能走到。
SSGI 还有一个很有意思的思想就是复用采样。当我对一个像素求出了采样点,我周围的点在也要进行球面采样的时候,如果不考虑 visibility ,他采样到的那个小灯泡实际上也是你的小灯泡,相当于帮你也做了一次采样。这个思想其实也是非常重要的。因为在我们真正做 GI 的时候,你不可能对每一个采样点的位置射那么多的Ray,不然整个计算就会爆炸。
和之前一样,我们提供采样的Scene Color 也会做Mipmap,对于远处的Cone Tracing 会采样到分辨率更低的贴图,这相当于对整个光照进行了一次 Filtering。
当然SSGI也会有一些问题,如果在屏幕空间没有的东西,我就看不到。比如像上面例子中框出的部分因为反射的数据拿不到,所以下面的整个都变白了。但是这不影响SSGI作为一个非常有用的算法。
SSGI 拥有很多优点:
- 因为Hi-Z的精度非常高,所以两个物体的交接面,非常细腻的几个 pixel 的内容误差,它都能把你算出来,因此SSGI能够处理非常近的Contact Shadow。而以前 Voxel的方法对它的处理很糟糕。
- 对于Hit的计算,因为用了Hi-Z 的方法非常准确,这比Voxel去估计要准得多。
- 无论场景有多复杂,SSGI对场景的 complexity 是无感的
- SSGI能够处理动态的物体。
这几个优势非常重要,这就是为什么在这么复杂的 Lumen 架构里面,还使用到了SSGI 。
Lumen
首先我们需要知道Lumen需要解决哪些问题。
很多人都会问,既然已经有了硬件的Raytracing ,我们为什么还要Lumen呢。这是由于很多硬件并不支持 Realtime Raytracing,对于支持的那些硬件, N 卡还算是勉强可以,而 A 卡支持的比较糟糕。并且实际测试中,3080大概 1 秒钟能做10 个billion 的计算,这个实际的计算量用来做GI 的话,实际还是比较费力的。
比如游戏里最常见的Indoor场景,你需要每像素500 根Ray才能得到想要的效果,但是我们只能付得起每像素二分之一个Ray。因此我们需要解决如何在软件层面解决掉快速Ray Tracing 的问题。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-9ba3cfe8699e14830de482f7aca5960e_1440w.jpg)
另一个大的问题就是Sampling。之前的章节我们也讲到了Sampling,过去几十年时间离线GI都在和 Important Sampling 做殊死搏斗,直到现在也没有让我们特别满意。而我们的Realtime 的 Sampling 数量又被卡的很死,这进一步增加了我们处理问题的难度。
如上图所示,靠近窗口的Sampling 结果基本是可以接受的,虽然它有些Noisy,但是你可以通过 Filtering 解决这个问题。但是如果离窗子稍微远一点,那 Filtering 也救不了你,最终你看到的就是一个一个的大色斑。因此Lumen也需要解决如何Sampling的问题。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-edfe9c7f91b3be0a3ee3af52e15fc905_1440w.jpg)
我们知道Indirect Lighting 可以在 Low Resolution 上面采样得出,Lumen的想法是在屏幕空间放一大堆的 Probes,屏幕空间 的特点是紧贴那些要被渲染的物体表面,通过 Probe获取光照。比如每16 个 pixel 我放置一个Probe,每个像素去 Shading 的时候,它的高频信息可以通过表面法线产生,最后得到右边非常逼真的效果。
因此Lumen最核心的思想是三点:
- 在不用硬件Ray Trace的前提下,我如何进行快速的Ray Trace。
- 尽可能的在 sample 里做的优化。
- 放置的Probe 尽可能贴着真实物体表面,使它的精度足够的高。
Phase 1 : Fast Ray Trace in Any Hardware
Lumen最核心的点就是要解决我怎么样的在任意硬件上能进行非常快速的 Raytracing ,它要解决如何我在射出一根Ray时,快速得到这个Ray到底能不能交到一个物体,并且知道和它相交的物体是谁。
这个 Raytracing 当然是可以用硬件Raytracing 去做,Lumen也提供能够开启硬件Raytracing 的功能来做更高精度Raytracing的表现,不过为了兼顾各种硬件,Lumen 最主要的是提供了基于软件的SDF(Signed Distance Field )的Tracing算法。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-755c260433e3262ac2fa15d670f136aa_1440w.jpg)
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非常大的好处。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-8752ffc5f0ff185072cb9dea16ad1835_1440w.jpg)
SDF第二个好处是做Cone Tracing 。比如说我们要做 Soft shadow ,从需要计算阴影强度的着色点o出发,向光源发射一条Shadow Ray,沿着该方向进行marching ,每marching 到一个点,我们可以得到一个圆心为当前点,半径为当前点的SDF值的球,从出发点向该圆作一条切线,可以得到一个夹角,取所有这些夹角中的最小值,根据这个值来确定半影大小。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-aa50c663a76444688376d0ec610eaf15_1440w.jpg)
由于SDF的显存占用过高,我们其实可以对SDF 进行稀疏化处理,使用间接存储的结构。如上图所示,我们可以把那些 Voxel的 distance 大于某个阈值(体外)和小于某个值(体内)的Voxel全部干掉,用一个简单的 index 就可以把SDF的存储减少很多。
但是我个人一直在怀疑一件事情,确实只要大于一个阈值,你把那些 Voxel 全部标为空,你这样只要存那些有用的区域,但它会导致你marching 迭代的步长变长。因为以前一次性可以跳一大步,但现在如果是空的话,我只能一步一步的试,但是这个里面工程上肯定有办法可以解决,我们就不展开了。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-b3d20440e9d13d4469472a997a768858_1440w.jpg)
SDF 还可以做LOD,而LOD 还有一个很有意思的属性,因为它是空间上连续的可导的,你可以用它反向求梯度,实际就是它的法线。也就是说我们用了一个 Uniform 的表达,可以表达出一个无限精度的一个 Mesh ,我既能得到它的面积,又能对它进行快速的求交运算,还能够迅速的求出它连续的这个法线方向。
如果你使用 LOD 和Sparse Mesh,可以节省40%到 60% 的空间,这在硬件上还是蛮可观的。对于远处的物体,可以先用这个 Low level 的SDF,当你切换到近处的时候再去使用密度高的SDF,这样也可以很好的控制内存消耗。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-68a05ce5075af56aff9d048e28a03b7a_1440w.jpg)
如果你用Per-Mesh SDF去做Raytracing,对于单个 mesh 来讲,你做 Raytracing速度会很快。但是你架不住场景的物体数量特别的多,比如说我一根Ray打过去的话,沿途所有的 Object 我都得问一遍。如果场景全是物体,那你的计算复杂度就会越来越高。如上图所示,越亮代表Step的次数越多,会发现越靠近 Mesh 的边界上的像素,它需要的Step 就越多。
一个非常简单的想法就是,既然你们都是分散的,那我把你这些SDF 合成一个大的低精度的 Global SDF ,这是对整个场景的表达。当然这里面的细节比较复杂,物体的移动,消失,增加,都需要提供一套 Update 的算法。今天就不展开了,如果有兴趣的同学可以自己研究。
不管怎么说,我们能形成一整个场景的 SDF 。如果有了整个场景的SDF,Raytracing 的速度就会非常快,因为它不再依赖于一个一个的物体。当然它的缺点是受制于存储空间,不能像 Per-Mesh SDF 那么精细,但它是一个非常好的加速的方法。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-fb2c39c9744d4efc6224a8205e9c5842_1440w.jpg)
在 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
那接下来我们要解决如何把光子注入到世界。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-cb0a15438332556eca003fa2f2118aa2_1440w.jpg)
在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。当然这里面会有两个重要的问题。
- 第一个就是 Surface Cache的某个点,这个光的照耀下它到底有多亮,它有可能会被 Shadow 挡住,那么我们怎么去知道这件事情呢。
- 第二件事情是,我既然知道你的GBuffer信息,直接光照我可以得到,但如果你是 Multi Bouncing 我怎么办?
如上图,最核心是要生成Surface Cache Final Lighting。
- 第一步,对于 Surface Cache 上的每个像素,我们可以很简单的计算 Direct Lighting 。对于刚才第一个问题,如果在Shadow里面,我们可以通过Shadowmap来做。
- 第二步,其实比较复杂,为了能够计算Indirect Lighting,我需要在 WorldSpace里面其实建了一批对光的 VoxelLighting的表达,这一步的 VoxelLighting并不是给当前帧用的,而是给下一帧使用。为什么要做这一步,并且VoxelLighting的具体细节将会在接下来的章节中讲述。
- 第三步,我们使用上一帧的第二步建立的Voxel Lighting,采样出对应的 Indirect Lighting ,把它和Direct Lighting 这两个合到一起,就变成了我的这一帧的这个 Final Lighting。
随着时间的积累,第一帧F0的时候只有一次 Bouncing , 第二帧F1 的时候,其实我就有这个两次 Bouncing 的值了。在F2时就具有三次Bouncing 的值。
Surface Cache Direct Lighting
第一步是计算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 里面去。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-3d356f86d22e785240b2d8fef7855a4f_1440w.jpg)
对于整个 Surface Cache的更新还是非常耗时的。因此Lumen里规定,每帧最多不超过 1024 x 1024 个texels更新,对于间接光照因为它要在很多Voxels 上采样,因此最多更新512 x512 个texels。对于每一个 Mesh Card来讲,他都要排队请求自身更新。这里面其实有一整套的比较复杂的排队算法。
Phase 3 : Build a lot of Probes with Different Kinds
前面讲的内容依旧无法直接给我们渲染提供直接帮助,这一节我们将来看如何能够拿到对应像素点的间接光数据。
Screen Space Probe
对于最终的渲染来讲,我们需要拿到当前像素点法线半球面的所有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,也就是说你需要环境感知。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-68b0dd9091d136593cfa200211d32f92_1440w.jpg)
如之前的章节所说,我要尽可能让我的这个概率函数P符合上面函数的分布。上面这个函数是两个函数的积,其中一个是光,另一个是我表面的BRDF。所以首先你需要知道光在哪儿,第二个你需要知道法线在哪,因为背面的光对我来说毫无意义。
如何得到光的方向呢,虽然并不知道这一帧的光在哪,但是我做一个假设,就是光的变化没有那么快。因此我们可以把上一帧的 Probe 采一遍,重投影上一帧周围四个的Probe的值,把对应SH的信息画在一个 8x8 的图里,这个图中亮的地方就是光比较亮的地方。这样我就大概知道哪个地方亮,哪个地方暗。当然如果上一帧这里不可见的时候,会出现有失败的情况。我们会Fallback到世界空间的Radiance Cache里面
第二点对于BRDF的部分,大家天然的能够想到,沿着 Normal 做一个 Cosine Lobe 。这听上去非常合理。但是这里面有一个很大的错误,那就是像素的Normal 非常非常高频。比如一个小区域中可能有一千多个pixel ,那一千多个 pixel 加权的法线朝向并不能由我这单独采样点的法线所代表。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-69b87eb735d577b0131b1fc86969b1ff_1440w.jpg)
如果真的把 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 的结果就会好很多。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-ba542dda8526b89c693052dfb303ee04_1440w.jpg)
如图所示,左边是没有做important Sampling,右面是做了这个important Sampling。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-6aca35a7ad7b06a1a2fd25c7c1dd8153_1440w.jpg)
这张图同样如此。
Denoising and Spatial Probe Filtering
接下来我们需要进行降噪,做 GI 的话Filtering 就是你逃不掉的东西。
按照16x16 Pixel的 Probe 得到的信息是非常不稳定的。因此我们使用周围3x3的 Probe 来做Filtering。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-1b30146bb1a266122a19c0905407867b_1440w.jpg)
每一个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射到的距离很近的,虽然角度是对的,但不好意思,我认为这种情况也是无效的,我还是只用我自己的数据。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-d64ff273679d46eef4e20889c8ffdde2_1440w.jpg)
这个问题不解决的话,它就会出现这种漏光的问题。如上图左边,你发现那个毛巾的这个内侧面,和它靠近墙的地方。如果你不考虑这个差值,很多光就会漏进来。
World Space Probes and Ray Connecting
Screen Space Probe如果它跑的太远,效率会很低。因此Lumen希望Screen Space Probe 你只处理周边的东西。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-64181fe31d4524e91b8183f74af4cfb4_1440w.jpg)
对于远处的物体,我们会在 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取就可以了。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-68783c6392730bd00b6e1d27a6401a1f_1440w.jpg)
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 才有必要进行采样。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-0b7c9f08358d5894119bdc4162a1d749_1440w.jpg)
如上图,如果你只是用Screen Space Probe 的话,它如果只有两米,你看到的结果大概是这个样子的。
但是如果你有World Space Probe,你可以看到这个光看上去就准确的多了。
Phase 4 : Shading Full Pixels with Screen Space Probes
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-c894f1dd1f27de9566363098e83e1a06_1440w.jpg)
虽然我们做了 Important Sampling ,但实际上 Indirect Lighting还是很不稳定,因此我们把这些光全部投影到 SH 上面去。 SH 本质上相当于对我们的整个 Indirect Lighting进行了一个低通滤波,把它变成了一个低频信号,用它来做 Shading 的时候看上去就柔和了非常多。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-f2792cd2c146396aaf76db3ba00a527c_1440w.jpg)
这就是我们最终能够得到的结果。
Overall, Performance and Result
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-ec5e2d55b37def8e89e26b4596ad85a9_1440w.jpg)
对于不同的 Raytracing方案,硬件上的成本是不一样的。最快的tracing就是基于 Global SDF 。其次就是在屏幕空间我进行 linear Screen 去插值。那么比它稍微慢一点的就是 Per-Mesh SDF 。 HZB 它稍微比 linear Screen 要慢,但是它的准确度其实会更高。硬件Raytracing的准确度肯定是最高的,但是它开销会更高。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-f16f79447188bb429d19df1162618189_1440w.jpg)
我们通过这张图可以看到Lumen使用了混合的Tracing方法。红色区我们使用 Screen Space 的Tracing;绿色的区域用 Per-Mesh SDF ;蓝图的部分也就是说更远的地方,我只能用 Global SDF 。
如果每一个 Pixel Tracing使用的方法是单一的,我们应该看到的是纯色图,但现在我们看到的图是渐变图。这其实也说明我们每个Probe采样中,混合使用了各种方法 。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-72df6b80403a22e64ec92d6067871a4f_1440w.jpg)
我们希望越靠前的方法,准确性越高。
- 我们首先是使用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上去。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-29adfe3318946fbac68c9d5dcd0d75c8_1440w.jpg)
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-e86c28900e9fb40e9c77d4e2ffabedf7_1440w.jpg)
这里我们需要重点提一下SSGI,如果没有SSGI只有Lumen的话,可以看到下面那个倒影其实很粗糙。因此对于近处高频的物体,SSGI 还是蛮重要的。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-d1bbd317cbd2c68b0355995006965c8d_1440w.jpg)
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-e9b5f73ea4e4b053185bd8df948e9d23_1440w.jpg)
Lumen最了不起的地方,还是在工程上真的完成了交付。在 PS5 的这个硬件平台上,能做到 3.74 毫秒。如果你愿意降低采样分辨率,你的效率可以更高,可以从将近3.74 毫秒下降到 2.15 毫秒。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-a23badece2d64d6311f5656c5d1b72e9_1440w.jpg)
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-b3aecd74b0d6c460aed9e3cee70fddca_1440w.jpg)
所以16x16 的 pixel 的选择,我相信作者自己肯定也做了大量的尝试,得出的一个兼顾性能和效果的参数。
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-ceda26cfc2cbaf20b129412fcff41242_1440w.jpg)
%E6%9C%80%E5%BC%BA%E5%88%86%E6%9E%90_%E4%B8%80%E6%96%87%E7%90%86%E8%A7%A3Lumen%E5%8F%8A%E5%85%A8%E5%B1%80%E5%85%89%E7%85%A7%E7%9A%84%E5%AE%9E%E7%8E%B0%E6%9C%BA%E5%88%B6_GAMES104%E5%AE%98%E6%96%B9%E8%B4%A6%E5%8F%B7/v2-eeaecfe71b721a8ebcb820eba32cb326_1440w.jpg)
Lumen对整个动画电影行业,都有着非常巨大的影响,将室内设计师,效果图公司所梦寐以求的离线渲染的效果,做到了可以实时产生,非常的了不起。。
Lumen也奠定了未来 10 年下一代的游戏引擎的渲染标杆,我们认为 GI是下一代顶级游戏引擎的标配,你只要做下一代游戏引擎,你的 GI 必须是实时的。
Lumen 只是这一系列伟大征程的开始,你可以看到Lumen 基于现有的硬件, 还是做了大量的妥协。未来十年随着硬件的发展, 实时GI 将会变得更加成熟,也会变得更加简洁。而Lumen 是一方向的开山鼻祖,他的创作者必然会载入史册。