3  Film

Imagine you’re taking a photo with your phone. Light from your environment come into the camera, across a bunch of lens, and finally hit the sensor - that’s what we’re going to do in this chapter, where the rendering result of our renderer will be stored!

3.1 Film Abstraction

The film is where we’ll store the intermidiate state when rendering, and it will be converted to the final image to be saved as output.

So the film should has the same size as the output we expect. There should also be a method to convert it into the result image. And by that way, we should be allowed to access each pixel by a 2D index, as well as updating the pixels by its index.

An important part of this film abstraction is the ability to be splitted into a lot of tiles, and we can then process and render into the film tile by tile, concurrently. To simplify, we will constraint that the tile size should fit the film evenly, otherwise you could just throw remainging pixels away, or do whatever you choose to do. We should also avoid copying tile data everywhere when we need to process the film in tiled-based way, so a view-based tile representation is also needed to avoid too much memory allocation and copy.

Demostrate the Tiled Film

Add a figure here to show how the film could be tiled.

3.2 Implementation Choice

3.2.1 2D Array - ndarray

We’ll leave the implmentation to others, and use the ndarray crate for this film struct. There’re mainly two way to create a new type based on another type:

  • Wrapper Pattern
  • Type Alias

The one I choose here is type alias. In this way, we can use methods provided by the ndarray crate naturally without the ugly .0 infix, or spend much time on delegating methods to the inner type. However, there’re also a lot problems with this approach. First the type inference rule won’t use this alias for the type. Second, it’s a problem that people can pass a wrong instance with the same type to our methods - we lost a great benefit of type-driven design! See Exercise 3.4.1.

3.2.2 Output Image

3.3 Implementation

Just like how we’ve implemented additional methods for the Color type, we can do the same thing for this Film type. We’ve also given an alias name to the Color type for convienience:

// color.rs
pub type Color = palette::LinSrgb<f32>;

// film.rs
pub type Film = Array2<Color>;
pub type OutputImage = ImageBuffer<Rgb<u8>, Vec<u8>>;

pub trait FilmExt {
    fn to_output(&self) -> OutputImage;
}

impl FilmExt for Film {
    fn to_output(&self) -> OutputImage {
1        OutputImage::from_fn(
            self.dim().0 as u32,
            self.dim().1 as u32,
            |x, y| self[(x as usize, y as usize)].to_output_color(),
        )
    }
}
1
from_fn is a method provided by the image crate, which can be used to create an image from a function. Here’s we’re copying data from the array to the image, pixel by pixel. You can also try from_par_fn, since we’ll use rayon for parallel rendering, and the image crate has this method to improve this performance.

Write a unit test for it like what we’ve done in the last chapter. A tip: use a simple film, like \(3 \times 3\) size film to test this.

3.4 Questions, Challenges & Future Possibilities

3.4.1 Wrapper Pattern for Film Implementation

As we’ve mentioned previously, the type alias approach leaves some problems. Try to re-implement it using the wrapper pattern. Provide some beatiful methods to access the methods originally provided by ndarray.

Wrapper pattern is widely used in Rust community. Think about is there any convient way to delegate methods to the inner type? Is there tools helping you to eliminate the ugly .0 or the boilerplate? Try to search on crates.io or make your own one!