' '

Difficulty

2

Prerequisites

ray-tracers/v2

Reading material

lighting/specular

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

Ray tracer v1 gave us ambient lighting. Ray tracer v2 improved upon this by introducing diffuse lighting. Ray tracer v3 will add specular highlights, which enables us to make objects look metallic.

\[\begin{array}{rcl} \textrm{v1} & = & \textrm{ambient photons} \\ \textrm{v2} & = & \textrm{ambient photons} + \sum_{L \in \textrm{lights}} \textrm{diffuse photons from }L \\ \textrm{v3} & = & \textrm{ambient photons} + \sum_{L \in \textrm{lights}} (\textrm{diffuse photons from } L + \textrm{specular photons from }L) \\ \end{array}\]

2. Additional Material Properties

2.1. MaterialProperties

Open the file materials/material-properties.h.

As you can see, the MaterialProperties class currently only contains fields for ambient and diffuse lighting. We will need to add fields so that materials can also exhibit specular highlights.

In the class MaterialProperties,

  • Add a field specular of type Color.

  • Add a field specular_exponent of type double.

Add extra parameters to the constructor to be used as initial values for these fields.

2.2. MaterialPropertiesBuilder

The MaterialProperties class will grow large as we develop more ray tracers. Having to specify values for each parameter will soon become cumbersome, especially since there will be reasonable default values for them. For this reason, a MaterialPropertiesBuilder class exists. It simplifies the creation of a MaterialProperties object:

MaterialProperties material_properties(ambient, diffuse, specular, specular_exponent);

// can be written as

auto material_properties =
  create_material_properties_with().ambient(ambient)
                                   .diffuse(diffuse)
                                   .specular(specular, specular_exponent);

The builder provides the following advantages:

  • We can specify the arguments in any order we want.

  • We can leave out any of the arguments and they will automatically be initialized to default values.

  • It is much more readable as the field being initialized is being mentioned by name. With the regular constructor syntax, there is no hint whatsoever as to what the nth argument means.

Extend MaterialPropertiesBuilder with an extra method named specular so that it can be used to initialize MaterialProperties's fields specular and specular_exponent as follows:

// specular and specular_exponent belong together,
// so we initialize them with a single builder method
auto material_properties =
  create_material_properties_with().specular(specular, specular_exponent);

You can use the existing code for ambient and diffuse as a guide.

2.3. Bindings

Open scripting/materials-module.cpp. Update uniform and uniform_by_map to add support for specular and specular_exponent. Base yourself on the existing code to find out how.

3. RayTracerV3

Ray tracer v3 will build upon v2. In code, this is reflected by the fact that RayTracerV3 should be a subclass of RayTracerV2.

code structure

3.1. process_light_ray

Remember process_light_ray from v2: it is called once for every light ray emitted by each light source in the scene. In the case of v2, it merely called compute_diffuse and returned that function’s result. In v3, we can override this function so that instead of only returning the diffuse color, it adds the specular highlight to this result.

Override RayTracerV3::process_light_ray using the pseudocode below as guide.

def process_light_ray(scene, matprops, hit, eye_ray, light_ray):
  result = black

  result += super().process_light_ray(scene, matprops, hit, eye_ray, light_ray)
  result += compute_specular(matprops, hit, eye_ray, light_ray)

  return result

3.2. compute_specular

Implement the specular highlight algorithm as a new protected method named compute_specular.

def compute_specular(matprops, hit, eye_ray, light_ray):
  # Collect data from arguments
  L = ...
  P = ...
  E = ...
  CL = ...
  CP = ...
  e = ...

  # Perform computations
  i = ...
  r = ...
  v = ...
  cos_alpha = ...

  if cos_alpha > 0:
    return CL * CP * cos_alpha**e
  else:
    return black
Tip
  • Rely on Vector3D::normalized to normalize vectors.

  • Rely on Vector3D::reflect_by to compute a reflection of a vector.

  • You can optimize the function a little bit by first checking whether the specular member of the given MaterialProperties is black. If it is, there’s no point performing all calculations.

3.3. Finishing Touches

Create the factory function v3().

Expose this factory function to the scripting language by adding the necessary code in scripting/raytracing-module.cpp.

4. Evaluation

Reproduce the scene below.

Make sure to pay attention to the size of the specular highlights.