Release Notes for v0.5.1

Documentation

I provide documentation for the latest official release and intermediate (between releases) updates. The main difference is that one links to source code (for example to the SPPMPixel struct), whereas the other does not, but is updated more frequently.

Stochastic Progressive Photon Mapping (SPPM)

The biggest changes for this release is that we support Stochastic Progressive Photon Mapping (SPPM) now (see also paper). Here an example scene (or a whole repository full of them):

Caustic Glass

The details about the progress and problems I ran into are documented in the related issue #86.

Lesson(s) Learned

One interesting detail mentioned in issue #86 above is that there was a major bottleneck slowing down the executable and after investigating a bit using perf I figured out that the HaltonSampler was part of the problem. In C++ (the original code) all instances of the class share a static vector<uint16_t>:

class HaltonSampler : public GlobalSampler {
...
  private:
    // HaltonSampler Private Data
    static std::vector<uint16_t> radicalInversePermutations;
...
};

In the Programming Rust book I found a solution for that problem. In chapter 17 there is a section talking about Building Regex Values Lazily, which mentions a crate called lazy_static. In chapter 19 there is a section about Global Variables, which mentions that static initializers (in Rust) can not call functions, but that the lazy_static crate can help to get around this problem. So the solution looks like this:

/// Generate random digit permutations for Halton sampler
lazy_static! {
    #[derive(Debug)]
    static ref RADICAL_INVERSE_PERMUTATIONS: Vec<u16> = {
        let mut rng: Rng = Rng::new();
        let radical_inverse_permutations: Vec<u16> = compute_radical_inverse_permutations(&mut rng);
        radical_inverse_permutations
    };
}

Defining a variable with the lazy_static! macro lets you use any expression you like to initialize it; it runs the first time the variable is dereferenced, and the value is saved for all subsequent uses.

The problem with SPPM was that it creates a HaltonSampler once, but clones it many times:

pub fn render_sppm(
    scene: &Scene,
    camera: &Arc<Camera + Send + Sync>,
    _sampler: &mut Box<Sampler + Send + Sync>,
    integrator: &mut Box<SPPMIntegrator>,
    num_threads: u8,
) {
...
        let mut sampler: Box<HaltonSampler> = Box::new(HaltonSampler::new(
            integrator.n_iterations as i64,
            pixel_bounds,
            false,
        ));
...
        for iteration in pbr::PbIter::new(0..integrator.n_iterations) {
...
                                    let mut tile_sampler = sampler.clone();
...
        }
...
}

New module, crates and structs

If you are interested in the details about new modules, structs, traits, and functions added by this release, here are some links to the documentation:

  1. The module sppm contains four structs: The SPPMIntegrator which basically stores the parameters described here for that particular integrator. SPPMPixel records (among other things) the weighted sum of emitted and reflected direct illumination for all camera path vertices for the pixel. It also contains information about a VisiblePoint structure, which records a point found along a camera path at which we’ll look for nearby photons during the photon shooting pass. Finally, the SPPMPixelListNode struct is used to create a linked list within a grid (see details here).

  2. Beside the lazy_static crate mentioned above rs-pbrt uses now the atom crate. It provides the Atom and AtomSetOnce structs.

It is interesting how the grid first builds a vector of linked lists by using Atom for the first entry, but AtomSetOnce for all linked nodes:

pub struct SPPMPixelListNode<'p> {
    pub pixel: &'p SPPMPixel,
    pub next: AtomSetOnce<Arc<SPPMPixelListNode<'p>>>,
}
...
            let mut grid: Vec<Atom<Arc<SPPMPixelListNode>>> = Vec::with_capacity(hash_size);
...
                                        // add pixel's visible point to applicable grid cells
...
                                                    // add visible point to grid cell $(x, y, z)$
                                                    let h: usize = hash(
                                                        &Point3i { x: x, y: y, z: z },
                                                        hash_size as i32,
                                                    );
                                                    let mut node_arc =
                                                        Arc::new(SPPMPixelListNode::new(pixel));
                                                    let old_opt = grid[h].swap(node_arc.clone());
                                                    if let Some(old) = old_opt {
                                                        node_arc.next.set_if_none(old);
                                                    }

Later that grid gets replaced by a vector which replaces the first Atom list entry by AtomSetOnce:

            let mut grid_once: Vec<AtomSetOnce<Arc<SPPMPixelListNode>>> =
                Vec::with_capacity(hash_size);
...
            for h in 0..hash_size {
                // take
                let opt = grid[h].take();
                if let Some(p) = opt {
                    grid_once[h].set_if_none(p);
                }
            }
...
                                                        if !grid_once[h].is_none() {
                                                            let mut opt = grid_once[h].get();
                                                            loop {
                                                                // deal with linked list
                                                                if let Some(node) = opt {
...
                                                                        // update opt
                                                                        opt = node.next.get();
...
                                                                        // update opt
                                                                        opt = node.next.get();
...
                                                            }
                                                        }

Minor changes

All cameras inplementing the Camera trait have to implement two new methods now:

pub trait Camera {
...
    fn get_shutter_open(&self) -> Float;
    fn get_shutter_close(&self) -> Float;
...
}

The struct Film has two additional methods:

impl Film {
...
    pub fn get_cropped_pixel_bounds(&self) -> Bounds2i {
        self.cropped_pixel_bounds.clone()
    }
...
    pub fn set_image(&self, img: &[Spectrum]) {
...
    }

I converted core::integrator::uniform_sample_one_light() to a generic function:

pub fn uniform_sample_one_light<S: Sampler + Send + Sync + ?Sized>(
    it: &SurfaceInteraction,
    scene: &Scene,
    sampler: &mut Box<S>,
    handle_media: bool,
    light_distrib: Option<&Distribution1D>,
) -> Spectrum {
...
}

This should make it easier to call that function for implementors of the Sampler trait, e.g. with a pointer to a HaltonSampler.

The trait GlobalSampler demands a method now:

pub trait GlobalSampler: Sampler {
    fn set_sample_number(&mut self, sample_num: i64) -> bool;
}

That currently affects SobolSampler and HaltonSampler:

$ rg -trust set_sample_number -B 1 ~/git/github/rs_pbrt
/home/jan/git/github/rs_pbrt/src/samplers/sobol.rs
211-impl GlobalSampler for SobolSampler {
212:    fn set_sample_number(&mut self, sample_num: i64) -> bool {

/home/jan/git/github/rs_pbrt/src/samplers/halton.rs
300-impl GlobalSampler for HaltonSampler {
301:    fn set_sample_number(&mut self, sample_num: i64) -> bool {

/home/jan/git/github/rs_pbrt/src/core/sampler.rs
38-pub trait GlobalSampler: Sampler {
39:    fn set_sample_number(&mut self, sample_num: i64) -> bool;

/home/jan/git/github/rs_pbrt/src/integrators/sppm.rs
220-                                        tile_sampler.start_pixel(&p_pixel);
221:                                        tile_sampler.set_sample_number(iteration as i64);

The End

I hope I didn't forget anything important. Have fun and enjoy the v0.5.1 release.