Unity 3070打开光线追踪_着色器


【前言】

光线跟踪是计算从某个点发射光线和物体相交的算法。不像光栅化算法将物体投影进相机,它可以看作反过来从相机或者直接利用物体某点发射光线,光线打到物体上以后可以再次发射,通过模拟出物体之间光线的传播,从而实现更真实的阴影、反射、焦散、环境光遮蔽乃至全局渲染效果。但是由于光线跟踪计算量大,并行难度大,难以高效集成到硬件上,因此过往一般只在电影制作等离线场景上使用。

直到2018年,微软推出DirectX Raytracing(DXR)实时光线跟踪框架,NVIDIA和UE4在GDC发布星球大战实时光线跟踪演示,随后NVIDIA发布了Turing显卡,其搭载硬件加速单元,光线跟踪这项传统上只能离线渲染的方法能够在单张显卡上实时跑起来,让游戏在画质上实现一大跨越。

同年,《逆水寒》跟NVIDIA合作,推出中国首款RTX光线跟踪技术demo,基于光线追踪的反射,阴影,焦散相比传统技术更符合物理真实世界,使得光线追踪有了质的突破。同时采用dlss,利用深度学习来达到抗锯齿的功能。今年六月,光线跟踪正式在《逆水寒》游戏中上线,作为一次重大游戏图形技术升级,使游戏更加身临其境,给广大玩家带来视觉上的升舱体验。

本篇文章就将从DXR光线跟踪框架和《逆水寒》光线跟踪应用两部分进行阐述。

【DXR 管线】

DXR是一套在GPU上实现光线跟踪的DirectX框架,它对DirectX作了两部分扩充,API部分添加构建新的PipelineStateObject、创建AccelerateStructure、设置ShaderTable和发射光线DispatchRays,着色器部分添加RayGenShader、IntersectionShader、AnyhitShader、ClosesthitShader、MissShader新的shader类型。

下面将分别介绍着色器部分、DXR扩充API部分:

  • DXR着色器


Unity 3070打开光线追踪_着色器_02

DXR GPU管线流程图[1]


RayGenShader:作为DXR发射光线的入口着色器,每个线程以grid形式调用这个着色器,类似计算着色器,每个线程可以通过DispatchRaysIndex获取grid里位置,DispatchRaysDimensions获取grid大小。通过这个着色器调用TraceRay函数来发射光线。

IntersectionShader:当光线与用户自定义轴对齐包围盒(简称AABB)相交时,DXR管线会调用这个着色器,这个着色器的作用是在用户定义几何体(比如过程性建模)的是否相交的判断以这个着色器形式提供给用户,当物体是三角形时,并不需要这个函数,DXR内建三角形求交方法,在光线击中三角形构成的包围盒时不会调用这个着色器。

AnyhitShader:这个着色器会在光线与三角形或者用户定义的几何体发生相交时被调用,通过调用IgnoreHit函数告诉DXR GPU管线忽视这次相交继续寻找新的相交点,或者调用AcceptHitAndEndSearch函数结束寻找求交,或者不调用上面两个函数那么DXR接受这次相交并继续寻找更近的新的相交点,注意DXR GPU管线调用这个着色器并不是沿光线方向有序的,其中一个使用场景是透明阴影,由于阴影只要有无相交即可判断,不要求有序。一般情况下不需要这个着色器,这样能够提升性能。

ClosesthitShader:

MissShader: 当光线没有与物体相交时就会调用这个着色器,这个着色器用来相交失败时处理,比如计算天空盒。

上述着色器在访问参数时除了接收从DX12 CommandList设置资源绑定外,还能够从ShaderTable里读取里面资源绑定,DX12 CommandList资源绑定,会被用于DXR流程里各个着色器,被称为全局根签名,而从ShaderTable读取的能够实现着色器与资源绑定,实现资源绑定实例化,这部分被称为本地根签名。

在光线与物体相交后DXR GPU管线会把物体相交的三角形编号和在三角形重心坐标系下交点位置传入ClosesthitShader和AnyHitShader,通过读取本地根签名绑定的顶点数据,还原出交点的各种顶点和材质属性用于渲染,在ClosestHitShader和MissShader除了作渲染计算外,还可以再次发射光线,实现光线多次反弹,实现复杂全局效果。

  • DXR API

