' '

Difficulty

1

Reading material

lighting/ambient

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() * 1,
                                            "diffuse": Colors.white() * 0.8,
                                            "reflectivity": 0.5 ] )

  var left_wall_material = Materials.uniform( [ "ambient": Colors.red() * 1,
                                                "diffuse": Colors.red() * 0.8 ] )

  var right_wall_material = Materials.uniform( [ "ambient": Colors.green() * 1,
                                                 "diffuse": Colors.green() * 0.8 ] )

  var back_wall_material = Materials.uniform( [ "ambient": Colors.blue() * 1,
                                                "diffuse": Colors.blue() * 0.8 ] )

  var ceiling_material = floor_material

  var sphere_material = Materials.uniform( [ "ambient": Colors.blue() * 0.5,
                                             "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() * 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.v1()
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. Introducing Ray Tracer v1

Go take a look at the code for the original ray tracer in raytracers/ray-tracer-v0. The trace member function must (among other things), given a ray and a scene, determine which object in the scene is hit by the ray first and find out what color this object has.

Ray tracer v0 is a rather lazy implementation: if there’s a hit, it simply returns white instead of actually trying to find out which color the object has. Ray tracer v1 will improve upon it.

Let’s first simply make a copy of RayTracerV0 called RayTracerV1. In a later step we will make modifications.

Add the following files to the project:

  • raytracers/ray-tracer-v1.cpp

  • raytracers/ray-tracer-v1.h

Change all occurrences of v0 to v1.

Make sure everything compiles.

We want to test this "new" ray tracer by using it in a script. However, the scripting language has not yet been informed of RayTracerV1's existence. We need to add a binding.

All script-related functionality resides in the scripting subdirectory.

  • Open raytracing-module.cpp.

  • Inside the scripting language, a module named Raytracers exists whose purpose is to offer all available ray tracers. Right now, only Raytracers.v0() is available, but for every new ray tracer you implement, you will need to add a method vn to this module. This Raytracers module is represented by RaytracerLibrary in C++. Add a method v1 to RaytracerLibrary with the correct body.

  • Adding a v1 method to RaytracerLibrary is not sufficient: you also need to explicitly export the method itself. At the bottom of the file, you’ll find a line BIND(v0). This is what actually exposes v0 to the scripting language. Add a similar line so that v1 is also made accessible.

As always, check that everything compiles. Create a simple script that references v1 and render it. It should produce the exact same result as when using v0.

2. Implementing Ambient Lighting

Ray tracer v1 adds support for ambient lighting. Be sure to read the linked material as otherwise you will not understand what you need to implement.

There are multiple ways to introduce ambient lighting to our RayTracer. We chose to associate an ambient lighting factor to materials: you can create a sphere with a lot of ambient lighting and one just next to the sphere with no ambient lighting at all. This is highly unrealistic, but it’s up to the creator of the scene to avoid such pitfalls.

In order to add support for ambient lighting, you need to update RayTracerV1::trace. Let’s first take a look at its current implementation (i.e., the one you copied from RayTraacerV0::trace).

TraceResult raytracer::raytracers::_private_::RayTracerV0::trace(const Scene& scene, // (1)
                                                                 const Ray& eye_ray // (2)
                                                                ) const
{
    Hit hit; // (3)

    if (scene.root->find_first_positive_hit(eye_ray, &hit)) // (4)
    {
        Color hit_color = colors::white(); // (5)
        unsigned group_id = hit.group_id; // (6)
        double t = hit.t; // (7)

        return TraceResult(hit_color, group_id, eye_ray, t); // (8)
    }
    else
    {
        return TraceResult::no_hit(eye_ray); // (9)
    }
}
  1. The scene parameter represents the entire scene, i.e., the primitives it contains, the lights and the camera.

  2. The eye_ray starts in the camera’s eye and travels through the scene. We need to find where it hits the scene and determine how to render that point.

  3. A Hit object is allocated on the stack.

  4. scene.root represents the entirety of all shapes in the scene as a single root object. find_first_positive_hit asks the root object whether the eye_ray does intersect with it. If so, true is returned and the hit object is filled in with extra information.

  5. If there’s a hit, v0 decides to return white as color.

  6. group_ids are required to implement edge rendering. You can ignore this line.

  7. hit.t tells us where on eye_ray the ray/scene intersection occurred.

  8. We construct a TraceResult that contains all relevant information about the hit.

  9. Here, we deal with the case where eye_ray does not hit the scene anywhere. We return an "empty" TraceResult.

We’ll do a slight bit of overengineering so that it becomes easier to build on top of it later. Visually, the structure looks as follows:

code structure

2.1. compute_ambient

Add a protected method with signature Color RayTracerV1::compute_ambient(const MaterialProperties&). It should simply return the ambient field of its argument.

2.2. determine_color

Add a method Color RayTracerV1::determine_color(const Scene& scene, const MaterialProperties& properties, const Hit& hit, const math::Ray&) const. As new ray tracer versions add features, this method will often be the one to host them.

  • determine_color should not be visible to client code, but do make sure it can still be overridden.

  • Currently, we support only one feature: ambient lighting. determine_color should call compute_ambient and return its result.

2.3. trace

determine_color will need to be called by trace in order to, well, determine the color. Instead of assigning colors::white to hit_color, assign determine_color's result. There’s a snag though: determine_color needs a MaterialProperties argument.

To find out where to find these MaterialProperties, you should keep in mind what we’re trying to implement. The material’s properties must depend on where the eye_ray hits the scene, and all information regarding that hit can, unsurprisingly, be found in hit.

hit contains a material field of type Material. Clearly we’re on the right track. The idea is that a Material represents the material the object hit by the eye_ray is made out of, for example wood. However, materials are seldom uniform: wood tends to have lines and circles and shades of brown. In other words, we need location-specific material information. This is where Material::at comes in: it asks a Material "what color are you at this specific location?" Interestingly, this at method returns a MaterialProperties object. Unfortunately, at requires a HitPosition as argument…

Where would we find a HitPosition? Again, it makes sense to look around in Hit.

Adapt RayTracerV1::trace so that it calls determine_color instead of assuming everything is white.

3. Evaluation

Reproduce the scene below.

challenge
Tip

Ray tracer v0 did not need primitives to be assigned materials. However, v1 does (as should be apparent from your code). So, don’t forget to decorate your shapes with a material:

var material = Materials.uniform([ "ambient": Colors.red() ])
var decorated_sphere = decorate(material, sphere())
Tip

In the scripting language, you can add colors together using +. There’s also the RGB function that allows you to specify colors using their R, G, B components as floating point values between 0 and 1, e.g., RGB(0.1, 0.2, 0.3).