Step 8:  Fun with the Poisson Distribution and Ghostly Boxes

Example Images for this step.

After last weeks step, this step will be rather short.  It's mainly getting you moved further along with your ray tracer and learning a bit about stochastic ray tracing.  This step builds on the work you did in step 7.  If you were unable to complete step 7, please let us know so we can help in some way to keep you moving.

I'm going to ask you to do two tasks independently to get started.  The first requires that you understand the Poisson distribution.  The second is a basic restructuring of your code.

First Task

Create a class CPoisson2D.  This class is a 2D Poisson distribution generator.  See the Poisson help page.  You should write this alone and then generate 100 number in the range (0, 0) to (1, 1).  Load these into Excel.  Then do a dot plot on them.  Also, do a dot plot in Excel on 100 uniformly distributed random numbers. 

Paste these two graphs into a Word document and turn them in with the step assignment. 

(Questions you must answer during this step and turn in in addition to the code you create are highlight as above).

Be sure to look at Lecture set 11 for details on the Poisson distribution used in computer graphics.  You'll use this class later.

BTW, if you look on Wiki, you'll find a definition of the Poisson distribution that is nothing like what we have covered.  That's actually not the same Poisson distribution.  That one was named after a dude named Simon-Denis Poisson.  The one we use is named after the fish.  Technically, it is sometimes called the Poisson-disk distribution, which sounds a bit macabre. 

When generating numbers in Windows, you have two choices.  You can use rand(), the standard C class for random numbers.  Then, double(rand()) / double(RAND_MAX) will give you uniform random numbers between 0 and 1.  You have to seed the random number generator to use this.  See this page for details. 

Microsoft recommends using the new replacement for rand():  rand_s().  I tried it and found it to be massively slower than rand(), so I don't recommend it. 

Second Task

Reorganize the code in your project so you have a function called RayColor with the following prototype: 

void CMyRaytraceRenderer::RayColor(const CRay &p_ray, 
                                      CRayColor &p_color, 
                                      int p_recurse, 
                                      const CRayIntersection::Object *p_ignore)
                                      

If you need some additional parameters, that's okay, but you have to justify why they were needed in the Word file you are turning in with this step assignment.  If you have put your renderer in some other class name, that is fine as well.

The parameters are:

You'll have to create an appropriate CRayColor class, BTW.  Your color in CRayColor should be represented with a floating point number, NOT a BYTE integer.  Make your code call this function and, when done, convert the result to the right range and range bound it.  You should be able to complete these tasks on your own.

Third Task

Add a menu item and dialog box to your project that allows you to configure the following values.  We did dialog boxes in Step 1, so review that step if necessary.  These dialog boxes should match variables in your rendering class.  So, for example, the Antialiasing count item should match an integer member variable in your rendering class, perhaps named m_antialiasing. 

What I generally do is to create a variable in CChildView that matches the dialog box item.  Then, when I instantiate my renderer object, I tell it the variable a value, something like this:

    // Instantiate a raytrace object
    CMyRaytraceRenderer raytrace;

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

    //
    // Render the Scene
    //

    raytrace.SetImage(m_rayimage, m_rayimagewidth, m_rayimageheight);
    raytrace.SetWindow(this);
    raytrace.SetAntialias(m_antialias);
    raytrace.Render(m_scene);             // Do this last
    Invalidate();

Antialiasing