1、DXR的管线状态对象

跟DX12创建管线状态对象流程不同,DXR的管线状态对象是一个StateObject,StateObject由一系列SubObject组成,每个SubObject包含一种对管线对象配置,DXR的管线对象一般需要下面几种SubObject类型:

D3D12_STATE_SUBOBJECT_TYPE_DXIL_LIBRARY:指定着色器编译成Library类型DXIL字节码,并设置需要哪些着色器的入口函数导出。

D3D12_STATE_SUBOBJECT_TYPE_GLOBAL_ROOT_SIGNATURE:用于指定全局根签名。

D3D12_STATE_SUBOBJECT_TYPE_LOCAL_ROOT_SIGNATURE:用于指定本地根签名。

D3D12_STATE_SUBOBJECT_TYPE_HIT_GROUP:将导出的IntersectionShader、AnyhitShader、ClosesthitShader组成HitGroup,设置HitGroup导出名字。

D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION:将SubObject与导出着色器相关联,把像全局根签名、本地根签名这类配置应用于着色器。

D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_SHADER_CONFIG:设置用户定义变量Payload和着色器传入Attribute的最大大小,这个配置可以通过D3D12_STATE_SUBOBJECT_TYPE_SUBOBJECT_TO_EXPORTS_ASSOCIATION来运用特定的着色器,但是对于同一个PSO内每个着色器的配置必须一致。

D3D12_STATE_SUBOBJECT_TYPE_RAYTRACING_PIPELINE_CONFIG:设置光线发射的最大递归次数,可用于特定着色器,但是对PSO每个着色器配置要一致。

增大递归次数或者Payload和Attribute的大小,会导致占用更多register/memory资源,可能会影响性能,按实际需要来指定大小。

这样基本配置完后通过CreateStateObject函数创建出DXR管线状态对象。

2、DXR的加速结构

DXR光线跟踪使用一种不透明结构来加速光线与物体的求交,这个加速结构分成BottomLevel和TopLevel两层,BottomLevel加速结构负责管理三角形网格或者轴对称包围盒,TopLevel加速结构引用BottomLevel加速结构,通过两级结构,使得BottomLevel能够被TopLevel加速结构下多个实例引用,在不改变BottomLevel加速结构下实现坐标变换,多实例化,快速更新动态场景,同时节省资源开销。

创建BottomLevel加速结构,需要三角形网格的顶点索引数据,或者AABB数据,有个Transform属性可以用来在创建加速结构时变化顶点的位置,设置相关输入参数,需要通过

GetRaytracingAccelerationStructurePrebuildInfo函数来获取创建加速结构的buffer大小,在分配好buffer后,通过BuildRaytracingAccelerationStructure创建。

创建TopLevel加速结构,TopLevel加速结构有多个实例组成,每个实例需要设置它的BottomLevel加速结构、坐标Transform和实例的各个属性,是通过D3D12_RAYTRACING_INSTANCE_DESC来描述。实例的属性包括:

InstanceID:设置InstanceID,可以在HitGroup着色器里获取对应实例InstanceID。

InstanceMask:指定掩码,在光线相交时跟TraceRay函数传入参数作与计算,来判断是否跳过这次相交,这样可以让光线跟特定物体相交,比如物体是否参与阴影或者反射。

InstanceContributionToHitGroupIndex:光线与物体相交后会计算ShaderTable索引位置,这个参数会参与这个计算。


Unity 3070打开光线追踪_着色器_03

DXR 加速结构的结构关系和ShaderTable的索引关系[1]

3、DXR ShaderTable

ShaderTable是一个存储了ShaderRecord结构体数组的Buffer,ShaderRecord包含了ShaderId和Shader的本地资源绑定,在光线相交时寻找ShaderTable对应ShaderRecord,调用对应着色器并传入全局资源绑定和本地资源绑定的参数。DXR管线是这样寻找ShaderRecord,对于HitGroup,ShaderRecord的位置由调用DispatchRay的传入参数HitGroupTable.StartAddress和HitGroupTable.StrideInBytes、TraceRay参数RayContributionToHitGroupIndex和MultiplierForGeometryContributionToHitGroupIndex、TopLevel加速结构里InstanceContributionToHitGroupIndex,BottomLevel加速结构的传入几何体位置GeometryContributionToHitGroupIndex所决定:

