The sample program presented in this section will show you how to use display lists, basic transforms, the matrix stack, and double buffering.
Once again, follow the above steps to get to a starting point
for this third sample program (or continue to modify the same
program). In this program we will be creating a "robot arm"
that you can control with your mouse. This "arm" will
actually be two rectangles where one rectangle rotates about a
point on the other rectangle. Begin by adding the public member
function "void RenderScene(void)" to the CGLSample3Doc
class. Modify CGLSample3View::OnPaint and CGLSample3Doc:: RenderScene
so that they look like this:
void CGLSample3View::OnPaint()
{
CPaintDC dc(this); // device context for painting
CGLSample3Doc* pDoc = GetDocument();
pDoc->RenderScene();
}
void CGLSample3Doc::RenderScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glFlush();
}
At this time our program generates a black screen. We will do
something about that in a minute, but first we need to add some
state variables to the CGLSample3Doc class. Add the following
enumerated types and variables to the document class. Then initialize
them in the document constructor.
enum GLDisplayListNames
{
ArmPart=1
};
double m_transY;
double m_transX;
double m_angle2;
double m_angle1;
CGLSample3Doc::CGLSample3Doc()
{
m_transY=100;
m_transX=100;
m_angle2=15;
m_angle1=15;
}
- ArmPart - This is a identifier for the display list that we
will be creating to draw the parts of the arm.
- m_transY - This is the y offset of the arm from the world
coordinate system origin
- m_transX - This is the x offset of the arm from the world
coordinate system origin
- m_angle2 - This is the angle of the second part of the arm
with respect to the first part.
- m_angle1 - This is the angle of the first part of the arm
with respect to the world coordinate axis.
We will be using what is known as a display list to draw the parts
of our arm. A display list is simply a list of OpenGL commands
that have been stored and named for future processing. Display
lists are often preprocessed, giving them a speed advantage over
the same commands called out of a display list. Once a display
list is created, its commands may be executed by calling glCallList
with the integer name of the list. Edit CGLSample3Doc::OnNewDocument
to look like this:
BOOL CGLSample3Doc::OnNewDocument()
{
if (!CDocument::OnNewDocument())
return FALSE;
glNewList(ArmPart);
glBegin(GL_POLYGON);
glVertex2f(-10.0f, 10.0f);
glVertex2f(-10.0f, -10.0f);
glVertex2f(100.0f, -10.0f);
glVertex2f(100.0f, 10.0f);
glEnd();
glEndList();
return TRUE;
}
Note: Microsoft has changed the OpenGL API since this was written. If you are using a newer version of
the API, you will need to make the following call to glNewList:
glNewList(ArmPart, GL_COMPILE);
GL_COMPILE tells OpenGL to just build the display list. Alternatively, you can pass GL_COMPILE_AND_EXECUTE
into glNewList. This will cause the commands to be executed as the display list is being built!
Now edit CGLSample3Doc::RenderScene to look like this:
void CGLSample3Doc::RenderScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
glCallList(ArmPart);
glFlush();
}
If you were to run the program now, all you would see is a small
red rectangle in the lower left hand corner of the screen. Now
add the following lines just before the call to glCallList:
glTranslated( m_transX, m_transY, 0);
glRotated( m_angle1, 0, 0, 1);
These two commands affect the ModelView matrix, causing our rectangle
to rotate the number of degrees stored in m_angle1 and translate
by the distance defined by (m_transX, m_transY). Run the program
now to see the results. Notice that every time the program gets
a WM_PAINT event the rectangle moves a little bit more ( you can
trigger this by placing another window over the GLSample3 program
and then going back to GLSample3 ). The effect occurs because
we keep changing the ModelView matrix each time we call glRotate
and glTranslate. Note that resizing the window resets the rectangle
to its original position ( OnSize clears the matrix to an identity
matrix, as you can see in the code) We need to leave the matrix
in the same state in which we found it. To do this we will use
the matrix stack. Edit CGLSample3Doc::RenderScene to look like
the code below. Then compile and run the program again.
void CGLSample3Doc::RenderScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glPushMatrix();
glTranslated( m_transX, m_transY, 0);
glRotated( m_angle1, 0, 0, 1);
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
glCallList(ArmPart);
glPopMatrix();
glFlush();
}
glPushMatrix takes a copy of the current matrix and places it
on a stack. When we call glPopMatrix, the last matrix pushed is
restored as the current matrix. Our glPushMatrix call preserves
the initial identity matrix, and glPopMatrix restores it after
we dirtied up the matrix. We can use this technique to position
objects with respect to other objects. Once again, edit RenderScene
to match the code below.
void CGLSample3Doc::RenderScene(void)
{
glClear(GL_COLOR_BUFFER_BIT);
glPushMatrix();
glTranslated( m_transX, m_transY, 0);
glRotated( m_angle1, 0, 0, 1);
glPushMatrix();
glTranslated( 90, 0, 0);
glRotated( m_angle2, 0, 0, 1);
glColor4f(0.0f, 1.0f, 0.0f, 1.0f);
glCallList(ArmPart);
glPopMatrix();
glColor4f(1.0f, 0.0f, 0.0f, 1.0f);
glCallList(ArmPart);
glPopMatrix();
glFlush();
}
When you run this you will see a red rectangle overlapping a green
rectangle. The translate commands actually move the object's vertex
in the world coordinates. When the object is rotated, it still
rotates around its own vertex, thus allowing the green rectangle
to rotate around the end of the red one. Follow the steps below
to add controls so that you can move these rectangles.
- Add the following member variables to the view class:
CPoint m_RightDownPos; // Initialize to (0,0)
CPoint m_LeftDownPos; // Initialize to (0,0)
BOOL m_RightButtonDown; // Initialize to FALSE
BOOL m_LeftButtonDown; // Initialize to FALSE
- Add member functions responding to WM_LBUTTONDOWN, WM_LBUTTONUP,
WM_RBUTTONDOWN, and WM_RBUTTONUP. Edit them as shown below:
void CGLSample3View::OnLButtonUp(UINT nFlags, CPoint point)
{
m_LeftButtonDown = FALSE;
CView::OnLButtonUp(nFlags, point);
}
void CGLSample3View::OnLButtonDown(UINT nFlags, CPoint point)
{
m_LeftButtonDown = TRUE;
m_LeftDownPos = point;
CView::OnLButtonDown(nFlags, point);
}
void CGLSample3View::OnRButtonUp(UINT nFlags, CPoint point)
{
m_RightButtonDown = FALSE;
CView::OnRButtonUp(nFlags, point);
}
void CGLSample3View::OnRButtonDown(UINT nFlags, CPoint point)
{
m_RightButtonDown = TRUE;
m_RightDownPos = point;
CView::OnRButtonDown(nFlags, point);
}
- Add a member function responding to WM_MOUSEMOVE and edit
it as shown below.
void CGLSample3View::OnMouseMove(UINT nFlags, CPoint point)
{
if (m_RightButtonDown)
{
CGLSample3Doc* pDoc = GetDocument();
CSize rotate = m_RightDownPos - point;
m_RightDownPos = point;
pDoc->m_angle1 += rotate.cx/3;
pDoc->m_angle2 += rotate.cy/3;
InvalidateRect(NULL);
}
if (m_LeftButtonDown)
{
CGLSample3Doc* pDoc = GetDocument();
CSize translate = m_LeftDownPos - point;
m_LeftDownPos = point;
pDoc->m_transX -= translate.cx/3;
pDoc->m_transY += translate.cy/3;
InvalidateRect(NULL);
}
CView::OnMouseMove(nFlags, point);
}
Build and run the program. You may now drag with the left mouse
button anywhere on the screen to move the arm, and drag with the
right button to rotate the parts of the arm. The above code uses
the Windows interface to change data. The OpenGL code then draws
a scene based on that data. The only problem with the program
now is that annoying flicker from the full screen refreshes. We
will add double buffering to the program and then call it complete.
Double buffering is a very simple concept used in most high performance
graphics programs. Instead of drawing to one buffer that maps
directly to the screen, two buffers are used. One buffer is always
displayed ( known as the front buffer ), while the other buffer
is hidden ( known as the back buffer ). We do all of our drawing
to the back buffer and, when we are done, swap it with the front
buffer. Because all of the updates happen at once we don't get
any flicker.
The only drawback to double buffering is that it is incompatible
with GDI. GDI was not designed with double buffering in mind.
Because of this, not GDI commands will not work in an OpenGL window
with double buffering enable. That being said, we first need to
change all of the "InvalidateRect(NULL);" calls to "InvalidateRect(NULL,
FALSE);". This will solve most of our flicker problem (the
rest of the flicker was mainly to make a point). To enable double
buffering for the pixel format, change the pixelDesc.dwFlags definition
in CGLSample3View::SetWindowPixelFormat to the following:
pixelDesc.dwFlags = PFD_DRAW_TO_WINDOW |
PFD_SUPPORT_OPENGL |
PFD_DOUBLEBUFFER |
PFD_STEREO_DONTCARE;
There are no checks when we set the pixel format to make sure
that ours has double buffering. I will leave this as an exercise
for the reader.
First we need to tell OpenGL to draw only onto the back buffer.
Add the following line to the end of CGLSample3View::OnSize:
glDrawBuffer(GL_BACK);
Each time we draw a new scene we need to swap the buffer. Add
the following line to the end of CGLSample3View::OnPaint:
SwapBuffers(dc.m_ps.hdc);
When you compile and run the program now you should see absolutely
no flicker. However, the program will run noticeably slower. If
you still see any flicker then ChoosePixelFormat is not returning
a pixel format with double buffering. Remember that ChoosePixelFormat
returns an identifier for the pixel format that it believes is
closest to the one you want. Try forcing different indices when
you call SetPixelFormat until you find a format that supports
double buffering.
In the final sample program, we will construct a three dimensional
cube. There may be some 3-D graphics concepts in this section
that those uninitiated to graphics will not understand. Explaining
these concepts is beyond the scope of this tutorial. For those
people, I recommend reading one of the books listed in the last section
of this tutorial.
Download Example Source
Source code for GLSample3
Place the file in the parent directory for the project and type "pkunzip -d GLSamp3.zip".
Then choose File - OpenWorkspace and open the glsample1.mak file unzipped into the GLSample3 directory.
You must have a version on PKZip that supports long file names.
Click here to get the latest version of PKZip.