The Big Honkin'
Ray Tracing Tutorial

 


Teal'c: "It is a weapon of great power O`Neill."
O'Neill: "Big and honkin'"

From the Stargate SG-1 Epsode Ascension

Project 2 consists of the implementation of a recursive ray tracing renderer.  The purpose of this tutorial is to provide you with a strong start on that project.  This tutorial will work you though getting a simple renderer operational.  It will not implement the lighting model, recursion, or antialiasing.  These issues will be up to you.  But, when you are done, you will have a fully operational renderer that really works.  The tutorial is rather long, but if you take it step-by-step, it won't seem too bad.
 

Notice:  Implementation of the Ray Tracer will require utilization of a ray intersection package I supply.  That package is dependent upon CGrPoint and CGrMaterial.   Since you have created your own scene graph components, you may need to either modify your version of CGrPoint to utilize some of the functions I've provided or replace it.  No member functions in CGrMaterial are called in software, it is only used as a pointer.  I suggest that you work through the tutorial given here using all of my classes, then adapt to utilize your own classes. 

I also suggest getting your ray tracer working with a small scene and then adding, so as to keep your develop turnaround time as small as possible.  Also, consider running in release mode as often as possible.  The speedup for this project is generally 10 to 1 or better.

You need to start with an OpenGL program that works.  I highly suggest that you do the Installing and Using CGrCamera tutorial first and work from that shell.    

The steps in the tutorial are:

Step 1 - Create a simple scene graph

You'll need for the rendering to be in a scene graph in order to render in some mode other than pure OpenGL.  Download the archive graphics.zip and obtain the following files that need to be added to the project in order to utilized the scene graph system.  Please put these files in a subdirectory named "graphics".  A few of these files are not needed yet, but we'll go ahead and copy them right now.  Add these files to your project:

Add a member variable of type CGrPtr<CGrObject> named m_scene to CChildView.  Don't forget to #include "graphics/GrObject.h".  Then, add the following code to CChildView::CChildView():
    CGrPtr<CGrComposite> scene = new CGrComposite;
    m_scene = scene;

    // A red box
    CGrPtr<CGrMaterial> redpaint = new CGrMaterial;
    redpaint->AmbientAndDiffuse(0.8f, 0.0f, 0.0f);
    scene->Child(redpaint);

    CGrPtr<CGrComposite> redbox = new CGrComposite;
    redpaint->Child(redbox);
    redbox->Box(1, 1, 1, 5, 5, 5);

    // A white box
    CGrPtr<CGrMaterial> whitepaint = new CGrMaterial;
    whitepaint->AmbientAndDiffuse(0.8f, 0.8f, 0.8f);
    scene->Child(whitepaint);

    CGrPtr<CGrComposite> whitebox = new CGrComposite;
    whitepaint->Child(whitebox);
    whitebox->Box(-10, -10, -10, 5, 5, 5);

Then add the line m_scene->glRender() to your OnGLDraw() function.  Delete any other things you may be drawing, but leave setup for any camera you may have.  This should compile and draw a simple cube.

Step 2 - Using an Alternative Renderer

You may have noticed the function void CGrObject::Render(CGrRenderer *p_renderer).  This is a virtual function just like glRender().  The difference is that glRender() is designed to render to OpenGL directly.  The implementations for different scene graph elements all know about OpenGL and directly draw in it.  Render() does not know about any particular renderer.  CGrRenderer is an abstract base class with a bunch of virtual functions for implementing a renderer of any type.  In fact, I've used it to implement an OpenGL renderer.  We'll change our program now to utilize the OpenGL renderer instead of glRender().

We're used to the idea of setting up OpenGL to render.  Here we're going to use a renderer that completely divorces us from the underlying mechanisms.  We won't care if it's OpenGL or whatever (well, we will in a bit).  Delete everything in OnGLDraw() and replace it with:

    //
    // Instantiate a renderer
    //

    COpenGLRenderer renderer;

    // Configure the renderer
    ConfigureRenderer(&renderer);

    //
    // Render the scene
    //

    renderer.Render(m_scene);

Add #include "graphics/OpenGLRenderer.h" to the top of ChildView.cpp.  This instantiates a renderer object, configures the renderer to use, and uses the renderer to render a scene graph.  Now, we need to add the ConfigureRenderer function.  Add the following function to CChildView:

//
// Name :         CChildView::ConfigureRenderer()
// Description :  Configures our renderer so it is able to render the scene.
//                Indicates how we'll do our projection, where the camera is,
//                and where any lights are located.
//

void CChildView::ConfigureRenderer(CGrRenderer *p_renderer)
{
    // Determine the screen size so we can determine the aspect ratio
    int width, height;
    GetSize(width, height);
    double aspectratio = double(width) / double(height);

    //
    // Set up the camera in the renderer
    //

    p_renderer->Perspective(m_camera.FieldOfView(), 
        aspectratio, // The aspect ratio.
        20., // Near clipping
        1000.); // Far clipping

    // m_camera.FieldOfView is the vertical field of view in degrees.

    //
    // Set the camera location
    //

    p_renderer->LookAt(m_camera.Eye()[0], m_camera.Eye()[1], m_camera.Eye()[2], 
        m_camera.Center()[0], m_camera.Center()[1], m_camera.Center()[2], 
        m_camera.Up()[0], m_camera.Up()[1], m_camera.Up()[2]);

    //
    // Set the light locations and colors
    //

    float dimd = 0.5f;
    GLfloat dim[] = {dimd, dimd, dimd, 1.0f};
    GLfloat brightwhite[] = {1.f, 1.f, 1.f, 1.0f};

    p_renderer->AddLight(CGrPoint(1, 0.5, 1.2, 0), 
        dim, brightwhite, brightwhite);
}

So, what does this do?  

Before, we configured OpenGL directly.  In this case, we're going to configure a generic renderer.  For now it will still be OpenGL, but note that everything is generic here and does not mean OpenGL.  We specify a perspective the same way we did in OpenGL because that makes a lot of sense.  We specify the LookAt location the same way as well.  Then, we add a light.  My Renderer superclass knows about lights and allows you to specify the location (in this case a vector), the ambient, diffuse, and specular color in a single line.  

We put this in a function called ConfigureRenderer, because we'll use it to configure a DIFFERENT renderer shortly.

When you run this, it should look almost identical to the way it looked before.  Note that there are now NO actual OpenGL calls in your code. 

Step 3 - Examining COpenGLRender

If you look at the header for CGrRenderer, you'll find the following functions:

    // The functions that make up the renderer
    // Some are abstract, others have default values
    virtual bool RendererStart();
    virtual bool RendererEnd();
    virtual void RendererBeginPolygon();
    virtual void RendererEndPolygon();
    virtual void RendererTexture(CGrTexture *p_texture);
    virtual void RendererNormal(const CGrPoint &n);
    virtual void RendererTexVertex(const CGrPoint &v);
    virtual void RendererPushMatrix();
    virtual void RendererPopMatrix();
    virtual void RendererRotate(double a, double x, double y, double z);
    virtual void RendererTranslate(double x, double y, double z);
    virtual void RendererTransform(const CGrTransform *p_transform);
    virtual void RendererMaterial(CGrMaterial *p_material);
    virtual void RendererColor(double *c);
    virtual void RendererSphere(const CGrPoint &center, double radius);

These are designed to be generic mechanisms for rendering.  COpenGLRenderer is derived from CGrRenderer and implements these functions.  Most simply map to an appropriate OpenGL call.  For example, the COpenGLRender implementation of RenderTranslate is:

void COpenGLRenderer::RendererTranslate(double x, double y, double z)
{
    glTranslated(x, y, z);
}

If you examine OpenGLRender.cpp, you'll see than many of the functions just map over to OpenGL functions.  The big difference is RendererEndPolygon().  COpenGLRender collects up the polygon information in lists (this is actually done by the superclass).  When we tell the system we have supplied everything it needs by calling RendererEndPolygon(), we then perform the OpenGL calls.  Examine the code in COpenGLRenderer::RendererEndPolygon() and be sure you understand what it all does.  You'll have to write your own version pretty soon.  

What It's Gonna Take

