Tutorial 9: Picking and Interaction

Many graphics programs are interactive.  You will use the mouse, keyboard, and other devices to control what is happening on the screen.  You will often need to be able to interact with your graphical environment with the keyboard and mouse.  This tutorial introduces the idea of "picking".  Picking is simply figuring out what object you clicked the mouse on.  But, that's not as easy as it sounds.  After all, the screen is 2D, but the world is 3D and may be moved around quite a bit.

For this tutorial you'll be working with some code I've created.  You'll find this code in tutorial9.zip.  This code displays a Duplo mat and some blocks.  Compile it and use the mouse to spin it around.  I've already installed the CGrCamera code to make it easy to flip this thing around. I also installed a menu with options to insert new blocks, delete blocks, rotate blocks, and raise/lower blocks.  Of course, most of these don't work just yet because you've not told the program how to select a block for this fun stuff to work on.

A Basic Tour of the Code

I've got a simply class call CDuplo that draws Duplo blocks.  This code knows how to draw two things:  a Duplo mat and simply blocks.  A block of any size can be drawn from 1 by 1 to anything by anything.  My New Block dialog box limits you to a 100 by 100 block, but that would be too big to see, anyway.  

Notice the following code in the CChildView constructor:

//
// An initial set of blocks
//
// The parameters to Blocks are:
// 1. The block color. Must be one of the CDuplo::COLOR enum
// 2. The length of the block (X dimension)
// 3. The width of the block (Z dimension)
// 4. The block row (Z dimension)
// 5. The block color (X dimension)
// 6. The height we'll raise the block
// 7. Any amount to rotate the block (degrees)
//
// We just build a vector of blocks.
//

m_blocks.push_back(Blocks(CDuplo::RED, 4, 2, 5, 2, 0, 0.));
m_blocks.push_back(Blocks(CDuplo::GREEN, 4, 2, 5, 6, 0, 0.));
m_blocks.push_back(Blocks(CDuplo::RED, 4, 2, 5, 10, 0, 0.));

m_blocks.push_back(Blocks(CDuplo::YELLOW, 4, 2, 5, 4, 1, 0.));
m_blocks.push_back(Blocks(CDuplo::GREEN, 4, 2, 5, 8, 1, 0.));

This code creates the initial blocks you see.  I am using a standard template library vector object to hold a list of blocks.  I use a vector instead of a list because I want to keep track of a "selected" block using the m_selection member variable.  You'll notice I initially set it to -1.  That means no block is selected.  All of the menu options other than "new" are disabled when nothing is selected.

If you run the code and select Duplo/New, you can create a new Duplo block.  When I create a new block, it is preselected.  I highlight the selected block in white.  Note that you can use the menu options or toolbar functions to raise/lower/rotate this new block or to delete it.  When the block is deleted, m_selection is set back to -1 to indicate nothing is selected.

If you look at OnGLDraw, you'll see that I broke drawing into two parts:  some basic setup and then a function ActualDraw.  This breakup is unusual compared to what we've done in the past, but is done to make picking easier.

If you read through ActualDraw, you'll find the loop that draws the Duplo blocks.  This loop does a translation and rotation based on the location and angle of the block.  It then sets the material properties for the block.  Note that I set the current selection properties to white. 

Using the Keyboard

We've not really used the keyboard much in this class.  However, it seems logical to use the keyboard to move blocks after they are created.  (Notice:  The code you are working on already has the OnKeyDown function created, so you can ignore the remainder of this paragraph).  Select CChildView in ClassView and right-click.  Select Add Windows Message Handler and insert a handler for WM_KEYDOWN.  This is the message that is generated by Windows when a key is pressed.  This will create a dummy function that looks like this:

void CChildView::OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags) 
{
    // TODO: Add your message handler code here and/or call default

    COpenGLWnd ::OnKeyDown(nChar, nRepCnt, nFlags);
}

Look up OnKeyDown in MSDN to get an idea of what the parameters mean.  We're really only interested in nChar, they key that was pressed.  Here's how we could handle the key for "UP":

if(m_selection >= 0)
{
   switch(nChar)
   {
   case VK_UP:
      m_blocks[m_selection].m_row--;
      break;
   }

   Invalidate();
}

Notice these things here: 

I used a switch statement.  That's because we're going to add a few more handlers.

I only do this is m_selection is greater than or equal to zero.  This indicates there is a valid selection.

I simply change the value for the row in the vector for the currently selected block.

Note that I used a struct and public data for my list of blocks.  I think this is quite resonable because the struct only exists to group the items in an array location.  There are no functions other than a constructor and the struct is very small.  

Get this code installed and working.  When you do Duplo/New, you can use this to move the new block.  Once you have that working add a handler for VK_DOWN which will decrement the row.  Then add handlers for VK_LEFT and VK_RIGHT which will change m_col, the column.

A Mouse Handler

We've been using the left mouse button for moving the space.  We'll use the right mouse button for selection.  User interfaces vary, and this is not a great one, but it will suffice for the purposes of this tutorial.  Add a handler to CChildView for WM_RBUTTONDOWN.  (Notice:  The code you are working on already has the OnRButtonDown function created). 

The OpenGL Context

Picking is done using OpenGL functions.  So, we'll have to call OpenGL functions from the mouse button handler.  But, I've been telling you all along that OpenGL calls could only be made from OnGLDraw.  That's because a set up a OpenGL context for OnGLDraw.   A context is an enabler for operations.  Windows has a drawing context for a window.  You can only draw if you have a drawing context (a DC).  OpenGL has a context as well.  We have to make that context "current".  Add the following code to the beginning of your mouse button handler:

// Create a device context for painting, even
// though we're not actually going to be painting...
CPaintDC dc(this); // device context for painting

// Make OpenGL current
wglMakeCurrent(dc.m_hDC, HGLRc());

Note that this is a "wgl" function.  That's a Windows GL function meaning it's a function for interfacing OpenGL to Windows.  X has similar functions.  All this does is connect us to OpenGL so we can make calls. 

The Select Render Mode

When we want to do picking, we enter the "select" render mode.  Normally, you are rending to a buffer.  However, the buffer you see on the screen has no memory of what polygons were drawn where.  It only remembers the colors it set the pixels to.  

Here's how selection will work.  Suppose you want to know what is under the location (212, 110) on the screen.  Now, that location is a bit small, so we're going to make our selection be based on a 3x3 rectangle centered on that location so we don't have to be too precise.  Imagine if you blew up the screen until that 3x3 rectangle that was round (212,110) took up the entire screen.  Then, the question is no longer "what is under the mouse", but rather, what is rendering onto that little section of screen.  

We'll do something like this.  But, instead of actually rendering that small block, we'll tell OpenGL to remember what would have been rendered had it actually rendered that small block.  You do this by setting a "selection buffer" and entering the select rendering mode.  You create a selection buffer like this:

GLuint selectbuffer[100];
glSelectBuffer(100, selectbuffer);

I chose to make this 100 locations.  The first parameter of glSelectBuffer is the size of the buffer.  We'll get into what will go into this in a moment.  Right now, just know that OpenGL is going to put a log of what it tried to draw into the buffer.

Now, we'll enter the GL_SELECT rendering mode:

glRenderMode(GL_SELECT);

Normally, when you do drawing in OpenGL, the output goes to a buffer.  When we change the rendering mode to GL_SELECT, the output will not actually be rendered.  Instead, only enough work will be done to know if something would have been rendered and to add it to our selection buffer log.  This can be quite fast.

You should have entered the previous code right after the wglMakeCurrent call.  Now, let's add that code to make the image blow up to just the 3x3 region around the cursor:

// Determine the screen size so we can determine the aspect ratio
int width, height;
GetSize(width, height);

// 
// Set the pick matrix
//

GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPickMatrix(point.x, height - point.y - 1, 3, 3, viewport);

What this does is set up the projection matrix with a matrix containing operations that will occur after regular projection.  When we set up the projection matrix with gluPerspective, it is set to project to the window dimensions.  gluPickMatrix() creates a matrix that will take the output of projection and BLOW IT UP until the entire window is taken up by what would have been a 3 by 3 pixel neighborhood around the mouse click.  

Note that height - point.y - 1.  This is because Windows numbers rows from the top and and OpenGL from the bottom up.

Now, we need to draw:

ActualDraw();
glFlush();

Note that this won't actually draw anything.  It only goes through the motions so it will know what is under the cursor.

Add the following to disable the selection mode:

int hits = glRenderMode(GL_RENDER);

This should compile, but it won't do anything, yet.

Naming Things

Drawing for the purposes of selection and drawing for the purposes of rendering differ in only one way:  when we draw for selection, we need to tell OpenGL the "names" of things we want to find out we drew.  There will be special calls for this purpose.  So, ActualDraw needs to know if it is drawing for the purpose of rendering for drawing for the purpose of selection.  Add a bool parameter to ActualDraw named p_select.  In the call to ActualDraw in OnGLDraw, set the parameter to false.  In the call in the mouse handler, set it to true.

Look at how ActualDraw is structured.  It's a bit different than what you've seen before.  It does not call glLoadIdentity for the projection matrix.  Instead, that's done before ActualDraw is called.  This allows the pick matrix to be preloaded when we're picking.  Then, it sets up the lights and the other usual stuff, draws the mat, then loops through drawing the duplo blocks.  Be sure you understand how the loop works that draws the blocks.

Before we can start naming, we need to initialize the naming system.  Add the following code right before the mat is drawn:

if(p_select)
{
   glInitNames();
}

Names are actually kept on a stack, just like matrices.  This is so you can have more than one name active at the same time.  You might have a name for a particular duplo block and the name for one of the bumps.  Then you could tell what block you clicked on if that was all you needed or you could tell what bump on that block.  We'll only set a single value each time, but we need to "push" it onto the name stack and pop it off when we are done.  

At the beginning of the loop over blocks, add the following code:

if(p_select)
    glPushName(i);

The name I have chosen to use for each block is simply the index to the block.  The first block we draw will be named zero, the second one, etc.  At the end of this loop, add this:

if(p_select)
   glPopName();

This should compile and run, though it will still not do anything.

Seeing if it is doing anything

Set a breakpoint on the line after int hits = glRenderMode(GL_RENDER).  Run your program in debug mode and right-click somewhere other than a block.  Look at the local variables in the debugger when the breakpoint is hit.  You should see hits is zero.  Let the program continue and click on a block.  Now, hits should be equal to one.  

This is simply telling me how many names I hit when I clicked the mouse.  Turn the image so you are edgewise on the blocks.  Now, when you click you should see 2 or 3 hits.  This is because OpenGL does not keep track of what is the closest item to you, only all items that would be under the cursor.  So, you get hits on all of the blocks.

When you hit something, data is added to selectbuffer and hits is incremented.  The format of the chunk of data added to selectbuffer is this:

As an example, if you hit a duplo block, you will see something like this:

What are those strange numbers?  When I drew the block that we hit, OpenGL computed a Z value that went into the Z buffer.  This is a somewhat arbitrarily scaled value.  It is only meaningful in that it tells me what is nearer and what is farther away.  These numbers are actual Z buffer values.  They are meaningless alone, but do tell us what is nearer and what is farther away.

So, you need to scan the records in selectbuffer to find out what record has the nearest near point hit value.  That will tell you the name of the block that is currently selected.  Only do that if hits > 0.  When you find the name, set m_selection to that name and Invalidate().

If hits is zero, set m_selection to -1 and invalidate.

When you get this working you should be able to select blocks and move them around.  

CSE872 Home