' '

Difficulty

4

Prerequisites

ray-tracers/v7

Reading material

physics/refraction

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

First, familiarize yourself with refraction. Also go take a look at MaterialProperties: you’ll find you’ll need the member variables refractive_index and transparency for this extension.

Correctly implementing refraction is not an easy task. We choose to introduce the following simplifications:

  • Technically, we should find out what medium the light ray originates in. In our implementation, we will make the assumption we always start in vacuum.

  • When encountering a transparent material, we assume that, after bending the ray, the next intersection brings us back to the outside world (i.e. \(n = 1\)). In reality, it can happen that upon exiting the \(n_1\)-material, we immediately enter a new \(n_2\)-material, but we ignore this possibility.

  • We ignore total internal reflection. Whenever it arises, we simply return black.

refraction

2. Implementation

Refraction is a new feature that will take its place next to compute_ambient, compute_reflection and compute_translucency.

code structure

2.1. MaterialProperties

We need a way to express some materials let light through. We already used translucency, so let’s settle for transparency.

disclaimer

Technically, transparent means "lets most of the light through" and translucent means "lets some of the light through". Here, however, we simply use these terms to refer to two different ways of computing "light going through stuff". There is nothing conventional about this usage: don’t expect anyone else to use the terms this way.

Add extra fields to MaterialProperties and update the constructor accordingly:

  • transparency of type double

  • refractive_index of type double

Add an extra method transparent to the MaterialPropertiesBuilder. We wish to specify transparency and refractive_index together as they go hand in hand.

MaterialProperties matprops = create_material_properties_with()
  .transparency(transparency, refractive_index);

The default values are 0.0 for transparency and 1.0 for refractive_index.

Update scripting/materials-module.cpp to add support for the newly added fields.

2.2. RayTracerV8

2.2.1. Setting Things Up

Create files ray-tracers/ray-tracer-v8.cpp and ray-tracers/ray-tracer-v8.h. Add a class RayTracerV8 that inherits from RayTracerV7.

2.2.2. compute_refraction

The algorithm for refraction then looks as follows:

def refract(i, n, n1, n2):
  ox = ...

  if internal_refraction:
    return None

  oy = ...
  o = ...

  return o


def compute_refraction(scene, material_properties, hit, ray, weight):
  transparency = material_properties.transparency

  if transparency > 0
    n1 = 1
    n2 = material_properties.refractive_index

    # Ray enters transparent object, compute how it is bent at point P1
    P1 = ...
    normal_at_P1 = ...
    refracted_direction = refract(ray.direction, normal_at_P1, n1, n2)
    if not refract_direction:
      return black
    refracted_ray = Ray(P1, refracted_direction)

    # Find exit point P2
    exit_hit = find_hit(scene, refracted_ray)

    if not exit_hit:
      # There is no exit point (1)
      return black

    P2 = exit_hit.location
    normal_at_P2 = ... # (2)
    refracted_direction = refract(refracted_ray.direction, normal_at_P2, n2, n1)
    if refracted_direction is None:
      return black
    exit_ray = Ray(P2, refracted_direction)

    # Continue tracing ray after it left the transparent object
    # Don't forget to decrease weight
    return trace(scene, exit_ray, weight * transparency) * transparency
  1. This can occur if we’re dealing with a plane: a ray will not go through the same plane twice.

  2. Make sure to check that the normal points in the opposite direction of refracted_ray.direction

Implement compute_refraction, which has signature

Color compute_refraction(const Scene&, const MaterialProperties&, const Hit&, const math::Ray&, double) const

2.2.3. determine_color

Let’s not forget to update determine_color.

Override determine_color.

def determine_color(...):
  result = black

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

  return result

2.2.4. Finishing Touches

Create a factory function v8().

Expose v8 to the scripting language.

3. Evaluation

Reproduce the scene below.