So much for that boring OpenGL stuff.  Let's get serious about ray tracing.  We're going to build a complete renderer here (though you'll have to add more features).  So, what will it take to do this?  Here are the steps we will work through:

Step 4 - Ray Trace Mode and Menu

We'll need to switch to ray tracing.  It is very common that graphics systems will utilize some fast on-screen renderer like OpenGL to see what the image looks like, then switch to a high-performance slow renderer to produce the final result.  This is exactly how your program is going to work.  When the program starts, it will display your model using OpenGL.  When you select a Ray Trace menu option, it will create an image using your slow, but much cooler, renderer and display that image instead of the OpenGL rendering.  

Add a bool value to CChildView called m_raytrace and initialize it to false in the CChildView constructor.  Then create a menu option:  Ray Trace.  I created a menu called Render and put the Ray Trace option in that menu.  Then create handlers for the menu option COMMAND and UPDATE_COMMAND_UI.  The body of OnUpdateRenderRaytrace() will simply be:

    pCmdUI->SetCheck(m_raytrace);

Put this following code into the body of OnRenderRaytrace():

    m_raytrace = !m_raytrace;
    Invalidate();
    if(!m_raytrace)
        return;

This will compile and you will be able to check and uncheck the menu option, but this obviously doesn't actually ray trace our image.  But, it's where we'll put the work.  

Step 5 - Create An Image to Render To

Our new renderer will render to an image rather than directly to the screen like OpenGL does.  We need to create a data structure to hold that image.  And, we want to make it easy to put that image on the screen.  First, add the following private member variables to CChildView:

    BYTE      **m_rayimage;
    int         m_rayimagewidth;
    int         m_rayimageheight;

In the constructor, set m_rayimage to NULL.  We're going to create a dynamically allocated 2D array for our image.  The height of the array (number of rows) will be the height of our image.  The width will be 3 times the width of our image.  This is so we can store a red, green, and blue value for each location in the image.  

We'll be adding code to the end of your Ray Trace menu handler (OnRenderRaytrace() above in my implementation). This code is executed when you select the Ray Trace menu option and turn on ray tracing.  First, let's get the current window size:

    GetSize(m_rayimagewidth, m_rayimageheight);

The way this image gets allocated is we allocate a set of pointers to the rows of the image:

    m_rayimage = new BYTE *[m_rayimageheight];

Now, we can allocate the memory for each row.  I want to utilize an OpenGL function to display this image once we've created it.  The function we will use is glDrawPixels.  The requirement for using this function is that the image be in one linear array and that the rows start on 4 byte boundaries.  We want our rows to contain 3 * m_rayimagewidth bytes.  Here's an easy way to get the right row width (add this):

    int rowwid = m_rayimagewidth * 3;
    while(rowwid % 4)
        rowwid++;

Now, we'll allocate the memory for the image using new BYTE[m_rayimageheight * rowwid].  The first rowwid bytes of this linear memory allocation are row 0, so I can do this:

    m_rayimage[0] = new BYTE[m_rayimageheight * rowwid];

The second rowwid bytes are row 1, the third are row 2, etc., so it could be set this way:

m_rayimage[1] = m_rayimage[0] + rowwid;           // Don't put this in your code...
m_rayimage[2] = m_rayimage[0] + 2 * rowwid;     // Don't put this in your code...

Let's create a simple loop to do this:

    for(int i=1; i<m_rayimageheight; i++)
    {
        m_rayimage[i] = m_rayimage[0] + i * rowwid;
    }

Pretty easy, huh?  This array will start with garbage in it, so let's write something into it to initialize it.  I recommend using some bright color so you'll be able to see your raytrace image fill it in, later.  This is from my code:

    for(int i=0; i<m_rayimageheight; i++)
    {
        // Fill the image with blue
        for(int j=0; j<m_rayimagewidth; j++)
        {
            m_rayimage[i][j * 3] = 0;               // red
            m_rayimage[i][j * 3 + 1] = 0;           // green
            m_rayimage[i][j * 3 + 2] = BYTE(255);   // blue
        }
    }

If you've taken CSE 471, you may know that Windows expects images to be in the order BGR (Blue, Green, Red).  But, we're going to use OpenGL to display our image rather than Windows, and it's more standard and expects the RGB order (though a Windows Extension allows BGR).

This fills the image with blue.  You should be able to compile now, though we still don't do anything.

BTW, what does it take to delete this image?  You should be able to figure that out and add something to the CChildView destructor to get rid of the image memory.  Also, we may ray trace more than once.  Add something that will delete a previous allocation if we ray trace a second time.

Here's what OnRenderRaytrace should look like so far.

Step 6 - Display the Ray Trace Image

Eventually we'll put something into the ray trace image.  OpenGL has a function to put images directly on the screen called glDrawPixels.  But, we want the pixels to correspond with pixels on the screen.  The easiest way to do this is to utilize a parallel orthographic projection.  Replace the body of OnGLDraw with this code that has two versions, a ray tracing version that just puts the image on the screen and an OpenGL rendered version::

    if(m_raytrace)
    {
        // Clear the color buffer
        glClearColor(0.0f, 0.0f, 0.0f, 0.0f) ;
        glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);

        // Set up for parallel projection
        int width, height;
        GetSize(width, height);

        glMatrixMode(GL_PROJECTION);
        glLoadIdentity();
        glOrtho(0, width, 0, height, -1, 1);

        glMatrixMode(GL_MODELVIEW);
        glLoadIdentity();

        // If we got it, draw it
        if(m_rayimage)
        {
            glRasterPos3i(0, 0, 0);
            glDrawPixels(m_rayimagewidth, m_rayimageheight, 
                GL_RGB, GL_UNSIGNED_BYTE, m_rayimage[0]);
        }

        glFlush();
    }
    else
    {
        //
        // Instantiate a renderer
        //

        COpenGLRenderer renderer;

        // Configure the renderer
        ConfigureRenderer(&renderer);

        //
        // Render the scene
        //

        renderer.Render(m_scene);
    }

