Fermat
BPTLib

Top: Contents

BPTLib is a flexible bidirectional path tracing library, thought to be as performant as possible and yet vastly configurable at compile-time. The module is organized into a host library of parallel kernels, BPTLib, and a core module of device-side functions, BPTLibCore. The underlying idea is that all the bidirectional sampling functions and kernels are designed to use a wavefront scheduling approach, in which ray tracing queries are queued from shading kernels, and get processed in separate waves.

BPTLibCore Description

BPTLibCore provides functions to:
  • generate primary light vertices, i.e. vertices on the light source surfaces, each accompanied by a sampled outgoing direction
  • process secondary light vertices, starting from ray hits corresponding to the sampled outgoing direction at the previous vertex
  • generate primary eye vertices, i.e. vertices on the camera, each accompanied by a sampled outgoing direction
  • process secondary eye vertices, starting from ray hits corresponding to the sampled outgoing direction at the previous vertex
In order to make the whole process configurable, all the functions accept the following template interfaces:
  1. a context interface, holding members describing the current path tracer state, including all the necessary queues, a set of options, and the storage for recording all generated light vertices; this class needs to inherit from BPTContextBase :
    template <typename TBPTOptions>
    {
    in_bounce(0) {}
    const RenderingContextView& _renderer,
    const VertexStorageView& _light_vertices,
    const BPTQueuesView& _queues,
    const TBPTOptions _options = TBPTOptions()) :
    in_bounce(0),
    light_vertices(_light_vertices),
    in_queue(_queues.in_queue),
    shadow_queue(_queues.shadow_queue),
    scatter_queue(_queues.scatter_queue),
    options(_options)
    {
    set_camera(_renderer.camera, _renderer.res_x, _renderer.res_y, _renderer.aspect);
    }
    // precompute some camera-related quantities
    void set_camera(const Camera& camera, const uint32 res_x, const uint32 res_y, const float aspect_ratio)
    {
    camera_frame(camera, aspect_ratio, camera_U, camera_V, camera_W);
    camera_W_len = cugar::length(camera_W);
    //camera_square_focal_length = camera.square_pixel_focal_length(res_x, res_y);
    camera_square_focal_length = camera.square_screen_focal_length();
    }
    uint32 in_bounce;
    RayQueue in_queue;
    RayQueue shadow_queue;
    RayQueue scatter_queue;
    VertexStorageView light_vertices;
    cugar::Vector3f camera_U;
    cugar::Vector3f camera_V;
    cugar::Vector3f camera_W;
    float camera_W_len;
    float camera_square_focal_length;
    TBPTOptions options;
    };

  2. a user defined "policy" class, configuring the path sampling process; this class is responsible for deciding what exactly to do at and with each eye and light subpath vertex, and needs to provide the following interface:
    struct TBPTConfig
    {
    uint32 max_path_length : 10;
    uint32 light_sampling : 1;
    uint32 light_ordering : 1;
    uint32 eye_sampling : 1;
    uint32 use_vpls : 1;
    uint32 use_rr : 1;
    uint32 direct_lighting_nee : 1;
    uint32 direct_lighting_bsdf : 1;
    uint32 indirect_lighting_nee : 1;
    uint32 indirect_lighting_bsdf : 1;
    uint32 visible_lights : 1;
    // decide whether to terminate a given light subpath
    //
    // \param path_id index of the light subpath
    // \param s vertex number along the light subpath
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    bool terminate_light_subpath(const uint32 path_id, const uint32 s) const;
    // decide whether to terminate a given eye subpath
    //
    // \param path_id index of the eye subpath
    // \param s vertex number along the eye subpath
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    bool terminate_eye_subpath(const uint32 path_id, const uint32 t) const;
    // decide whether to store a given light vertex
    //
    // \param path_id index of the light subpath
    // \param s vertex number along the light subpath
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    bool store_light_vertex(const uint32 path_id, const uint32 s, const bool absorbed) const;
    // decide whether to perform a bidirectional connection
    //
    // \param eye_path_id index of the eye subpath
    // \param t vertex number along the eye subpath
    // \param absorbed true if the eye subpath has been absorbed/terminated here
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    bool perform_connection(const uint32 eye_path_id, const uint32 t, const bool absorbed) const;
    // decide whether to accumulate an emissive sample from a pure (eye) path tracing estimator
    //
    // \param eye_path_id index of the eye subpath
    // \param t vertex number along the eye subpath
    // \param absorbed true if the eye subpath has been absorbed/terminated here
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    bool accumulate_emissive(const uint32 eye_path_id, const uint32 t, const bool absorbed) const;
    // process/store the given light vertex
    //
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    void visit_light_vertex(
    const uint32 light_path_id,
    const uint32 depth,
    const VertexGeometryId v_id,
    TBPTContext& context,
    RenderingContextView& renderer) const;
    // process/store the given eye vertex
    //
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    void visit_eye_vertex(
    const uint32 eye_path_id,
    const uint32 depth,
    const VertexGeometryId v_id,
    const EyeVertex& v,
    TBPTContext& context,
    RenderingContextView& renderer) const;
    };

    In practice, an implementation can inherit from the pre-packaged BPTConfigBase class and override any of its methods.

  3. a user defined sample "sink" class, specifying what to do with all the generated bidirectional path samples (i.e. full paths); this class needs to expose the same interface as SampleSinkBase :
    {
    template <typename TBPTContext>
    FERMAT_HOST_DEVICE
    void sink(
    const uint32 channel,
    const cugar::Vector4f value,
    const uint32 light_path_id,
    const uint32 eye_path_id,
    const uint32 s,
    const uint32 t,
    TBPTContext& context,
    {}
    template <typename TBPTContext>
    FERMAT_HOST_DEVICE
    const Bsdf::ComponentType component,
    const cugar::Vector4f value,
    const uint32 eye_path_id,
    const uint32 t,
    TBPTContext& context,
    {}
    };

  4. a user defined class specifying the primary sample space coordinates of the generated subpaths; this class needs to expose the following itnerface:
    struct TPrimaryCoords
    {
    // return the primary sample space coordinate of the d-th component of the j-th vertex
    // of the i-th subpath
    //
    // \param idx the subpath index 'i'
    // \param vertex the vertex index 'j' in the given subpath
    // \param dim the index of the dimension 'd' of the given subpath vertex
    FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
    float sample(const uint32 idx, const uint32 vertex, const uint32 dim) const;
    };
The complete list of functions can be found in the BPTLibCore module documentation.

BPTLib Description

BPTLib contains the definition of the full bidirectional path tracing pipeline; as for the lower level BPTLibCore functions, the pipeline is customizable through a TBPTConfig policy class, a TSampleSink, and a set of TPrimaryCoordinates.
While the module itself defines all separate stages of the pipeline, the entire pipeline can be instanced with a single host function call to:

// A host function dispatching a series of kernels to sample a given number of full paths.
// The generated paths are controlled by two user-defined sets of primary space coordinates, one
// for eye and light subpaths sampling.
// Specifically, this function executes the following two functions:
//
// - \ref sample_light_subpaths()
// - \ref sample_eye_subpaths()
//
// \tparam TEyePrimaryCoordinates a set of primary space coordinates, see TPrimaryCoordinates
// \tparam TLightPrimaryCoordinates a set of primary space coordinates, see TPrimaryCoordinates
// \tparam TSampleSink a sample sink, specifying what to do with each generated path sample
// \tparam TBPTContext a bidirectional path tracing context clas
// \tparam TBPTConfig a policy class controlling the behaviour of the path sampling process
//
// \param n_eye_paths the number of eye subpaths to sample
// \param n_light_paths the number of light subpaths to sample
// \param eye_primary_coords the set of primary sample space coordinates used to generate eye subpaths
// \param light_primary_coords the set of primary sample space coordinates used to generate light subpaths
// \param sample_sink the sample sink
// \param context the bidirectional path tracing context
// \param config the config policy
// \param renderer the host-side rendering context
// \param renderer_view a view of the rendering context
// \param lazy_shadows a flag indicating whether to resolve shadows lazily, after generating
// all light and eye vertices, or right away as each wave of new vertices is processed
template <
typename TEyePrimaryCoordinates,
typename TLightPrimaryCoordinates,
typename TSampleSink,
typename TBPTContext,
typename TBPTConfig>
const uint32 n_eye_paths,
const uint32 n_light_paths,
TEyePrimaryCoordinates eye_primary_coords,
TLightPrimaryCoordinates light_primary_coords,
TSampleSink sample_sink,
TBPTContext& context,
const TBPTConfig& config,
RenderingContext& renderer,
RenderingContextView& renderer_view,
const bool lazy_shadows = false)
sample_paths() generates bidirectional paths with at least two eye vertices, i.e. t=2 in Veach's terminology. A separate function allows to process paths with t=1, connecting directly to a vertex on the lens:

