-
Notifications
You must be signed in to change notification settings - Fork 7
Raytracing
Hardware raytracing is an optional feature, that can be integrated by specifying the GraphicsDeviceFeatures::RayTracing
build flag. The raytracing sample demonstrates how to use it. The following guide will lead you through the steps required to perform raytracing in more detail.
Hardware raytracing behaves different to rasterization in may ways, not only due to the way it calculates light transport, but on a lower level in the way it interacts with the GPU. In standard rasterization, only one pipeline state and consequentially only one shader can be active at a time, whilst with ray tracing, the active shader is selected depending on the geometry that has been hit. This requires the geometry to be carefully prepared to work with raytracing pipelines. Geometry for raytracing workloads is expressed in terms of acceleration structures. Those structures are optimized representations of the scene, kept on GPU memory that allow for efficient ray traversal. There are two types of acceleration structures: bottom-level and top-level, also referred to in a short form as BLAS and TLAS. Bottom-level acceleration structures describe the geometry, i.e. they contain mesh vertices and indices. On the other hand, top-level acceleration structures are also referred to as geometry instances. They reference a BLAS and assign it properties for ray traversal. Multiple top-level acceleration structures can reference the same bottom-level acceleration structure.
In LiteFX, acceleration structures are built in a multi-stage process. This process begins with first creating sets of BLAS and TLAS structures and assigning them with their respective data, i.e., defining geometries and geometry instances. Afterwards, acceleration structures need to be built. Building an acceleration structure means to transform them into a hardware-specific optimal representation for ray traversal. The result of this process is written into a buffer on the GPU. There is one such buffer for each acceleration structure and before being able to build them, those buffers must be allocated. This is done by calling IAccelerationStructure::allocateBuffers()
on them. The last step in this process is to actually perform the build. Building an acceleration structure is done directly on the GPU, so just like any other workload, it is an asynchronous process that is recorded onto a command buffer, in this case using ICommandBuffer::buildAccelerationStructure
. This process takes an additional temporary buffer called scratch buffer, that can be released afterwards. If none is provided, the engine creates one for the duration of the building process. If multiple acceleration structures are built, it can be more efficient to pre-allocate a larger buffer and re-use it for all building commands. The required memory amount can be computed by querying the IAccelerationStructure::requiredScratchMemory
on each acceleration structure to find and reserve the largest chunk.
The following diagram shows the general workflow for how to build acceleration structures in LiteFX:
---
title: Acceleration structure creation workflow
---
flowchart TD
classDef result fill:#A5FF7F
classDef input fill:#FFB27F
classDef resource fill:#D1FFE9
subgraph blasBuiltup["Bottom-level acceleration structure (BLAS)"]
direction TB
createBlas["IGraphicsFactory::createBottomLevelAccelerationStructure"] -.- blas{{"BLAS<br>Triangle meshes or axis-aligned bounding boxes"}}
allocBlas["IBottomLevelAccelerationStructure::allocateBuffer"]
createBlas --> allocBlas
class blas resource
end
subgraph tlasBuiltup["Top-level acceleration structure (TLAS)"]
direction TB
createTlas["IGraphicsFactory::createTopLevelAccelerationStructure"] -.- tlas{{"TLAS<br>Instance data (id, hit group, ...)"}}
allocTlas["ITopLevelAccelerationStructure::allocateBuffer"]
createTlas --> allocTlas
class tlas resource
end
blasBuiltup --> tlasBuiltup
blas -..- tlas
commandBuffer{{"ICommandBuffer"}}
class commandBuffer input
subgraph allocAndBuild["Allocation and build"]
direction LR
allocScratchBuffer["IGraphicsFactory::createBuffer(BufferType: Storage, ResourceHeap: Resource, ResourceUsage: AllowWrite)"] -.- scratchBuffer{{"Scratch buffer"}}
buildBlas["ICommandBuffer::buildAccelerationStructure"] -.- blasBuffer{{"BLAS buffer"}}
buildTlas["ICommandBuffer::buildAccelerationStructure"] -.- tlasBuffer{{"TLAS buffer"}}
commandBuffer -..- buildBlas
scratchBuffer -..- buildBlas
blas -..- buildBlas
commandBuffer -..- buildTlas
scratchBuffer -..- buildTlas
tlas -..- buildTlas
allocScratchBuffer --> buildBlas
buildBlas --> buildTlas
class scratchBuffer resource
class blasBuffer result
class tlasBuffer result
end
tlasBuiltup --> allocAndBuild
As mentioned above, the ray-tracing pipeline is vastly different from the rasterization pipeline, as it can store different shaders for different kinds of geometry and invoke them based on what geometry was hit.
There are different types of shaders within a ray tracing pipeline, each have a different role and differ in the situation when they are invoked.
A hit group refers to a group of shaders that is invoked, if a ray hits a geometry. It is defined by a unique index, that is passed to individual geometries when building the BLAS. Hit group indices must be continuous and unique, i.e., there must be no gaps between indices and no index must exist twice. A hit group must contain either an Intersection
module, if the geometry is an bounding box or an Any Hit
/Closest Hit
module, if the geometry is a triangle mesh. In the later case, both module types are allowed to be combined, but at least one of them needs to exist in a hit group.
There are two ways to bind resources to ray-tracing shaders:
- Global resources are defined through the pipeline layout of the ray-tracing pipeline and are available for all shaders.
- Local resources are defined for each shader individually. There are some restrictions to them, as detailed below.
Local resources only support buffers and constants. Textures cannot be bound to local shader resources. Instead, a constant can be used as a (non-uniform) resource index into a bindless texture array. From a shader they are accessed through an annotated ConstantBuffer
like so:
struct BindingTableLayout
{
StructuredBuffer<float3> VertexColors; // Buffer Reference
uint MaterialId; // Constant
};
[[vk::shader_record_ext]]
ConstantBuffer<BindingTableLayout> ShaderBindingTable : register(b0, space1);
Or equally in GLSL:
layout(buffer_reference, buffer_reference_align=8, scalar) buffer VertexColors {
vec3 colors[];
};
layout(shaderRecordEXT, std430) buffer ShaderBindingTable {
VertexColors colors; // Buffer reference
uint materialId; // Constant
};
Keep in mind that each shader only allows one shader record structure.
The contents of this Wiki, including text, images and other resources are licensed under a Creative Commons Attribution 4.0 International License.