' '

Difficulty

3

Prerequisites

ray-tracers/v6

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,
                                             "translucency": 0.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.v7()
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

This ray tracer version adds a simplified version of transparency. A more realistic form will be implemented in the next version.

The idea behind this ray tracer is very simple: if a ray hits a translucent object, the ray passes through and we simply continue tracing the same eye_ray to the next object in the scene.

In the image below, the ray hits the red sphere at \(P_1\). The red sphere is translucent, so the ray goes through it and we continue tracing. We find a new second hit at \(P_2\). This is the same translucent material, so the ray continues. We bump into the blue sphere at \(P_3\). Because this blue sphere is not translucent, so tracing stops there.

translucency

Of course, the red sphere could also be reflective, in which case we would need to both trace the "penetrating" and the reflected ray and add the results together.

Important

Some people misinterpret the above description. They think that, when having found \(P_1\), they need to look for the matching \(P_2\) as that’s where the ray leaves the object. That is not the case. While it makes sense, it would be impossible to achieve given the current design of the ray tracer. Instead, the algorithm goes as follows:

  • Look for the first intersection between ray and scene. Find \(P_1\).

  • Because the material at \(P_1\) is translucent, we need to continue tracing. We create a new ray just beyond \(P_1\) and the same direction and trace. We find \(P_2\).

  • Again the material is translucent at \(P_2\), meaning we need to continue tracing. We create a new ray starting just beyond \(P_2\).

  • We hit \(P_3\). Material is opaque: no further tracing is necessary.

2. MaterialProperties

First things first: we need a way to express the fact that a material can be translucent. Let’s add it as an extra field to MaterialProperties.

Add an extra field translucency of type double to MaterialProperties. Update the constructor accordingly.

Update MaterialPropertiesBuilder so that it supports translucency. The default value should be 0.0.

Update scripting/materials-module.cpp so that translucency can be specified in scripts.

3. RayTracerV7

code structure

Create files ray-tracers/ray-tracer-v7.cpp and ray-tracers/ray-tracer-v7.h. Define a class RayTracerV7 that inherits from RayTracerV6.

3.1. compute_translucency

Let’s give compute_ambient and compute_reflection a little brother who will be responsible for dealing with translucent objects.

Add a protected method with signature

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

Implement it based on the following pseudocode:

def compute_translucency(scene, matprops, hit, eye_ray, weight):
    translucency = matprops.translucency

    if translucency > 0:
        new_origin = ... # Just beyond hit.position in the right direction
        new_direction = ...
        new_ray = Ray(new_origin, new_direction)
        new_weight = ...
        color = trace(scene, new_ray, new_weight).color

        return ...
    else:
        return black

3.2. determine_color

We need to update determine_color so that is takes into account compute_translucency.

Override determine_color.

def determine_color(...):
    result = black

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

    return result

3.3. Finishing Touches

Implement the factory function v7().

Expose v7 to the scripting language.

4. Evaluation

Reproduce the scene below.