' '

Difficulty

2

Prerequisites

ray-tracers/v3

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.v4()
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

Previous versions of the ray tracer added new lighting models (ambient, diffuse, specular highlights), but none took shadows into account. Now, to be fair, ambient lighting is not supposed to take into consideration shadows, as that would defeat the entire point. But there is no such excuse for diffuse lighting and specular highlights.

Ray tracer v2 and v3 iterate over all light sources in the scene and determine the amount of light based on the incoming angle of the light, without checking whether this incoming light is not obstructed by something else in the scene.

shadow ray

In the figure above, it is clear that \(P\) cannot be illuminated by \(L\) as the blue sphere blocks the photons. Fortunately, it is quite easy to detect that this is the case: we already have the necessary infrastructure in place to cast rays and find intersections with the scene. So, when computing the lighting effect of \(L\) on \(P\), we need only to check if the ray going through \(L\) and \(P\) hits the scene.

However, we have to be careful not to be overzealous: \(P\) is shadowed only if the hit occurs between \(L\) and \(P\). The ray through \(L\) and \(P\) also hits the red and green spheres, but these have no effect on whether \(P\) is illuminated or not, as they are not located between \(L\) and \(P\).

The easiest way to check if a hit occurs between \(L\) and \(P\) is to rely on the \(t\)-value of the hit. Say we want to determine whether photons can get from \(L\) to \(P\). We define a ray whose origin lies in \(L\) and whose direction reaches from \(L\) to \(P\), i.e. we do not use a unit vector. We then get the following \(t\)-values:

t value

As you can see, by choosing the origin and direction carefully, finding out whether there’s a shadow becomes quite simple.

There’s further good news: we don’t even need to create this ray ourselves. Instead, we delegate this task to the light classes: whenever a light is asked for its light rays, it is its responsibility to create these rays in such a way that \(t = 0\) coincides with the photon’s origin and \(t = 1\) to be equal to \(P\).

So, what do we actually need to do in ray tracer v4? Not much, really: we need only update process_light_ray so that it checks whether the given light ray intersects the scene at \(t\) values between \(0\) and \(1\). If not, it can continue in the same way as v3 did, otherwise it returns black.

2. Implementation

code structure

RayTracerV3::process_light_ray indiscriminately calls compute_diffuse and compute_specular for each light ray. RayTracerV4 will override process_light_ray so that it becomes a bit more selective: only if there are not objects between the light and the ray-scene intersection \(P\) will this position be illuminated.

2.1. process_light_ray

Define the class RayTracerV4 in its own set of files (ray-tracer-v4.cpp and ray-tracer-v4.h). Override process_light_ray:

def process_light_ray(scene, matprops, hit, eye_ray, light_ray):
    light_hit = scene.find_first_positive_hit(light_ray)

    if light_hit and not (0 <= light_hit.t < 1):
        return super.process_light_ray(scene, matprops, hit, eye_ray, light_ray)
    else:
        return black

2.2. Finishing Touches

Create a factory function v4().

Add the necessary bindings to make v4 available to scripts.

3. Approximation Errors

Note that if there’s no shadow, the ray will hit the scene at \(t = 1\). However, your CPU is not capable of performing arbitrarily precise arithmetic and it might be that instead of \(t = 1\), you get \(t = 0.99999999978\) or \(1.0000000096\). In other words, there’s a high chance that the \(t\) will randomly be slightly too high or slightly too low.

If you use the test \(0 \leq t < 1\), it will add shadow at positions where \(t\) happens to be slightly too low, causing some "noise" to appear on your renderings as shown in the video below.

Remedy this by slightly changing the \(0 \leq t < 1\) to account for the approximation errors.

4. Evaluation

Reproduce the scene below.