Parsing is handled in two stages. First, the entire RAY file is read in (handled by the read() method of each object). Each object is allocated and external files are parsed in as well. Once this is complete, the second stage is to connect all the components (handled by the makeLinks() method of each object). Things like shapes pointing to materials or pasting macros into appropriate spots are handled. This approach is used so that objects can be defined in any order without worrying about where certain things are defined relative to each other.
Parsing is handled by individual classes as much as possible. That is, the Sphere class is responsible for reading in its relevant information, including its flags. The RayFile class handles the top-level parsing of the RAY file, including parsing directives and handing the stream off to the appropriate classes.
There is also a pair of files, parse.h and parse.cpp, which have some
additional file parsing code common to all the classes. There's a
function for flag parsing, a function to read in an arbitrary shape group
object (any shape primitive, a group, a macro instance or a rayfile
instance), and functions for reading in tokens and skipping comments -
nextDouble, nextInt, nextToken. Use these functions instead of the infix
operator for base types (ie. don't use cin >> myInt;, use
myInt = nextInt(cin);). Look at some of the parsing in the
supplied code for examples, and read the comments in parse.h for more
information.
If you decide to create a new RAY file directive that could or should have flags, use parseFlags(). It's functionality is documented in parse.h, and using it will make your directive consistent with the rest of the RAY file syntax.
The function to read an arbitrary shape group, parseShapeGroup(), was added because there are three separate places that ShapeGroup objects can legally be found - inside Groups, inside Macros, and inside RayFiles - and if there wasn't a parseShapeGroup() method, each of those classes (Group, Macro and RayFile) would have to know about all the acceptable primitives. This way, only one function needs to be modified to add extra primitives.
You should only need to modify parsing if you add some non-standard
functionality to your raytracer, such as adding a triangle mesh primitive.
Even so, most extra functionality can be handled by adding extra flags to
the currently existing directives, such as a -b bumpMap flag
to the material directive. If you find that you need to make
extensive changes to the parsing, make sure that your changes are 100%
backwards compatible (that is, any RAY files not implementing your
advanced functionality will still parse and display properly. If your
modified parsing does not properly handle standard RAY files (that is,
fatally fails on any RAY file conforming to the RAY file format here), your grade will suffer.
OpenGL rendering is handled by each object's glDraw() method, or in a few cases, glLoad() and glUnload(). The top level call is RayFile::glDraw(), which sets up the matrices, turns on lights and draws the scene geometry.
The Camera class provides two methods, gluPerspectiveWrapper() and gluLookAtWrapper() which simply call the OpenGL functions glutPerspective and gluLookAt respectively, with arguments that reflect the current camera settings. RayFile::glDraw() calls these methods to set up the current ModelView and Projection matrices.
Lights have two methods, glLoad() and glDraw(). Light::glLoad() enables an OpenGL light with all the correct settings (position, direction, attenuation, color, etc.) for that light. Light::glDraw() renders the light in the scene so it is visible for modelling purposes (when raytraced, lights don't show up).
To render the scene geometry, RayFile calls glDraw() on the root of the scene heirarchy, and the call recurses throughout the entire heirarchy. Group's glDraw() method modifies the ModelView matrix with it's local transform, before calling glDraw() on everything inside it. Each primitive has a glDraw() method that makes a good approximation of its appearance with GL or GLU calls. Each primitive also automatically generates texture coordinates if they're needed, and calls glLoad() and glUnload() on its material before and after rendering.
Material::glLoad() sets the current material to the appropriate values for the material, including texture state. Material::glUnload() exists to unload any textures that may have been loaded by the call to glLoad(), and turn off texturing so that untextured objects don't receive the wrong properties.
To approximate transparency, RayFile renders the scene geometry in two passes. First, it renders all the opaque objects. Once that's finished, it enables blending and renders all the transparent objects. Unfortunately, it can't be guaranteed that objects will be rendered in back-to-front order, and so colors may appear incorrectly. Also, GLU objects (spheres and cylinders) have internal polygons that give them unusual appearances when they're transparent. The only way to get around this would be to rewrite GLU's quadric rendering calls for ourselves, and well, we didn't want to do that.
Quaternions are used in part to handle the camera's interactive rotation. Specifically in the case of rotation around two axis simultaneously, error can quickly be introduced to the camera's orientation with lots of moving around when using eulerian angles. To fix the problem, the rotation around the x and y axes are represented by quaternions and then composed by quaternion multiplication. The resulting combined transformation is converted to a matrix to perform both rotations simultaneously to the camera's orientation, greatly reducing error.
See http://www.cs.berkeley.edu/~laura/cs184/quat/quaternion.html for more information about quaternion rotations.