Difficulty |
3 |
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.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.

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\).

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)
-
After ambient, diffuse and specular lighting, reflection is added as a fourth source of photons.
-
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)
-
Because
0 ≤ reflectivity ≤ 1
,trace
's result is reduced, or at best kept unchanged.
|
Update the |
Update the script bindings in |
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:
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.

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)
-
weight
represents how much effect it will have on the final result. Once the weight is below a certain threshold (say0.01
), we can simply stop ray tracing. -
compute_reflection
recursively callstrace
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.
5.1.1. determine_color
Create the necessary files to host |
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
Right now, it can simply call the base class’s version of |
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 |
Add a protected method with signature
Copy |
5.2. Reflection
We are now ready to incorporate reflection in our new ray tracer.
5.2.1. compute_reflection
Add a protected method with signature
Implement its body as follows:
|
5.2.2. determine_color revisited
|
5.3. Finishing Touches
Create a factory function |
Expose |
6. Evaluation
Reproduce the scene below. |