Now, when you select the Ray Trace menu option you should get a bright-blue screen (or whatever color you used).  If you want to have some quick fun, try settings some pixel values in this array.  It's easy to draw a diagonal line and see it displayed on the screen.

Step 7 - Create a Renderer Class

Now, create a new Generic class called CMyRaytraceRenderer.  The easiest way to do this is to choose Project/Add Class... and select the C++ tab and C++ class. Enter the class name as CMyRaytraceRenderer and the Base Class as CGrRenderer.  This class is derived from CGrRenderer (public).  Be sure MyRaytraceRenderer.h is include "graphics/GrRenderer.h".  The latest Visual Studio has a bad habit of putting absolute directories in the include statements.  Add the following to the bottom of OnRenderRaytrace (the Ray Trace menu handler function):

    // Instantiate a raytrace object
    CMyRaytraceRenderer raytrace;

    // Generic configurations for all renderers
    ConfigureRenderer(&raytrace);

    //
    // Render the Scene
    //

    raytrace.Render(m_scene);
    Invalidate();

Be sure to add #include "MyRaytraceRenderer.h".  This should compile, though it doesn't do anything.

What does it take to configure a renderer?  Well, some things are generic and we're already taking care of that.  But, there are some things that are special to a ray tracer.  One is the destination image.  We need to tell the system the address and size of the destination image.  So, add these member variables to the new class CMyRaytraceRenderer:

    int     m_rayimagewidth;
    int     m_rayimageheight;
    BYTE  **m_rayimage;

 

Aside:  Why am I saving the size and why is it in two places?  Well, the first time I get the size (with GetSize above), it's because I want to allocate an image file.  That image file will remain fixed in size and we'll always need to know how many rows and columns it has.  The window may change in size after we render.  Once you have the system up and going, try resizing when you are displaying your raytrace image and see what happens.  The other part:  why in two places?  The first member variables are in CChildView because that's where we're creating our image array to render to.  The second versions are in CMyRaytraceRenderer.  We will tell it where to render to and it will remember by setting these member variables

Your variables should be private, of course, so add a member function to set them and call it before the call to raytrace.Render() above. I did one function that is passed the pointer to the image and the size.

There's one more variable I want to add to CMyRaytraceRenderer:

    CWnd   *m_window;

In the CMyRaytraceRenderer constructor, set this value to NULL.  Add a member function like this:


void CMyRaytraceRenderer::SetWindow(CWnd *p_window) { m_window = p_window; }

Then, in OnRenderRaytrace, before the raytrace.Render line, add this line:

    raytrace.SetWindow(this);
What This Does

Ray tracing can take a long time.  I've had images that took hours to render.  At the least you are talking minutes.  It's nice to see what's happening as the rendering goes on.  Windows has a function called UpdateWindow() that forces an immediate redraw of the window.  We can call m_window->Invalidate() and  m_window->UpdateWindow() at any time to see what the image looks like so far.

Step 8 - Adding the Intersection System

