' '

Difficulty

3

Prerequisites

ray-tracers/v5

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.v6()
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. Background Information

Reflection is conceptually very simple, but implementing it requires some modifications to the ray tracer structure. Fortunately, no changes will need to be made to existing ray tracers; all structural changes can be local to v6.

When a light ray reaches a reflective material, part of it gets reflected in a mirror-like fashion.

reflection

As always with ray tracing, we need to work backwards. How does reflection fit into the ray tracing process?

  • We wish to know what light arrives from a certain direction. We construct a ray starting in our eye \(E\) and has the right direction.

  • The ray hits the scene at some position \(P\) of some object. Whatever color this object has on position \(P\) is the color we see. However, we notice the material at that location happens to be reflective.

  • We make the ray bounce off the object’s surface. This produces a new ray which we can again trace. The result of this new tracing determines what color gets reflected at \(P\).

reflection2

In the diagram above, both the color of \(P\) and \(P'\) will arrive in \(E\).

2. Recursive Ray Tracing

Implementing reflection works as follows:

def trace(scene, ray):
  hit = scene.find_first_positive_hit(eye_ray)

  if hit:
    matprops = ...
    color = determine_color(scene, matprops, hit, eye_ray)
  else:
    return no_hit


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

  # Add ambient
  result += compute_ambient(...)

  # Add diffuse, specular
  result += process_light_sources(...)

  # Add reflection
  result += compute_reflection(...) //(1)

  return result


def compute_reflection(scene, matprops, hit, eye_ray):
  # Bounce off the object's surface
  reflected_ray = reflection of eye_ray at hit.position

  # Start the tracing algorithm all over again
  return trace(scene, reflected_ray) //(2)
  1. After ambient, diffuse and specular lighting, reflection is added as a fourth source of photons.

  2. Notice the recursive call.

3. Reflectivity

There are two details we need to take into account:

  • Infinite recursion can occur if a ray gets caught between reflective materials.

  • Materials are not necessarily 100% reflective. As mentioned before, only part of the photons get reflected.

One solution solves both problems: we associate a reflectivity with each material, which is a number between 0 and 1 indicating how great a fraction of photons get reflected. A nonreflective material has reflectivity 0, a perfect mirror has reflectivity 1.

We update the pseudocode to take into account reflectivity:

def trace(scene, ray):
  hit = scene.find_first_positive_hit(eye_ray)

  if hit:
    matprops = ...
    color = determine_color(scene, matprops, hit, eye_ray)
  else:
    return no_hit


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

  # Add ambient
  result += compute_ambient(...)

  # Add diffuse, specular
  result += process_light_sources(...)

  # Add reflection
  result += compute_reflection(...)

  return result


def compute_reflection(scene, matprops, hit, eye_ray):
  # Bounce off the object's surface
  reflected_ray = reflection of eye_ray at hit.position

  # Start the tracing algorithm all over again
  return matprops.reflectivity * trace(scene, reflected_ray) // (1)
  1. Because 0 ≤ reflectivity ≤ 1, trace's result is reduced, or at best kept unchanged.

  • Add an extra field reflectivity to MaterialProperties.

  • Add an extra constructor parameter to initialize this reflectivity.

Update the MaterialPropertiesBuilder.

Update the script bindings in scripting/materials-module.cpp.

4. Preventing Infinite Recursion

However, we have not yet solved the problem of infinite recursion. Say we create a scene full of objects with reflectivity 0.9. We cast our ray beginning in the \(E\). It hits the scene at \(P\). Since the material is reflective, we create a new ray starting in \(P\) and trace it. We know that the resulting color will be multiplied by \(0.9\). The secondary ray hits the scene in \(P'\). Again we reflect the ray, but we know this color will only count for \(0.9 \times 0.9 = 0.81\). As the ray continuous bouncing around, the resulting’s color effect will diminish at each step:

\[ 1 \quad 0.9 \quad 0.81 \quad 0.729 \quad 0.6561 \quad 0.59049 \quad 0.531441 \quad 0.4782969 \quad \dots\]

After a number of reflections we will arrive at a point where the next computed color will only matter for less than 1%. We can easily stop tracing there without it visibly affecting the result. The 1% threshold is chosen arbitrarily: if you prefer you can choose a higher or lower value. Higher values mean tracing will end sooner and hence will reduce rendering times at the cost of losing details.

weight

To implement this, we need to pass along the "weight" to the trace function. The weight indicates how much the result of the call will count in the final result. trace can then choose to immediately return black if this weight is below 1%. This way, recursion is guaranteed to end, as long as with each recursive call, trace passes along an updated version of the weight which is lower than its own.

def trace(scene, ray, weight):
  if weight < THRESHOLD: // (1)
    return no_hit
  else:
    hit = scene.find_first_positive_hit(eye_ray)

    if hit:
      matprops = ...
      color = determine_color(scene, matprops, hit, eye_ray, weight)
    else:
      return no_hit


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

  # Add ambient
  result += compute_ambient(...)

  # Add diffuse, specular
  result += process_light_sources(...)

  # Add reflection
  result += compute_reflection(..., weight)

  return result


def compute_reflection(scene, matprops, hit, eye_ray, weight):
  # Bounce off the object's surface
  reflected_ray = reflection of eye_ray at hit.position

  # Start the tracing algorithm all over again
  new_weight = weight * matprops.reflectivity
  return matprops.reflectivity * trace(scene, reflected_ray, new_weight) // (2)
  1. weight represents how much effect it will have on the final result. Once the weight is below a certain threshold (say 0.01), we can simply stop ray tracing.

  2. compute_reflection recursively calls trace with an updated weight.

5. Implementation

5.1. Introducing weight

We will first restructure the internals a bit to accomodate the weight parameter. Once that is in place, we will be able to add reflection.

weight adaptations

5.1.1. determine_color

Create the necessary files to host RayTracerV6. Define a class RayTracerV6 that inherits from RayTracerV5, but is empty otherwise for now.

We need to add a weight parameter to both trace and determine_color. Let’s start with the latter.

Add a protected method with signature

Color RayTracerV6::determine_color(const Scene& scene,
                                   const MaterialProperties& material_properties,
                                   const Hit& hit,
                                   const Ray& eye_ray,
                                   double weight) const

Right now, it can simply call the base class’s version of determine_color.

5.1.2. trace

Let’s turn our attention to trace. However, the supertype RayTracerImplementation demands that trace only has two parameters, namely scene and eye_ray. Luckily, we can easily solve this by simply having two trace methods, one with and one without weight.

Override trace(const Scene& scene, const math::Ray& ray) const. Have it do nothing more than call trace(scene, ray, 1.0).

Add a protected method with signature

TraceResult trace(const Scene& scene,
                  const math::Ray& eye_ray,
                  double weight) const

Copy RayTracerV1::trace's body and adapt it so that it looks like the pseudocode shown above.

5.2. Reflection

We are now ready to incorporate reflection in our new ray tracer.

code structure

5.2.1. compute_reflection

Add a protected method with signature

Color compute_reflection(const Scene& scene,
                         const MaterialProperties& material_properties,
                         const Hit& hit,
                         const math::Ray& eye_ray,
                         double weight) const;

Implement its body as follows:

def compute_reflection(scene, matprops, hit, eye_ray, weight):
  reflectivity = matprops.reflectivity

  if reflectivity > 0:
    reflected_ray_origin = ...
    reflected_ray_direction = ...
    reflected_ray = Ray(reflected_ray_origin, reflected_ray_direction)
    reflected_color = trace(scene, reflected_ray, weight * reflectivity).color
    return reflectivity * reflected_color
  else:
    return black
Tip

Rely on Vector3D::reflect_by. You need a Vector3D to be reflected and a normal on the surface the ray is bouncing off of. Both bits of information can be found in the parameters.

Tip

You may need to give reflected_ray a little nudge. Remember this.

5.2.2. determine_color revisited

determine_color is currently limited to a call to its base class’s version. We need it to also call compute_reflection.

def determine_color(...):
  result = black

  result += super.determine_color(...)
  result += compute_reflection(...)

  return result

5.3. Finishing Touches

Create a factory function v6.

Expose v6 to the scripting language.

6. Evaluation

Reproduce the scene below.