OpenGL has a feature called the accumulation buffer. The accumulation buffer is simply an extra image buffer that is used to accumulate composite images. It's quite useful for some special effects you may be interested in doing such as antialiasing, depth-of-field, and motion blur. Note that not all implementations of OpenGL support the accumulation buffer, but it is generally common today.
You'll find a project in tutorial5.zip. Copy this project and make sure it compiles. When you run it you should set a set of cones. I've installed my CGrCamera class to make it easy to pitch and roll the cones with the mouse.
OpenGL has several buffers that are available for your use. A buffer is simply an image that is manipulated by OpenGL. You've been using two of them fairly regularly the entire term. The GL_FRONT buffer contains what is displayed on the screen. The GL_BACK buffer is an off-screen buffer you've been drawing into when double-buffering is selected. The back buffer is drawn into off-screen, then swapped onto the screen.
You can choose what buffer you draw in using the glDrawBuffer function. To make drawing go into the GL_BACK buffer, use:
glDrawBuffer(GL_BACK);
To draw in the front buffer, use:
glDrawBuffer(GL_FRONT);
The COpenGLWnd superclass calls glDrawBuffer(GL_BACK) before it calls your OnGLDraw function if you select double buffering. After your OnGLDraw function returns, the superclass performs a SwapBuffers call to swap the back buffer data into the front buffer so what you have drawn appears on the screen.
Try adding a glDrawBuffer(GL_FRONT) call to OnGLDraw() before you start drawing and see what happens as you move the mouse. Note that you never draw in the back buffer, so you can see the cones draw, but the SwapBuffers call swaps in the back buffer when drawing if complete, overwriting what you draw in the front buffer.
Go ahead and remove that call, now.
If you are using a stereo display, there are even more buffers. Look up the documentation for glDrawBuffer() to see what buffers it is possible to draw into. |
OpenGL has another buffer called the accumulation buffer. The idea of the accumulation buffer is that you can take what you have drawn in the current drawing buffer (front or back) and move that or add it to the accumulation buffer. You can also transfer an image from the accumulation buffer to the current drawing buffer. It's important to note that the accumulation buffer must always works on one image in its entirety. You can't manipulate the pixels individually.
Here are the important accumulation buffer functions in OpenGL:
glClear(GL_ACCUM_BUFFER_BIT): This function clears the accumulation buffer to the current clear color (set by glClearColor(). The default is black.)
glAccum(GL_ACCUM, mult): This takes the contents of the current draw buffer, multiplies each pixel value by mult and adds this to the current accumulation buffer contents.
glAccum(GL_RETURN,mult): This function takes the current content of the accumulation buffer, multiplies each pixel value by mult, and places the result in the current draw buffer.
Suppose I want to render two images and produce an image that is the average of the two. I will do the following steps:
Things to watch out for: The accumulation buffer is not range checked. If you add two white images with mult values greater than 0.5, the result is undefined. Generally, it looks like garbage. As an example, if a pixel value is (1,1,1) in the two images and a mult value of 0.7 is used, the pixel in the accumulation buffer will be (1.4,1.4,1.4), but that's not what you will get because the values are not range checked. You'll probably get something more like (0.4, 0.4, 0.4) or it may even be a negative color.
Just make sure the total of all mult values you use is less than or equal to 1.0. Above I used 0.5 twice, so the total is 0.5+0.5=1.0 and everyone is happy happy.
Let's try something really simple as a first try on the accumulation buffer. Replace the call to ActualDraw() in your program with this:
glClear(GL_ACCUM_BUFFER_BIT);
for(int i=0; i<4; i++)
{
glPushMatrix();
glRotated(i * 10., 0, 1, 0);
ActualDraw();
glPopMatrix();
glAccum(GL_ACCUM, 0.25);
}
glAccum(GL_RETURN, 1.0);
What this does is:
Yes, this looks pretty yucky, but it gives you an idea of what is going on.
A problem with the accumulation buffer is that it is often very, very slow. Generally, accumulation buffer techniques are used to make high-quality images, not real-time images, so the implementations have not tried to make it very fast at all. So, you will often find yourself starting at a boring screen waiting for the image to pop up.
What I find useful is to draw the current state of the accumulation buffer as we add things in. After the call to glAccum(GL_ACCUM, 0.25), add the following three lines:
glDrawBuffer(GL_FRONT);
glAccum(GL_RETURN, 1.0);
glDrawBuffer(GL_BACK);
What this does is switch the current draw buffer to the front buffer, transfer the accumulation buffer contents into that buffer, and swap the draw buffer back. Run this and you will see that you can tell what's happening as the images are accumulated.
You will often utilize the mult factor in these calls. If I am averaging 4 images, the first two would be added in with a mult of 0.25, so the accumulation buffer image would be at half intensity. I can increase to full intensity by using a mult value of 2.0. Here's a fairly common way you will set up loops like this:
glClear(GL_ACCUM_BUFFER_BIT);
for(int j = 0; j < iterations; j++)
{
// Draw something here.
...
glAccum(GL_ACCUM, float(1.0 / iterations));
glDrawBuffer(GL_FRONT);
glAccum(GL_RETURN, float(iterations) / (j + 1));
glDrawBuffer(GL_BACK);
}
glAccum(GL_RETURN, 1.f);
Go ahead and restore the original code. Now, we want to add antialiasing. The idea of antialiasing is that we will draw our image multiple times, jittering each version in the x and y dimensions so that the center of the pixels we draw are at various points in a pixel in the final image. I have included in this project a file called jitter.h that includes jitter values from selected numbers of jitter images from 1 to 16. Take a look at this file. Note that the Z values are ignored. You'll see the values for JITTER1 are (0.5, 0.5). This is simply the center of the pixel. The values for JITTER2 are (0.25, 0.75) and (0.75, 0.25). These are two corner pixels.
A note about using the JITTER array provided by jitter.h. Not all values are supplied. For example, no jitter values are supplied for 10 and 11. For those values, JITTER[10] is NULL. Be sure you advance to the first non-null value. For example:
int antialiasing = m_iterations;
// Ensure we are not past the end of the table.
if(antialiasing > 16)
antialiasing = 16;
// Advance to the next non-null value if antialiasing is avalue like 10 which is not defined.
while(JITTER[antialiasing] == NULL && antialiasing < 16)
antialiasing ++;
The problem with the jitter values is that we need to jitter in screen coordinates, not world coordinates. You can't just translate by these amounts, the values would not be right. And, moving something near the camera a small distance is the same as moving something far away from the camera a larger distance. Hence, we can't just use glTranslated to do our jittering.
If you have the OpenGL red book, you'll find routines in it that replace gluPespective, but provide extra parameters for jittering. I've provided slightly modified versions of these routines in accjitter.cpp. The routine you'll want to use is:
void accPerspective(GLdouble fovy, GLdouble aspect, GLdouble zNear, GLdouble zFar,
GLdouble pixdx, GLdouble pixdy, GLdouble eyedx, GLdouble eyedy,
GLdouble focus)
If you include accjitter.h, you can use these routines. The first four parameters to accPerspective are the same as for gluPerspective. pixdx and pixdy are parameters you can use to jitter the renderered image by an x and y value in screen coordinates. eyedx and eyedy are values you can use to jitter the eye location in screen coordinates (more on this later) and focus is where you want the center of focus to be if you move the eye location (again, more on this later).
Now, you need to create a few things. Add a bool member variable to CChildView called m_antialias and add a menu option that toggles the value of m_antialias and does an Invalidate(). Set m_antialias to false in the constructor. The idea is that you select this option to get an antialiased image. Make this option checked in your menu.
Then, add a int member variable named m_iterations. This value will determine how many iterations of antialiasing we will utilize. Larger numbers are higher quality, but take more time. Set the variable to 1 in the constructor. Then, add a dialog box that will allow you to set the value of this variable.
Now, replace the existing code starting at "Set up the camera" and ending with ActualDraw() with the following code. note that the else part of the if is just the original code. The if part is the new antialiasing loop. Note the use of accPerspective and jitter values.
if(m_antialias)
{
int antialiasing = m_iterations;
if(antialiasing > 16)
antialiasing = 16;
while(JITTER[antialiasing] == NULL && antialiasing < 16)
antialiasing ++;
glClear(GL_ACCUM_BUFFER_BIT);
for(int j = 0; j < antialiasing; j++)
{
accPerspective(m_camera.FieldOfView(), // Vertical field of view in degrees.
aspectratio, // The aspect ratio.
20., // Near clipping
1000.,
JITTER[antialiasing][j].X(), JITTER[antialiasing][j].Y(),
0.0, 0.0, 1.0);
m_camera.gluLookAt();
ActualDraw();
glAccum(GL_ACCUM, float(1.0 / antialiasing));
glDrawBuffer(GL_FRONT);
glAccum(GL_RETURN, float(antialiasing) / (j + 1));
glDrawBuffer(GL_BACK);
}
glAccum(GL_RETURN, 1.0);
}
else
{
//
// Set up the camera
//
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// Set the camera parameters
gluPerspective(m_camera.FieldOfView(), // Vertical field of view in degrees.
aspectratio, // The aspect ratio.
20., // Near clipping
1000.); // Far clipping
// Set the camera location
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
m_camera.gluLookAt();
ActualDraw();
}
Important: accPerspective() not only sets up the projection matrix, but it also sets up the modelview matrix. You'll notice that the antialiased version of the code above does not have a call to glMatrixMode(GL_MODELVIEW) and glLoadIdentity() like the second version does. This is because these operations are done by accPerspective().
Now, I want you to implement depth-of-field. Depth-of-field means that you have a specific point you are focusing on and as you get nearer are farther away from that point, things get out of focus. Cameras have depth-of-field because they are not really a pinhole model. Instead, they have an aperture that allows light in with a certain diameter. This is equivalent to taking a pinhole camera (like in OpenGL) and taking a lot of pictures within that aperture and averaging them. If you always point the camera at some specific point, that point will stay in focus, but things that are nearer the camera or farther away will be out of focus because you have moved the camera.
You implement depth-of-field by moving the OpenGL in space while still pointing at some single point in the distance. This is the focus point. The last parameter of accPerspective is the distance to the focus point. My cones are centered on 0, 0, 0. I would like that to be the focus point. So, I'll set the focus value to: double depth = CGrPoint(m_camera.Eye()).Length3(); This is how far the focus point is from the eye. Were the center elsewhere, I would use (CGrPoint(m_camera.Eye()) - center).Length3(). This is simply the length of a vector to the eye position in space from the center of focus.
The best way to jitter the eye when doing depth-of-field is to use random values. Add the following code to the CChildView constructor to initialize the random number generator:
srand((unsigned int)time(NULL));
This seeds the random number generator with the current time. Then, each time you call accPerspective, set the eyedx and eyedy values to m_eyedist * (double(rand()) / RAND_MAX - 0.5) each (two different random numbers). We'll not use any jitter values in our depth-of-field solution. Yes, you can combine them, but it is a bit tricky to get right. I encourage you to experiment with it, some.
I'll leave the solution to this problem up to you, now...