Unite 2019 | Unity中的实时光线追踪技术剖析
高性能实时光线追踪通过NVIDIA RTX平台,提供了逼真的图像质量和光照,适用于对视觉保真度要求很高的任务与项目。该技术模拟了光线的物理属性,帮助开发者能够制作并动态调整作品,使实时效果和现实画面的界线变得模糊。
为了展示实时光线追踪所能实现的效果,Unity与NVIDIA和宝马集团进行合作,在现实对比虚拟:Unity实时光线追踪功能演示中展示了2019宝马8系列车型。
在Unite Shanghai 2019大会中,英伟达的技术工程师李元亨将为你剖析《Unity中的实时光线追踪技术》。
演讲内容
演讲内容
大家好,我是李元亨,来自英伟达,今天分享的主题是《Unity中的实时光线追踪技术剖析》。在开始之前先请大家欣赏一段视频。
在视频中,我们可以看到有二组宝马汽车,一组是我们和Unity合作使用实时光线追踪技术渲染出来的,另一组是直接在影棚使用摄象机拍摄的影片。
相信大家看到二者的差别已经很小了,这充分展示了光线追踪技术在渲染照片级真实图像上的强大实力。
我的演讲主要分三个部分:NVIDIA RTX、微软DXR光线追踪渲染管线、以及Unity现已实现的一些光线追踪功能。
NVIDIA RTX
我们在去年正式推出了基于新的Turing架构的RTX系列显卡,除了全新的架构之外,我们还引入了一个全新的硬件单元叫RTCcore,主要是用来加速射线和几何体求交,为实时光线追踪奠定硬件上的基础。除此之外,我们还加入了Tensor Core用来加速AI计算。
今天我们关注的重点是RTCore及光线追踪。
光线追踪需要快速的找到射线和场景中物体的交点,所以快速对场景进行遍历求交就显得尤为重要。
为了加速这个过程,通常我们需要对场景里面的物体建立加速结构,就像普通的剔除一样,对场景中的物体建立八叉树之类的结构进行管理。
这里以BVH也就是层次包围盒子举例,我们来看一看射线和几何体求交的大概过程。
如下图所示,左侧四张图是演示如何找到一条射线和兔子头部的交点。
第一步,先找到包含整个兔子头部的大的包围盒,然后发现这个包围盒的子节点是更小的包围盒。
第二步,看射线与哪个子包围盒相交。如果找到了相交的子包围盒,就重复递归以上过程,直到找到子节点里包含的不再是包围盒而是具体的图元信息,图元信息一般为三角形,我们就开始求交三角形与射线的交点,并返回交点信息。
在Turing之前这些步骤都是在Shader里面模拟的。右侧的图可以看到Shader里面先产生一条射线,然后射线进入到加速结构遍历求交的过程。
首先取出包围盒数据,包围盒解码。然后判断下一级是子包围盒还是三角形,如果是包围盒就重复以上过程,直到找到三角形,然后进行射线三角形求交计算并返回交点。
黄色的部分就是加速结构的遍历求交过程,这一部分通常需要上千条指令,是比较耗时的一个过程。在Turing中就把黄色的这块加速结构遍历求交的计算,放在全新的硬件单元RTCore上去执行,所以速度会提升很多,而Shader还是在SM上执行的。
那么光线追踪的过程就变成了执行在SM单元上的Shader里面产生射线信息,然后将这条射线的求交查询请求发往RTCore,RTCore收到请求后就进行加速结构的遍历及相交测试,如果找到交点就将交点信息返回到SM单元里面的Shader进行着色计算。所以在Turing中RTCore是放在SM旁边的,这就是硬件的架构图。
RTCore极大的提升了射线与物体求交的速度,性能方面我们这边有组数据,上图中,在1080Ti上,我们使用Shader模拟大概支持的射线是每秒1G左右,在2080Ti上有RTCore可以达到每秒10G,所以性能提升了大概10倍。
在光线追踪接口方面,去年微软正式宣布在DirectX中加入光线追踪的接口DirectX Raytracing,简称DXR。在Vulkan下面,我们通过拓展的方式对光线追踪接口进行了支持,所以开发者可以自由选择适合自己的接口来实现光线追踪技术。
在光线追踪中,我们通常会使用一种叫做蒙特卡罗随机采样的方法来求解渲染方程。实时的情况下样本数量可能会不够,所以通常情况下是会有很多噪点的。
针对这个问题我们去年推出了Gameworks Raytracing Denoiser。这个库里面包含了有三种Denoiser,第一种是针对反射,第二种是针对阴影,第三种是针对环境遮挡(AO)。
我们打算把这个库做成独立的SDK叫RTX DENOISER SDK,会制作成C++及HLSL库的形式,并且不依赖于具体的图形API,预计在今年5月发布,以供开发者使用。
上图右侧是反射降噪前后对比。上面这张带噪点的图是1SPP光线追踪生成的,1SPP就是每个像素只使用一条光线,1SPP对于非常光滑的表面还好,因为它的反射lobe范围比较小,反射光线的方向基本固定。
如果对于粗糙的表面1SPP是不太够的,所以会有很多的噪点,这就依赖于降噪器来降噪。下方就是降噪后的效果,效果还是非常不错的。
下图是阴影,上面是1SPP降噪前的结果,下面是降噪后结果。
下图是AO降噪前和降噪后的效果对比,也是1SPP,降噪效果很好并且性能也完全可以满足实时应用的需求。
自RTX技术发布近一年来,根据开发者在实际使用过程中的反馈,我们做了很多优化工作,光在Compiler方面,就有25项以上大改进,同时在内存方面,还有加速结构的异步构建方面也做了很多工作。
下图数据是二个版本的性能对比,我们可以看到性能提升比较大。
为了让广大拥有GTX 10系列显卡的玩家也体验到实时光线追踪特效,4月的时候,我们正式发布了新版本的驱动,可以让GTX1060 6GB以上的显卡支持光线追踪。
我们支持光线追踪的显卡大概分为三类:
GTX系列的显卡。
包含Turing架构但是没有RTCore的1660系列以及VOLTA系列。
最新的RTX系列显卡。
在GTX系列上,因为是软件模拟的,所以只能支持很少量的光线,因此只能跑一些很简单的光线追踪特效。
在RTX显卡中,因为有RTCore的加持,支持的光线数量会多很多,所以可以运行比较复杂的光线追踪特效,而且可以支持同时运行多种特效,例如:同时开启光线追踪的反射和阴影等。
微软DXR光线追踪渲染管线
下面介绍微软的DXR。从上层来看DXR主要分为三部分:光线追踪管线、加速结构、着色器表。
光线追踪的管线:主要包含光线追踪所需要的所有Shader及一些状态配置,它控制着光线追踪的流程。
加速结构:为了快速找到射线与几何体的交点,我们通常需要对场景里的几何体重组划分,建立树状的层次结构。
着色器表:包含了一次光线追踪需要的所有物体引用的资源,可以理解为每个物体的材质。包括使用的Shader的ID及所引用的资源,这些都需要放在着色器表里面。
我们具体看一下光线追踪管线,它主要控制着光线追踪的整个过程,分为以下部分:
发射光线:在DXR种加入了一个新的函数DispatchRays( ),这是API端的入口函数。还加入了新的Shader类型Ray Generation Shader。同时HLSL里加入了新的函数TraceRay( )用于实际追踪一条光线。
场景遍历与求交:这是RTCore的工作范畴,主要的工作是遍历加速结构,并找出射线与几何体的交点。新的类型是Intersection Shader用来计算交点。
交点处理:我们可以判断交点有效性,计算光照,选择是不是继续发射新的光线。这里有二种新的Shader类型:Closest Hit Shader在找到的距离最近的那个交点上触发。Any Hit Shader则是只要找到有效的交点就会触发。
无交点处理:我们可以终止射线的追踪或者选择继续发射新的射线。例如:反射的光线没有找到任何的交点时,通常我们需要采样天光或者环境贴图,这就需要使用到Miss Shader。
下面这张图描绘了光线追踪的大概过程。
首先调用DispatchRays就会触发Ray Generation Shader,这会通过TraceRay( )函数产生一条射线,进入到加速结构遍历求交的过程。
这个过程会调用Intersection Shader来计算交点,如果有交点需要通过ReportHit( )函数来报告一个交点。每条射线会一个有效范围T-Min及T-Max,这类似于相机的近远裁剪面的概念。如果ReportHit报告的交点在有效范围内,Any Hit Shder就会被触发。
在Any Hit Shader中,我们可以选择是否忽略这个交点。如果忽略则会跳过交点的信息,如果不忽略,这个交点就会被提交为目前为止距离最近的交点,也就是更新成为新的T-Max。
在Any Hit Shder中,我们还可以选择要不要终止射线。例如:制作阴影,只需要查询遮挡信息。我们向光源方向发射一条光线,只要被任何物体遮挡,那么当前点就是阴影之下,所以不需要继续,可以终止当前射线。
如果Any Hit Shader里没有终止射线,则遍历求交的过程会重复进行直到遍历结束返回。
如果有交点,那么最后提交的这个点就是距离最近的点,触发Closest Hit Shader。我们一般在Closest Hit Shader中进行着色计算,可以理解为类似于光栅化里Pixel Shader里的所做的工作,当然着色也可以放在Ray Generation Shader里完成。
Miss和Closest Hit Shader可以再次调用TraceRay发射新的射线。例如:制作GI需要光线多次反射。找到当前光线最近的交点,产生一条反射射线,再找到下一个最近的交点然后再产生一条新的反射射线,最终实现GI里的光线多次反射的效果。
反射可以做多次递归,但递归深度是有限制的,目前DXR限制的最大递归深度是32层。但是我们可以在Ray Generation里面实现类似的多次反弹效果,从而避免递归,跳过32次的限制,当然反射的次数越多做性能损耗越大。
现在看一下发射光线。DispatchRays函数可以与普通的Draw,Dispatch函数放在同一个队列中,可以放在Graphics Cotext中,也可以放在Compute Context中。
DispatchRays与普通的Compute Shader的Dispatch函数类似,一般是发起一个2D的阵列,对阵列里的每一个元素调用一次Ray Generation Shader。
这个2D的阵列一般跟一个Viewport相对应。例如:要做反射效果的话,2D阵列的大小一般与GBuffer相对应,阵列里的每一个元素对应GBuffer里的一个像素,所以在Ray Generation Shader中,我们可以根据当前像素的位置和法线信息,计算反射射线的原点和方向,然后通过TraceRay这个函数发起一条射线追踪的请求。
在TraceRay时,一个射线可以携带一个自定义的Payload的结构,在Payload中可以存储任意需要的信息,例如:交点处材质贴图的颜色或者光照的结果等。Payload是与各自的射线绑定的,它的生命周期跟所属的射线一样。
在交点处理方面,有一个叫Hit Group的概念,由Closest Hit,Any Hit,Intersection三种Shader而构成的,当然这三种Shader都不是必需的。
如果不是半透明的物体,不需要Alpha Test/Blend之类的操作,就不需要提供Any Hit Shader。这里有一个优化是如果使用了Opaque标记,就算提供了Any Hit Shader,它也不会被调用。
因为Any Hit Shader是在加速结构遍历过程中调用的,它会打断RT Core的当前遍历进程,所以使用它的代价还是比较大的。而且只要有效的交点找到了,Any Hit Shader就会被触发。如果不做Shading,例如:当前是Shadow Ray,那么也可以不提供Closest Hit Shader。
Intersection Shade用来处理射线与自定义几何体求交,一般使用较少,三角形和射线的求交是RTCore里内置的而且速度非常快,所以我们一般推荐使用三角形。
Intersection Shader通过计算,如果发现射线与几何体有交点,则需要通过调用Report Hit报告交点,同时传递交点的属性Attributes给Hit Shaders。Any Hit Shader是射线有任一交点时被触发,但是它不保证触发顺序,也不保证在每个可能的交点上都会被触发。
如上图所示,有三个交点。如果首先就找到这个最近的交点,那么该交点就会被更新为T-Max,那么后面的交点就不在射线范围内,从而会被丢弃。
同时也不能保证每次Trace时,这三个交点一定是先找到最近点还是先找到最远点,没有顺序,不能对交点触发的顺序有任何假设。
Closest Hit Shader是针对最近的有效交点触发,并且每条射线最多触发一次。
下面我们看一下加速结构。DXR分二层加速结构,底层的加速结构包含了基本的几何体信息,包括三角形或者是包围盒数据。
顶层加速结构里包含了指向底层几何体的实例,包含了相应的资源索引和坐标变换数据。每个顶层加速结构的叶子节点中,都有一个字段用来指向它在着色器表里的位置,即该Instance用到那些Shader的ID及资源的索引。
我们可以简单理解为这样就把几何体和它所使用到材质进行了绑定,否则遍历时找到了相交的几何体,我们怎么知道它使用的是什么材质呢?
顶层加速结构这样设计比较类似于光栅化下面的Instance Draw。因为这样的设计可以让顶层加速结构共享底层加速结构。
如上图所示,有三个底层加速结构,顶层加速结构这里有四个叶子节点,其中第一个和第二个节点共享了同一个底层加速结构。虽然它们指向同样的几何体,但是它们指向了不同的资源。所以这二个节点分别是二个Instance,它们用了相同的Mesh,但用了不同的材质。
进行遍历求交的时候,首先通过顶层加速结构找到相交的是哪一组几何体,然后进入对应的底层加速结构,找到相交的是哪个几何体的哪个三角形。
加速结构的格式是应用程序不可见的,我们也不能假设现在底层用的是什么格式的加速结构,也不能依赖于具体的加速结构格式,它对上层应用是透明的。我们只能通过调用接口(BuildRaytracingAccelerationStructure),更新加速结构或者重新构建一个加速结构。
第三部分是着色器表,包含了一次光线追踪所用到的所有资源。在传统的光栅化方式下,我们甚至可以在Draw这个几何体的时候,才把它用到的资源加载进来,但是光线追踪必须要在Trace之前,把所有可能用到几何体的信息全部加载进来。
因为一条射线发射出去,你无法预测它到底会与哪些几何体相交。着色器表是由多条等长的记录构成,每条记录包含了使用的Shader的ID及引用的资源,例如CBV、UAV、SRV等。
顶层加速结中的节点指向着色器表中对应的纪录,这个位置就是这个Instance里引用资源的记录的起始地址。
如上图所示,这个Instance A里有一个几何体,因为有二种射线类型,所以对应二条记录。这边的Instnce B里有二个几何体,所以有四条记录。这就是为了绑定Instance中的各个几何体和它们使用的资源或者说是材质。
最后我们来整体看一下光线追踪的过程,当我们调了DispatchRays时,它会去着色器表里找到Ray Generation Shader并调用,Ray Generation Shader中通过TraceRay函数开始追踪一条射线,进入到加速结构的遍历求交阶段:依次是顶层加速结构,底层加速结构。
如果找到了交点,则从顶层加速结构里找到该Instance在着色器表里的起始地址,然后找到对应的材质资源,然后调用Hit Group进行相应的处理。Hit Group里的Closeset Hit Shader中可以再次产生新的射线来重复这个过程。
这就是如何发起一条射线,找到交点,再找到几何体对应的材质,整个过程就是这样的。
Unity中的光线追踪NITY中的光线追踪
现在,我们了解Unity中的光线追踪。在Unity支持DXR的Experimental版中,目前主要支持:半透明/折射、反射、面光源软阴影及环境遮挡(AO)。
我们对这些特性进行归类,首先是按照初级射线及次级射线来分类。先解释下什么是初级射线和次级射线。
初级射线英文叫Primary Ray,它一般是指从光源或者摄像机(视点)直接发射出去射线。
次级射线的英文叫Secondary Ray,它是指除初级射线以外的射线,也就是初级射线经过反射、折射、散射后的产生的射线。
如果按照这样的分类,半透明/折射效果需要初级射线的支持才可以完成。反射、阴影、AO及间接光照只使用次级射线就可以完成,相当于节省了一次初级射线的Trace。
如果按照是不是需要着色来分类,阴影及AO是不需要着色的,因为它们仅需查询遮挡信息就够了。那么反射、间接光照、半透明/折射则需要着色的。需要着色的Feature一般性能会差一些,因为光线追踪中的着色运算在一致性方面较光栅化差一些。
因为不同的光线有不同的方向,可能击中不同的物体,所以需要同时访问不同的Mesh、不同的材质及Shader。因此贴图的Cache,指令的Cache,以及Shader执行时的Divergence都面临挑战。
有兴趣的开发者可以下载Unity支持DXR的实验性版本:
https://github.com/unity-Technologies/Unity-Experimental-DXR
下面分别讲讲每个特性的实现及参数,首先是反射。
Max Reflections Rya length:射线的长度,它定义了射线的范围,就是前面讲的T-Max。射线的范围越大,可能交到的物体就越多,性能相对就会差一些。
Reflections Min Smoothness:最小光滑度,反射是需要着色的,为了性能考虑通常会做一些优化,Reflections Min Smoothness算一个优化。因为对于绝对粗糙的表面,例如纯Diffuse表面,反射效果非常微弱,可以忽略其反射。所以这里定义了一个最小光滑度阈值,如果超过这个阈值就不发射反射射线。
Reflections Clamp Value:为了避免出现亮度异常的值,我们使用Reflections Clamp Value把结果限定在一个合理的范围内。
Reflections Quality:为了性能考虑,我们可以使用Reflections Quality进行一些优化。如果要求高质量就在全分辨率下Trace反射射线。如果是性能模式就可以把分辨率减半,这样光线数量会减少很多,从而提升性能。
Reflections Quality下包含三个子参数。
Reflections Num Samples:每像素发射的射线数量,我们简称为SPP。如果是镜面反射只要一条反射射线就好了,如果是光滑的表面可能就需要多条。
理论上讲,越粗糙的表面需要反射射线越多,但出于性能考虑,通常我们建议的每像素反射射线也就一二条,再加上适当降噪处理,基本上是看不出来什么噪点的。
Reflections Filter Mode:反射的降噪模式。
Filter Radius:降噪器或者过滤器的半径,半径越大可能结果越模糊,但噪点越少。
下面我们看一下效果对比,请注意下图红圈位置。
左侧图是光线追踪的反射,看一下桌面上叶子的反射,上图红圈部分是可以看到反射的叶子的。
如果切换到SSR,这个地方有部分叶子已经反射不出来了。因为SSR是屏幕空间反射,所以只有在屏幕空间内的信息才能反射到。SSR一般只能做比较粗糙一点的反射,不太好做镜面反射,因为它是一个相对粗略的求交模拟。
下面看一下阴影,Shadows Num Samples,Shadows Bilateral Radius等参数与反射中的含义类似。
Split Integration是什么意思呢?因为一般使用光线追踪来做面积光源的软阴影,物理上讲纯粹的点光源,方向光是无法产生软阴影的,但是面积光源的光照计算是一个问题,所以有一种LTC的算法,可以对多边形面光源的光照直接求出解析解。
Unity实现了二种方案,第一种是同时计算面积光的光照和阴影,所有的代码都放在Raytracing的Shader中一次完成。代码比较复杂,指令多,所以比较耗时。
第二种是Split Integration参数控制的Split模式,将面光源的光照及阴影拆开,光线追踪中只计算面光源的阴影,光照放到普通的Pass里去做。所以效果是没有差别的,只是性能上的考量。
我们来看下图效果对比,下图中的电视就是一个矩形面光源,请注意红色部分阴影的变化。
打开光线追踪的阴影后,可以看到椅子下产生的软阴影。
相对于光滑的反射来说阴影可能需要的SPP会多一点,光源面积越大,需要的SPP越多,但一般也不建议使用超过4个。
我们再来看下环境遮挡AO。
AO需要的SPP数量可能相对要多一点,因为AO通常需要在半球上发射射线去检测有没有遮挡。但是它有一个特性是只需要检测很短的范围内,所以射线的长度一般会设置的的比较小。
Unity为AO提供了二种降噪器:Spatio Temporal以及NVIDIA的AO Denoiser供大家选择使用。
我们来看看效果对比,请点击下方大图查看红圈部分。
我们可以看到打开光线追踪的AO的时候,在墙角及窗户边缘的地方,AO的效果比较明显。
现在谈谈初级射线Primary Ray制作的半透明折射效果。
光线追踪渲染半透明类型的物体一定要从视点发射初级光线,Unity里把这个特性叫做Primary Visibility。
Raytracing Maximal Depth:最多能产生的新的射线的次数。例如一次折射就要产生一次新的射线。如果一个有厚度的杯子要完全穿透过去需要4次折射,那么Raytracing Maximal Depth最大深度要设置的大于4,才能穿透一个带厚度的杯子。
我们看一下效果,请注意红色区域的效果变化。
这是玻璃杯,上面的图是光线追踪的,后面这张是普通的光栅化的。经过对比发现效果差距还是非常明显的,光线追踪出来的结果明显更接近真实的玻璃折射。
最后是间接漫反射,这个特性参数与前面类似,就不多解释了。需要注意的是一般的情况下间接漫反射需要的SPP会较多,因为它也需要在半球上进行采样,所以在SPP数量有限的情况,对降噪器要求较高。
我们回到最开始的宝马视频中,看一看视频中有哪些特性是刚才讲过到的。
第一组是反射的对比图,左边的是实际拍摄的照片,右边是渲染的结果,肉眼已经难以区分出哪个效果更好了。
下方的图是阴影,左边部分是带阴影的,右边是没有的。软阴影是由后面这个大屏幕代表的面光源产生的,效果还是非常真实的。
第三组是AO的对比,可以看到座椅靠背的角落的地方,左边也是真实的照片,右边是渲染的结果,已经比较接近真实的效果。
最后是最复杂的汽车的头灯。因为汽车头等里面有很多反射的面片,还有透镜折射,通常情况下除了光线追踪外没有很好的办法。因为要经过多次的反射折射,光路非常复杂。
左边是实际拍摄的照片,右边是Unity实时光线追踪的结果,可以看到效果非常惊艳。
今天我的演讲的就是这些,谢谢大家。
小结
感谢英伟达技术工程师为我们带来关于《Unity中的实时光线追踪技术剖析》的分享。我们将陆续分享Unite 2019部分技术演讲内容,如果你有哪些大会技术演讲希望深入了解,请后台进行留言。
观看部分Unity官方视频,请关注B站帐户:Unity官方。下载Unity Connect APP,请点击此处。
推荐阅读
官方活动
5月29日晚8点,Unity平台部技术总监杨栋将为你揭秘《异教徒》背后的故事,详解HDRP后期处理新变化。
直播时间:5月29日 20:00-21:00(星期三晚 )
直播地址:
https://connect.unity.com/events/hdrp_post_processing
点击“阅读原文”访问Unity Connect