' '

Difficulty

3

Prerequisites

ray-tracers/v4

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": RGB(0.1,0.1,0),
                                             "diffuse": RGB(0.8,0.8,0),
                                             "specular": Colors.white() * 0.8,
                                             "specular_exponent": 10,
                                             "light_filtering":  RGB(0.8,0.8,0),
                                             "transparency": 0.7,
                                             "refractive_index": 2.5 ] )

  var sphere_material2 = Materials.uniform( [ "ambient": RGB(0.1,0,0.1),
                                              "diffuse": RGB(0.8,0,0.8) * 0.8,
                                              "specular": Colors.white() * 0.8,
                                              "specular_exponent": 10,
                                              "light_filtering": RGB(0.8,0,0.8),
                                              "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) ] )
  var sphere_position2 = Animations.circular( [ "position": pos(0,1.1,0.5),
                                                "around": pos(0,0,0),
                                                "duration": seconds(5),
                                                "axis": vec(0,-1,0) ] )
  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( sphere_material2, translate(sphere_position2[now] - pos(0,0,0), scale(0.25, 0.25, 0.25, 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.v5()
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. How It Works

The previous version of the ray tracer, objects were considered 100% opaque: they would block 100% of the light. However, we would like objects to be able to be translucent, i.e., that they can only block part of the light.

Below we have three disks that each let through certain colors. The shadows shows what light "survives".

White light (FFF) travels from the top to the plane below.

  • If it only encounters the cyan disk (0FF), then red (F00) gets blocked.

  • If the light beam needs to get through both the cyan disk (0FF) and the magenta disk (F0F), only blue (00F) survives.

  • If light encounters all three disks, all photons are blocked, resulting in a full black shadow.

Note

Admittedly, the disks themselves don’t look very translucent. This extension focuses solely on the shadows. Actually translucent looking disks will be possible in ray tracer v7.

2. Material Properties

We need a way to specify how much light gets through an object. For this, an extra MaterialProperties field will be necessary.

In materials/material-properties.cpp and its corresponding header file,

  • Add a field light_filtering of type Color.

  • Let the constructor accept an extra parameter to initialize this field with.

Update the MaterialPropertiesBuilder class.

Update scripting/materials-module.cpp so that we can specify light_filtering in the scripting language.

3. RayTracerV5

The previous ray tracer version operated in a very simple way: to render some position \(P\), process_light_ray checks if there is any object between the light source’s position \(L\) and \(P\). This was achieved by taking the light ray, which travels from \(L\) through \(P\) and find an intersection with the scene. If there is an intersection where \(0 \leq t < 1\), there is something obstructing the light from getting from \(L\) to \(P\), causing us to disregard all photons originating from \(L\), creating a shadow. We will add some nuance to this approach.

code structure

3.1. process_light_ray

An improved form of the process_light_ray algorithm goes as follows:

def process_light_rays(scene, material_props, hit, eye_ray, light_ray):
  hits = scene.find_all_hits(light_ray)
  color_filter = white

  for hit in [h for h in hits if 0 < h.t < 1]:
      # Determine material properties at hit
      matprops = ...

      # Use color multiplication
      color_filter *= matprops.light_filtering

  # Make new light ray
  surviving_light_ray = LightRay(original_light_ray.ray,
                                 original_light_ray.color * color_filter)

  # Delegate further work to v3 implementation
  return RayTracerV3.process_light_ray(self, scene, matprops, hit, eye_ray, surviving_light_ray)

Notice how we rely on MaterialProperties::light_filtering to determine how much light a primitive lets through.

  • If light_filtering equals black, no light gets through. This is the default.

  • If light_filtering equals white, all light gets through. Such an object will never cast shadows.

  • If light_filtering equals red, only red light will get through. Such an object will cast a red shadow.

  • Create new files raytracers/ray-tracer-v5.cpp and raytracers/ray-tracer-v5.h.

  • Implement a class RayTracerV5 that inherits from RayTracerV4.

  • The class only needs to override process_light_ray. The algorithm is given in Pythonesque code above.

3.2. Finishing Touches

Create a factory function v5.

Add bindings so that v5 becomes accessible from the scripting language.

4. Evaluation

Reproduce the scene below.