谢邀。
“光线追踪就是未来,且一直会如此!”几十年来,当图形开发者被问及实时光线追踪是否可行时,这句话一直都是他们的口头禅。
大家似乎都同意前半句的说法:光线追踪就是未来。因为光线追踪是我们所知道的唯一能够渲染真正意义上具有照片级真实感图像的技术。电影业无法接受在图像质量上的妥协,这一领域中所采用的所有离线渲染器都是基于光线追踪,这并非偶然。光栅化多年来取得了巨大的进步,并且即使到如今也仍在发展,但它在所能计算的效果类型上有着根本性的限制。要想真正让图形效果再上一层楼,就需要新的底层技术。这就是光线追踪的用武之地,这也是为什么实时光线追踪一直是游戏玩家和游戏开发者的梦想。
那么光线追踪会不会总是未来的梦想,而永远不会在当下实现呢?在GDC 2018上,NVIDIA推出了RTX,其基于NVIDIA Volta GPU及未来GPU的超高性能,将赋力NVIDIA所支持的所有光线追踪API。同时,微软也宣布将光线追踪纳入其行业标准DirectX API中。
这两种技术构成了强大的组合,我们可以自信地回答上述问题:未来已来!这种说法毫不夸张:现在领先的游戏工作室已经开始采用RTX,通过DirectX开发即将推出的游戏。光线追踪在游戏中的应用不再是梦想。它此时此刻正在发生,且将迎来一个实时图形的新时代。
关于实时光线追踪的演示,大家可观看以下几个视频:
https://www.zhihu.com/video/974987051536461824星球大战短片——在NVIDIA RTX上运行的虚幻引擎(Unreal Engine)实时光线追踪演示
NVIDIA RTX实时光线追踪演示
运行于NVIDIA RTX的EA Project PICA实时光线追踪演示
下面我们将从微软的DXR API以及NVIDIA的RTX技术两方面带大家了解实时光线追踪的实现。
微软公布的DirectX Raytracing(DXR)API是DirectX 12的自然扩展。它将光线追踪完全集成到DirectX中,并使其能够与光栅化和计算共同发挥作用,而非将其替代。
与早期版本的DirectX 12相同,DXR API的重点在于通过提供应用程序重要的底层访问控制来提供高性能。其中的一些设计决策就反映了这一点:
DXR为DirectX引入了三个新概念,这也是应用程序必须管理的:
传统的光栅图形管线定义了许多着色器类型:顶点着色器(vertex shader)、几何着色器(geometry shader)、像素着色器(pixel shader)等。与其类似,光线追踪管线也包括五类新的着色器,它们在不同的阶段发挥作用:
由这些着色器构建的管线定义了单一光线编程模型。在语义上,每个GPU线程一次处理一条光线,且无法与其他线程通信或查看当前正被处理的其他光线。这能够简化开发者的工作,同时让特定供应商能够围绕API进行优化。
不同着色器类型相互通信的主要方式是ray payload。ray payload只是一个用户定义的结构,作为inout参数传递给TraceRay()。Any hit、closest hit和miss着色器都能够读取并写入ray payload,进而将其计算结果传回给TraceRay()的调用者。
为了更清楚地解释上述着色器,让我们来看一些简单的示例:
追踪主要光线的基础ray generation着色器
// An example payload struct. We can define and use as many different ones as we like. struct Payload { float4 color; float hitDistance; }; // The acceleration structure we'll trace against. // This represents the geometry of our scene. RaytracingAccelerationStructure scene : register(t5); [shader("raygeneration")] void RayGenMain() { // Get the location within the dispatched 2D grid of work items // (often maps to pixels, so this could represent a pixel coordinate). uint2 launchIndex = DispatchRaysIndex(); // Define a ray, consisting of origin, direction, and the t-interval // we're interested in. RayDesc ray; ray.Origin = SceneConstants.cameraPosition. ray.Direction = computeRayDirection( launchIndex ); // assume this function exists ray.TMin = 0; ray.TMax = 100000; Payload payload; // Trace the ray using the payload type we've defined. // Shaders that are triggered by this must operate on the same payload type. TraceRay( scene, 0 /*flags*/, 0xFF /*mask*/, 0 /*hit group offset*/, 1 /*hit group index multiplier*/, 0 /*miss shader index*/, ray, payload ); outputTexture[launchIndex.xy] = payload.color; }
以上代码显示了被追踪的主要光线的简单情况,即光线从虚拟相机发送到场景中。当然,ray generation着色器决不局限于此。将ray generation基于光栅化g-buffer数据(例如追踪反射)是另一种常见用例。这通常成本较低,因为许多引擎总会生成一个g-buffer。这也是光线追踪如何与光栅化互补、而非替代它的一个很好的例子。
Closest hit着色器将重心可视化
// Attributes contain hit information and are filled in by the intersection shader. // For the built-in triangle intersection shader, the attributes always consist of // the barycentric coordinates of the hit point. struct Attributes { float2 barys; }; [shader("closesthit")] void ClosestHitMain( inout Payload payload, in Attributes attr ) { // Read the intersection attributes and write a result into the payload. payload.color = float4( attr.barys.x, attr.barys.y, 1 - attr.barys.x - attr.barys.y, 1 ); // Demonstrate one of the new HLSL intrinsics: query distance along current ray payload.hitDistance = RayTCurrent(); }
当使用传统的光栅化时,只要求正绘制的当前对象所需的着色器在GPU上处于工作状态。因此,光栅化管线对象相对较小,包含单一组的顶点着色器、像素着色器等。与光线追踪相比,这是一个很重要的区别,因为我们有权随意将光线射入场景,这意味着它们可以照射到任何对象!因此,用于可能被光线照射到的所有对象的所有着色器都必须位于GPU上并做好执行准备。
DXR中,将着色器组合在一起执行的机制就是状态对象(state objects)。在应用程序层面,光线追踪管线状态对象可以被视为二进制可执行文件,其源自为场景编译的所有着色器的链接步骤。在创建状态对象时就指定了不同着色器之间的关系。例如,intersection、any hit和closest hit三个着色器都被捆绑到了hit组中。应用程序会在从命令列表中调用DispatchRays()时指定将要被执行的管线状态对象。应用程序可以创建任意数量的管线状态对象,并且还可以重新使用预编译着色器来实现。
光线追踪需要空间搜索数据结构来有效地计算光线与场景中几何体的交点。应用程序使用新的命令列表方法BuildRaytracingAccelerationStructure()来显式构建这些数据结构。NVIDIA RTX包含精心优化的构建算法,能够以极快的速度产出高质量的结果,从而能够实时构建并更新这些结构。应用程序可进一步优化不同类型内容的加速结构,如静态与动画。
场景中的所有几何体都由两层加速结构来表示。底层加速结构由几何图元构建而成,例如三角形。这些构建的输入图元通过一个或多个几何描述符来指定。几何描述符包括一个顶点和一个索引缓冲区,使得其粒度与光栅化中的一个draw call大致相当。
顶层加速结构是基于对底层结构的引用而构建的。我们称这些引用为实例描述符(instance descriptor)。每个实例描述符还包括一个转换矩阵以将其定位到场景中,以及相对于着色器表的偏移量以查找材料信息(请参见下文)。请注意,在我们之前的ray generation着色器示例中,TraceRay()的“场景”参数是顶层加速结构 - 它表示交点搜索的“入口点”。
由顶层和底层定义的双层结构可实现高效的刚体动画和实例化。顶层加速结构通常足够小,能够以非常低的成本构建。
我们已经探讨了光线追踪管线——指定存在于场景中的着色器,以及加速结构——指定几何体。着色器表是将两者联系在一起的数据结构。换句话说,它定义了哪个着色器与场景中的哪个对象相关联。此外,它还包含有关每个着色器所访问资源的信息,例如纹理、缓冲区和常量。
在应用程序层面,着色器表只是由应用程序全权管理的一大块GPU内存。应用程序负责分配资源,填充有效数据,将其传输到GPU,并正确地将其与光线追踪调度同步,就像任何其他GPU内存资源一样。应用程序也可以自由地维护多个着色器表,例如对其进行多缓冲,对一个进行更新,同时用另一个进行渲染。
着色器表是一批大小一致的记录。每条记录都将着色器(或hit组)与一组资源相关联。通常场景中每个几何对象都有一条记录,所以着色器表通常有数千条目。
一条记录以一个不透明的着色器标识符开始,应用程序通过从已编译的着色器进行查询以获取该标识符。其后包含着色器资源的根表(root table),其布局由着色器的本地根签名(local root signature)定义。一如既往地,根签名可以包含常量、描述符表和根描述符的任意组合。然而,使用光线追踪,应用程序可直接访问根表内存(而不是使用“setter”方法),从而实现非常高效的更新。由于它只是“内存”,所以甚至可以通过GPU着色器来更新着色器表!
还记得我们在从实例描述符构建顶层加速结构时,探讨了着色器表偏移量?现在就开始明白了吧,只要TraceRay()找到交点,系统就会使用这些偏移量来定位正确的着色器表记录。然后,它可以绑定记录中定义的资源,并为相交的几何体执行合适的intersection、any hit或closest hit着色器。
通过以上内容带大家快速了解了一下DirectX Raytracing API。在这里,我们讲到的只是冰山一角。您还可以阅读Microsoft关于DXR的博客文章,浏览更多的示例代码,更深入地探索微软的API。或者在微软的DXR开发者支持论坛上寻找答案。有关NVIDIA RTX光线追踪技术的更多信息,请参阅NVIDIA GameWorks 光线追踪页面。
为着手开发由RTX加速的DirectX Raytracing应用,您需要具备:
为完成设置,我们还推荐采用NVIDIA NSight Graphics,这也是与RTX技术一同发布的,并具有一流的DXR支持。
请注意,DXR是Windows 10 RS4的DirectX实验性功能,仅针对开发者。这意味着必须在Windows中启用开发者模式才能运行DXR应用程序。(设置→更新与安全→面向开发者)。
DXR是推进光线追踪广为采用的关键一步。将这一功能融入DirectX将使更多的开发者能够尝试采用以前为高端内容创建应用程序所用的技术。如今,DXR仍然是面向开发者的功能,但随着更多图形编程人员尝试采用光线追踪,DXR成为主流的可能性也将会增加。包括Epic、Remedy和Electronic Arts在内的公司已经开始尝试在其游戏引擎中添加实时光线追踪功能。
查看NVIDIA在Github上的DXR教程,其中包括示例代码和完整文档。您还可以使用NVIDIA的渲染原型框架Falcor(更新即将推出)对DXR展开探索。想要采用NVIDIA RTX和DXR进行开发的开发者可直接向NVIDIA垂询更多信息。