Ray Tracing Scenery

CS11 Advanced C++ Lab 3

This lab continues the construction of our ray tracer.

The Ray Tracing Process

Ray tracing is conceptually very simple. Every object in a scene has a mathematical description of its surface. For example, a sphere with a center C and radius r includes all points X within a distance of r from its center: |X - C| = r. (Or, you can square both sides, which makes little difference in the solution, but definitely speeds up the math.)

Rays have two components, an origin P and a direction D. The direction is usually a normalized vector. The ray itself is modeled as another equation: P + D * t. The variable t only ranges over nonnegative values, since the ray starts from the origin point. When t = 0, the ray is at point P, and as t increases, the ray moves in the direction of D. (If D is normalized, notice that the ray has traversed a distance t at time t, which is sometimes useful to know...)

For simple objects like spheres, we can just solve for the intersection points of the ray's equation and the sphere's equation. (Actually, we solve for the values of t when intersections occur, then use these values to compute the intersection points.) In the case of a sphere, this may result in no intersections, one intersection (a grazing hit), or two intersections. Normally the closest intersection is taken, although for implementing features like Constructive Solid Geometry, you may want all of the intersection points.

Since this is a programming course and not a math course, we will give you the math equations for finding intersections with various simple objects. Your main job will be to implement the necessary classes in the ray tracer, and hopefully to implement the equations correctly! Each kind of scene object will provide its own intersection-test functionality, so that the ray tracer can render scenes with a variety of object types.

Once an intersection is found with an object, the color at that location must be properly computed. This involves, among other things, the surface normal at the point of intersection. Thus, scene objects must also provide a mechanism for determining the surface normal at a location.

If you want to learn much more about ray tracing, you can read the Wikipedia page on the subject. However, we'll tell you everything that's necessary to know for each lab.

Rays

It is very convenient to have a class to represent rays themselves, because they have multiple components, and a lot of operations will involve rays. So, you might as well make your life a little easier. Create a class to represent a ray being traced. You can call it Ray, or if you come up with a better name, use that.

As mentioned above, rays require two data members:

You can represent both of these values with your vector class from last week. (Yes, technically the origin is a point and not a vector, but representing these as separate types leads to a lot of headaches.)

Your ray's constructor should take the origin and direction values for the ray as its arguments. The one nuance of the Ray constructor is that sometimes you will want the constructor to normalize the direction vector automatically, and sometimes you will want to leave the direction value alone. Do this by having a third argument, a flag that controls whether the direction value is normalized. Make this flag default to normalizing the direction vector, since this will be the typical usage. For example, you might do this:

    Ray(const Vector3F &orig, const Vector3F &dir,
        bool normalize = true);

You should provide accessors to the origin and direction values as well. However, you shouldn't need mutators on the ray object at all, since it doesn't need to be manipulated for any computations. When a new ray is fired from a particular location, you can just create a new Ray object with new values.

Your ray also need to provide one member function that returns the actual 3D point for a particular t value. During ray intersection tests, we don't want to store actual points of intersection because we really don't need to; we just use these values of t where intersections occur. But, ultimately, we will need to convert that into a 3D point. So, provide a function to do this. You might call it getPointAtT(float t), for example. (Hint: Assert that t >= 0 in your code!)

Scene Objects

You will need to implement a class to represent each object in a raytraced scene. I recommend that you call them "scene objects" to clearly indicate their purpose. This class should be an abstract base class, since there is no well-defined way that generic scene objects should behave. Eventually your scene object will include detailed information about its surface characteristics, but for now your objects will have one characteristic: their surface color. So, give your scene object class a single data-member, a "surface color" field, specified using the color class you created last week.

Once you have the basic framework for your class, you need to add the following functions:

Scene Object Subclasses

All objects in the scene being ray-traced will be subclasses of the scene-object base class. For now, you can create the following subclasses:

In a few weeks you will add cylinders to your raytracer. This will involve a few new math operations, but the big win is that you can reuse your above sphere-intersection code, if you provide a mechanism to get all intersection points with the sphere. So, it's a bit of extra work right now, but it will save a lot of time down the line.

Lights!

In order to actually see anything in a ray-traced scene, there must be some light source. We can also use lights to render shadows that objects cast on other objects. For now, we will have a very simple lighting model, where all lights are point-lights of a specific color.

Create a class to represent point lights, with the following state:

The constructor should take arguments to store for these values. The class should also provide accessors (but not mutators) for these values.

We won't do anything else with lights this week, but we will incorporate them into the raytracing process next week.

The Scene

A scene is simply a collection of scene-objects being raytraced, and another collection of lights illuminating the scene. Although we could certainly be much more sophisticated than this, we will stick with the "simple" theme and represent scenes in this simple way.

Create a class to represent a scene. The objects in the scene will be dynamically allocated, as will the lights. (Since you only do this once to set up the scene, this will not hinder performance.) Use STL vectors to store the object-pointers and light-pointers. You should provide the following functionality:

Next week we will take this scene object and implement the ray tracing process in it. For now, it will just function as a collection of objects and lights.

Testing

When you have finished writing all of these classes, you should write some test code to exercise your intersection code, to make sure that it works properly. Construct very simple tests, such as shooting a ray from (0,0,0) at a sphere of radius 1 at (2,0,0), and making sure you get back a result of (1,0,0). You can also try your sphere-intersection helper function, and check that you get both (1, 0, 0) and (3, 0, 0) as the results.

Once you are reasonably convinced that your code works properly, submit a tarball of your work on csman.


Copyright (C) 2007-2008, California Institute of Technology.
Last updated January 30, 2008.