// A host function dispatching a series of kernels to process pure light tracing paths.
// Specifically, this function executes the following two functions:
//
// - \ref light_tracing()
// - \ref solve_shadows()
//
// This function needs to be called <i>after</i> a previous call to \ref generate_light_subpaths(), as it assumes
// a set of light subpaths have already been sampled and it is possible to connect them to the camera.
//
// \tparam TSampleSink a sample sink, specifying what to do with each generated path sample, see \ref SampleSinkAnchor
// \tparam TBPTContext a bidirectional path tracing context class, see \ref BPTContextBase
// \tparam TBPTConfig a policy class controlling the behaviour of the path sampling process, see \ref BPTConfigBase
//
template <
typename TSampleSink,
typename TBPTContext,
typename TBPTConfig>
const uint32 n_light_paths,
TSampleSink sample_sink,
TBPTContext& context,
const TBPTConfig& config,
RenderingContext& renderer,
RenderingContextView& renderer_view)

An Example

At this point, it might be useful to take a look at the implementation of the BPT renderer to see how this is used. We'll start from the implementation of the render method:
void BPT::render(const uint32 instance, RenderingContext& renderer)
{
// pre-multiply the previous frame for blending
renderer.multiply_frame(float(instance) / float(instance + 1));
cugar::Timer timer;
timer.start();
// get a view of the renderer
RenderingContextView renderer_view = renderer.view(instance);
// initialize the sampling sequence for this frame
m_sequence.set_instance(instance);
// setup our BPT context
BPTContext context(*this,renderer_view);
// setup our BPT configuration
BPTConfig config(context);
// sample a set of bidirectional paths corresponding to our current primary coordinates
TiledLightSubpathPrimaryCoords light_primary_coords(context.sequence);
PerPixelEyeSubpathPrimaryCoords eye_primary_coords(context.sequence, renderer.res().x, renderer.res().y);
ConnectionsSink<false> sink;
// debug only: regenerate the VPLs
//regenerate_primary_light_vertices(instance, renderer);
m_n_eye_subpaths,
m_n_light_subpaths,
eye_primary_coords,
light_primary_coords,
sink,
context,
config,
renderer,
renderer_view);
// solve pure light tracing occlusions
{
ConnectionsSink<true> atomic_sink;
m_n_light_subpaths,
atomic_sink,
context,
config,
renderer,
renderer_view);
}
timer.stop();
const float time = timer.seconds();
// clear the global timer at instance zero
m_time = (instance == 0) ? time : time + m_time;
fprintf(stderr, "\r %.1fs (%.1fms) ",
m_time,
time * 1000.0f);
}
Besides some boilerplate, this function instantiates a context, a config, some light and eye primary sample coordinate generators (TiledLightSubpathPrimaryCoords and PerPixelEyeSubpathPrimaryCoords), and executes the sample_paths() and light_tracing() functions above. What is interesting now is taking a look at the definition of the sample sink class:
template <bool USE_ATOMICS>
struct ConnectionsSink : SampleSinkBase
{
FERMAT_HOST_DEVICE
ConnectionsSink() {}
// accumulate a bidirectional sample
//
FERMAT_HOST_DEVICE
void sink(
const uint32 channel,
const cugar::Vector4f value,
const uint32 light_path_id,
const uint32 eye_path_id,
const uint32 s,
const uint32 t,
BPTContext& context,
{
const float frame_weight = 1.0f / float(renderer.instance + 1);
if (USE_ATOMICS)
{
cugar::atomic_add(&renderer.fb(FBufferDesc::COMPOSITED_C, eye_path_id).x, value.x * frame_weight);
cugar::atomic_add(&renderer.fb(FBufferDesc::COMPOSITED_C, eye_path_id).y, value.y * frame_weight);
cugar::atomic_add(&renderer.fb(FBufferDesc::COMPOSITED_C, eye_path_id).z, value.z * frame_weight);
if (channel != FBufferDesc::COMPOSITED_C)
{
cugar::atomic_add(&renderer.fb(channel, eye_path_id).x, value.x * frame_weight);
cugar::atomic_add(&renderer.fb(channel, eye_path_id).y, value.y * frame_weight);
cugar::atomic_add(&renderer.fb(channel, eye_path_id).z, value.z * frame_weight);
}
}
else
{
renderer.fb(FBufferDesc::COMPOSITED_C, eye_path_id) += value * frame_weight;
if (channel != FBufferDesc::COMPOSITED_C)
renderer.fb(channel, eye_path_id) += value * frame_weight;
}
}
// record an eye scattering event
//
FERMAT_HOST_DEVICE
const Bsdf::ComponentType component,
const cugar::Vector4f value,
const uint32 eye_path_id,
const uint32 t,
BPTContext& context,
{
if (t == 2) // accumulate the albedo of visible surfaces
{
const float frame_weight = 1.0f / float(renderer.instance + 1);
if (component == Bsdf::kDiffuseReflection)
renderer.fb(FBufferDesc::DIFFUSE_A, eye_path_id) += value * frame_weight;
else if (component == Bsdf::kGlossyReflection)
renderer.fb(FBufferDesc::SPECULAR_A, eye_path_id) += value * frame_weight;
}
}
};
As you may notice, this implementation is simply taking each sample, and accumulating its contribution to the corresponding pixel in the target framebuffer. Here, we are using the fact that the eye path index corresponds exactly to the pixel index, a consequence of using the PerPixelEyeSubpathPrimaryCoords class.