You should now have a main loop in your renderer that looks something like this:

    for(int r=0;  r<       ;  r++)
    {
        for(int c=0;  c<       ;  c++)
        {
            double x =      + (c + 0.5) /          *     ;
            double y =      + (r + 0.5) /          *     ;

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

            CRayColor color;
            RayColor(ray, color, 1, NULL);

            m_rayimage[r][c * 3] = ByteRange(color.Red());
            m_rayimage[r][c * 3 + 1] = ByteRange(color.Grn());
            m_rayimage[r][c * 3 + 2] = ByteRange(color.Blu());
        }

        if((r % 10) == 0)
        {   
            m_window->Invalidate();
            MSG msg;
            while(PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
                DispatchMessage(&msg);
        }
    }

I've left a few blanks here for things you should have figured out for step 7. To modify this to implement antialiasing, all you have to do is add another level of looping:

    for(int r=0;  r<       ;  r++)
    {
        for(int c=0;  c<       ;  c++)
        {
            for(int a = 0; a < m_antialias;  a++)
            {

Add this into your code, indenting the body and adding the appropriate end curly brace.  This should compile and run, but increasing m_antialias won't do anything other than slow the renderer down.

We're going to need to average the color of the rays we shoot.  What you do NOT want to do is this:

                m_rayimage[r][c * 3] += ByteRange(color.Red());		// Bad
                m_rayimage[r][c * 3 + 1] += ByteRange(color.Grn());	// Evil
                m_rayimage[r][c * 3 + 2] += ByteRange(color.Blu());	// Satanic

and then after the loop this:

            m_rayimage[r][c * 3] /= m_antialias;		// Dumb
            m_rayimage[r][c * 3 + 1] /= m_antialias;		// Awful
            m_rayimage[r][c * 3 + 2] /= m_antialias;		// Oh, poo

This would be bad, evil, and look really, really ugly.  The colors will not be right at all. 

Conversion to a BYTE is the absolute last thing you will do for a pixel.  Any math needs to be done in floating point.  We can do something as simple as:  double color[3] and do this just fine.  Add the following line after the for statements for r and c and before the for statement for a:

            double colorTotal[3] = {0, 0, 0};

Now, change the existing code that puts the colors into the image to this:

                colorTotal[0] += color.Red();
                colorTotal[1] += color.Grn();
                colorTotal[2] += color.Blu();

Finally, after the end of the "a" loop, add this code to put the pixel into the image:

            m_rayimage[r][c * 3] = ByteRange(colorTotal[0]/m_antialias);
            m_rayimage[r][c * 3 + 1] = ByteRange(colorTotal[1]/m_antialias);
            m_rayimage[r][c * 3 + 2] = ByteRange(colorTotal[2]/m_antialias);

This should run okay for different values of m_antialias, but it is not antialiased yet.  We've not changed where the ray goes through the pixel, so every color we compute will be the same.  Here's where we'll add that Poisson distribution.

First off, do you know where the ray loop is and where the pixel loop is?  The ray loop is the inner loop that computes the ray colors.  The pixel loop is the next level out, where we iterate over the pixels of the image.  Be sure you are clear, because I'm going to use that terminology now. 

Add the following lines in the pixel loop before the beginning of the ray loop to initialize your Poisson number generator.

            CPoisson2D poisson;
            poisson.SetMinDistance(1. / sqrt((double)m_antialias) * 0.5);

I have assumed you called the function to set the minimum distance SetMinDistance().  If you named it something else, just change this line to match your naming.  You do have a function to set the minimum distance, right?

Finally, use the generated random number to jitter the ray randomly for each value of a.  My code looks something like this:

                poisson.Generate();

                double x =      + (c + poisson.X()) /          *     ;
                double y =      + (r + poisson.Y()) /          *     ;

The Generate() function generates a new number.  Then the function X() and Y() obtain the random numbers.  Your functions may vary, but they should do the same thing.  Try this for m_antialias values of 1, 4, 8, and 16.  BTW, now is probably a good time to read my Ray Tracing tips page. 

You will probably note that, for m_antialias = 1, this looks awful.  In general, this technique doesn't really work all that well for values less than 8 and really works badly for a value of 1. 

In the Word document you are turning in, provide a reason why the image looks bad for a
value of one and how you fixed the problem, so the the image looks good for a
value of 1 (up to the aliasing, of course). 

Of course, fix your program in the way you suggested.

Try this

Just temporarily, change the lines above to this:

                poisson.Generate();

                double x =      + (c + poisson.X() * 5) /          *     ;
                double y =      + (r + poisson.Y() * 5) /          *     ;

Now, try running your ray tracer with antialias counts of 4, 16, and 32.  Make the image smaller so this does not take a long time. 

Why does this image look like it does?  What is this code doing?  Put your answer in the Word file.

Transparency

We're going to play with transparency a bit.  We're NOT going to fully implement it, that's up to you, should you decide to do so, but we'll get a good start just to see how recursion is done. 

First add this code to the end of CChildView::CChildView().  This adds another thin box in front of everything else.

    // Another box in front of things
    // A grey box
    CGrPtr<CGrMaterial> greypaint = new CGrMaterial;
    greypaint->AmbientAndDiffuse(0.3f, 0.3f, 0.3f);
    scene->Child(greypaint);

    CGrPtr<CGrComposite> whitebox2 = new CGrComposite;
    greypaint->Child(whitebox2);
    whitebox2->Box(-10, -10, 10, 20, 20, 1);

You've probably not worried in your color computations as to if we hit the front or back of an object.  Without transparency, we would have never hit the back.  But, we might now, so you need to make your code deal with that.  It's easy to tell if you hit the back of an polygon.  Just compute Dot3(N, V).  If the value is less than or equal to zero, don't compute any color from the illumination.  What I did was just put a big if statement around my lights loop.  The start of that loop now looks like this:

    // Compute the view vector
    CGrPoint V = -p_ray.Direction();

    if(Dot3(N, V) > 0)
    {
        for(unsigned int l=0;  l<m_lights.size();  l++)
        {
            const Light &light = m_lights[l];
            int c;

            // Ambient light term
            if(material->Ambient(0) >= 0.)
            {
                for(c=0;  c<3;  c++)
                {
                    p_color[c] += light.m_ambient[c] * material->Ambient(c) * tcolor[c];
                }
            }

Go to the very end of your RayColor function.  Put in the following code at the end of your if statement body (this code should be executed whenever there is an intersection):

    if(p_recurse > 1)
    {
        double etaInside = 1.;
        double etaR = 1. / etaInside;

        // Are we hitting the front or the back?
        if(Dot3(N, V) <= 0)
        {
            // We are hitting the back
            N = -N;
            etaR = 1. / etaR;
        }

        double rootTerm =                                       ;
        if(rootTerm >= 0)
        {
            CGrPoint T =                                                       ;

            CRayColor color;
            CRay rayT(intersect, T);
            RayColor(rayT, color, p_recurse-1, nearest);

            for(int c=0;  c<3;  c++)
                p_color[c] += color[c];
        }
    }

You'll notice that two parts of this code have been omitted.  You need to fill those in so they complete the equation from Lecture set 8, slide 36, the transmission direction equation.  I have provided the variable eta sub R above.  In your Word file, answer the following two questions:

  1. What is the code doing inside the body of the if statement that begins if(Dot3(N, V) <= 0?
  2. Why are we testing rootTerm >= 0?

Finally, in your main pixel loop, change the value for recursion to the value set by your dialog box, so the routine does recurse.  Try recursion values of 6.

I've put in an initial etaInside of 1.  Change that to a larger value to see refraction, but get this working with a value of 1 first.  You will see no refraction and see the cubes through the front cube unmoved.  When you increase etaInside, you'll see refraction of the cubes behind.  Be sure to try different views.

Notice:  I would not accept the above code in a project as an implementation of transparency.  There's a lot missing.  You must add the eta values to your material properties so you can change them for different objects.  Also, this code just adds in the transparency color.  It does not implement a real color model.  You must do the Hall color model correctly.