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:
- 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 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);
}
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_square_focal_length = camera.square_screen_focal_length();
}
uint32 in_bounce;
float camera_W_len;
float camera_square_focal_length;
TBPTOptions options;
};
- 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;
FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
bool terminate_light_subpath(const uint32 path_id, const uint32 s) const;
FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
bool terminate_eye_subpath(const uint32 path_id, const uint32 t) const;
FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
bool store_light_vertex(const uint32 path_id, const uint32 s, const bool absorbed) const;
FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
bool perform_connection(const uint32 eye_path_id, const uint32 t, const bool absorbed) const;
FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
bool accumulate_emissive(const uint32 eye_path_id, const uint32 t, const bool absorbed) const;
FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
void visit_light_vertex(
const uint32 light_path_id,
const uint32 depth,
TBPTContext& context,
FERMAT_HOST_DEVICE FERMAT_FORCEINLINE
void visit_eye_vertex(
const uint32 eye_path_id,
const uint32 depth,
TBPTContext& context,
};
In practice, an implementation can inherit from the pre-packaged BPTConfigBase class and override any of its methods.
- 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
const uint32 channel,
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 uint32 eye_path_id,
const uint32 t,
TBPTContext& context,
{}
};
- a user defined class specifying the primary sample space coordinates of the generated subpaths; this class needs to expose the following itnerface:
struct TPrimaryCoords
{
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:
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,
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:
template <
typename TSampleSink,
typename TBPTContext,
typename TBPTConfig>
const uint32 n_light_paths,
TSampleSink sample_sink,
TBPTContext& context,
const TBPTConfig& config,
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:
{
m_sequence.set_instance(instance);
BPTConfig config(context);
ConnectionsSink<false> sink;
m_n_eye_subpaths,
m_n_light_subpaths,
eye_primary_coords,
light_primary_coords,
sink,
context,
config,
renderer,
renderer_view);
{
ConnectionsSink<true> atomic_sink;
m_n_light_subpaths,
atomic_sink,
context,
config,
renderer,
renderer_view);
}
const float time = timer.seconds();
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>
{
FERMAT_HOST_DEVICE
ConnectionsSink() {}
FERMAT_HOST_DEVICE
const uint32 channel,
const uint32 light_path_id,
const uint32 eye_path_id,
const uint32 s,
const uint32 t,
{
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)
{
}
}
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;
}
}
FERMAT_HOST_DEVICE
const uint32 eye_path_id,
const uint32 t,
{
if (t == 2)
{
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.