In a Z-Buffer system like OpenGL, calls to glBegin() and glEnd() create polygons.  These polygons are transformed with the current transformation matrix, transformed with the current projection matrix, and draw directly onto the screen.  This takes advantage of the fact that systems like OpenGL do everything related to a polygon immediately and then forget the polygon ever existed other than the pixels it has set in the frame buffer.  In a Ray Tracer, we have to maintain a data structure that contains all of the transformed polygons and repeatedly test this data structure for intersections of rays with polygons.  I've provided a data structure already to do this call CRayIntersection.  Add the following file to your project:

I've created the ray intersection system as a DLL, so it can always be in release mode, not matter what mode you are compiling your code in.  Add the following file from that graphics .zip file to your graphics directory (if you just unzipped, it will already be in your graphics directory). 

Now, we need to make Visual Studio link to the file.  This is the same process we used to make Visual Studio link to the OpenGL libraries.  Select the project name in Class View or Solution Explorer and choose the menu option Project/Properties.. Select Linker and Input.  Then set the Configuration to All Configurations.  In the additional dependencies line, add the following item:

Finally, put the following file in the main directory of your project:

At this point, be sure you project will compile and run.  If you didn't get the DLL in the right place, you'll get an error indicating the library cannot be found.  This error will occur at runtime.

Now, add a member variable to CMyRaytraceRenderer of type CRayIntersection named m_intersection.  Be sure to include "graphics/RayIntersection.h".  We now have an intersection system object available to use.

Step 9 - Renderer Startup and the Matrix Stack

Add a member function bool RendererStart() to CMyRaytraceRenderer.  Go ahead and make the function return true.  We'll not have anything that can fail in our renderer.  The first thing this function will do is to initialize the ray intersection data structure:

    m_intersection.Initialize();

We will need a transformation matrix stack just like the one in OpenGL.  But, we're not in OpenGL, so we have to do this ourselves!  Basically, this is a stack of CGrTransform objects.  Add the following data item to CMyRaytraceRenderer:

    std::list<CGrTransform> m_mstack;

Be sure to add the headers <list> and "graphics/GrTransform.h" so these values can be identified.  In RendererStart(), go ahead and clear this matrix stack this way:

    m_mstack.clear();

Our renderer should start out with a camera transformation matrix on the stack.  In OpenGL, the first thing you did in rendering was to glSetIdentity() and gluLookAt().  That put a transformation onto the current matrix that transformed the camera location to the origin and it's orientation to look down the Z axis.  We'll need the same thing here.  Fortunately, if you are using my CGrTransform class, there's a function to create such a matrix.  You are free to borrow that function if you are using your own transform class.  Add this code to RendererStart():

    // We have to do all of the matrix work ourselves.
    // Set up the matrix stack.
    CGrTransform t;
    t.SetLookAt(Eye().X(), Eye().Y(), Eye().Z(),
                Center().X(), Center().Y(), Center().Z(),
                Up().X(), Up().Y(), Up().Z());

    m_mstack.push_back(t);

Eye, Center, and Up are all functions from the renderer superclass.  

One last thing we need to keep track of:  the currently selected material.  Add a member variable:  CGrMaterial *m_material; to CMyRaytraceRenderer.  Set it to NULL in RendererStart() (until we have a material defined).  

Hints of Things to Come

I'm not going to utilize lighting in this tutorial other than a pretend ambient.  You'll have to set the lights up later.  Note that you'll have to transform the lights to their locations after the lookat matrix application.  You would do that here.  In my implementation I made a copy of the lights list after the transformation.  Simply multiply every light location by the current matrix.  

One simple thing:  Let's provide an implementation for RendererMaterial().  This function is called when a new material is specified as the scene graph is traversed:

void CMyRaytraceRenderer::RendererMaterial(CGrMaterial *p_material)
{   
    m_material = p_material;
}

If this ain't easy, I don't know what is.  m_material just keeps a pointer to the last selected material.

Things You'll Have to Supply

This tutorial does not do everything.  There are several virtual functions in CGrRenderer that you must provide implementations of for your CMyRenderer to work.  The scene graph in my example did not have any transformations other than LookAt, which I've given you.  You'll have to provide implementations of:

Hint:  To push the current matrix requires m_mstack.push_back(m_mstack.back());  These implementations require very little code.  My implementation required 8 lines of code for all four function bodies combined.

Step 10 - RendererEndPolygon()

Here's a complete  implementation of RendererEndPolygon():

//
// Name : CMyRaytraceRenderer::RendererEndPolygon()
// Description : End definition of a polygon. The superclass has
// already collected the polygon information
//

void CMyRaytraceRenderer::RendererEndPolygon()
{
    const std::list<CGrPoint> &vertices = PolyVertices();
    const std::list<CGrPoint> &normals = PolyNormals();
    const std::list<CGrPoint> &tvertices = PolyTexVertices();

    // Allocate a new polygon in the ray intersection system
    m_intersection.PolygonBegin();
    m_intersection.Material(m_material);

    if(PolyTexture())
    {
        m_intersection.Texture(PolyTexture());
    }

    std::list<CGrPoint>::const_iterator normal = normals.begin();
    std::list<CGrPoint>::const_iterator tvertex = tvertices.begin();

    for(std::list<CGrPoint>::const_iterator i=vertices.begin(); i!=vertices.end(); i++)
    {
        if(normal != normals.end())
        {
            m_intersection.Normal(m_mstack.back() * *normal);
            normal++;
        }

        if(tvertex != tvertices.end())
        {
            m_intersection.TexVertex(*tvertex);
            tvertex++;
        }

        m_intersection.Vertex(m_mstack.back() * *i);
    }

    m_intersection.PolygonEnd();
}

This basically gets a polygon that's been collected by the renderer superclass and puts it into the ray intersection system.  I figured I could give you the code faster than I could document the various functions here. 

Just to be clear, the process for defining a polygon to the intersection system is:

  1. Call PolygonBegin()
  2. Set the material for the polygon using the Material() function.
  3. Set any texture that is used using the Texture() function.
  4. For each polygon, provide the vertices, normals, and, optionally, texture coordinates, then call PolygonEnd()

Step 11 - RendererEnd()

The last major function in your renderer to be called is bool CMyRaytraceRenderer::RendererEnd().  Add that function now and make it return true.

What this function means:  All of the rendering of the scene graph is done.  This means the intersection data structure is full.  Add the following line to tell it that we're through putting in polygons:

    m_intersection.LoadingComplete();

Here is where the ray-tracing loop will go.  This loop will create rays that originate at the camera location (0, 0, 0) and shoot through the pixels of the display screen.  The display screen can go anywhere.  To make things simple, I'll assume the display screen is centered on the Z axis at Z= -1.  Now, I want to know where it is in space.  

The function ProjectionAngle() returns the angle we set for our projection (in degrees, be sure you convert to radians if you call any regular C++ trig functions).  The height of the image should be such that the projection angle sees the top and bottom of the image.  Given the next diagram, you should be able to work out the math for ymin and yhit:

The function ProjectionAspect() gives us the ratio of width / height for our projection window.  So, xmin is simply ymin * ProjectionAspect().  You can figure out how to compute xwid, the width.  Be sure you use double for all of these values.

Example:

Let ProjectionAngle() be 60 degrees and ProjectionAspect() 1.333.  Then ymin will be -0.577 and yhit will be 1.1547.  xmin will be -0.7698 and xwid will be 1.539.  Note that these are the sizes of the window in the projection space, not the size of the screen.  These locations map to the image we will render to.

So, where are the pixels?  Well, the pixels range from xmin to xmin + xwid in the X dimension and ymin to ymin + ywid in the Y dimension.  So, a given pixel lower left corner will be at:  (c / m_rayimagewidth * xwid + xmin, r / m_rayimageheight * yhit + ymin), where (c,r) is the column and row of the pixel.  BUT, if we consider each pixel to be a square, we would consider the center of the pixel to be:  

(xmin + (c + 0.5) / m_rayimagewidth * xwid, ymin + (r + 0.5) / m_rayimageheight * yhit)

Now, write a loop that loops over the pixels in the image and computes an x,y value at each pixel that corresponds to the point in space.  

Step 12 - Constructing a Ray

Given this information, it's easy to make a ray.  My Intersection code defines a CRay class.  Make a ray like this:

            // Construct a Ray
            CRay ray(CGrPoint(0, 0, 0), Normalize3(CGrPoint(x, y, -1, 0)));

This assumes you have computed the x,y location on the display plane in the variables x,y.  Now let's shoot that puppy off into space and see what it hits:

            double t;                                   // Will be distance to intersection
            CGrPoint intersect;                         // Will by x,y,z location of intersection
            const CRayIntersection::Object *nearest;    // Pointer to intersecting object
            if(m_intersection.Intersect(ray, 1e20, NULL, nearest, t, intersect))
            {
                // We hit something...
                m_rayimage[r][c * 3] = BYTE(255);
                m_rayimage[r][c * 3 + 1] = BYTE(255);
                m_rayimage[r][c * 3 + 2] = BYTE(255);
            }
            else
            {
                // We hit nothing...
                m_rayimage[r][c * 3] = 0;
                m_rayimage[r][c * 3 + 1] = 0;
                m_rayimage[r][c * 3 + 2] = 0;
            }

If you get errors in this code stating that the subscript is not of integral type, it's probably because you didn't make r and c integers.  They need to be integers that loop over the pixel locations!

This should run and give you an image with white where objects are (meaning the ray hit something) and black elsewhere.  

Helpful Hint

It will help you to see what is going on if you add the following code once per row:

m_window->Invalidate();
m_window->UpdateWindow();

This will cause the window to be drawn each row.  Setting double-buffering on really makes this look nicer.  Note that if you do this, most of the time in this simple example will be spent updating the screen, but that will change.  I often make this code only update once every 10 or 20 rows, so the rendering is faster.

The input parameters to m_intersection.Intersect() are:

The first parameter is the ray we are testing.  The second is a maximum t value we'll accept.  Right now I'm using a large value (ten to the twentieth) to try everything.  Later you'll see a use for this.  The third parameter is some polygon we ignore in the testing.  The last three parameters are outputs that will be set if we have an intersection.  If we get an intersection, the function returns true.

Why would we ever ignore a polygon?  We'll discuss issues of self-shadowing and self-intersections in class.  Well, if we want to know if a light hits a surface at a point, we can shoot a ray off into space from that point.  That ray starts at a point on a polygon, so the distance to the intersection with the polygon it was shot from is zero (or worse, some tiny number due to roundoff errors).  Generally, we'll ignore the polygon that we are shooting the ray from.h.  

Step 13 - Getting More Information

The function m_intersection.Intersect() just determines if an intersection occurred.  Often that's all of the information you'll need.  However, you sometimes need more information.  That information is provided by m_intersection.InsersectInfo():

// Determine information about the intersection
CGrPoint N;
CGrMaterial *material;
CGrTexture *texture;
CGrPoint texcoord;

m_intersection.IntersectInfo(ray, nearest, t,
                                           N, material, texture, texcoord);

Only call this function AFTER you have had an intersection.  What you pass to the function as parameters are:  ray, nearest, and t.  The other parameters are passed by reference and used for returning results.  The results are:

Replace the code above in the second that starts with the comment // We hit something with:

                // We hit something...
                // Determine information about the intersection
                CGrPoint N;
                CGrMaterial *material;
                CGrTexture *texture;
                CGrPoint texcoord;

                m_intersection.IntersectInfo(ray, nearest, t,
                                             N, material, texture, texcoord);

                if(material != NULL)
                {
                    m_rayimage[r][c * 3] = BYTE(material->Diffuse(0) * 255);
                    m_rayimage[r][c * 3 + 1] = BYTE(material->Diffuse(1) * 255);
                    m_rayimage[r][c * 3 + 2] = BYTE(material->Diffuse(2) * 255);
                }

When you run this you'll get the polygons in the right color (but, with no lighting, yet).

Hint:

Notice that I multiply the colors by 255 to make them BYTE values.  Be sure that you check for range here.  It's easy for the color you compute to exceed 1.0.  If it were 1.5, or example, then 1.5 * 255 is 382.  Cram that in a byte and you get 126, not the color you expect.  If the value goes over 1.0, cap it at 1.0.  (This is called range bounding) Also watch out for negative values.  A negative color does not exist.

If you get really desperate, here is a completed RendererEnd function to look at.  I would try to get it working before you look at it. 

Other Things You'll Need to Know

There are some other functions you'll need to know:

struct Light
{
   Light();

   CGrPoint m_pos; // Where be the light?
   float m_ambient[4];
   float m_diffuse[4];
   float m_specular[4];
};