欢迎您访问 最编程 本站为您分享编程语言代码,编程技术文章!
您现在的位置是: 首页

Unity 光线追踪的实施和应用--帮助在移动设备上实现光线追踪

最编程 2024-07-17 10:43:15
...
近几年,光线追踪技术的兴起使主机游戏画质提升到了新高度,带给玩家更逼真的视觉效果和沉浸式的游戏体验,获得了广泛认可。 随着移动游戏的发展,玩家对在移动端开启更高帧率与更真实画质的体验需求越发强烈,Unity 也在探索助力开发者在移动端游戏开发中实现光线追踪的工具集和性能优化方案。
在2023年4月11日/4月13日举办的天玑开发者日—追光行动上,Unity 图形技术主管金晓宇分享了题为《Unity Ray Tracing 的实现和应用——助力移动端光追的落地实现》的演讲,介绍了 Unity 对光线追踪技术的理解、开发计划和方案。
大家好!我是来自 Unity 中国的金晓宇,主要在公司里负责图形渲染相关的方向,今天我分享的题目是 Unity 的 Ray Tracing 实现,助力移动端光追的落地实现。
Ray Tracing 是现代 API 提供的功能,比如 DXR 或者 Vulkan,旨在利用提供硬件加速的 GPU 进行光线追踪,通常作为光栅化的补充而不是替代。为什么需要 Ray tracing pipeline?首先,Ray Tracing 可以用来实现反射或折射,以渲染那些不在 camera view 中的对象;第二,Ray Tracing 可以作为现有一些方案的进化,像一些焦散、半透明物体、以及动态的全局光照,都可以用 Ray Tracing 做出更好的效果。
首先我们回顾一下基本的两个概念,一个是 Ray Tracing 的加速结构,这关系到我们在场景里面画什么东西。第二是 Ray Tracing Shaders,关系到如何在场景画物体。
加速结构(Acceleration Structure)是我们进行求交运算的空间结构。右图是这个场景的 AS 示例图,用于左图所示的 cornel box,我们可以看到它是由底层 BLAS 和顶层 TLAS 结构组成的。
底层的加速结构就是像球、棱柱、平面这些东西,为每个 mesh 定义一个 BLAS。有了这些东西之后,我们场景还有很多的不同材质,通过指向关联的 BLAS 并包括相关的 transform 和 material 信息来构建相对应的实例,可以看到有红色、绿色的墙,然后驱动程序将根据实例来创建 BVH 结构,最终生成顶层加速结构 (TLAS)。这都是由系统帮助我们来构建的。
在 Unity 中有这样一个东西,叫做 RayTracingAccelerationStructure,加入这样一个类,把加速结构进行封装。封装的加速结构功能可以有两种管理模式:一种是自动化的模式,一种是手动的模式。使用自动化的模式,所有符合条件的游戏对象都会在添加到场景时添加到 AS。如果使用手动模式,那么我们必须使用 AddInstance 显式地将 GameObjects 添加到 AS 并使用 UpdateInstanceTransform 更新 transform,其好处是可以手动管理加速结构的构建。在以上两种情况下,我们都可以在创建时指定 layer mask 以过滤哪些 game object 可以添加到加速结构中。
最后我们需要调用 BuildRayTracingAccelerationStructure 方法来更新或者重建加速结构,它会判断 transform 当前是不是有改变,自动判断是不是需要在 GPU 上更新或者重建加速结构。
除此之外,我们在 Renderer 加了一个 RayTracingMode,这个 Mode 表达加速结构的运行方式,让加速结构知道实例更新的频率,以及我们以何种方式更新。
四种 Mode 从上到下性能开销逐渐增大。第一个 Off 就是关闭了 Ray Tracing,性能开销是最小的。Static,说明当前物体是不会移动不会变形的物体, 加速结构预计它永远不会以任何方式更新。DynamicTransform,这个物体可以更新 transform,但不允许有顶点的更新。DynamicGeometry,允许更新 transform 以及 vertex & index buffer 的更新。
接下来我想回顾一下 Ray Tracing Shaders,我发现根据它们在 Unity 中的使用将它们分为两类很有帮助:一种是 Raytrace Shaders,一种是 Surface Shaders (不同于 Builtin Pipeline 的 Surface Shader)。Raytrace Shaders 包括 Ray Generation Shader,第一个发起 TraceRay 的 shader。Miss Shader,光线没有击中任何物体的话,会调用一个 Miss Shader。Surface Shaders 包括 ClosestHit 和 AnyHit Shader。ClosestHit Shader 是获取到最近的点之后调用的 Shader,AnyHit Shader 在每一次的 intersection 都会调用。
我们再来看一下整个执行的 workflow。从 Ray Generation 开始,Ray 发出之后进入到加速结构的遍历过程中。发现交点之后,加速结构会调用 Intersection Shader。当 geometry 是一个三角形的时候,Intersection Shader 是系统默认提供的。如果有一些其他的应用是 procedureral 的 geometry,可以自定义 Intersection Shader。一条发出的 Ray 是有 TMin 和 TMax 的,我们可以简单把它类比到 camera 的远近裁剪面。当 intersection 发生的时候,如果说 Hit 的交点处于 Ray 的 TMin 和 TMax 之间,那么就是一个有效的 Hit,才会继续调用 AnyHit Shader。 在 AnyHit Shader 里我们可以忽略这个交点,可以 confirm 这个交点,也可以主动结束加速结构遍历的过程。当确定这个交点有效的时候,Ray 的 TMax 就会被更新成一个新值。
加速结构遍历完成的时候,就可以判断出当前这个 Hit 到底有没有。当没有的时候,就会调用 Miss Shader;当有的时候,就可以判定这个 Hit 是最近的 Hit,执行 ClosestHit Shader。有一点需要提醒的是,ClosestHit Shader 是在加速结构遍历完成之后才去执行的。最后 Callable Shaders 可以称作 hit shader 的 hit group,基本都是函数调用。Ray Tracing 的 pipeline 可以简单理解成由系统调用的、提供了一些接口的一个回调的方式,这和接下来要讲的 Inline Ray Tracing 的 pipeline 是不太一样的。
我们来看一个例子,更加细致地了解一下在遍历的时候发生了什么。
首先从 Ray Generation Shader 执行,从相机发出 Ray。第一种是完全没有击中场景中的任何物体,自然执行 Miss Shader;第二条 Ray 击中了两个物体,一个球体,一个圆柱体。球体执行 ClosestHit Shader,第二个圆柱不会执行。因为 ClosestHit Shader 是在加速结构遍历之后才会执行,所以此处只会执行最近物体的 ClosestHit Shader。第三条光线有可能击中一个圆柱体和一个棱镜,因为 AnyHit Shader 是在加速结构中间执行的,所以 AnyHit Shader 有可能先被执行到。但执行完加速结构遍历之后有可能执行前面物体的 ClosestHit,它把棱镜遮挡住了,所以 AnyHit Shader 执行的结果可能是无用的,这是 AnyHit Shader 消耗原因之一。当然如果要做一些特殊效果,比如半透明渲染,AnyHit Shader 也是需要的。比如这里第四条光线有两个半透明的棱镜,都可能执行 AnyHit Shader,我们可以根据光线的 payload 去做半透明混合的效果。
为了提供 Ray Tracing Shader,我们添加了一种新的 Shader 类型,是以 .raytrace 来结尾的。除此之外我们也在 CommandBuffer 中添加了一些新的 API,比如说 SetRayTracingShaderPass;SetRayTracingAccelerationStructure;SetRayTracing*Param 去绑定一些资源;.DispatchRays 是来作为发出 Rays 的开始,和用 Compute Shader 的 dispatch 是比较类似的。除此之外,类似的绑定也可在立即模式下从 RayTracingShader 类本身来执行,实现差不多相同的功能。
这是 Ray Tracing 在 HDRP 中的架构 Architecture,以及它们在 HDRP pipeline 中的大致位置。需要说明的是,这个图省略了许多不相关的功能。
可以看到前四个都是从 GBuffer 来生成的,基本上从 depth/normal 的 buffers 就可以计算出来,而 RT Reflections 和 RT Indirect Diffuse 是需要依赖 RT Light Clusters 这个 pass 的结果才能计算的。最后,像 RT Transparents 这个 pass 是在 Deferred Lighting 之后和后处理之前去做的,也就是在不透明物体完全渲染之后,在后处理之前,渲染半透明物体。加速结构是在 immediate mode 下构建和更新的,与此处显示的 render graph 是异步的。在 native code 中我们通过使用 barrier 来与渲染进行同步。
前面回顾了一下 Unity Ray Tracing 的 pipeline。现在简单介绍一下 Inline Ray Tracing(也叫Ray Query)的方法。Inline Ray Tracing 是 Ray Tracing pipeline 的替代方案,它没有用到 driver 层系统自动调度的功能,也没有用到 shader tables,它好处是可以用在任意的 shader stage 中,比如 compute shaders、pixel shaders 等,可以大大增强灵活性,当然也带来了新的 shader 指令。它和 Ray Tracing Shader 会使用相同的 AS。
我们为什么要有 Inline Ray Tracing 呢?Inline Ray Tracing 提供给了开发者更多的控制方式,为开发人员提供了驱动更多 Ray Tracing 过程的选项,而不是将工作调度完全交给驱动。
它的优点是,对一些简单的场景来说,把 shader 的调度交给系统会产生 overhead,可能是不值得的,用 Inline Ray Tracing 就可以回避掉这部分 overhead。第二,它可以用在 compute shaders、pixel shaders 中,增强灵活性,从实际来看对于光栅化的补充更为合适。第三,可以把 Inline Ray Tracing 和 Ray Tracing 相结合使用。第四,Ray Tracing pipeline 做递归光线的调用会比较费,对于简单的递归光线可以用 Inline Ray Tracing 更有效地做光线追踪,采用简单的 control flow 可能会减轻系统的负担,从而获得更高的效率。还有一点,做 Ray Tracing pipeline 的时候,有些 Ray Tracing shader 不支持 trace ray,比如 Intersection Shader 和 AnyHit Shader,因为是在加速结构遍历的过程中调用的,Inline Ray Tracing 就可以不受这个限制,在大部分 shader 中调用。
这是 Inline Ray Tracing 简化版的 control flow。
开始的时候要声明一个 Ray Query 对象,主要是两部分需要定义出来,第一个是 Ray 的属性,比如 Ray 的原点在哪儿?方向在哪儿?TMin 和 TMax 是多少?第二是设置 query 的行为,这里我们主要关注一下 Ray Query 的三个 flag。比如第一个 RAY_FLAG_CULL_NON_OPAQUE,把半透明的物体都忽略掉,ray 只会击中不透明的物体;第二个会跳过 procedural primitives;第三个 RAY_FLAG_ACCEPT_FIRST_HIT_AND_END_SEARCH,接受第一个 Hit 就结束加速结构的遍历。
总结下来,这条声明的对象会跳过半透明物体,并且返回最近的一个交点。像这样的简化可以生成更高性能的 Inline Ray Tracing 代码。
初始化过后,我们就可以调用 RayQuery::Proceed 方法来进行 Ray Query 的查询,同样,我们也可以调用 Abort 来 stop 当前的查询。
看一下流程图,从 Proceed 下来之后进行加速结构的遍历,会先查找加速结构中是否有候选,如果没有就结束了,如果有的话,会继续进行 triangle intersection 的检测,当 ray 和 triangle 有相交的时候,就会有 hit commit 并且更新 TMax。当我们查询结束时,也可以重复调用 RayQuery::Proceed 方法来发出想要的射线。
当 Proceed 方法结束之后,我们可以紧接着调用 RayQuery::CommittedStatus() 方法来获取 Hit 的结果。如果返回 Committed_Nothing,自然是没有 Hit 任何三角形,这里就可以执行类似 Ray Tracing pipeline 中类似 Miss Shader 的代码;如果返回 Committed_Triangle_Hit 的话,就可以执行类似 ClosestHit Shader 的代码或效果。
我们简单看一个代码 sample。可以看到它没有 Ray Tracing pipeline 里面通过各种 shader 的配置来实现效果,而是很简单地套用到更加过程化的方法。
我们重点关注一下 Ray Query 对象的声明,flag 的声明,ray 的具体参数包括原点、方向、TMin 和 TMax 的设定,然后进行 Ray Tracing Internalize 初始化,对象构建完毕后可以执行 Proceed 方法,返回之后可以立刻拿到结果,判断是不是和 triangle 有交点,做我们想要的效果。以上这些代码都是 HLSL 的代码。
如何把 Inline Ray Tracing 引入到 shader 中?我们需要用 #pragma require inlineraytracing。它会强制使用 SM 6.5 编译 shader,因为 SM 6.5 定义了 RayQuery 对象。
我们是如何支持跨平台的呢?第一点,会用一个 UnityRayquery 代替 RayQuery 对象;第二点,编译的 HLSL 代码会用 DirectX Shader Compiler (DXC) 来自动编译成对应平台的代码,比如 Vulkan 上的 spir-v 代码。
这是我们 Inline Ray Tracing 在 URP 上大概的架构,这里省略了一些不相关的功能。这是基于 forward rendering 的架构,基于 deferred rendering 的架构后续会支持。
可以看到 Ray Query 的 AO 和 shadows 也会从 depth/normal buffer 去生成。Ray Query Reflection 效果分为了 2 个 passes 来实现。加速结构的更新与创建与 Ray Tracing 是一致的。
这个是我们简单做的 Shadow 和 AO 的效果。左边是 URP Shadows,右边是 Inline Ray Tracing Shadows 的效果,可以看到 Inline Ray Tracing 的效果会更柔和自然,artifacts 更少。这是开发中的效果,最终版本出来会比现在更好。
One more thing,Unity 上海的研发团队正在与 MediaTeK 合作开发一款应用了 Inline Ray Tracing 的 demo,以提供开发者比较常用的内建功能。
开发过程中我们也遇到了很多困难,第一个是很难 debug,因为我们缺少工具链的支持,且功能不是很完善。第二,是受限于移动平台的性能和功耗,我们需要在性能和质量之间做平衡。因此,为了解决这两件事情,不让开发者经历我们同样的痛苦,我们与 MTK 进行了深度合作,不仅将给大家提供内建的制作好的 feature,还计划把开发过程中比较成熟的工具集内嵌到 Unity 中国版。
最后总结一下,目前来说 Ray Tracing pipeline 和 Inline Ray Tracing 都是光栅化的补充,Inline Ray Tracing 更适合和光栅化结合。Inline Ray Tracing 能给开发者更多的选择,让开发者自己调度 Ray Tracing process。以上的 Inline Ray tracing 只在 Unity China 里发布。未来将有机会结合天玑平台的 Debug/profiling tools 到 Unity 中国版里。
谢谢大家的聆听!