Raytracers are often big programs. They're very much the compilers of the graphics world: they compile some sort of scene description into an array of colors. Fortunately, this raytracer is actually pretty small. It has a limited set of base features, and only has about 5000 lines of C++ code behind it.
Fortunately, its features are a good solid core, and there’s plenty of room for you to add and adjust to your heart’s content.
After you have unpacked the skeleton and looked at the sample solution (ray-sample.exe or raytracer-demo), you will probably want to take some time to just look through the code and figure out what is going on where. In this section, I’ll describe some of the main files and data structures, as well as show what calls what in a typical run of the program.
vecmath/vec.h
vecmath/mat.h
These are good files to become very familiar with. They contain the classes for
performing basic vector and matrix operations. The main classes you will need are
Vec3f and Vec4f (three- and four-component vectors) and Mat4f (4x4 matrices).
Almost all the math you do will be in terms of vectors. The interface is fairly
intuitive, letting you do most algebra operations on vectors with operators:
vecA + vecB adds two vectors, vecA * vecB takes the dot product
of two vectors, and vecA ^ vecB takes the cross product of two three-component
vectors. Note that there are some unimplemented methods in the class files, but
hopefully everything you'll need is in there.
scene/ray.h
A useful one to know as well. A ray is basically a position and a direction,
someplace in 3D space. Also defined in this file is the isect class, which
contains information about the point where a ray intersected an object. It contains,
among other things, a pointer to the object, the surface normal at the intersection,
and a t value to use in calculating the intersection point.
RayTracer.cpp
This is where raytracing begins. For each pixel in the image, traceRay() gets
called. It gets passed a pointer to the scene geometry information, a ray, and two
variables you can use to manage recursion. (Remember, adding in recursion is your job.)
It calls scene->intersect() to find the first intersection of that ray with an
object, and gives you an isect to work with.
scene/material.cpp When an intersection happens, you need to figure out what color the surface is at that point. For that, you need a handy shading model, and someplace in the program that knows how to do it. That’s what goes in this file. This is the place where color gets calculated from material properties. Right now it only does one thing: return a diffuse color without shading.
Another thing that you will find in the material.{cpp,h} files is texture mapping support; we have included a skeleton to make adding this easier. You need only implement the getMappedValue() function in the TextureMap class to get this going properly (along with calculating uv-coordinates at intersect time). We have provided you with a lower-level interface to the bitmap which you can use to implement this.
scene/light.cpp
As part of shading, you generally need to look at light sources. This is the code
that knows how to handle them. This is a good place to deal with attenuation.
parser/Parser.cpp
parser/Token.cpp
Okay, after looking this all over you decide life is too simple, and you’re ready
to add extra features, like spotlights for instance. How do you work them in to
the scene graph? That’s where this file comes in. As a .ray file is opened, it
is converted into a stream of tokens (Token.cpp, Tokenizer.cpp), and then each the
tokens are processed in turn into the scene graph (Parser.cpp). Additions to the
file format would start in the processScene() function in this file.
SceneObjects/*.cpp
This is where most of the intersection code is written. Look in here to get an
idea of how intersections work and to implement the triangle mesh intersection.
ui/TraceUI.cpp
ui/GraphicalUI.cpp
ui/CommandLineUI.cpp
This is where the user interface code resides. Look at how the example sliders
are implemented. This is where you will put any custom controls you create. You
can access these controls by adding an extern TraceUI* traceUI to the top
of each file you need UI access in (the global traceUI object resides in
main.cpp). Then, any code inside that file can make calls to
traceUI->someFunction().
If you want to add new functionality to the UI, you should add a class variable to the base TraceUI class and then add the FLTK control to the GraphicalUI class. CommandLineUI is if you want to run the raytracer off the command line (often useful for testing), and you may wish to add extra command line options for your new features as well.
For the most part I’ll leave you to explore the data structures in those files on your own. But to get a good headstart, here are some of the pieces you will need to work with:
Inside class ray:
In general, a ray is a self-contained object with a position and direction. You can call getPosition and getDirection to get those vectors as you need. One additional useful method is at which calculates the linear formula [P + t*D] and returns a new point.
Inside class isect:
const SceneObject *obj;
A pointer to the object intersected, in case you need to access the object.
double t;
This is the linear value from where the ray intersected the object. In other
words, if you wrote out the ray formula as [P + t*D] where P is the position
of the ray and D is the direction, this t is the t in the formula. You can
use it to find the point in 3D space where the intersection took place.
vec3 N;
This is the surface normal where the intersection happened.
const Material &getMaterial() const;
And finally, this method gets the material properties of the surface at the
intersection. It’s just a time-saver, using the intersection’s own material
pointer if it’s defined, or getting the object material if not.
vec2f uvParameters;
This should store the surface parametric coordinates of the intersection, which
will be used in the texture mapping stage to find the location in the 2-dimensional
map. Should range from 0 to 1. These are currently only used in the Box
and Square shapes.
Inside class material:
vec3 ke();
Emmisive property
vec3 ka();
Ambient property
vec3 ks();
Specular property
vec3 kr();
Reflective property
vec3 kd();
Diffuse property
vec3 kt();
Transmissive property
double shininess();
The shininess exponent when calculating specular highlights
double index();
Index of refraction for use in forming transmitted rays.
Note that for each of these functions you must pass in an isect& since the functions may need the isect to figure out parametric coordinates of the intersection for 2D texture mapping, space coordinates for use with solid textures, etc.
The TraceUI class starts off the rendering process whenever the user presses the ‘render’ button. You can follow the code from there, starting with the TraceUI::cb_render method. This method calls RayTracer:: tracePixel() repeatedly, once for each window coordinate.
From traceRay(), the process can go in different directions. For the simple ray-caster we gave you, the ray that was passed in is intersected against the geometry, the material properties of the intersection are retrieved, and a call to shade() on that material class calculates a color. Your version will probably use these same methods; all you need to do is flesh them out.
Some other pointers to get you moving:
The Trace project is written in C++ and takes advantage of the whole object-oriented paradigm. While this makes it very versatile, it also means it may be significantly different than most of the other C++ code you have seen. Becoming familiar with the class hierarchy is not particularly difficult, but one item that you may find confusing if you’ve never dealt with it before is the Standard Template Library, or STL.
STL is a very large collection of templated C++ classes that implement a large number of data structures and algorithms. Here, we have used the STL to keep track of several data items, including a list of light sources -- something you will need to use. As such, this is a very brief description of how to use the STL list structure.
Access to the STL's container classes, like ‘list,’ is in general managed through objects called iterators. These sort of work like smart pointers.
If I defined this:
class foo_c { /* some stuff */ }; listfoolist;
It makes a STL list of class foo_c. It also defines an iterator type called
"list
class foo_c { /* some stuff */ }; listfoolist; typedef list ::iterator fooiter;
Now, when I'm ready to add stuff to that list, I use "push_front" to add items to the beginning.
foo_c myFoo; /* do what you want to myFoo */ foolist.push_front( myFoo );
And when I want to access the list items, I iterate through them like this:
fooiter iter1; for( iter1 = foo.begin(); iter1 != foo.end(); ++iter1 ) { // At this point, *iter1 is the current list item, just // as if iter1 were a pointer. cout << *iter1; *iter1 = something; }
And finally, it's easy to delete an item from the list too. If you have an iterator pointing to the item you want to get rid of, you can call this:
erase( iter2 );
That's in general, how any of the storage classes work, whether a list or some other type. Applied to the trace program, you would for instance use an iterator to loop through all the lights in the scene. The only additional trick is that some of the data you might want to access is encapsulated with member functions to get at it. For instance, the Scene::beginLights() and Scene::endLights() methods exist for no reason than to pass back iterators to the list of lights.