From 3b0c3536e061dc5be2c73758263c6c508c6d3705 Mon Sep 17 00:00:00 2001 From: mklefrancois Date: Wed, 19 May 2021 10:21:33 +0200 Subject: [PATCH] Revert accidental delete --- docs/index.html | 2 +- docs/vkrt_tuto_indirect_scissor.md.html | 1505 +++++++++++++++++++++++ 2 files changed, 1506 insertions(+), 1 deletion(-) create mode 100644 docs/vkrt_tuto_indirect_scissor.md.html diff --git a/docs/index.html b/docs/index.html index 5cfd81c..e7c1755 100644 --- a/docs/index.html +++ b/docs/index.html @@ -9,4 +9,4 @@ + \ No newline at end of file diff --git a/docs/vkrt_tuto_indirect_scissor.md.html b/docs/vkrt_tuto_indirect_scissor.md.html new file mode 100644 index 0000000..b649eaf --- /dev/null +++ b/docs/vkrt_tuto_indirect_scissor.md.html @@ -0,0 +1,1505 @@ + +**NVIDIA Vulkan Ray Tracing Tutorial** +**Trace Rays Indirect** + +Authors: David Zhao Akeley + +![](Images/indirect_scissor/intro.png) + +This is an extension of the [Vulkan ray tracing tutorial](vkrt_tutorial.md.html). + +We will discuss the `vkCmdTraceRaysIndirectKHR` command, which allows the +`width`, `height`, and `depth` of a trace ray command to be specifed by a +buffer on the device, rather than directly by the host. As a demonstration, +this example will add colorful lanterns to the scene that add their own light +and shadows, with a finite radius of effect. A compute shader will calculate +scissor rectangles for each lantern, and an indirect trace rays command will +dispatch rays for lanterns only within those scissor rectangles. + +# Outline + +The basic idea is to split up ray tracing into seperate passes. The first pass +is similar to the original tutorial: it fills in the entire output image, +calculating lighting from the main light in the scene. Subsequently, one +pass is run within a scissor rectangle for each lantern to add its light +contribution to the output image. + +The steps to accomplish this are: + +* Add a buffer to store lantern positions, colors, and scissor rectangles. + These lanterns are separate from the OBJ geometry loaded in the main + tutorial. Run a compute shader each frame to fill in the scissor rectangles. + +* Build a BLAS for a lantern, and add lantern instances to the TLAS. + As lanterns are self-illuminating, the closest hit shader used to shade + ordinary OBJ geometry is inappropriate, so, we will add a new hit group (new + closest-hit shader) for lanterns to the SBT, and set `hitGroupId` + for lantern instances to `1` so this hit group is used. + +* Modify the ray generation shader so that it emulates additive blending, + and supports drawing within scissor rectangles (in the main tutorial, + the raygen shader assumes the ray trace dispatch covers the whole screen). + +* Add shadow rays in lantern passes, cast towards the lantern whose light + contribution is being added in the current pass. To detect whether the + expected lantern was hit, we add a new miss shader and two new closest + hit shaders (one for OBJ instances, one for lanterns) that return the + index of the lantern hit (if any). + +* Add one `vkCmdTraceRaysIndirectKHR` call in `HelloVulkan::raytrace` for + each lantern in the scene. + +If everything goes well, we should see something like this (the "lantern debug" +checkbox enables visualizing the scissor rectangles). + +![](Images/indirect_scissor/bounding.png) + +# Lantern Scissor Rectangles + +In this step we set up the buffer storing lantern info, and a compute shader +for calculating the scissor rectangles. + +## Allocate Host Storage + +We first need to stage the vector of lanterns on the host in a vector. +Since this is not an animation example, we won't be concerned with +keeping track of changes to this vector: changes are forbidden after the +acceleration structures are built. + +In `hello_vulkan.h`, declare a struct for holding information about +the lanterns on the host. + +```` C + // Information on each colored lantern illuminating the scene. + struct Lantern + { + nvmath::vec3f position; + nvmath::vec3f color; + float brightness; + float radius; // Max world-space distance that light illuminates. + }; +```` + +Then declare a vector of `Lantern` and add +a new function for configuring a new lantern in the scene. + +```` C + // Array of lanterns in scene. Not modifiable after acceleration structure build. + std::vector m_lanterns; + void addLantern(nvmath::vec3f pos, nvmath::vec3f color, float brightness, float radius); +```` + +The `addLantern` function is implemented as + +```` C +// Add a light-emitting colored lantern to the scene. May only be called before TLAS build. +void HelloVulkan::addLantern(nvmath::vec3f pos, nvmath::vec3f color, float brightness, float radius) +{ + assert(m_lanternCount == 0); // Indicates TLAS build has not happened yet. + + m_lanterns.push_back({pos, color, brightness, radius}); +} +```` + +In `main.cpp`, we insert calls for adding some lanterns. + +```` C + // Creation of the example + helloVk.loadModel(nvh::findFile("media/scenes/Medieval_building.obj", defaultSearchPaths, true)); + helloVk.loadModel(nvh::findFile("media/scenes/plane.obj", defaultSearchPaths, true)); + helloVk.addLantern({ 8.000f, 1.100f, 3.600f}, {1.0f, 0.0f, 0.0f}, 0.4f, 4.0f); + helloVk.addLantern({ 8.000f, 0.600f, 3.900f}, {0.0f, 1.0f, 0.0f}, 0.4f, 4.0f); + helloVk.addLantern({ 8.000f, 1.100f, 4.400f}, {0.0f, 0.0f, 1.0f}, 0.4f, 4.0f); + helloVk.addLantern({ 1.730f, 1.812f, -1.604f}, {0.0f, 0.4f, 0.4f}, 0.4f, 4.0f); + helloVk.addLantern({ 1.730f, 1.862f, 1.916f}, {0.0f, 0.2f, 0.4f}, 0.3f, 3.0f); + helloVk.addLantern({-2.000f, 1.900f, -0.700f}, {0.8f, 0.8f, 0.6f}, 0.4f, 3.9f); + helloVk.addLantern({ 0.100f, 0.080f, -2.392f}, {1.0f, 0.0f, 1.0f}, 0.5f, 5.0f); + helloVk.addLantern({ 1.948f, 0.080f, 0.598f}, {1.0f, 1.0f, 1.0f}, 0.6f, 6.0f); + helloVk.addLantern({-2.300f, 0.080f, 2.100f}, {0.0f, 0.7f, 0.0f}, 0.6f, 6.0f); + helloVk.addLantern({-1.400f, 4.300f, 0.150f}, {1.0f, 1.0f, 0.0f}, 0.7f, 7.0f); +```` + +## Lantern Device Storage + +In `hello_vulkan.h`, declare a struct for storing lanterns on the device. +This includes the host information, plus a scissor rectangle. +```` C + // Information on each colored lantern, plus the info needed for dispatching the + // indirect ray trace command used to add its brightness effect. + // The dispatched ray trace covers pixels (offsetX, offsetY) to + // (offsetX + indirectCommand.width - 1, offsetY + indirectCommand.height - 1). + struct LanternIndirectEntry + { + // Filled in by the device using a compute shader. + // NOTE: I rely on indirectCommand being the first member. + VkTraceRaysIndirectCommandKHR indirectCommand; + int32_t offsetX; + int32_t offsetY; + + // Filled in by the host. + Lantern lantern; + }; +```` + +!!! NOTE + `VkTraceRaysIndirectCommandKHR` is just a struct of 3 `int32_t` defining + the `width`, `height`, `depth` of a trace ray command. + + +We also declare an equivalent structure for shaders in the file +`LanternIndirectEntry.glsl`. We avoid using `vec3` due to differences +in alignment in C++ and GLSL. +```` C +struct LanternIndirectEntry +{ + // VkTraceRaysIndirectCommandKHR + int indirectWidth; + int indirectHeight; + int indirectDepth; + + // Pixel coordinate of scissor rect upper-left. + int offsetX; + int offsetY; + + // Lantern starts here: + // Can't use vec3 due to alignment. + float x, y, z; + float red, green, blue; + float brightness; + float radius; +}; +```` + +To store the lanterns on the device, declare the Vulkan buffer of `LanternIndirectEntry` +in `hello_vulkan.h` + +```` C + // Buffer to source vkCmdTraceRaysIndirectKHR indirect parameters and lantern color, + // position, etc. from when doing lantern lighting passes. + nvvk::Buffer m_lanternIndirectBuffer; + VkDeviceSize m_lanternCount = 0; // Set to actual lantern count after TLAS build, as + // that is the point no more lanterns may be added. +```` + +and fill it with a `createLanternIndirectBuffer` function. For performance, +we allocate a device-local buffer. We need usage flags for + +* Storage buffer use, so the compute shader can write to it. +* Indirect buffer use, so we can source indirect parameters from it when dispatching a ray trace. +* Device address use, as `vkCmdTraceRaysIndirectKHR` expects a device address. +* Transfer dst use, so the buffer can be initialized with the lantern colors and positions from `m_lanterns`. + +```` C +// Allocate the buffer used to pass lantern info + ray trace indirect parameters to ray tracer. +// Fill in the lantern info from m_lanterns (indirect info is filled per-frame on device +// using a compute shader). Must be called only after TLAS build. +// +// The buffer is an array of LanternIndirectEntry, entry i is for m_lanterns[i]. +void HelloVulkan::createLanternIndirectBuffer() +{ + assert(m_lanternCount > 0); + assert(m_lanternCount == m_lanterns.size()); + + // m_alloc behind the scenes uses cmdBuf to transfer data to the buffer. + nvvk::CommandPool cmdBufGet(m_device, m_graphicsQueueIndex); + vk::CommandBuffer cmdBuf = cmdBufGet.createCommandBuffer(); + + using Usage = vk::BufferUsageFlagBits; + m_lanternIndirectBuffer = + m_alloc.createBuffer(sizeof(LanternIndirectEntry) * m_lanternCount, + Usage::eIndirectBuffer | Usage::eTransferDst + | Usage::eShaderDeviceAddress | Usage::eStorageBuffer, + vk::MemoryPropertyFlagBits::eDeviceLocal); + + std::vector entries(m_lanternCount); + for (size_t i = 0; i < m_lanternCount; ++i) entries[i].lantern = m_lanterns[i]; + cmdBuf.updateBuffer(m_lanternIndirectBuffer.buffer, 0, entries.size() * sizeof entries[0], entries.data()); + + cmdBufGet.submitAndWait(cmdBuf); +} +```` + +Call this function in `main.cpp`, after the AS build (the AS build will be modified later). + +```` C + helloVk.initRayTracing(); + helloVk.createBottomLevelAS(); + helloVk.createTopLevelAS(); + helloVk.createLanternIndirectBuffer(); +```` + +## Set up Compute Shader + +The compute shader will need the view and projection matrices, plus the Z near plane, +screen dimensions, and length of the `LanternIndirectBuffer` array, in order to +compute the scissor rectangles. Feed all of this in with a push constant, declared +in `hello_vulkan.h` + +```` C + // Push constant for compute shader filling lantern indirect buffer. + // Barely fits in 128-byte push constant limit guaranteed by spec. + struct LanternIndirectPushConstants + { + nvmath::vec4 viewRowX; // First 3 rows of view matrix. + nvmath::vec4 viewRowY; // Set w=1 implicitly in shader. + nvmath::vec4 viewRowZ; + + nvmath::mat4 proj; // Perspective matrix + float nearZ; // Near plane used to create projection matrix. + + // Pixel dimensions of output image (needed to scale NDC to screen coordinates). + int32_t screenX; + int32_t screenY; + + // Length of the LanternIndirectEntry array. + int32_t lanternCount; + } m_lanternIndirectPushConstants; +```` + +!!! NOTE Push Constant Limit + The Vulkan spec only guarantees 128 bytes of push constant, so to make everything + fit, we have to chop off the implicit bottom row of the view matrix. + +This push constant is consumed in the compute shader `shaders/lanternIndirect.comp`. +We go through `lanternCount` iterations of a loop that fill in the scissor +rectangle for each `LanternIndirectEntry` (splitting the work among 128 +invocations of the work group). + +```` C +#version 460 +#extension GL_GOOGLE_include_directive : enable + +// Compute shader for filling in raytrace indirect parameters for each lantern +// based on the current camera position (passed as view and proj matrix in +// push constant). +// +// Designed to be dispatched with only one work group; it alone fills in +// the entire lantern array (of length lanternCount, in also push constant). + +#define LOCAL_SIZE 128 +layout(local_size_x = LOCAL_SIZE, local_size_y = 1, local_size_z = 1) in; + +#include "LanternIndirectEntry.glsl" + +layout(binding = 0, set = 0) buffer LanternArray { LanternIndirectEntry lanterns[]; } lanterns; + +layout(push_constant) uniform Constants +{ + vec4 viewRowX; + vec4 viewRowY; + vec4 viewRowZ; + mat4 proj; + float nearZ; + int screenX; + int screenY; + int lanternCount; +} +pushC; + +// Copy the technique of "2D Polyhedral Bounds of a Clipped, +// Perspective-Projected 3D Sphere" M. Mara M. McGuire +// http://jcgt.org/published/0002/02/05/paper.pdf +// to compute a screen-space rectangle covering the given Lantern's +// light radius-of-effect. Result is in screen (pixel) coordinates. +void getScreenCoordBox(in LanternIndirectEntry lantern, out ivec2 lower, out ivec2 upper); + +// Use the xyz and radius of lanterns[i] plus the transformation matrices +// in pushC to fill in the offset and indirect parameters of lanterns[i] +// (defines the screen rectangle that this lantern's light is bounded in). +void fillIndirectEntry(int i) +{ + LanternIndirectEntry lantern = lanterns.lanterns[i]; + ivec2 lower, upper; + getScreenCoordBox(lantern, lower, upper); + + lanterns.lanterns[i].indirectWidth = max(0, upper.x - lower.x); + lanterns.lanterns[i].indirectHeight = max(0, upper.y - lower.y); + lanterns.lanterns[i].indirectDepth = 1; + lanterns.lanterns[i].offsetX = lower.x; + lanterns.lanterns[i].offsetY = lower.y; +} + +void main() +{ + for (int i = int(gl_LocalInvocationID.x); i < pushC.lanternCount; i += LOCAL_SIZE) + { + fillIndirectEntry(i); + } +} + +/** Center is in camera space */ +void getBoundingBox( + in vec3 center, + in float radius, + in float nearZ, + in mat4 projMatrix, + out vec2 ndc_low, + out vec2 ndc_high) { +```` +!!! TIP + Omitted code for computing scissor rectangles, taken from "2D Polyhedral Bounds of a Clipped, + Perspective-Projected 3D Sphere" by Michael Mara and Morgan McGuire. + http://jcgt.org/published/0002/02/05/paper.pdf +```` C +} + +void getScreenCoordBox(in LanternIndirectEntry lantern, out ivec2 lower, out ivec2 upper) +{ + vec4 lanternWorldCenter = vec4(lantern.x, lantern.y, lantern.z, 1); + vec3 center = vec3( + dot(pushC.viewRowX, lanternWorldCenter), + dot(pushC.viewRowY, lanternWorldCenter), + dot(pushC.viewRowZ, lanternWorldCenter)); + vec2 ndc_low, ndc_high; + float paperNearZ = -abs(pushC.nearZ); // Paper expected negative nearZ, took 2 days to figure out! + getBoundingBox(center, lantern.radius, paperNearZ, pushC.proj, ndc_low, ndc_high); + + // Convert NDC [-1,+1]^2 coordinates to screen coordinates, and clamp to stay in bounds. + + lower.x = clamp(int((ndc_low.x * 0.5 + 0.5) * pushC.screenX), 0, pushC.screenX); + lower.y = clamp(int((ndc_low.y * 0.5 + 0.5) * pushC.screenY), 0, pushC.screenY); + upper.x = clamp(int((ndc_high.x * 0.5 + 0.5) * pushC.screenX), 0, pushC.screenX); + upper.y = clamp(int((ndc_high.y * 0.5 + 0.5) * pushC.screenY), 0, pushC.screenY); +} +```` + +Now we just have to fill out the usual boilerplate for setting up the descriptor +set (passes the `LanternIndirectEntry` array) and compute pipeline. We only have +to allocate one descriptor as the `LanternIndirectEntry` array never changes. + +`hello_vulkan.h`: + +```` C + nvvk::DescriptorSetBindings m_lanternIndirectDescSetLayoutBind; + vk::DescriptorPool m_lanternIndirectDescPool; + vk::DescriptorSetLayout m_lanternIndirectDescSetLayout; + vk::DescriptorSet m_lanternIndirectDescSet; + vk::PipelineLayout m_lanternIndirectCompPipelineLayout; + vk::Pipeline m_lanternIndirectCompPipeline; +```` + +`hello_vulkan.cpp`: + +```` C +//-------------------------------------------------------------------------------------------------- +// The compute shader just needs read/write access to the buffer of LanternIndirectEntry. +void HelloVulkan::createLanternIndirectDescriptorSet() +{ + using vkDT = vk::DescriptorType; + using vkSS = vk::ShaderStageFlagBits; + using vkDSLB = vk::DescriptorSetLayoutBinding; + + // Lantern buffer (binding = 0) + m_lanternIndirectDescSetLayoutBind.addBinding( // + vkDSLB(0, vkDT::eStorageBuffer, 1, vkSS::eCompute)); + + m_lanternIndirectDescPool = m_lanternIndirectDescSetLayoutBind.createPool(m_device); + m_lanternIndirectDescSetLayout = m_lanternIndirectDescSetLayoutBind.createLayout(m_device); + m_lanternIndirectDescSet = + m_device.allocateDescriptorSets({m_lanternIndirectDescPool, 1, &m_lanternIndirectDescSetLayout})[0]; + + assert(m_lanternIndirectBuffer.buffer); + vk::DescriptorBufferInfo lanternBufferInfo{ + m_lanternIndirectBuffer.buffer, 0, m_lanternCount * sizeof(LanternIndirectEntry)}; + + std::vector writes; + writes.emplace_back(m_lanternIndirectDescSetLayoutBind.makeWrite(m_lanternIndirectDescSet, 0, &lanternBufferInfo)); + m_device.updateDescriptorSets(static_cast(writes.size()), writes.data(), 0, nullptr); +} + +// Create compute pipeline used to fill m_lanternIndirectBuffer with parameters +// for dispatching the correct number of ray traces. +void HelloVulkan::createLanternIndirectCompPipeline() +{ + // Compile compute shader and package as stage. + vk::ShaderModule computeShader = + nvvk::createShaderModule(m_device, // + nvh::loadFile("shaders/lanternIndirect.comp.spv", true, defaultSearchPaths, true)); + vk::PipelineShaderStageCreateInfo stageInfo; + stageInfo.setStage(vk::ShaderStageFlagBits::eCompute); + stageInfo.setModule(computeShader); + stageInfo.setPName("main"); + + // Set up push constant and pipeline layout. + constexpr auto pushSize = static_cast(sizeof(m_lanternIndirectPushConstants)); + vk::PushConstantRange pushCRange = {vk::ShaderStageFlagBits::eCompute, 0, pushSize}; + static_assert(pushSize <= 128, "Spec guarantees only 128 byte push constant"); + vk::PipelineLayoutCreateInfo layoutInfo; + layoutInfo.setSetLayoutCount(1); + layoutInfo.setPSetLayouts(&m_lanternIndirectDescSetLayout); + layoutInfo.setPushConstantRangeCount(1); + layoutInfo.setPPushConstantRanges(&pushCRange); + m_lanternIndirectCompPipelineLayout = m_device.createPipelineLayout(layoutInfo); + + // Create compute pipeline. + vk::ComputePipelineCreateInfo pipelineInfo; + pipelineInfo.setStage(stageInfo); + pipelineInfo.setLayout(m_lanternIndirectCompPipelineLayout); + m_lanternIndirectCompPipeline = static_cast(m_device.createComputePipeline({}, pipelineInfo)); + + m_device.destroy(computeShader); +} +```` + +`main.cpp` (add after indirect buffer initialization). + +```` C + // #VKRay + helloVk.initRayTracing(); + helloVk.createBottomLevelAS(); + helloVk.createTopLevelAS(); + helloVk.createLanternIndirectBuffer(); + helloVk.createRtDescriptorSet(); + helloVk.createRtPipeline(); + helloVk.createLanternIndirectDescriptorSet(); + helloVk.createLanternIndirectCompPipeline(); +```` + +## Call Compute Shader + +In `HelloVulkan::raytrace`, we have to fill in the earlier push constant and +dispatch the compute shader before moving +on to the actual ray tracing. This is rather verbose due to the need for a +pipeline barrier synchronizing access to the `LanternIndirectEntry` array +between the compute shader and indirect draw stages. + +```` C +void HelloVulkan::raytrace(const vk::CommandBuffer& cmdBuf, const nvmath::vec4f& clearColor) +{ + // Before tracing rays, we need to dispatch the compute shaders that + // fill in the ray trace indirect parameters for each lantern pass. + + // First, barrier before, ensure writes aren't visible to previous frame. + vk::BufferMemoryBarrier bufferBarrier; + bufferBarrier.setSrcAccessMask(vk::AccessFlagBits::eIndirectCommandRead); + bufferBarrier.setDstAccessMask(vk::AccessFlagBits::eShaderWrite); + bufferBarrier.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED); + bufferBarrier.setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED); + bufferBarrier.setBuffer(m_lanternIndirectBuffer.buffer); + bufferBarrier.offset = 0; + bufferBarrier.size = m_lanternCount * sizeof m_lanterns[0]; + cmdBuf.pipelineBarrier( // + vk::PipelineStageFlagBits::eDrawIndirect, // + vk::PipelineStageFlagBits::eComputeShader,// + vk::DependencyFlags(0), // + {}, {bufferBarrier}, {}); + + // Bind compute shader, update push constant and descriptors, dispatch compute. + cmdBuf.bindPipeline(vk::PipelineBindPoint::eCompute, m_lanternIndirectCompPipeline); + nvmath::mat4 view = getViewMatrix(); + m_lanternIndirectPushConstants.viewRowX = view.row(0); + m_lanternIndirectPushConstants.viewRowY = view.row(1); + m_lanternIndirectPushConstants.viewRowZ = view.row(2); + m_lanternIndirectPushConstants.proj = getProjMatrix(); + m_lanternIndirectPushConstants.nearZ = nearZ; + m_lanternIndirectPushConstants.screenX = m_size.width; + m_lanternIndirectPushConstants.screenY = m_size.height; + m_lanternIndirectPushConstants.lanternCount = m_lanternCount; + cmdBuf.pushConstants( + m_lanternIndirectCompPipelineLayout, + vk::ShaderStageFlagBits::eCompute, + 0, m_lanternIndirectPushConstants); + cmdBuf.bindDescriptorSets( + vk::PipelineBindPoint::eCompute, m_lanternIndirectCompPipelineLayout, 0, {m_lanternIndirectDescSet}, {}); + cmdBuf.dispatch(1, 1, 1); + + // Ensure compute results are visible when doing indirect ray trace. + bufferBarrier.setSrcAccessMask(vk::AccessFlagBits::eShaderWrite); + bufferBarrier.setDstAccessMask(vk::AccessFlagBits::eIndirectCommandRead); + cmdBuf.pipelineBarrier( // + vk::PipelineStageFlagBits::eComputeShader, // + vk::PipelineStageFlagBits::eDrawIndirect, // + vk::DependencyFlags(0), // + {}, {bufferBarrier}, {}); + + + // Now move on to the actual ray tracing. + m_debug.beginLabel(cmdBuf, "Ray trace"); +```` + +!!! TIP `eDrawIndirect` + `vk::PipelineStageFlagBits::eDrawIndirect` (`VK_PIPELINE_STAGE_DRAW_INDIRECT_BIT`) + covers the stage that sources indirect paramaters for compute and ray trace + indirect commands, not just graphics draw indirect commands. + +Since the near plane and view/projection matrices are used in multiple places now, +they were factored out to common code in `hello_vulkan.h`. + +```` C + nvmath::mat4 getViewMatrix() + { + return CameraManip.getMatrix(); + } + + static constexpr float nearZ = 0.1f; + nvmath::mat4 getProjMatrix() + { + const float aspectRatio = m_size.width / static_cast(m_size.height); + return nvmath::perspectiveVK(CameraManip.getFov(), aspectRatio, nearZ, 1000.0f); + } +```` + +The function for updating the uniform buffer is tweaked to match. + +```` C +void HelloVulkan::updateUniformBuffer(const vk::CommandBuffer& cmdBuf) +{ + const float aspectRatio = m_size.width / static_cast(m_size.height); + + CameraMatrices ubo = {}; + ubo.view = getViewMatrix(); + ubo.proj = getProjMatrix(); +```` + +# Lantern Acceleration Structures and Closest Hit Shader + +## Bottom-level Acceleration Structure + +Lanterns will be drawn as spheres approximated by a triangular mesh. Declare +in `hello_vulkan.h` functions for generating this mesh, and declare Vulkan +buffers for storing the mesh's positions and indices, and a `BlasInput` +for delivering this sphere mesh to the BLAS builder. + +```` C +private: + void fillLanternVerts(std::vector& vertices, std::vector& indices); + void createLanternModel(); + + // Used to store lantern model, generated at runtime. + const float m_lanternModelRadius = 0.125; + nvvk::Buffer m_lanternVertexBuffer; + nvvk::Buffer m_lanternIndexBuffer; + nvvk::RaytracingBuilderKHR::BlasInput m_lanternBlasInput{}; + + // Index of lantern's BLAS in the BLAS array stored in m_rtBuilder. + size_t m_lanternBlasId; +```` + +In order to focus on the ray tracing, I omit the code for generating those vertex and index +buffers. The relevent code in `HelloVulkan::createLanternModel` for creating the `BlasInput` is + +```` C + // Package vertex and index buffers as BlasInput. + vk::DeviceAddress vertexAddress = m_device.getBufferAddress({m_lanternVertexBuffer.buffer}); + vk::DeviceAddress indexAddress = m_device.getBufferAddress({m_lanternIndexBuffer.buffer}); + + uint32_t maxPrimitiveCount = uint32_t(indices.size() / 3); + + // Describe buffer as packed array of float vec3. + vk::AccelerationStructureGeometryTrianglesDataKHR triangles; + triangles.setVertexFormat(vk::Format::eR32G32B32Sfloat); // vec3 vertex position data. + triangles.setVertexData(vertexAddress); + triangles.setVertexStride(sizeof(nvmath::vec3f)); + // Describe index data (32-bit unsigned int) + triangles.setIndexType(vk::IndexType::eUint32); + triangles.setIndexData(indexAddress); + // Indicate identity transform by setting transformData to null device pointer. + triangles.setTransformData({}); + triangles.setMaxVertex(vertices.size()); + + // Identify the above data as containing opaque triangles. + vk::AccelerationStructureGeometryKHR asGeom; + asGeom.setGeometryType(vk::GeometryTypeKHR::eTriangles); + asGeom.setFlags(vk::GeometryFlagBitsKHR::eOpaque); + asGeom.geometry.setTriangles(triangles); + + // The entire array will be used to build the BLAS. + vk::AccelerationStructureBuildRangeInfoKHR offset; + offset.setFirstVertex(0); + offset.setPrimitiveCount(maxPrimitiveCount); + offset.setPrimitiveOffset(0); + offset.setTransformOffset(0); + + // Our blas is made from only one geometry, but could be made of many geometries + m_lanternBlasInput.asGeometry.emplace_back(asGeom); + m_lanternBlasInput.asBuildOffsetInfo.emplace_back(offset); +```` + +The principle difference from before is that the vertex array is now a packed array of +float 3-vectors, hence, we call `triangles.setVertexStride(sizeof(nvmath::vec3f));`. + +Then, we add a call to create a lantern model and add the lantern model to the list of +BLAS to build in `HelloVulkan::createBottomLevelAS`. Since we'll need the index of +the lantern BLAS later to add lantern instances in the TLAS build, store the +BLAS index for the lantern in `m_lanternBlasId`. + +```` C +// Build the array of BLAS in m_rtBuilder. There are `m_objModel.size() + 1`-many BLASes. +// The first `m_objModel.size()` are used for OBJ model BLASes, and the last one +// is used for the lanterns (model generated at runtime). +void HelloVulkan::createBottomLevelAS() +{ + // BLAS - Storing each primitive in a geometry + std::vector allBlas; + allBlas.reserve(m_objModel.size() + 1); + + // Add OBJ models. + for(const auto& obj : m_objModel) + { + auto blas = objectToVkGeometryKHR(obj); + + // We could add more geometry in each BLAS, but we add only one for now + allBlas.emplace_back(blas); + } + + // Add lantern model. + createLanternModel(); + m_lanternBlasId = allBlas.size(); + allBlas.emplace_back(m_lanternBlasInput); + + m_rtBuilder.buildBlas(allBlas, vk::BuildAccelerationStructureFlagBitsKHR::ePreferFastTrace); +} +```` + +## Top-level acceleration structure + +In the TLAS build function, we add a loop for adding each lantern instance. This is also +the point that the lanterns are set-in-stone (no more modifications to `m_lanterns`), so +write `m_lanternCount`. + +```` C +// Build the TLAS in m_rtBuilder. Requires that the BLASes were already built and +// that all ObjInstance and lanterns have been added. One instance with hitGroupId=0 +// is created for every OBJ instance, and one instance with hitGroupId=1 for each lantern. +// +// gl_InstanceCustomIndexEXT will be the index of the instance or lantern in m_objInstance or +// m_lanterns respectively. +void HelloVulkan::createTopLevelAS() +{ + assert(m_lanternCount == 0); + m_lanternCount = m_lanterns.size(); + + std::vector tlas; + tlas.reserve(m_objInstance.size() + m_lanternCount); + + // Add the OBJ instances. + for(uint32_t i = 0; i < static_cast(m_objInstance.size()); i++) + { + nvvk::RaytracingBuilderKHR::Instance rayInst; + rayInst.transform = m_objInstance[i].transform; // Position of the instance + rayInst.instanceCustomId = i; // gl_InstanceCustomIndexEXT + rayInst.blasId = m_objInstance[i].objIndex; + rayInst.hitGroupId = 0; // We will use the same hit group for all OBJ + rayInst.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR; + tlas.emplace_back(rayInst); + } + + // Add lantern instances. + for(int i = 0; i < static_cast(m_lanterns.size()); ++i) + { + nvvk::RaytracingBuilderKHR::Instance lanternInstance; + lanternInstance.transform = nvmath::translation_mat4(m_lanterns[i].position); + lanternInstance.instanceCustomId = i; + lanternInstance.blasId = m_lanternBlasId; + lanternInstance.hitGroupId = 1; // Next hit group is for lanterns. + lanternInstance.flags = VK_GEOMETRY_INSTANCE_TRIANGLE_FACING_CULL_DISABLE_BIT_KHR; + tlas.emplace_back(lanternInstance); + } + + m_rtBuilder.buildTlas(tlas, vk::BuildAccelerationStructureFlagBitsKHR::ePreferFastTrace); +} +```` + +The principle differences are: + +* `instanceCustomId` is set to the index of the lantern in `m_lanternIndirectBuffer`, so we + can look up the lantern color in the forthcoming closest hit shader. + +* `hitGroupId` is set to `1`, so that lanterns will use a new closest hit shader instead + of the old one for OBJs. + +!!! TIP Helper Reminders + `instanceCustomId` corresponds to `VkAccelerationStructureInstanceKHR::instanceCustomIndex` in host + code and `gl_InstanceCustomIndexEXT` in shader code. + + `hitGroupId` corresponds to `VkAccelerationStructureInstanceKHR::instanceShaderBindingTableRecordOffset`. + + `blasId` has no Vulkan equivalent; it is translated to a BLAS device address in the `m_rtBuilder` helper. + +## Lantern Primary Ray Closest Hit Shader + +We now implement the closest hit shader for lanterns hit by primary rays (rays +cast starting from the eye). First, we need to do a bit of preparation: + +* Add a bool to `hitPayload` to control whether additive blending is enabled or + not. The lanterns will be drawn at a constant brightness, so additive blending + is enabled for rays hitting OBJ instances and disabled for rays hitting lanterns. + The raygen shader will be updated later to take this bool into account. + +* Access the GLSL definition of `LanternIndirectEntry` so we can look up the lantern color. + +* Add a descriptor set to the raytrace pipeline to deliver the + +We do the first two tasks in `raycommon.glsl`. + +```` C +#include "LanternIndirectEntry.glsl" + +struct hitPayload +{ + vec3 hitValue; + bool additiveBlending; +}; +```` + +The last task is done in `HelloVulkan::createRtDescriptorSet` + +```` C +// This descriptor set holds the Acceleration structure, output image, and lanterns array buffer. +// +void HelloVulkan::createRtDescriptorSet() +{ + using vkDT = vk::DescriptorType; + using vkSS = vk::ShaderStageFlagBits; + using vkDSLB = vk::DescriptorSetLayoutBinding; + + // ... + + // Lantern buffer (binding = 2) + m_rtDescSetLayoutBind.addBinding( // + vkDSLB(2, vkDT::eStorageBuffer, 1, vkSS::eRaygenKHR | vkSS::eClosestHitKHR)); + assert(m_lanternCount > 0); + + // ... + + std::vector writes; + + // ... + + writes.emplace_back(m_rtDescSetLayoutBind.makeWrite(m_rtDescSet, 2, &lanternBufferInfo)); + m_device.updateDescriptorSets(static_cast(writes.size()), writes.data(), 0, nullptr); +} +```` + +Now we can implement the new closest hit shader. Name this shader `lantern.rchit`. + +```` C +#version 460 +#extension GL_EXT_ray_tracing : require +#extension GL_EXT_nonuniform_qualifier : enable +#extension GL_EXT_scalar_block_layout : enable +#extension GL_GOOGLE_include_directive : enable +#include "raycommon.glsl" + +// Closest hit shader invoked when a primary ray hits a lantern. + +// clang-format off +layout(location = 0) rayPayloadInEXT hitPayload prd; + +layout(binding = 2, set = 0) buffer LanternArray { LanternIndirectEntry lanterns[]; } lanterns; + +// clang-format on + +void main() +{ + // Just look up this lantern's color. Self-illuminating, so no lighting calculations. + LanternIndirectEntry lantern = lanterns.lanterns[nonuniformEXT(gl_InstanceCustomIndexEXT)]; + prd.hitValue = vec3(lantern.red, lantern.green, lantern.blue); + prd.additiveBlending = false; +} +```` + +This shader is fairly simple, we just had to look up the lantern color and return it in the +payload. Here, we used the fact that in the TLAS instances setup, we set a lantern instance's +`gl_InstanceCustomIndexEXT` to its position in the lanterns array. + +Now we just have to add the new hit group to the pipeline. This is more of the same, +in the `HelloVulkan::createRtPipeline` function, we add the lantern closest hit +group after the OBJ hit group, to match the `hitGroupId`s assigned earlier in the +TLAS build. + +```` C + // OBJ Primary Ray Hit Group - Closest Hit + AnyHit (not used) + vk::ShaderModule chitSM = + nvvk::createShaderModule(m_device, // + nvh::loadFile("shaders/raytrace.rchit.spv", true, paths, true)); + + vk::RayTracingShaderGroupCreateInfoKHR hg{vk::RayTracingShaderGroupTypeKHR::eTrianglesHitGroup, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR}; + hg.setClosestHitShader(static_cast(stages.size())); + stages.push_back({{}, vk::ShaderStageFlagBits::eClosestHitKHR, chitSM, "main"}); + m_rtShaderGroups.push_back(hg); + + // Lantern Primary Ray Hit Group + vk::ShaderModule lanternChitSM = + nvvk::createShaderModule(m_device, // + nvh::loadFile("shaders/lantern.rchit.spv", true, paths, true)); + + vk::RayTracingShaderGroupCreateInfoKHR lanternHg{ + vk::RayTracingShaderGroupTypeKHR::eTrianglesHitGroup, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR}; + lanternHg.setClosestHitShader(static_cast(stages.size())); + stages.push_back({{}, vk::ShaderStageFlagBits::eClosestHitKHR, lanternChitSM, "main"}); + m_rtShaderGroups.push_back(lanternHg); + + // ... + + m_device.destroy(lanternChitSM); +```` + +We don't have to modify `HelloVulkan::createRtShaderBindingTable`. Changes to the number of +group handles to copy into the SBT are picked up automatically from `m_rtShaderGroups.size()`. + +# Ray Generation Shader + +## Draw Within Scissor Rectangle + +The original ray generation shader assumed that `gl_LaunchSizeEXT` is the size of the entire +screen. As this is no longer the case for scissor rectangles, we communicate the screen +size through push constant instead. In addition, we also add to the push constants a number +indicating which lantern pass is currently being drawn (-1 for the original full screen pass). + +Modify `m_rtPushConstants` in `hello_vulkan.h`. + +```` C + // Push constant for ray trace pipeline. + struct RtPushConstant + { + // Background color + nvmath::vec4f clearColor; + + // Information on the light in the sky used when lanternPassNumber = -1. + nvmath::vec3f lightPosition; + float lightIntensity; + int32_t lightType; + + // -1 if this is the full-screen pass. Otherwise, this pass is to add light + // from lantern number lanternPassNumber. We use this to lookup trace indirect + // parameters in m_lanternIndirectBuffer. + int32_t lanternPassNumber; + + // Pixel dimensions of the output image. + int32_t screenX; + int32_t screenY; + + // See m_lanternDebug. + int32_t lanternDebug; + } m_rtPushConstants; +```` + +We also update the GLSL push constant to match. Since the raygen shader now needs +access to the push constant, move the push constant definition from `raytrace.rchit` +to `raycommon.glsl`. + +```` C +layout(push_constant) uniform Constants +{ + vec4 clearColor; + vec3 lightPosition; + float lightIntensity; + int lightType; // 0: point, 1: infinite + int lanternPassNumber; // -1 if this is the full-screen pass. Otherwise, used to lookup trace indirect parameters. + int screenX; + int screenY; + int lanternDebug; +} +pushC; +```` + +(`lanternDebug` will be used later to toggle visualising the scissor rectangles) + + +This move also requires us to tweak `raytrace.rmiss`. + +```` +#version 460 +#extension GL_EXT_ray_tracing : require +#extension GL_GOOGLE_include_directive : enable +#include "raycommon.glsl" + +layout(location = 0) rayPayloadInEXT hitPayload prd; + +void main() +{ + prd.hitValue = pushC.clearColor.xyz * 0.8; + prd.additiveBlending = false; +} +```` + +We will cover initializing the new push constants later, when we look at `vkCmdTraceRaysIndirectKHR`. + +In `raytrace.rgen`, we have to replace the old code for calculating the pixel center. + +```` C +void main() +{ + const vec2 pixelCenter = vec2(gl_LaunchIDEXT.xy) + vec2(0.5); + const vec2 inUV = pixelCenter / vec2(gl_LaunchSizeEXT.xy); +```` + +with + +```` C +layout(binding = 2, set = 0) buffer LanternArray { LanternIndirectEntry lanterns[]; } lanterns; + +void main() +{ + // Global light pass is a full screen rectangle (lower corner 0,0), but + // lantern passes are only run within rectangles that may be offset. + ivec2 pixelOffset = ivec2(0); + if (pushC.lanternPassNumber >= 0) + { + pixelOffset.x = lanterns.lanterns[pushC.lanternPassNumber].offsetX; + pixelOffset.y = lanterns.lanterns[pushC.lanternPassNumber].offsetY; + } + + const ivec2 pixelIntCoord = ivec2(gl_LaunchIDEXT.xy) + pixelOffset; + const vec2 pixelCenter = vec2(pixelIntCoord) + vec2(0.5); + const vec2 inUV = pixelCenter / vec2(pushC.screenX, pushC.screenY); + vec2 d = inUV * 2.0 - 1.0; +```` + +Let's recap why this works. If `pushC.lanternPassNumber` is negative, we're drawing +the first, full-screen pass, and this code behaves identically as before, except +that `inUV` performs division by `(pushC.screenX, pushC.screenY)` instead of +relying on `gl_LaunchSizeEXT` to be the screen size. + +Otherwise (`pushC.lanternPassNumber >= 0`), we're drawing a scissor rectangle for +the given lantern number. Look up that lantern's `LanternIndirectEntry` in the +array (notice that the descriptor binding for it is added). Its scissor rectangle +is defined by: + +* `LanternIndirectEntry::offsetX`,`offsetY`: the pixel coordinate of the scissor box's + upper-left. + +* `LanternIndirectEntry::width`,`height`: the dimensions of the scissor box (not + directly used here; consumed by `vkCmdTraceRaysIndirectKHR`). + +The `gl_LaunchIDEXT` variable ranges from `(0,0)` to `(width-1, height-1)`, so to +cover the correct pixels within the scissor, we just have to reposition +`gl_LaunchIDEXT` by the offset `(offsetX, offsetY)`. + +## Additive Blending + +We also have to emulate additive blending. Instead of always writing to the output +image: + +```` C + imageStore(image, ivec2(gl_LaunchIDEXT.xy), vec4(prd.hitValue, 1.0)); +```` + +we do + +```` C + // Either add to or replace output image color based on prd.additiveBlending. + // Global pass always replaces color as it is the first pass. + vec3 oldColor = vec3(0); + if (prd.additiveBlending && pushC.lanternPassNumber >= 0) { + oldColor = imageLoad(image, pixelIntCoord).rgb; + } + imageStore(image, pixelIntCoord, vec4(prd.hitValue + oldColor, 1.0)); +```` + +thus adding the ray payload's color to the old image color if `prd.additiveBlending` +is true and this is not the first, full-screen pass (the first pass must replace the +output image color as its existing contents are garbage). + +# Lantern Shadow Rays + +We now have to set up a system for casting shadow rays from the OBJ closest hit +shader to the lanterns. This requires us to + +* Detect in `raycast.rchit` whether we are in a lantern pass, and use this + to decide between casting shadow rays to the main light (as in the base + tutorial) or casting shadow rays to a lantern. + +* Declare a payload for which lantern (if any) was hit, and add a new miss shader + and two new closest hit shaders for filling that payload. + +* Use the `sbtRecordOffset` parameter of `traceRayEXT` to skip over the earlier + hit groups. + +## New payload + +In `raytrace.rchit` (called when an OBJ instance is hit by a primary ray), declare +the new payload and the array of lanterns. + +```` C +layout(location = 2) rayPayloadEXT int hitLanternInstance; + +layout(binding = 0, set = 0) uniform accelerationStructureEXT topLevelAS; +layout(binding = 2, set = 0) buffer LanternArray { LanternIndirectEntry lanterns[]; } lanterns; +```` + +## New shaders + +We need a few simple shaders to report the number of the lantern hit (if any) by the shadow ray. +First is the miss shader, `lanternShadow.rmiss`. + +```` C +// Miss shader invoked when tracing shadow rays (rays towards lantern) +// in lantern passes. Misses shouldn't really happen, but if they do, +// report we did not hit any lantern by setting hitLanternInstance = -1. +layout(location = 2) rayPayloadInEXT int hitLanternInstance; + +void main() +{ + hitLanternInstance = -1; +} +```` + +Then a closest hit shader for OBJ instances hit by a lantern shadow ray. +This also returns `-1` for "no lantern". Call this `lanternShadowObj.rchit`. + +```` C +#version 460 +#extension GL_EXT_ray_tracing : require +#extension GL_GOOGLE_include_directive : enable +#include "raycommon.glsl" + +// During a lantern pass, this closest hit shader is invoked when +// shadow rays (rays towards lantern) hit a regular OBJ. Report back +// that no lantern was hit (-1). + +// clang-format off +layout(location = 2) rayPayloadInEXT int hitLanternInstance; + +// clang-format on + +void main() +{ + hitLanternInstance = -1; +} +```` + +Finally, a closest hit shader for lantern instances, named `lanternShadowLantern.rchit`. + +```` C +#version 460 +#extension GL_EXT_ray_tracing : require +#extension GL_GOOGLE_include_directive : enable +#include "raycommon.glsl" + +// During a lantern pass, this closest hit shader is invoked when +// shadow rays (rays towards lantern) hit a lantern. Report back +// which lantern was hit. + +// clang-format off +layout(location = 2) rayPayloadInEXT int hitLanternInstance; + +// clang-format on + +void main() +{ + hitLanternInstance = gl_InstanceCustomIndexEXT; +} +```` + +Note that we really need to report back the lantern number, and +not just a boolean "lantern hit" flag. In order to have lanterns cast +shadows on each other, we must be able to detect that the shadow ray +hit the "wrong" lantern. + +![](Images/indirect_scissor/rgb.png) + +## Add Shaders to Pipeline + +We add the new miss shader as miss shader 2 in the SBT, and the closest hit +shaders as hit groups 2 and 3 in the SBT, following the earlier 2 hit +groups for primary rays. Add the following code to `HelloVulkan::createRtPipeline` +after loading `raytraceShadow.rmiss.spv`. + +```` C + // Miss shader 2 is invoked when a shadow ray for lantern lighting misses the + // lantern. It shouldn't be invoked, but I include it just in case. + vk::ShaderModule lanternmissSM = nvvk::createShaderModule( + m_device, nvh::loadFile("shaders/lanternShadow.rmiss.spv", true, paths, true)); +```` + +and add this code for loading the last 2 closest hit shaders after loading +`lantern.rchit.spv`: + +```` C + // OBJ Lantern Shadow Ray Hit Group + vk::ShaderModule lanternShadowObjChitSM = + nvvk::createShaderModule(m_device, // + nvh::loadFile("shaders/lanternShadowObj.rchit.spv", true, paths, true)); + + vk::RayTracingShaderGroupCreateInfoKHR lanternShadowObjHg{ + vk::RayTracingShaderGroupTypeKHR::eTrianglesHitGroup, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR}; + lanternShadowObjHg.setClosestHitShader(static_cast(stages.size())); + stages.push_back({{}, vk::ShaderStageFlagBits::eClosestHitKHR, lanternShadowObjChitSM, "main"}); + m_rtShaderGroups.push_back(lanternShadowObjHg); + + // Lantern Lantern Shadow Ray Hit Group + vk::ShaderModule lanternShadowLanternChitSM = + nvvk::createShaderModule(m_device, // + nvh::loadFile("shaders/lanternShadowLantern.rchit.spv", true, paths, true)); + + vk::RayTracingShaderGroupCreateInfoKHR lanternShadowLanternHg{ + vk::RayTracingShaderGroupTypeKHR::eTrianglesHitGroup, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR, + VK_SHADER_UNUSED_KHR, VK_SHADER_UNUSED_KHR}; + lanternShadowLanternHg.setClosestHitShader(static_cast(stages.size())); + stages.push_back({{}, vk::ShaderStageFlagBits::eClosestHitKHR, lanternShadowLanternChitSM, "main"}); + m_rtShaderGroups.push_back(lanternShadowLanternHg); +```` + +We need to destroy the added shader modules at the end of the function. + +```` C + m_device.destroy(shadowmissSM); + // ... + m_device.destroy(lanternShadowObjChitSM); + m_device.destroy(lanternShadowLanternChitSM); +```` + +Through all this, we still load shader stages in the same order as they will appear +in the SBT in order to keep things simple (note `stages.size()`). Add a comment +at the top of this function to help us keep track of all the new shaders. + +```` C +// Shader list: +// +// 0 ====== Ray Generation Shaders ===================================================== +// +// Raygen shader: Ray generation shader. Casts primary rays from camera to scene. +// +// 1 ====== Miss Shaders =============================================================== +// +// Miss shader 0: Miss shader when casting primary rays. Fill in clear color. +// +// 2 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// +// Miss shader 1: Miss shader when casting shadow rays towards main light. +// Reports no shadow. +// +// 3 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// +// Miss shader 2: Miss shader when casting shadow rays towards a lantern. +// Reports no lantern hit (-1). +// +// 4 ====== Hit Groups for Primary Rays (sbtRecordOffset=0) ============================ +// +// chit shader 0: Closest hit shader for primary rays hitting OBJ instances +// (hitGroupId=0). Casts shadow ray (to sky light or to lantern, +// depending on pass number) and returns specular +// and diffuse light to add to output image. +// +// 5 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// +// chit shader 1: Closest hit shader for primary rays hitting lantern instances +// (hitGroupId=1). Returns color value to replace the current +// image pixel color with (lanterns are self-illuminating). +// +// 6 - - - - Hit Groups for Lantern Shadow Rays (sbtRecordOffset=2) - - - - - - - - - - - +// +// chit shader 2: Closest hit shader for OBJ instances hit when casting shadow +// rays to a lantern. Returns -1 to report that the shadow ray +// failed to reach the targetted lantern. +// +// 7 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +// +// chit shader 3: Closest hit shader for lantern instances hit when casting shadow +// rays to a lantern. Returns the gl_CustomInstanceIndexEXT [lantern +// number] of the lantern hit. +// +// 8 ===================================================================================== +```` + +## Compute Lighting Intensity + +Because the lanterns have color, we have to replace the scalar `lightIntensity` +in `raytrace.rchit` with an RGB `colorIntensity`. + +```` C + // Vector toward the light + vec3 L; + vec3 colorIntensity = vec3(pushC.lightIntensity); + float lightDistance = 100000.0; +```` + +Then, we have to check if we're in a lantern pass (`lanternPassNumber >= 0`). +If so, look up the lantern location in the `LanternIndirectEntry` array, +and compute the light direction and intensity based on that position. + +```` C + // ray direction is towards lantern, if in lantern pass. + if (pushC.lanternPassNumber >= 0) + { + LanternIndirectEntry lantern = lanterns.lanterns[pushC.lanternPassNumber]; + vec3 lDir = vec3(lantern.x, lantern.y, lantern.z) - worldPos; + lightDistance = length(lDir); + vec3 color = vec3(lantern.red, lantern.green, lantern.blue); + // Lantern light decreases linearly. Not physically accurate, but looks good + // and avoids a hard "edge" at the radius limit. Use a constant value + // if lantern debug is enabled to clearly see the covered screen rectangle. + float distanceFade = + pushC.lanternDebug != 0 + ? 0.3 + : max(0, (lantern.radius - lightDistance) / lantern.radius); + colorIntensity = color * lantern.brightness * distanceFade; + L = normalize(lDir); + } +```` + +otherwise, do the old lighting calculations, except we again have to +replace `float lightIntensity` with `vec3 colorIntensity`. + +```` C + // Non-lantern pass may have point light... + else if(pushC.lightType == 0) + { + vec3 lDir = pushC.lightPosition - worldPos; + lightDistance = length(lDir); + colorIntensity = vec3(pushC.lightIntensity / (lightDistance * lightDistance)); + L = normalize(lDir); + } + else // or directional light. + { + L = normalize(pushC.lightPosition - vec3(0)); + } +```` + +!!! NOTE `lanternDebug` + When `lanternDebug` is on, I disable diminishing lighting with distance, so + that the light will reach the edge of the scissor box, making the scissor + box easy to see. To toggle this variable, I declare `bool m_lanternDebug` + in `hello_vulkan.h`, and allow ImGui to control it: + + ```` C + void renderUI(HelloVulkan& helloVk) + { + ImGuiH::CameraWidget(); + if(ImGui::CollapsingHeader("Light")) + { + // ... + ImGui::Checkbox("Lantern Debug", &helloVk.m_lanternDebug); + } + } + ```` + + Then, every frame I copy `m_lanternDebug` to the push constant. The reason + I cannot directly modify the push constant through ImGui is that ImGui expects + a `bool` (usually 8-bits) while Vulkan expects a 32-bit boolean. + +## Casting Lantern Shadow Rays + +Use an `if` to ensure the original shadow rays are cast only in the non-lantern pass. + +```` C + // Ordinary shadow from the simple tutorial. + if (pushC.lanternPassNumber < 0) { + isShadowed = true; + uint flags = gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsOpaqueEXT + | gl_RayFlagsSkipClosestHitShaderEXT; + traceRayEXT(topLevelAS, // acceleration structure + flags, // rayFlags + 0xFF, // cullMask + 0, // sbtRecordOffset + 0, // sbtRecordStride + 1, // missIndex + origin, // ray origin + tMin, // ray min range + rayDir, // ray direction + tMax, // ray max range + 1 // payload (location = 1) + ); + } +```` + +Otherwise, we cast a ray towards a lantern. This ray is different in that + +* We actually need the closest hit shaders to run to return `hitLanternInstance`, + so do not provide the flags + ` gl_RayFlagsTerminateOnFirstHitEXT | gl_RayFlagsSkipClosestHitShaderEXT`. + +* Use miss shader 2, which we added earlier. + +* Pass 2 as `sbtRecordOffset`, so that the closest hit shaders we just added (number 2 and 3) + are used when hitting OBJ instances (`hitGroupId=0`) and lantern instances (`hitGroupId=1`) + respectively. + +The code is + +```` C + // Lantern shadow ray. Cast a ray towards the lantern whose lighting is being + // added this pass. Only the closest hit shader for lanterns will set + // hitLanternInstance (payload 2) to non-negative value. + else { + // Skip ray if no light would be added anyway. + if (colorIntensity == vec3(0)) { + isShadowed = true; + } + else { + uint flags = gl_RayFlagsOpaqueEXT; + hitLanternInstance = -1; + traceRayEXT(topLevelAS, // acceleration structure + flags, // rayFlags + 0xFF, // cullMask + 2, // sbtRecordOffset : lantern shadow hit groups start at index 2. + 0, // sbtRecordStride + 2, // missIndex : lantern shadow miss shader is number 2. + origin, // ray origin + tMin, // ray min range + rayDir, // ray direction + tMax, // ray max range + 2 // payload (location = 2) + ); + // Did we hit the lantern we expected? + isShadowed = (hitLanternInstance != pushC.lanternPassNumber); + } + } +```` + +Notice that we determine whether this lantern is shadowed at this pixel by +checking if the hit lantern number matches the lantern whose light is being +added this pass; again, this ensures lanterns correctly shadow each others' light. + +## Write Payload + +Replace `lightIntensity` with `colorIntensity` and ask the raygen shader +for additive blending. + +```` C + prd.hitValue = colorIntensity * (attenuation * (diffuse + specular)); + prd.additiveBlending = true; +```` + +# Trace Rays Indirect + +Everything is finally set up to actually run the extra lantern passes +in `HelloVulkan::raytrace`. We've already dispatched the compute +shader in an earlier section. After that, we can run the first raytrace +pass. There are minimal changes from before, we just have to + +* Initialize the new push constant values (especially setting + `lanternPassNumber=-1` to indicate this is not a lantern pass). + +```` C + // Initialize push constant values + m_rtPushConstants.clearColor = clearColor; + m_rtPushConstants.lightPosition = m_pushConstant.lightPosition; + m_rtPushConstants.lightIntensity = m_pushConstant.lightIntensity; + m_rtPushConstants.lightType = m_pushConstant.lightType; + m_rtPushConstants.lanternPassNumber = -1; // Global non-lantern pass + m_rtPushConstants.screenX = m_size.width; + m_rtPushConstants.screenY = m_size.height; + m_rtPushConstants.lanternDebug = m_lanternDebug; +```` + +* Update the addresses of the raygen, miss, and hit group sections of the SBT + to account for the added shaders. + +```` C + using Stride = vk::StridedDeviceAddressRegionKHR; + std::array strideAddresses{ + Stride{sbtAddress + 0u * groupSize, groupStride, groupSize * 1}, // raygen + Stride{sbtAddress + 1u * groupSize, groupStride, groupSize * 3}, // miss + Stride{sbtAddress + 4u * groupSize, groupStride, groupSize * 4}, // hit + Stride{0u, 0u, 0u}}; // callable + + // First pass, illuminate scene with global light. + cmdBuf.traceRaysKHR( + &strideAddresses[0], &strideAddresses[1], // + &strideAddresses[2], &strideAddresses[3], // + m_size.width, m_size.height, 1); +```` + +After that, we can open a loop for performing all lantern passes. + +```` C + // Lantern passes, ensure previous pass completed, then add light contribution from each lantern. + for (int i = 0; i < static_cast(m_lanternCount); ++i) + { +```` + +Because the additive blending in the shader requires read-modify-write operations, +we need a barrier between every pass. + +```` C + // Barrier to ensure previous pass finished. + vk::Image offscreenImage{m_offscreenColor.image}; + vk::ImageSubresourceRange colorRange( + vk::ImageAspectFlagBits::eColor, 0, VK_REMAINING_MIP_LEVELS, 0, VK_REMAINING_ARRAY_LAYERS + ); + vk::ImageMemoryBarrier imageBarrier; + imageBarrier.setOldLayout(vk::ImageLayout::eGeneral); + imageBarrier.setNewLayout(vk::ImageLayout::eGeneral); + imageBarrier.setSrcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED); + imageBarrier.setDstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED); + imageBarrier.setImage(offscreenImage); + imageBarrier.setSubresourceRange(colorRange); + imageBarrier.setSrcAccessMask(vk::AccessFlagBits::eShaderWrite); + imageBarrier.setDstAccessMask(vk::AccessFlagBits::eShaderRead); + cmdBuf.pipelineBarrier( + vk::PipelineStageFlagBits::eRayTracingShaderKHR, // + vk::PipelineStageFlagBits::eRayTracingShaderKHR, // + vk::DependencyFlags(0), // + {}, {}, {imageBarrier}); +```` + +Then, we can pass the number of the lantern pass being performed (`i`), and look +up the indirect parameters for that entry. Unlike draw and dispatch indirect, the +indirect paramater location is passed as a raw device address, so we need to +perform manual device pointer arithmetic to look up the $i^{th}$ entry of the +`LanternIndirectEntry` array. Take advantage of the fact that `VkTraceRaysIndirectCommandKHR` +is the first member of `LanternIndirectEntry`. + +```` C + // Set lantern pass number. + m_rtPushConstants.lanternPassNumber = i; + cmdBuf.pushConstants(m_rtPipelineLayout, + vk::ShaderStageFlagBits::eRaygenKHR + | vk::ShaderStageFlagBits::eClosestHitKHR + | vk::ShaderStageFlagBits::eMissKHR, + 0, m_rtPushConstants); + + // Execute lantern pass. + cmdBuf.traceRaysIndirectKHR( + &strideAddresses[0], &strideAddresses[1], // + &strideAddresses[2], &strideAddresses[3], // + m_device.getBufferAddress({m_lanternIndirectBuffer.buffer}) + i * sizeof(LanternIndirectEntry)); + } +```` + +Everything should be in order now. We can see in this image that the cyan and purple lanterns +are both shadowed by the doodad hanging off the side of the building, and the spikes on the +roof cut shadows in the yellow lantern's light. + +![](Images/indirect_scissor/shadows.png) + +Zoom out and enable the lantern debug checkbox to see the scissor rectangles. + +![](Images/indirect_scissor/bounding2.png) + +## Cleanup + +One last loose end, we have to clean up all the new resources is `HelloVulkan::destroyResources`. + +```` C +// Destroying all allocations +// +void HelloVulkan::destroyResources() +{ + // ... + + // #VKRay + // ... + m_device.destroy(m_lanternIndirectDescPool); + m_device.destroy(m_lanternIndirectDescSetLayout); + m_device.destroy(m_lanternIndirectCompPipeline); + m_device.destroy(m_lanternIndirectCompPipelineLayout); + m_alloc.destroy(m_lanternIndirectBuffer); + m_alloc.destroy(m_lanternVertexBuffer); + m_alloc.destroy(m_lanternIndexBuffer); +} +```` + +# Final Code + +You can find the final code in the folder [ray_tracing_indirect_scissor](https://github.com/nvpro-samples/vk_raytracing_tutorial_KHR/tree/master/ray_tracing_indirect_scissor) + + + + + + + +