' '

Difficulty

2

Prerequisites

ray-tracers/v1

Reading material

lighting/diffuse

def scene_at(now)
{
  var camera = Cameras.perspective( [ "eye": pos(0,0,5),
                                      "look_at": pos(0,0,0) ] )

  var floor_material = Materials.uniform( [ "ambient": Colors.white() * 0.1,
                                            "diffuse": Colors.white() * 0.8,
                                            "reflectivity": 0.5 ] )

  var left_wall_material = Materials.uniform( [ "ambient": Colors.red() * 0.1,
                                                "diffuse": Colors.red() * 0.8 ] )

  var right_wall_material = Materials.uniform( [ "ambient": Colors.green() * 0.1,
                                                 "diffuse": Colors.green() * 0.8 ] )

  var back_wall_material = Materials.uniform( [ "ambient": Colors.blue() * 0.1,
                                                "diffuse": Colors.blue() * 0.8 ] )

  var ceiling_material = floor_material

  var sphere_material = Materials.uniform( [ "ambient": Colors.blue() * 0.1,
                                             "diffuse": Colors.blue() * 0.8,
                                             "specular": Colors.white() * 0.8,
                                             "specular_exponent": 10,
                                             "transparency": 0.7,
                                             "refractive_index": 2.5 ] )

  var small_sphere_material = Materials.uniform( [ "ambient": Colors.white() * 0.1,
                                                   "diffuse": Colors.white() * 0.8,
                                                   "reflectivity": 0.8 ] )


  var primitives = []
  primitives.push_back( translate(vec(0,-2,0), decorate(floor_material, xz_plane())) )
  primitives.push_back( translate(vec(0,2,0), decorate(ceiling_material, xz_plane())) )
  primitives.push_back( translate(vec(-2,0,0), decorate(left_wall_material, yz_plane())) )
  primitives.push_back( translate(vec(2,0,0), decorate(right_wall_material, yz_plane())) )
  primitives.push_back( translate(vec(0,0,-2), decorate(back_wall_material, xy_plane())) )


  var sphere_position = Animations.circular( [ "position": pos(0,0,1),
                                               "around": pos(0,0,0),
                                               "duration": seconds(5) ] )
  primitives.push_back( decorate( sphere_material, translate(sphere_position[now] - pos(0,0,0), scale(0.5, 0.5, 0.5, sphere())) ) )
  primitives.push_back( decorate( small_sphere_material, scale(0.25, 0.25, 0.25, sphere()) ) )

  var root = union(primitives)

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

  create_scene(camera, root, lights)
}

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

pipeline( scene_animation(scene_at, seconds(5)),
          [ Pipeline.animation(30),
            Pipeline.renderer(renderer),
            Pipeline.studio() ] )

1. Implementation

Ray tracer v1 added support for ambient lighting. This ray tracer adds another kind of lighting, namely diffuse lighting. Take some time to read up on it.

When ray tracing a scene, our goal is to be able to simulate each photon so that we know which effect it has on our final image. Ray tracer v1 only took into consideration a small fraction of these photons, namely only those due to ambient lighting. Ray tracer v2 will also take into account photons that are the product of diffuse lighting.

\[ \begin{array}{rcl} \textrm{v1} & = & \textrm{ambient photons} \\ \textrm{v2} & = & \textrm{ambient photons} + \sum_{L \in \textrm{lights}} \textrm{diffuse photons from } L \\ \end{array}\]

RayTracerV2 will be structured as follows (red indicates new additions compared to v1):

code structure

We now discuss each of the methods' responsibilities in turn.

Note

This may come across as a lot of unnecessary indirections, but we’re simply giving each method one responsibility. Future ray tracers will reap the rewards: implementing them will be a matter of overriding the appropriate method.

1.1. Setting Things Up

Create two new files raytracers/ray-tracer-v2.cpp and raytracers/ray-tracer-v2.h. Add a class declaration for RayTracerV2. The class must inherit from RayTracerV1.