HitGroupTable.StartAddress + HitGroupTable.StrideInBytes * (RayContributionToHitGroupIndex + MultiplierForGeometryContributionToHitGroupIndex * GeometryContributionToHitGroupIndex) + InstanceContributionToHitGroupIndex

对于MissShader,ShaderRecord的位置由调用DispatchRay的传入参数MissShaderTable.StartAddress和MissShaderTable.StrideInBytes、TraceRay参数MissShaderIndex决定,关系如下

MissShaderTable.StartAddress + MissShaderTable.StrideInBytes * MissShaderIndex

设置好ShaderTable后,使用DispatchRays函数来调用RayGenShader,RayGenShader则发射光线,按照DXR GPU管线步骤,调用相应着色器完成一轮光线跟踪的渲染。

【逆水寒DXR光线跟踪管线】


Unity 3070打开光线追踪_Unity 3070打开光线追踪_04


逆水寒DXR光线跟踪使用混合光栅化渲染管线,在GBuffer计算与LightPass之间插入Raytracing流程,直接利用GBuffer信息,跳过摄像机与物体求交,直接在物体表面上发射多条光线,实现阴影、反射、焦散效果。

  • RT软阴影

阴影是光源发出光线被物体遮蔽后产生黑暗区域,区域分为本影区和半影区,本影区是光源完全被物体遮蔽区域,而半影区是部分被物体遮蔽区域,传统ShadowMap方法只能记录光的视角下遮蔽信息,这种只能产生本影区,不能计算半影区的阴影值,而且由于ShadowMap精度问题会出现ShadowAcne等问题,而利用光线跟踪利用阴影可见性公式直接计算出任意光源下任意物体遮蔽下产生的真实阴影。



上面是一个积分,用MonteCarlo积分将连续性计算转变成离散采样问题,随机发射一条光线,记录下是否被物体挡住,然后除以概率分布。



由于随机采样,所以每个样本不断抖动,但是随着样本越多,抖动就越少,当收集到足够的样本时结果收敛到期望结果,但是由于样本数目跟计算时间成正比,所以较少样本生成高质量结果比较重要,其中低差异序列[2]产生随机数能够更快地样本收敛至期望值,被广泛用于光线跟踪中。

由于光线的方向可以立体角表示,需要用二维的随机数来表示这个光线方向,这里需要根据不同光源来建立均匀随机数到对于光源来说均匀的光线方向的映射关系,对于点光源,由于它没有体积这样不会有软阴影,所以在逆水寒实现里是把点光源看作带体积的球状光源,将体积作为参数,供美术来调节软硬程度。




(点光源的随机数映射公式)

目前光线跟踪要做到实时,每个像素的光线是比较有限的,一般一个像素一条光线(1spp),在1spp下显然渲染出来的阴影充满噪点,为了能够在很低样本下得到比较好的结果,学术界和工业界提出一系列除噪算法,像SVGF[3]、NVIDIA RTX Shadow Denoiser[4]之类能够在1spp下完成除噪,这些算法的基本原理是复用样本,增大有效样本数目来降低噪声,首先空间上使用卷积的方法,比如用高斯核收集周边的样本,但是这样会把图像模糊化,所以需要像SVGF通过自适应核控制核的宽度或者RTX Shadow Denoiser代替图像空间使用世界空间核,对核的长宽方向进行优化,来避免模糊的现象,然后在时间上使用类似TAA的方法收集样本,对于静态场景时间样本是可靠的,所以一边收集一边调整空间核的大小,进一步提高除噪效果,但是运动场景TAA找不到正确的运动向量,会出现鬼影问题,就需要一系列类似neighbor clamping方法来除去鬼影。


Unity 3070打开光线追踪_光线跟踪_05

未除噪的阴影效果

Unity 3070打开光线追踪_采用光线跟踪绘制场景 c++_06

空间除噪效果

Unity 3070打开光线追踪_ci_07

加上时间累积后除噪效果

  • RT反射

传统光栅化管线的反射效果[5],一般采用屏幕空间反射、平面反射,屏幕空间是用raymarching方法找到在GBuffer里交点,能够很清晰地表现镜面反射的图像,由于使用GBuffer信息,在屏幕外或者物体在屏幕上不可见部分就没法显示出来;平面反射镜像地绘制一边场景,但存在每个平面都需要渲染场景一遍开销大,而且只能够在平面上反射问题,其它使用Voxel Cone Tracing[6]能够在任意物体表面上作反射,但是存在精度低,只能用作镜面模糊反射上。光线跟踪能够清晰地反射屏幕外物体,配合显卡硬件加速单元,实现实时任意物体上作反射效果。


Unity 3070打开光线追踪_着色器_08

SSR反射效果

Unity 3070打开光线追踪_ci_09

光追反射效果

不同物体都有不同材质,当光线击中物体后,需要在ClosestHitShader中计算材质渲染,材质渲染在光栅化在Pixel Shader里处理,它接收Vertex Shader解出来GBuffer里物体属性,而在RT反射中没有GBuffer而是击中物体的位置,需要一个函数将击中物体位置相关信息计算出Pixel Shader所需的各种物体属性,用统一函数去计算击中点的位置,读取物体的VBIB属性,计算出物体位置,读取所需要的物体属性,再调用Pixel Shader里面函数,完成物体的材质渲染。

对于不透明物体上述就处理好了,但是对于半透明物体或者需要Alphatest的物体(比如树叶),需要模拟光栅化管线的Alphatest和Alphablend流程,实现中将渲染分成两个部分:不透明渲染、半透明渲染,物体也根据参与哪种渲染分成两组,对于不透明渲染,沿着光的方向不断与不透明渲染的物体求交,当物体的alpha值大于阈值就停止并输出结果,对于半透明渲染,在沿着光的方向不断与半透明的物体求交,记录下物体的渲染的结果和Alphablend的类型,当物体的Alpha值也大于一定阈值或者光线长度等于不透明渲染时结果输出的长度时停止,然后从后往前依次根据Alphablend类型作混合,这样得到反射图像的渲染效果与光栅化管线渲染物体保证一致,然后限制物体的粗糙度,只有小于某个物体粗糙度阈值才发射光线,避免光线开销。

  • RT焦散

当光线打到光泽表面反射到粗糙表面时,由于光泽表面的不规则导致,存在多条光线汇聚到粗糙表面上某个点,出现明亮不规则的纹路,这个就是焦散效果。从被照亮物体出发寻找有效的光路在这种情况比较困难,对于这个问题,采用光子映射方法来处理焦散,光子映射做法是从光源处发射大量光子,每个光子携带能量值,光子经过光泽表面反射打到粗糙表面,计算粗糙表面下的光子能量分布。

在逆水寒实现,采用GDC上水面焦散[7]的方法的变种,先以太阳光视角记录水面网格的位置和法线方向,生成一张CausticsMap,然后从CausticsMap所记录的水面网格位置出发,沿着反射光线的方向作光线跟踪,将打到物体的焦点投影到屏幕空间,记录下屏幕空间下每个光子的数目,乘上供美术调节的颜色换算值,得到焦散贴图,供后续光照部分计算使用。

由于光子的数目有限,焦散贴图存在颗粒状分布,就需要除噪,使用带权重均匀核作个卷积计算就可以得到较好效果。


Unity 3070打开光线追踪_采用光线跟踪绘制场景 c++_10

未除噪焦散效果

Unity 3070打开光线追踪_采用光线跟踪绘制场景 c++_11

除噪后焦散效果

光线跟踪版本阴影、反射、焦散加入,明显提高三种渲染的效果,上述三种光线跟踪效果只是在游戏里应用光线跟踪的开始。随着GPU性能提升,接下来还有很多工作在探索和继续进行中,像基于物理模型的反射,多次反射和折射,乃至全局光照,都将在未来一步一步提高光线跟踪场景下质感。