This tutorial demonstrates how to implement a simple hybrid renderer that combines rasterization with ray tracing.
Ray tacing is very useful for creating high-quality effects that are difficult to do with rasterization (e.g. reflections), but its performance is generally much lower. This tutorial demonstrates a simple hybrid rendering approach, where the scene is first rendered into the G-buffer with rasterization, and then ray-tracing is used to compute reflections and shadows. The rendering process consists of three stages:
This tutorial runs in Vulkan, DirectX12 and Metal (on compatible hardware).
Ray tracing shaders need to have access to the entire scene, since, unlike rasterization, it is unknown ahead of time which object a ray will hit. To achieve this, we use bindless mode, where all required textures, buffers, samplers and other resources are bound once and can be dynamically indexed by any draw call. In this tutorial, we use a single vertex and index buffer that store multiple meshes, but a real application may use buffer arrays for multiple index and vertex buffers.
Each scene objects is described by the following structure:
ModelMat
and NormalMat
are local-to-world transformations for object positions and normals. MaterialId
indicates the object material. FirstIndex
and FirstVertex
specify the position of the first index and first vertex in the index and vertex buffers correspondingly. MeshId
is currently unused, but may indicate e.g. an index in the vertex buffer array.
For ray tracing, each mesh needs a bottom-level acceleration structure (BLAS). Note that in some cases, it may better to merge all static meshes into a single BLAS, which may improve ray tracing performance and speed up top-level AS construction.
BLAS and TLAS construction is performed similar to previous tutorial.
To decrease the number of draw calls, objects with the same mesh are drawn using instancing.
Note that when we access a resource by index in the shader, we need to use NonUniformResourceIndex()
qualifier, since otherwise the compiler may assume that this index is constant during the draw call and may apply optimizations that will result in an undefined behaviour:
All shaders are written in HLSL and can be used by DirectX12 and Vulkan backends directly. For compatibility with Metal and MSL, we use a wrapper on top of Metal raytracing::intersector<...>
that emulates the RayQuery
functionality. The wrapper supports a minimal set of functions that are needed in this tutorial. In particular, there is no support for non-opaque objects that require iterating through multiple intersections. Note that instead of using a builtin RayQuery::CommittedObjectToWorld4x3()
, as in the previous tutorial, we use matrices from ObjectAttribs
.
RayQuery
wrapper can be further extended to use e.g. TLASInstancesBuffer
, for example:
While rasterization shaders can be cross-compiled by Diligent Engine from HLSL to MSL, ray tracing shaders require a bit more care. It is currently not possible to use verbatim HLSL for ray tracing. While HLSL is generally very similar to MSL, and core logic will work in both languages, resource declaration differs. We use macros to hide the differences, e.g.:
This tutorial only defines the required macros, but a real application may follow this idea to add more functionality.
Shader function declaration requires a bit more macros trickery as in HLSL, shader resources are defined as global variables, while in MSL, all resources are inputs to the shader function:
Please take a look at RayTracing.csh for more details.
This tutorial uses explicit pipeline resource signatures to split resources of a ray-tracing pipeline into two groups: scene resources (m_pRayTracingSceneResourcesSign
) and resources that depend on the window size (m_pRayTracingScreenResourcesSign
). While in DirectX12 and Vulkan, the engine can perform required shader bindig remappings automatically under the hood, it is not currently possible in Metal backend. As a result, an application must explicitly define bindings that match the signature (using the MTL_BINDING
macro in this example).
The ray tracing shader performs the following steps:
CastShadow(...)
function. The function searches for any intersection and returns 0 if the intersection is found, and 1 otherwise.Reflection(...)
function. This function finds the closest intersection with the scene, applies material and calculates lighting by casting a secondary shadow ray.Post-processing is the final stage of the rendering process that does the following: