7bvhg本文主要参考NVIDIA Vulkan Ray Tracing Tutorial教程,环境配置与程序均可参照此文档执行(个人水平有限,如有错误请参照原文)。
一、光线追踪调用
我们首先创建一个函数来执行调用光线追踪着色器的命令。首先,将声明添加到头文件中:
void raytrace(const VkCommandBuffer& cmdBuf, const nvmath::vec4f& clearColor);
接下来绑定光追管线及其布局,并设置将在整个管线中可用的推送常量:
//--------------------------------------------------------------------------------------------------
// 执行光线追踪
//
void HelloVulkan::raytrace(const VkCommandBuffer& cmdBuf, const nvmath::vec4f& clearColor)
{
m_debug.beginLabel(cmdBuf, "Ray trace");
// 初始化推入常量
m_pcRay.clearColor = clearColor;
m_pcRay.lightPosition = m_pcRaster.lightPosition;
m_pcRay.lightIntensity = m_pcRaster.lightIntensity;
m_pcRay.lightType = m_pcRaster.lightType;
std::vector<VkDescriptorSet> descSets{m_rtDescSet, m_descSet};
vkCmdBindPipeline(cmdBuf, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, m_rtPipeline);
vkCmdBindDescriptorSets(cmdBuf, VK_PIPELINE_BIND_POINT_RAY_TRACING_KHR, m_rtPipelineLayout, 0,
(uint32_t)descSets.size(), descSets.data(), 0, nullptr);
vkCmdPushConstants(cmdBuf, m_rtPipelineLayout,
VK_SHADER_STAGE_RAYGEN_BIT_KHR | VK_SHADER_STAGE_CLOSEST_HIT_BIT_KHR | VK_SHADER_STAGE_MISS_BIT_KHR,
0, sizeof(PushConstantRay), &m_pcRay);
其中所有的数据都在之前的函数中有赋值。
我们最后调用vkCmdTraceRaysKHR()函数将光线追踪命令添加到命令缓冲区中。
接下来的具体说一下其调用的参数:之前我们多次提到了 SBT 缓冲区地址,这是因为可以将 SBT 分成多个缓冲区,每种类型一个缓冲区,即对应的光线生成、未命中着色器、命中着色器组和可调用着色器(本教程未涉及)四个参数。儿最后三个参数相当于需要计算的屏幕像素网格计大小,代表所有光追线程总数。由于我们希望每个像素进行发射一条射线执行光追,因此网格大小和输出图像的宽度和高度一致,且深度为 1。
vkCmdTraceRaysKHR(cmdBuf, &m_rgenRegion, &m_missRegion, &m_hitRegion, &m_callRegion, m_size.width, m_size.height, 1);
m_debug.endLabel(cmdBuf);
}
Raygen着色器的选择
如果你用多个raygen shader构建了一个pipeline,可以通过改变设备地址来选择raygen shader。
SBT 的封装
使用 SBTWrapper 时,上述内容可以替换为以下内容。
auto& regions = m_stbWrapper.getRegions();
vkCmdTraceRaysKHR(cmdBuf, ®ions[0], ®ions[1], ®ions[2], ®ions[3], size.width, size.height, 1);
调用光线追踪
现在我们已经设置好可以追踪光线的一切:加速结构、描述符集、光线追踪光线和着色器绑定表。接下来就是进行光追绘制图像了。
在main函数中,我们将定义一个局部变量来切换光栅化和光线追踪。在光线追踪初始化调用之后添加以下内容:
bool useRaytracer = true;
之后,在之前渲染的模块添加如下代码,使得我们的应用程序可以有两种渲染模式:
// Rendering Scene
if(useRaytracer)
{
helloVk.raytrace(cmdBuf, clearColor);
}
else
{
vkCmdBeginRenderPass(cmdBuf, &offscreenRenderPassBeginInfo, VK_SUBPASS_CONTENTS_INLINE);
helloVk.rasterize(cmdBuf);
vkCmdEndRenderPass(cmdBuf);
}
需要注意的是,光线追踪的行为更像是一个计算着色器,而不是图形任务,并且在渲染通道之外。
我们现在应该能够在光栅化和光线追踪之间交替使用了。然而,光线追踪结果只渲染了一个平坦的灰色图像:因为上述简单的光线生成着色器还没有追踪任何光线(未击中任何物体),只是返回一个固定的颜色。
光栅化
光线追踪
二、相机矩阵
相机的矩阵存储在Ubuffer缓冲区中并在函数updateUniformBuffer()中更新。光线追踪也需要这些矩阵,因此我们需要在创建buffer时更改使用阶段的标识。
auto uboUsageStages = VK_PIPELINE_STAGE_VERTEX_SHADER_BIT | VK_PIPELINE_STAGE_RAY_TRACING_SHADER_BIT_KHR;
2.1 光线生成着色器 (raytrace.rgen)
因为我们需要在shader中包含头文件,所以需要加上 GLSL的 扩展:
#extension GL_GOOGLE_include_directive : enable
现在是时候处理光线生成着色器以允许它执行正确的光追了。我们将首先添加一个新绑定以允许着色器访问相机矩阵。
#include "host_device.h"
layout(set = 1, binding = eGlobals) uniform _GlobalUniforms { GlobalUniforms uni; };
其中host_device.h如下:
#ifndef COMMON_HOST_DEVICE
#define COMMON_HOST_DEVICE
#ifdef __cplusplus
#include "nvmath/nvmath.h"
// GLSL Type
using vec2 = nvmath::vec2f;
using vec3 = nvmath::vec3f;
using vec4 = nvmath::vec4f;
using mat4 = nvmath::mat4f;
using uint = unsigned int;
#endif
// clang-format off
#ifdef __cplusplus // Descriptor binding helper for C++ and GLSL
#define START_BINDING(a) enum a {
#define END_BINDING() }
#else
#define START_BINDING(a) const uint
#define END_BINDING()
#endif
START_BINDING(SceneBindings)
eGlobals = 0, // Global uniform containing camera matrices
eSceneDesc = 1, // Access to the scene buffers
eTextures = 2 // Access to textures
END_BINDING();
START_BINDING(RtxBindings)
eTlas = 0, // Top-level acceleration structure
eOutImage = 1, // Ray tracer output image
ePrimLookup = 2 // Lookup of objects
END_BINDING();
// clang-format on
// Scene buffer addresses
struct SceneDesc
{
uint64_t vertexAddress; // Address of the Vertex buffer
uint64_t normalAddress; // Address of the Normal buffer
uint64_t uvAddress; // Address of the texture coordinates buffer
uint64_t indexAddress; // Address of the triangle indices buffer
uint64_t materialAddress; // Address of the Materials buffer (GltfShadeMaterial)
uint64_t primInfoAddress; // Address of the mesh primitives buffer (PrimMeshInfo)
};
// Uniform buffer set at each frame
struct GlobalUniforms
{
mat4 viewProj; // Camera view * projection
mat4 viewInverse; // Camera inverse view matrix
mat4 projInverse; // Camera inverse projection matrix
};
// Push constant structure for the raster
struct PushConstantRaster
{
mat4 modelMatrix; // matrix of the instance
vec3 lightPosition;
uint objIndex;
float lightIntensity;
int lightType;
int materialId;
};
// Push constant structure for the ray tracer
struct PushConstantRay
{
vec4 clearColor;
vec3 lightPosition;
float lightIntensity;
int lightType;
int frame;
};
// Structure used for retrieving the primitive information in the closest hit
struct PrimMeshInfo
{
uint indexOffset;
uint vertexOffset;
int materialIndex;
};
struct GltfShadeMaterial
{
vec4 pbrBaseColorFactor;
vec3 emissiveFactor;
int pbrBaseColorTexture;
};
#endif
绑定说明
如host_device.h中描述的相机缓冲区使用绑定位0。 set = 1位置在HelloVulkan::createRtPipeline()函数中设置给给第二个描述符集(pipelineLayoutCreateInfo.pSetLayouts)
当进行光线追踪时,命中或未命中着色器需要能够向调用光线追踪着色器程序返回一些信息。这就得通过使用rayPayloadEXT限定符标识定义的有效光路值来完成。
由于有效光路结构值将在多个着色器中重复使用,因此我们需要创建一个新的着色器文件raycommon.glsl,并将其添加到文件夹中。
此文件仅包含有效光路相关信息的定义:
struct hitPayload
{
vec3 hitValue;
uint seed;
uint depth;
vec3 rayOrigin;
vec3 rayDirection;
vec3 weight;
};
我们现在修改raytrace.rgen以包含这个新文件。
#include "raycommon.glsl"
有效光路值使用rayPayloadEXT标识为我们的hitPayload结构体。
layout(location = 0) rayPayloadEXT hitPayload prd;
着色器的main函数主要是计算浮点像素坐标,并进行在 0 - 1 归一化处理:
- gl_LaunchIDEXT 指的是正在渲染的像素的整数坐标;
- gl_LaunchSizeEXT 对应于调用traceRayEXT()函数时使用的图像尺寸。
void main()
{
const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5);
const vec2 inUV = pixelCenter/vec2(gl_LaunchSizeEXT.xy);
vec2 d = inUV * 2.0 - 1.0;
从像素坐标上,我们可以应用相机的视图和投影矩阵的逆矩阵来获得射线的原点和方向。
vec4 origin = uni.viewInverse * vec4(0, 0, 0, 1);
vec4 target = uni.projInverse * vec4(d.x, d.y, 1, 1);
vec4 direction = uni.viewInverse * vec4(normalize(target.xyz), 0);
此外,我们为光线提供了一些额外的标志:
- rayFlags 标志:正如我们在创建加速结构时指出的那样,此标识表明所有几何体都将被视为不透明。
- tMin、tMax :沿射线的交点开始的最小和最大距离。如果给定点之前或之后的相交无关紧要,那么这些距离可用于降低光线追踪成本。典型的用例是计算环境遮挡。
uint rayFlags = gl_RayFlagsOpaqueEXT;
float tMin = 0.001;
float tMax = 10000.0;
接下来,我们需要调用跟踪光线函数traceRayEXT。
traceRayEXT(topLevelAS, // acceleration structure
rayFlags, // rayFlags
0xFF, // cullMask
0, // sbtRecordOffset
0, // sbtRecordStride
0, // missIndex
origin.xyz, // ray origin
tMin, // ray min range
direction.xyz, // ray direction
tMax, // ray max range
0 // payload (location = 0)
);
这就需要详细介绍下本函数的其他参数了:
-
acceleration structure:用于搜索命中的TLAS加速结构。
-
rayFlags:控制光线跟踪的标志。
-
cullMask: 8位的“剔除掩码”,每个实例在创建 TLAS 的时候都包含一个这个8位掩码。实例掩码与此处设置的剔除掩码进行二元与运算,如果 AND 结果为
0,则跳过此交点。本教程未使用此特性所以我们此处设置为0xFF,在创建实例的时候也会设置为0xFF。 -
sbtRecordOffset、sbtRecordStride:它控制如何使用每个实例的 hitGroupId (
VkAccelerationStructureInstanceKHR::instanceShaderBindingTableRecordOffset) 在 SBT 中查找命中组。由于我们只有一个hit group,所以都设置为0。算是比较复杂的计算,具体可参照文章刚开始的计算规则介绍;也可以参照The RTX Shader Binding Table Three Ways文章。 -
missIndex:表示在 SBT 中未命中着色器组数组中的索引。光线如果没有与场景发生碰撞,则要调用该索引的着色器。
-
射线的原点、最小范围、方向和最大范围。
-
payload: 在此着色器中声明的有效光路的位置,在本例中为location=0。这个编译常量确定了调用者/被调用者之间rayPayloadInEXT的关系,并可以让你选择希望被调用着色器输出的位置。对于作为traceRayEXT直接结果调用的shaders (被调用者),它们的rayPayloadInEXT变量将又会被叫做traceRayEXT的调用者指定的位置的rayPayloadInEXT。为了使其能够在shader之间正常工作,两个变量应该具有相同的结构。这允许我们在运行时确定被调用者着色器输出写入的位置,这对于递归光线跟踪器特别有用。
最后,我们将生成的有效光路值写入输出图像。
imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(prd.hitValue, 1.0));
}
上述函数中介绍的参数,我们需要注意着色器中用到的两个标识:
-
rayPayloadEXT标识位
本标识位限定词是用来给traceRayEXT代表的有效光路载荷值标定的唯一标识符。出于某种原因,你不能仅按名称将有效值传递给 traceRayEXT(这被认为是 un-GLSL-y)。
此标识位的作用域仅在一个着色器的一次调用内生效。因此:-
如果链接到同一光线追踪管线中的两个不同着色器模块声明具有相同location编号的有效载荷,这些有效载荷不会相互干扰。
-
如果以递归方式调用着色器,则每次调用的有效光路都是独立的,即使它们的location数量相同。这就是光线追踪着色器需要 GPU堆栈的原因,此处也算计算机图形中一个新的概念。
-
此处请注意有效载荷标识位与描述符集和绑定位或顶点属性locations 之类的内容有何不同,后者的范围是整个管道的全局范围。
- rayPayloadInEXT标识位
rayPayloadInEXT变量也有一个标识,因为它也可以作为traceRayEXT. 在这种情况下,调用着色器的传入值本身成为被调用着色器的传入值。
注意,此处不要求被调用方传入的有效负载的位置与调用方传递给traceRayEXT的有效负载参数必须一致 !这与用于连接顶点着色器和片段着色器的in/out变量不同。
2.1 未命中着色器(raytrace.miss)
为了与光线追踪管线共享光栅化的清空色,我们使用推送常量传递的清空色更改未命中着色器的返回值。此处使用一个小技巧:虽然推入常量结构体中包含许多其他成员,但我们这里直接使用结构体中第一个成员就可以,不必声明后续成员。
具体shader如下:
#extension GL_GOOGLE_include_directive : enable
#extension GL_EXT_shader_explicit_arithmetic_types_int64 : require
#include "raycommon.glsl"
#include "wavefront.glsl"
layout(location = 0) rayPayloadInEXT hitPayload prd;
layout(push_constant) uniform _PushConstantRay
{
PushConstantRay pcRay;
};
void main()
{
prd.hitValue = pcRay.clearColor.xyz * 0.8;
}
之后执行程序,光线追踪渲染如下:
光栅化渲染如下: