软件系统设计方案---GPU光线跟踪系统
1.项目简介
在 GPU 架构上实现光线追踪相关渲染算法,采用最新的渲染技术对其进行优化,提升渲染质量并且加快渲染速度。基于 DXR 等相关技术构建可交互的渲染器,调研新的光线追踪降噪技术以改善渲染结果。2.软件设计方案 本系统在运行在GPU 架构上,使用MicroSoft提供的DXR接口,需要使用Nvidia的RTX系列显卡提供的技术支持。本项目偏向研究型,主要聚焦于模拟现实世界的场景效果,包括光照、阴影、纹理等元素的处理,力求渲染出一个比较逼真的画面。DXR主要运用在离线渲染中,如果要运行在实时渲染上,对于市面上的主流GPU有比较大的压力。 该项目需要包含若干个场景,以表现出不同场景下DXR系统的运行性能。此外,我们还需要对渲染所需要的各项参数进行调节,并对部分可选功能进行扩展,具体方案如下图所示:
在内部,光线追踪流程为,产生光线->测试光线与物体相交->计算出对应像素点的颜色,若光线与物体相交,可能还要进一步进行递归,如下图所示:
整个系统的设计是要保持光线(ray)的独立处理,所有与ray有关的shader,ray都是作为一个输入,在每个shader里面不能和其它ray进行交互,也不能依赖于其它线程中的ray,尽管有些ray的运行结果会产生其它的ray,但是运行中的任何ray之间都是不能有依赖关系的。这样就会并行性提供了可能性。
光线追踪算法对(尤其是GPU中的)缓存结构是非常不友好的,为了尽可能地提高光线的计算效率,一般的光线追踪实现都需要对光线排序,以使其算法对内存访问具有一定的连贯性。为了最大化这种光线处理(例如对场景的遍历和着色计算)的效率,这样的工作显然作为固定管线会更加合适,上面说过固定管线的目标就是提供最大效率。上图演示了光线在一次追踪中的大概流程,其中一个shader用于生产rays,然后就提交给固定管线,固定管线最后给出ray与表面的交点,交回给一个表面着色shader。在这中间发生了光线对整个几何场景加速数据结构的遍历以及求交计算,这个过程是完全基础与固定管线的,这个做法其为了在遍历和求交计算中尽可能对针对GPU进行优化。这个固定管线实际上帮助开发者省了很多事情,这也是整个DXR最重要和最大的固定管线,这也是整个实时光线追踪的重要基础。
基于三角形网格的加速结构只是DXR内建的一种建构,这种内建结构甚至不需要我们知道任何具体数据结构的细节,我们只需要传给他一个双层结构,其中底层包含所有的三角形网格,这个数据就类似于在光栅化中一样传给他一些基本几何体的顶点数据,然后上层的是一个关于整个场景中的物体结构,例如每个物体的多个instance,变换矩阵等。
对于光线求交计算,DXR内建的加速结构是基于三角形网格的,但是也支持其它自定义体积数据结构,DXR也允许提供自定义的加速结构或者其它场景表述的体积结构但是如果想要使用这些结构,就需要手动构建自己的遍历算法,求交计算等。
DXR相关的shaders主要包括以下五种:
Ray Generation Shader:这个Shader负责初始化光线,是整个DXR可编程管线的入口,我们在其中调用TraceRay函数向场景发射一条光线。在Ray Generation Shader中,每条光线是彼此独立的,他们之间不需要线程间的同步和共享。此外RayGeneration Shader也负责最终着色结果的输出(通常是输出到一个UAV对应的Texture上)。
Intersection Shader:这个Shader是可选的,它只负责一件事,就是定义场景内基本的几何单元和光线的相交判定方法。如果场景的基本几何单元是三角面,则用户不需要自定义这个Shader,但如果不是三角面,而是用户自定义的几何形式,则用户需要提供提供相交判定的方法。这样的设定使得一些过程生成式模型(细分曲面,烟雾,粒子等)也能够使用光线追踪的框架进行渲染。需要注意的是,对于三角面来说,通过相交测试返回的是交点的重心坐标,用户需要根据重心坐标自行插值得到交点的相关几何数据(uv, normal等)。
Any Hit Shader:这个Shader的作用是验证某个交点是否有效,典型的应用是alpha test,比如我们找到了一个光线和场景的交点,但该位置刚好是草地中被alpha通道过滤掉的像素位置,我们希望光线穿过它继续查找新的交点,那么就可以在这个Shader中忽略找到的交点,此外,我们也可以在这个Shader中发射新的光线,一个可能的应用场景是折射/反射效果的实现。
Closest Hit Shader:当一条发射的光线经过和场景的若干次求交最终找到一个有效的最近交点后,就会进入这个Shader它用于某个找到的样本点的最终着色。
Miss Shader:决定如果找不到有效的交点之后的操作,在真实世界中一条光线总是会和某个表面相交,但虚拟场景的有限空间内却并非如此,这时候我们可能希望进行一次cube map的采样,或者告诉Ray Generation Shader它没找到交点,这些行为都可以在Miss Shader里定义。
3.接口API
本系统本身是基于DirectX 3D 12进行开发,主要使用了其中的dxr部分,UI界面使用imgui。
4.实现视图
如图所示
由EnkiTS负责管线与任务的调度
由External DLLs负责读取外部库
由Graphics负责渲染的具体处理,包括对物体参数的确定、着色器、照相机等
HosekSky是一个天空光照以及太阳辐射的模型,由布拉格查尔斯大学的Lukas Hosek 和 Alexander Wilkie提供
ImGui是用户使用的UI界面
5.核心数据结构
DXSample 主要包含了我们的运行窗口的一些参数,如窗口大小,接受io设备的输入对应的操作等,是一个抽象类。
class DXSample : public DX::IDeviceNotify{public: DXSample(UINT width, UINT height, std::wstring name); virtual ~DXSample(); virtual void OnInit() = 0; virtual void OnUpdate() = 0; virtual void OnRender() = 0; virtual void OnSizeChanged(UINT width, UINT height, bool minimized) = 0; virtual void OnDestroy() = 0; // IO设备输入产生的各项事件. virtual void OnKeyDown(UINT8 /*key*/) {} virtual void OnKeyUp(UINT8 /*key*/) {} virtual void OnWindowMoved(int /*x*/, int /*y*/) {} virtual void OnMouseMove(UINT /*x*/, UINT /*y*/) {} virtual void OnLeftButtonDown(UINT /*x*/, UINT /*y*/) {} virtual void OnLeftButtonUp(UINT /*x*/, UINT /*y*/) {} virtual void OnDisplayChanged() {}protected: void SetCustomWindowText(LPCWSTR text); // 窗口尺寸. UINT m_width; UINT m_height; float m_aspectRatio; RECT m_windowBounds;// DirectX 3D 设备资源 UINT m_adapterIDoverride; std::unique_ptr<DX::DeviceResources> m_deviceResources;};
接下来是实现类:
class D3D12RaytracingProceduralGeometry : public DXSample{public: D3D12RaytracingProceduralGeometry(UINT width, UINT height, std::wstring name); // 处理消息 virtual void OnInit(); virtual void OnKeyDown(UINT8 key); virtual void OnUpdate(); virtual void OnRender(); virtual void OnSizeChanged(UINT width, UINT height, bool minimized); virtual void OnDestroy(); virtual IDXGISwapChain* GetSwapchain() { return m_deviceResources->GetSwapChain(); } bool m_metalBallsExist;private: static const UINT FrameCount = 3; // 常量. const UINT NUM_BLAS = 2; // Triangle AABB BLAS. const float c_aabbWidth = 2; // AABB width. const float c_aabbDistance = 2; // Distance between AABBs. // DirectX Raytracing (DXR) 属性 ComPtr<ID3D12Device5> m_dxrDevice; ComPtr<ID3D12GraphicsCommandList5> m_dxrCommandList; ComPtr<ID3D12StateObject> m_dxrStateObject; // 根签名 ComPtr<ID3D12RootSignature> m_raytracingGlobalRootSignature; ComPtr<ID3D12RootSignature> m_raytracingLocalRootSignature[LocalRootSignature::Type::Count]; // 描述符 ComPtr<ID3D12DescriptorHeap> m_descriptorHeap; UINT m_descriptorsAllocated; UINT m_descriptorSize; // 光线追踪场景 ConstantBuffer<SceneConstantBuffer> m_sceneCB; StructuredBuffer<PrimitiveInstancePerFrameBuffer> m_aabbPrimitiveAttributeBuffer; std::vector<D3D12_RAYTRACING_AABB> m_aabbs; // 根常量 PrimitiveConstantBuffer m_planeMaterialCB; PrimitiveConstantBuffer m_aabbMaterialCB[IntersectionShaderType::TotalPrimitiveCount]; // 几何体参数缓冲区 D3DBuffer m_indexBuffer; D3DBuffer m_vertexBuffer; D3DBuffer m_aabbBuffer; // 顶层/底层加速结构 ComPtr<ID3D12Resource> m_bottomLevelAS[BottomLevelASType::Count]; ComPtr<ID3D12Resource> m_topLevelAS; // 光线追踪输出 ComPtr<ID3D12Resource> m_raytracingOutput; D3D12_GPU_DESCRIPTOR_HANDLE m_raytracingOutputResourceUAVGpuDescriptor; UINT m_raytracingOutputResourceUAVDescriptorHeapIndex; // 着色器绑定表 static const wchar_t* c_hitGroupNames_TriangleGeometry[RayType::Count]; static const wchar_t* c_hitGroupNames_AABBGeometry[IntersectionShaderType::Count][RayType::Count]; static const wchar_t* c_raygenShaderName; static const wchar_t* c_intersectionShaderNames[IntersectionShaderType::Count]; static const wchar_t* c_closestHitShaderNames[GeometryType::Count]; static const wchar_t* c_missShaderNames[RayType::Count]; ComPtr<ID3D12Resource> m_missShaderTable; UINT m_missShaderTableStrideInBytes; ComPtr<ID3D12Resource> m_hitGroupShaderTable; UINT m_hitGroupShaderTableStrideInBytes; ComPtr<ID3D12Resource> m_rayGenShaderTable; // 应用程序状态,包括运行时间、摄像机位置 DX::GPUTimer m_gpuTimers[GpuTimers::Count]; StepTimer m_timer; float m_animateGeometryTime; bool m_animateGeometry; bool m_animateCamera; bool m_animateLight; XMVECTOR m_eye; XMVECTOR m_at; XMVECTOR m_up;};
6.运行环境和技术选型
该项目运行在Visual Studio 2019上,需要10.0.19041.0或更新版本的SDK,需要RTX系列的GPU以支持DXR。
7.概念原型的核心工作机制
光线追踪把一个场景的渲染任务拆分成了从摄像机出发的若干条光线对场景的影响,这些光线彼此不知道对方,但却知道整个场景的信息。每条光线会和场景并行地求交,根据交点位置获取表面的材质、纹理等信息,并结合光源信息计算光照。在Raytrace Pipeline中,几何数据由两层结构存储,其中底层的数据结构称之为BLAS,上层的数据结构叫做TLAS。基于这样双层的数据结构,我们就可以调用DXR API创建整个场景的查询结构,这个查询结构是一个BVH(Bounding Volume Hierarchy),用来加速光线-场景的相交测试。可以想象,当一个相交测试开始时,光线首先会和BVH进行相交测试,通过的对象才会进一步访问其BLAS数据执行具体的光线-三角面相交测试。由于整个BVH只用于进行光线-场景相交测试,因此它只包含顶点位置的信息,如果我们需要顶点位置之外的信息(uv,normal等),则往往需要额外自定义一个SRV/UAV Buffer用于存储这些数据。Raytrace Pipeline以可编程管线为主,只有极少的固定单元。任何一个光线追踪的渲染程序都是从Ray Generation Shader开始,它负责初始的光线生成,生成的初始光线会通过固定的软/硬件单元对整个场景进行遍历求交,这个求交过程可以是用户自定义的一个Intersection Shader,也可以是默认的三角形相交测试。一旦相交测试通过,即得到了一个交点,这个交点将会被送给Any Hit Shader去验证其有效性,如果该交点有效,则它会和已经找到的最近交点去比较并更新当前光线的最近交点。当整个场景和当前光线找不到新的交点后,则根据是否已经找到一个最近交点去调用接下来的流程,若没有找到则调用Miss Shader,否则调用Closest Hit Shader进行最终的着色。