1.2. determine_color

RayTracerV2 must override determine_color.

  • Add a declaration for determine_color in the .h file. Remember to add the override specifier.

  • Create an empty-bodied definition in the .cpp file.

The v1 version of determine_color simply returns compute_ambient. v2 will add diffuse lighting to this.

Implement RayTracerv2::determine_color using the pseudocode below as guide:

def determine_color(scene, matprops, hit, eye_ray):
  result = black

  # Call old version of determine_color
  result += super().determine_color(scene, matprops, hit, eye_ray)

  # Process all lights in the scene
  result += process_lights(scene, matprops, hit, eye_ray)

  return result

You may have to look up how to call the base class’s version of a method in C++.

1.3. process_lights

This method’s sole responsibility is to iterate over each light and return the sum of all the returned colors.

Create a declaration (in .h) and definition (in .cpp) for RayTracerV2::process_lights.

def process_lights(scene, matprops, hit, eye_ray):
  result = black

  for light_source in scene.light_sources:
    result += process_light_source(scene, matprops, hit, eye_ray, light_source)

  return result
  • Set its visibility to protected.

  • Think of const-correctness.

  • Make it overridable.

1.4. process_light_source

This method asks the given light source to enumerate all light rays it emits in the direction of the hit position and passes each to process_light_ray. For point lights, there is only one such ray, but area lights can produce many.

Point Light Area Light

point light

area light

Note how light sources produce LightRay objects, not just Rays. A LightRay is a Ray combined with a Color, meaning it specifies not only where the light comes from, but also which color it is.

Create a declaration (in .h) and definition (in .cpp) for RayTracerV2::process_light_source.

def process_light_source(scene, matprops, hit, eye_ray, light_source):
  result = black

  for light_ray in light_source.lightrays_to(hit.position):
    result += process_light_ray(scene, matprops, hit, eye_ray, light_ray)

  return result
  • Make it protected.

  • Think of const-correctness.

  • Make it overridable.

1.5. process_light_ray

Create a declaration (in .h) and definition (in .cpp) for RayTracerV2::process_light_ray.

def process_light_ray(scene, matprops, hit, eye_ray, light_ray):
  result = black

  result += compute_diffuse(matprops, hit, eye_ray, light_ray)

  return result
  • Make it protected.

  • Think of const-correctness.

  • Make it overridable.

1.6. compute_diffuse

Finally comes the time to actually compute diffuse lighting. The arguments provide you with all necessary data. You need:

  • The direction of the incoming light.

  • The light’s color.

  • The position to be lit.

  • The normal vector at the position to be lit.

Translate the mathematical formula for diffuse lighting into a protected method compute_diffuse.

def compute_diffuse(matprops, hit, eye_ray, light_ray):
  # First extract all information from arguments
  n = ...
  L = ...
  P = ...
  CL = ...
  CS = ...

  cos = compute_cos_of_angle between n and (L-P)

  if cos > 0:
    return cos * CL * CS
  else:
    return black
Tip
  • You can make use of Vector3D::normalized to normalize the vector, i.e., you can translate

    \[\frac{L-P}{|L-P|}\]

    to (L - P).normalized().

  • Make sure to use the right multiplication: \(a \cdot b\) corresponds to a.dot(b) if both \(a\) and \(b\) are vectors, otherwise you need to use *.

1.7. Finishing Touches

  • Create the factory method raytracer::raytracers::v2().

  • Add the appropriate include to raytracers/ray-tracers.h.

  • Update scripting/raytracing-module.cpp.

2. Evaluation

Reproduce the scene below.

Note

Make sure that, while setting up the scene, you pick a low value for the ambient color, such as 0.1 * colors::white(). For example, if you were to set ambient to colors::white(), the object would already be maximally illuminated and the diffuse lighting would simply "drown" in the ambient lighting. It’s as if you point a flashlight to the sun.