在之前关于体积云的文章《软光栅下体积云理论与实现》中,提到了CS2的可交互体积云;
Counter-Strike 2的可交互烟雾
这个效果如果想移植到PIccolo引擎中,需要把原来的Vulkan rasterization模式的渲染管线修改为ray tracing模式。这个任务需要分为三步:
- 构建出Vulkan ray tracing的管线
- 在引擎结构中做pipeline的替换
- 实现volume rendering的组件
首先就来聊一下在Vulkan中ray tracing pipeline和 rasterization pipeline 的区别,为后续在引擎中做替换做准备。Let’s go.
流程对比
假设使用rasterization的手段来渲染一个模型,一般都有这样的流程:
rasterization pipeline
对于ray tracing的流程,很多部分也是相同的,区别主要集中在三个部分
- 实例配置中增加对于raytracing扩展的配置
- pipeline的配置和加速结构的构建
- Shader binding table 处理
Vulkan 应用起手都会涉及到可交互的window以及Vulkan API、逻辑设备的实例化,对于rasterization pipeline 和ray tracing pipeline都是相同的不再赘述。它们的区别主要从application的配置开始。
Setup application
- setup extension
对于渲染任务来说,涉及到一般会涉及到instance 和 device,这两类extension需要分别进行配置。
例如,常用的swapchain相关涉及展示的扩展就是device extension。
对于ray tracingpipeline 则需要在rasterization的基础上增加一些Device extension。
例如:对于ray tracing pipeline所需要的feature:VK_KHR_RAY_TRACING_PIPELINE_EXTENSION_NAME
。
还有对于构建加速结构所需要的VK_KHR_ACCELERATION_STRUCTURE_EXTENSION_NAME
。
- Setup swapchain
首先创建成对的surface和swapchain以展示图形。还需要配置surface format和深度缓冲所用到的depth format。
此部分对于ray tracing和rasterization都是相同的。不少和成像逻辑无关的整体配置两者都没有区别。
- Setup depth buffer
创建depth image的vkImage、以及对应处理内存分配的vkDeviceMemory,同时为了让程序能够访问vkImage还要绑定 Imageview。
Ray tracing 天生自带depth test,自然不需要depth texture。
- Setup render pass、framebuffer
初始化 render pass与两个 attachments:color、depth,不涉及g-buffer相关的程序一般颜色和深度两个attachment就足够了,同时根据swapchain的数量配置对应的framebuffer。
不过Ray tracing管线不涉及renderpass这个部分。
- 模型载入
载入的模型数据一般会包含vertex 和index相关的数据,对于rasterization pipeline会把这部分数据作为VkBuffer进行处理,最后作为descriptor set传入shader中参与运算。
而对于ray tracing的管线,需要先将vertex数据构建了acceleration structures,之后同样也需要作为descriptor set参与shader中的计算。
Setup pipeline
pipeline开始两种成像方式开始出现区分,还是以rasterization为基础进行对比
- Setup render target
对于渲染的target,首先肯定得有一个承载图形的容易,所以需要为color imageview 和 depth imageview 配置 image layout。之后为offscreen render 准备对应的renderpass和framebuffer作为渲染的target。
raytracing并不涉及offscreen renderpass的部分。
- Setup pipeline
Ray tracing和rt分别有自己的pipeline。pipeline一般包括三个部分:shader stage、各类资源的layout、 fix-function。
而Ray tracing 由于有多个shader的协作涉及到更加复杂的shader stage、以及shader binding table的绘制,会在ray tracing pipline的概念部分展开。
- 配置uniform、descriptor set 等资源
对于通用的camera信息、模型顶点信息等,ray tracing pipeline 和rasterization pipeline两者是相同的。
Render loop
rasterization的方式通过在commandbuffer中提交关于vertex、index的信息,最后通过"vkCmdDrawIndexed" 确定绘制的图元,最终由vkQueuePresentKHR
进行展示。
对于ray tracing,则不会涉及具体的vertex信息,因为已经在加速结构中进行处理,不需要直接暴露在render
loop的过程中,同时在descriptor set中已经提供了足够的信息,则采用“vkCmdTraceRaysKHR”的方法进行绘制。
对于渲染完成后的cleanup两者大同小异,这里不再赘述。所以抽象下来,ray tracing pipline的流程可以抽象为:
Ray tracing pipeline
raytracing引入的新概念
Acceleration Structures
加速结构(AS)的目的是提高ray tracing的计算效率,我们可以将其与soft ray tracing render(就是CPU端实现的ray tracing)中的实现方式进行对比。
Ray tracing需要对于vertex构成的物体进行更高效的组织,以提高计算速度并避免不必要的计算。在面向大量三角形的场景中,相交测试计算可能非常耗时,而加速结构的目的就是解决这个问题。
在soft ray tracing render 中有许多实现方式,例如:BVH(Bounding volume hierarchy)、八叉树(Octree)和kd-树(kd-tree)等多种数据结构可供选择。在硬件实现中,我们无需亲自管理底层数据结构,因为硬件提供了其他方式。
硬件将这个问题分为多个层次的架构,对于软件开发者而言,有两个层次是可编程的:一个是顶层加速结构(Top-Level Acceleration Structures,TLAS),另一个是与之配合的底层加速结构(Bottom-Level Acceleration Structures,BLAS)。这种分层架构有助于简化开发过程,同时充分利用硬件资源以提高光线追踪性能。
AS的分类
底层加速结构(BLAS),就像BVH结构中最深层叶子结点。它包含每个图元(Primitive)的AABB bounding box(Axis-Aligned Bounding Box),在对整个场景进行分割时,可以找到特定物体并获取该物体包含的图元的一些属性信息,如具体的顶点数据。
然而,BLAS的构成比这更为复杂。在场景中,相同几何形状的物体可能会重复出现,它们的几何形状是相同的,但位置或旋转属性可能不同。因此,BLAS不仅包含顶点缓冲区的数据,还可以通过应用物体变换的变换矩阵(Transform Matrix)来描述,这使得单个BLAS能更高效地描述场景中的所有物体。
顶层加速结构(TLAS),类似于BVH中的叶子节点,它包含各种物体实例。借助BLAS的特性,多个TLAS可以引用同一个BLAS,因为可以通过变换矩阵实现转换。
每个实例还具有相应的ID,以便进行区分。从结构上看,TLAS类似于BLAS的容器,它会保存BLAS的指针以及其他信息,如变换矩阵。这种设计使得TLAS和BLAS可以共同描述复杂的三维场景,同时提高光线追踪的效率。
TLAS与BLAS的组织方式
在工程中,解析模型文件之后,就可以得到Vertex、Index等信息,在这个阶段就可以开始构建AS结构了,与soft render的过程是非常类似的。
Pipeline
Pipeline部分是rasterization pipeline 和 ray tracing pipeline之间的核心区别所在。
对于光线追踪,有两种管线可供选择:Ray Query 和Ray Tracing。
Ray Query更接近soft render的工程结构并且理解起来更容易,通过使用Compute shader进行处理,更适用于通用计算任务,如视频编解码和图像处理。
然而,Ray Tracing管线在实际工程中使用更广泛,更适合成像任务,因此我们将重点讨论这部分。
Ray tracing的一个挑战在于,与光栅化不同,无法根据不同的材质划分不同的渲染顺序来选择着色器。
但是对于ray tracing 是不可能根据intersect的情况选择的,在tracing的过程中,所有的shader都必须随时可用。这部分的功能会通过 Shader Binding Table(SBT)功能来实现,SBT将在后续进行解释
为解决这个问题,引入了Shader Binding Table( SBT )的概念,后续将对SBT进行详细解释。
光线追踪管线主要由三个部分组成:shader stage、fix-function、layout;与rasterization有明显区别的部分在shader部分。Ray tracing shader stage 中涉及到多个shader:
-
Ray generation shader
在图像中的每一个像素都需要通过 tracing来获取,在soft render中,光线的生成涉及到相机位置和FOV(Field of View)配置。同样在光线追踪管线中,需要使用着色器内置函数traceRayEXT进行处理。 -
Miss shader
光线生成后,需要在场景中进行相交测试,判断光线与哪些物体相交。这里的光线不受材质限制,会检测所有可能的交点。当加速结构(AS)判断没有交点时,Miss着色器开始工作。通常需要多个Miss着色器,因为hadow ray 和primary ray需要分开处理。 -
Closest hit shader
根据上一个着色器的相交测试结果,生成距离光线起点最近的交点,并计算光线的radiance;类似rasterization pipeline中fragment shader的作用,控制最终的图像结果。 -
Intersection shader
对于特定的图元进行intersect test,因为有些特殊的图元可能不再blas中,所以需要自定义判断相交的条件 -
Any hit shader
通常用于透明场景,用于判断多个点中哪个点走Closest Hit shader,从而构建一些风格化效果。
而在descriptor set方面,ray tracing 还需要一些额外的信息,例如TLAS就需要通过descriptor set 以告知shader,大部分descriptor set的配置与rasterization pipeline类似,不再展开赘述。
Shader Binding Table
ray tracing pipeline中包含多个shader stage,因此需要一种机制进行调度和管理。
这个机制就是Shader Binding Table (SBT),它包含了所有的着色器以及一些附加参数。这样,在光线追踪过程中,可以调用各种着色器及其参数。这整套结构作为SBT的基本单元 - Shader Record。
Shader Record包含一个或多个着色器以及一些附加数据。这些附加数据会在运行时传入着色器中。
每个Shader Record都需要作为一组function handle才能写入SBT中,因此,涉及到GPU中句柄对齐(Handle Alignment)的问题。为了提高GPU的效率,通常需要进行对齐处理。在同一个组中,两个handle的地址差距必须是shaderGroupHandleAlignment的整数倍。
Shader Record包含一个或多个着色器以及一些附加数据。这些附加数据会在运行时传入着色器中。每个Shader Record需要作为一组函数句柄才能写入SBT。因此,涉及到GPU中句柄对齐(Handle Alignment)的问题。为了提高GPU的效率,通常需要进行对齐处理。在同一个组中,两个句柄的地址差距必须是shaderGroupHandleAlignment的整数倍。
解决了着色器的组织管理问题后,还有一个调度问题:当光线与一个几何体相交时,该如何知道哪一个着色器应被调用?
如果是soft rendering的情况下,反正geometry和material的信息都在CPU侧,通常在判断交点时可以直接获取对应的geometry和material了,自然可以选择对应的方式来计算radiance。
但是在GPU环境下,具体的geometry的排列顺序、BLAS中的信息、光线的相交判断都在不同的shader、甚至可能在不同的pipeline中,这问题就变得难以解决。
因此,需要一种机制,能让shader与BLAS中的geometry进行联动,定位到底需要哪一个shader,在BLAS中由于有各种geometry的reference,可以提供一个index用于定位,类似每个geometry分配一个ID。这样可以提供给SBT一个offset,让SBT的hit group获得起始位置。
可以将SBT的hit group records类比为一个array,我们知道SBT的地址头,也知道剩下的shader是对齐排列的,但是如何知道所需的着色器在哪里,就需要一个公式:
其中HG
就是地址,所需要的地址就是: 地址头 + (预设的offset + SBT hit group recoreds的步长 + geometry的ID + sub-table的hit group recored所开始地方的offset )
通过这种方式,最终构建出着色器的索引。
在着色器中,对应的代码如下:
void traceRayEXT(accelerationStructureEXT topLevel,
uint rayFlags,
uint cullMask,
uint sbtRecordOffset,
uint sbtRecordStride,
uint missIndex,
vec3 origin,
float Tmin,
vec3 direction,
float Tmax,
int payload);
(sbtRecoredOffset对应公式中的 $$R_{offset}$$ 、sbtRecordStride 对应公式中的 $$R_{stride}$$
)
通过这种方法,可以完成对着色器的调度。假设是miss shader,首先确保所有的Hit Group经过shaderGroupBaseAlignment
处理后,每个着色器的入口再经过shaderGroupHandleAlignment
的对齐处理,就可以通过公式获得着色器组的handle。
实现与优化
Ray tracing pipeline 创建
创建Ray Tracing管线的核心步骤可以分为以下几个部分:
- 填充VkPipelineShaderStageCreateInfo结构,为多个shader准备createinfo。
- 填充VkRayTracingShaderGroupCreateInfoKHR结构,用于描述Ray Tracing管线中的shader group,将需要的shader编成一个group。
- 对于没有自定义需求的shader进行设置,例如groupInfo.anyHitShader = VK_SHADER_UNUSED_KHR。
- 定义push constant,用于后续在shader中提供一些关于光源位置、强度等常量数据。
- 创建pipeline layout,包括三部分:shader stage、固定功能和Ray Tracing特有的类似TLAS的信息。而在stage中,与传统的图形管线中只有两个可编程的stage不同,Ray Tracing shader stage可以有更多的自定义空间。
- 设置最大光线深度为2,表示光线可以反射两次以实现全局光照效果。
- 销毁对应的资源,例如shader module、layout
SBT的创建与管理
SBT的核心目的是实现shader 管理和调度,在具体实现上的主要步骤是:
- 假设有一个shader group,其中包含ray generation、miss、hit三类shader。
- 进行group align和shader handle align处理,得到shader的size和stride数据。
- 根据handle的数据计算SBT大小,并申请对应大小的buffer。
- 分配SBT buffer,其大小为ray generation、miss、hit group区域大小之和。
- 根据SBT buffer的设备地址,分配ray generation、miss和hit group的deviceAddress。
- 将所需的handle映射到SBT buffer。对于ray generation、miss和hit,进行内存复制操作。
- 最后,清除临时资源并解除SBT buffer的映射。
Tracing 过程
在光栅化过程中,通过调用vkCmdDrawIndexed
来启动绘制操作。
而在光线追踪过程中,使用vkCmdTraceRaysKHR
来启动绘制。这部分的区别仅仅是之前管线区别的延伸,不再赘述。
以上就是对于Vulkan中ray tracing pipeline与rasterization pipeline在实现中的一些对比,Vulkan概念繁多复杂,上面也省略了不少细节,lz自己也跟着做了一些实践,如果感兴趣可以参考:(Vulkan-ray-query)
若有错误疏漏,恳请包涵指点。