Author: Martin-Karl Lefrançois
Tutorial (Setup)
This is an extension of the Vulkan ray tracing tutorial.
Ray tracing allow to use callable shaders in ray-generation, closest-hit, miss or another callable shader stage. It is similar to an indirect function call, whitout having to link those shaders with the executable program.
(insert setup.md.html here)
Data can only access data passed in to the callable from parent stage. There will be only one structure pass at a time and should be declared like for payload.
In the parent stage, using the callableDataEXT
storage qualifier, it could be declared like:
layout(location = 0) callableDataEXT rayLight cLight;
where rayLight
struct is defined in a shared file.
struct rayLight
{
vec3 inHitPosition;
float outLightDistance;
vec3 outLightDir;
float outIntensity;
};
And in the incoming callable shader, you must use the callableDataInEXT
storage qualifier.
layout(location = 0) callableDataInEXT rayLight cLight;
To execute one of the callable shader, the parent stage need to call executeCallableEXT
.
The first parameter is the SBT record index, the second one correspond to the 'location' index.
Example of how it is called.
executeCallableEXT(pushC.lightType, 0);
In HelloVulkan::createRtPipeline()
, immediately after adding the closest-hit shader, we will add
3 callable shaders, for each type of light.
First create the shader modules
enum StageIndices
{
eRaygen,
eMiss,
eMiss2,
eClosestHit,
eCall0,
eCall1,
eCall2,
eShaderGroupCount
};
...
// Call0
stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/light_point.rcall.spv", true, defaultSearchPaths, true));
stage.stage = VK_SHADER_STAGE_CALLABLE_BIT_KHR;
stages[eCall0] = stage;
// Call1
stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/light_spot.rcall.spv", true, defaultSearchPaths, true));
stage.stage = VK_SHADER_STAGE_CALLABLE_BIT_KHR;
stages[eCall1] = stage;
// Call2
stage.module = nvvk::createShaderModule(m_device, nvh::loadFile("spv/light_inf.rcall.spv", true, defaultSearchPaths, true));
stage.stage = VK_SHADER_STAGE_CALLABLE_BIT_KHR;
stages[eCall2] = stage;
Then 3 groups of callable shaders and the stages that goes with it.
// Callable shaders
group.type = VK_RAY_TRACING_SHADER_GROUP_TYPE_GENERAL_KHR;
group.closestHitShader = VK_SHADER_UNUSED_KHR;
group.generalShader = eCall0;
m_rtShaderGroups.push_back(group);
group.generalShader = eCall1;
m_rtShaderGroups.push_back(group);
group.generalShader = eCall2;
m_rtShaderGroups.push_back(group);
Here are the source of all shaders
In this example, we will use the nvvk::SBTWrapper
. It is using the information to create the ray tracing pipeline, to
create the buffers for the shading binding table.
In the hello_vulkan.h
header, include the wrapper and add a new member.
#include "nvvk/sbtwrapper_vk.hpp"
...
nvvk::SBTWrapper m_sbtWrapper;
In HelloVulkan::initRayTracing()
, initialize it the following way.
m_sbtWrapper.setup(m_device, m_graphicsQueueIndex, &m_alloc, m_rtProperties);
In HelloVulkan::createRtPipeline()
, immediately after creating the pipeline call to vkCreateRayTracingPipelinesKHR()
,
create the SBT with the following command.
m_sbtWrapper.create(m_rtPipeline, rayPipelineInfo);
In HelloVulkan::raytrace()
, we have to tell where the callable shader starts. Since they were added after the hit shader, we have in the SBT the following.
The SBT wrapper class give back the information we need. So instead of computing the various offsets, we can get directly the
VkStridedDeviceAddressRegionKHR
for each group type.
auto& regions = m_sbtWrapper.getRegions();
vkCmdTraceRaysKHR(cmdBuf, ®ions[0], ®ions[1], ®ions[2], ®ions[3], m_size.width, m_size.height, 1);
In the closest-hit shader, instead of having a if-else case, we can now call directly the right shader base on the type of light.
cLight.inHitPosition = worldPos;
//#define DONT_USE_CALLABLE
#if defined(DONT_USE_CALLABLE)
// Point light
if(pushC.lightType == 0)
{
vec3 lDir = pushC.lightPosition - cLight.inHitPosition;
float lightDistance = length(lDir);
cLight.outIntensity = pushC.lightIntensity / (lightDistance * lightDistance);
cLight.outLightDir = normalize(lDir);
cLight.outLightDistance = lightDistance;
}
else if(pushC.lightType == 1)
{
vec3 lDir = pushC.lightPosition - cLight.inHitPosition;
cLight.outLightDistance = length(lDir);
cLight.outIntensity =
pushC.lightIntensity / (cLight.outLightDistance * cLight.outLightDistance);
cLight.outLightDir = normalize(lDir);
float theta = dot(cLight.outLightDir, normalize(-pushC.lightDirection));
float epsilon = pushC.lightSpotCutoff - pushC.lightSpotOuterCutoff;
float spotIntensity = clamp((theta - pushC.lightSpotOuterCutoff) / epsilon, 0.0, 1.0);
cLight.outIntensity *= spotIntensity;
}
else // Directional light
{
cLight.outLightDir = normalize(-pushC.lightDirection);
cLight.outIntensity = 1.0;
cLight.outLightDistance = 10000000;
}
#else
executeCallableEXT(pushC.lightType, 0);
#endif