Ray Tracing Non-Triangle Primitives in DirectX

DirectX Raytracing (DXR) provides dedicated hardware acceleration for testing ray intersections against triangle meshes, but rendering custom geometric shapes requires a different approach. The intersection shader enables developers to bypass the fixed-function triangle test and execute user-defined mathematical equations to detect ray hits against non-triangle primitives, such as spheres, cylinders, curves, or volumetric fields. By enclosing custom shapes within axis-aligned bounding boxes (AABBs) and handling intersection math directly via HLSL, developers can seamlessly integrate arbitrary procedural geometries into the standard DXR pipeline.

Procedural Geometry in the Acceleration Structure

To ray trace non-triangle primitives, the geometry must be declared to the Bottom-Level Acceleration Structure (BLAS) as procedural primitives rather than standard triangle meshes. Instead of vertex and index buffers, developers supply the API with an array of bounding boxes defined by D3D12_RAYTRACING_GEOMETRY_TYPE_PROCEDURAL_PRIMITIVE_AABB.

During the acceleration structure build process, the system organizes these bounding volumes into the bounding volume hierarchy (BVH). The underlying hardware or driver traverses this BVH using standard ray-box intersection algorithms. The custom intersection shader is only invoked when a ray actually penetrates one of these bounding boxes.

Execution of the Intersection Shader

When a ray intersects a procedural primitive's AABB during traversal, the pipeline temporarily suspends hardware traversal and launches the assigned intersection shader.

The shader receives contextual information through system-value intrinsics, including:

  • ObjectRayOrigin() and ObjectRayDirection(): The ray transformed into the local coordinate space of the procedural primitive.
  • RayTMin() and RayTCurrent(): The valid parametric range along the ray where a hit can occur.
  • PrimitiveIndex(): The identifier of the specific primitive within the geometry block being tested.

Inside the shader, the developer writes the analytic solution for the shape. For example, testing a sphere requires solving a quadratic equation to find the values of \(t\) where the ray intersects the sphere's surface. For hair or fur ribbons, developers evaluate spline intersections, while for implicit surfaces, ray marching or sphere tracing algorithms can be executed.

Reporting the Hit to the Pipeline

If the mathematical test yields a valid intersection within the range \([RayTMin(), RayTCurrent()]\), the shader informs the pipeline by calling the intrinsic function:

bool ReportHit(float tHit, uint HitKind, BuiltInTriangleIntersectionAttributes Attributes);
  • tHit: The parametric distance along the ray where the surface contact occurred.
  • HitKind: A user-defined 8-bit identifier that can classify the type of hit (for instance, entering versus exiting a volume).
  • Attributes: A user-defined structure passing surface data—such as surface normals, texture coordinates, or tangent vectors—calculated during the intersection test.

When ReportHit is called, the pipeline evaluates any active Any-Hit shaders. If the hit is accepted, RayTCurrent() shrinks to tHit, ensuring that subsequent intersection tests only consider closer geometry. If the call returns true, the intersection was accepted as the new potential closest hit; if false, the hit was rejected (e.g., by an Any-Hit shader).

Handoff to Downstream Shaders

Once all potential intersections along the ray have been tested and the traversal concludes, the pipeline invokes the Closest-Hit shader for the nearest confirmed hit.

The Closest-Hit shader receives the custom attribute structure passed by the successful ReportHit call. This allows the shading stage to calculate final lighting, reflection, and materials using precise analytical data (such as exact mathematical normals) rather than interpolated vertex normals, preserving visual fidelity for smooth curved surfaces without requiring dense polygon meshes.