Difficulty |
2 |
Prerequisites |
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.

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:

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
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
|
2.2. Finishing Touches
Create a factory function |
Add the necessary bindings to make |
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. |