' '

Difficulty

3

Prerequisites

samplers/random

ray-tracers/v1

Reading material

cameras/depth-of-field

design/cameras

def scene_at(now)
{
  var lookat = Animations.animate( pos(0,0,0), pos(0,0,-15), seconds(3) )[now]


  var camera = Cameras.depth_of_field( [ "eye": pos(0, 0, 5),
                                         "look_at": lookat,
                                         "up": vec(0, 1, 0),
                                         "distance": 1,
                                         "aspect_ratio": 1,
                                         "eye_size": 0.1,
                                         "eye_sampler": Samplers.multijittered(3) ] )

  var white = Materials.uniform( [ "ambient": Colors.white() * 0.1,
                                   "diffuse": Colors.white() * 0.8,
                                   "specular": Colors.white(),
                                   "specular_exponent": 20,
                                   "reflectivity": 0,
                                   "transparency": 0,
                                   "refractive_index": 0 ] )

  var black = Materials.uniform( [ "ambient": Colors.black(),
                                   "diffuse": Colors.white() * 0.1,
                                   "specular": Colors.white(),
                                   "specular_exponent": 20,
                                   "reflectivity": 0,
                                   "transparency": 0,
                                   "refractive_index": 0 ] )

  var checkered = Materials.from_pattern(Patterns.checkered(1, 1), black, white)

  var spheres   = []

  for_each([-2..5], bind(fun (i, spheres) {
                     spheres.push_back(translate(vec(-2,0,-i*3), sphere()))
                     spheres.push_back(translate(vec(2,0,-i*3), sphere()))
                   }, _, spheres))

  var spheres_union = decorate(white, union(spheres))

  var plane     = decorate(checkered, translate(vec(0,-1,0), xz_plane()))

  var root      = union( [spheres_union, plane] )

  var lights    = [ Lights.omnidirectional( pos(0, 5, 5), Colors.white() ) ]

  return create_scene(camera, root, lights)
}


var anim = scene_animation(scene_at, seconds(3))

var raytracer   = Raytracers.v6()

var renderer    = Renderers.standard( [ "width": 500,
                                        "height": 500,
                                        "sampler": Samplers.multijittered(2),
                                        "ray_tracer": raytracer ] )


pipeline( anim,
          [ Pipeline.animation(30),
            Pipeline.renderer(renderer),
            Pipeline.studio() ] )

1. Explanation

The depth of field camera can be parameterized as follows:

  • The eye, a Point3D.

  • The look at point (Point3D), which specifies what the camera is looking at. This point is also the focal point: objects around this location will be sharp, far away objects will look blurry.

  • The up vector (Vector3D).

  • The distance (double) between the eye and the canvas.

  • The aspect ratio (double) of the canvas.

  • The eye size (double).

  • The eye sampler (Sampler).

The depth of field camera can be seen as a multiple-eyed perspective camera.

perspective camera
Figure 1. Single perspective camera
eyes
Figure 2. Multiple eyes looking at same point

Given a point on the canvas, the perspective camera only shoots one ray through it. This ray has its origin in the eye, i.e. \((0,0,0)\).

A depth of field camera’s eye is not a single point, but a square. Ideally, for a given point \(P\) on the canvas, the depth of field camera would should a ray from every point of this square through \(P\), but since there are infinitely many, this is not a realistic option. Instead, we pick a finite number of points spread across the eye area and cast rays from these, thereby hopefully approximating the ideal case.

The eye’s size is determined by the eye size parameter. The larger the eye, the more blurry out-of-focus objects will appear. We suggest a value of 0.5. The choice of points within the eye area is left to the eye sampler.

2. Implementation

Let’s start off with the definition of a DepthOfFieldPerspectiveCamera class.

Create new files cameras/depth-of-field-perspective-camera.cpp and cameras/depth-of-field-perspective-camera.h. Use perspective camera’s implementation as a guide.

class DepthOfFieldPerspectiveCamera(DisplaceableCamera):
  def __initialize__(self, transformation, cameras):
    super().__init__(transformation)
    self.__cameras = cameras

  def _enumerate_untransformed_rays(point, callback):
    assert 0 <= point.x <= 1
    assert 0 <= point.y <= 1

    for camera in self.__cameras:
      camera.enumerate_rays(point, callback)

The factory function is where most complexity lies. The function will create a series of "almost-canonical" perspective cameras:

  • The eye of each perspective camera is located around \((0,0,0)\). They are randomly picked by the eye_sampler.

  • The cameras are looking straight in front of them in the direction of positive Z-axis.

  • Each camera is standing straight up, i.e., not tilted in any direction. Up is up.

Placing each of the cameras in the correct position is done in a separate step using a transformation. Applying this transformation is already implemented in DisplaceableCamera.

Declare the factory function depth_of_field_perspective in the header file. It should be a member of the raytracers::cameras namespace. Use the implementation of the perspective camera as a guide to translate the code below.

# Factory function
def depth_of_field_perspective(eye,
                               look_at,
                               up,
                               distance,
                               aspect_ratio,
                               eye_size,
                               eye_sampler):
  assert up is a unit vector

  transformation = create_transformation(...)

  # We pretend that the camera looks straight ahead along the Z axis
  canonical_look_at = ...
  assert canonical_look_at.x == 0
  assert canonical_look_at.y == 0
  assert canonical_look_at.z == distance(eye, look_at)

  # We pretend that the camera stands straight up
  canonical_up = ...

  # Create empty camera list
  cameras = []

  # Create square around eye in XY place
  eye_area = Rectangle2D(...)
  assert eye_area.width == eye_area.height == eye_size
  assert eye_area.center == (0,0,0)

  # Use lambda in C++
  def add_camera(eye_in_xy_plane):
    # eye_in_xy_plane is a 2D point, we need to give it a Z-coordinate
    # Pick it so that it is in the XY-plane
    canonical_eye = ...
    camera = create_perspective_camera(canonical_eye,
                                       canonical_look_at,
                                       canonical_up,
                                       distance,
                                       aspect_ration)
    cameras.append(camera)

  # Ask for samples in eye_area and create a camera for each
  eye_sampler.sample(eye_area, add_camera)

  return DepthOfFieldPerspectiveCamera(transformation, cameras)

Make the finishing touches.

  • Update cameras/cameras.h.

  • Expose the camera to the scripting language. Use perspective_by_map as guide.

3. Evaluation

What happens if you use a deterministic sampler (e.g., stratified sampler) vs a nondeterministic one (e.g., random sampler)?

By deterministic sampler we mean a sampler that always returns the same samples when given the same rectangle.

Recreate the scene shown below: