========================================
Using the OpenGL Selection Buffer
Ryan Schmidt
ryansc@cpsc.ucalgary.ca
CopyLeft 2000 - no rights restricted
========================================
This file explains how to do selection of
OpenGL objects using the mouse in a QT widget.
If you find any bugs, let me know. -RMS
=====================
Doing Selection in QT
=====================
To do GL selection with QT we need to know when the mouse is
clicked on the viewport. You do this by over-riding the
void QTWidget::mousePressEvent(QMouseEvent *event) method.
The QMouseEvent object has methods to find out the x and y
coordinates of a click (x() and y()) and to find out what
button is clicked (eg event->button() & RightButton). You'll
need to know the x and y coordinates to do the selection, so
keep them in globals or otherwise available somehow. Your
mousePressEvent method should probably look like a little
state machine that calls the selection code if all the
conditions are right. This part is pretty straightforward.
======================
Basic Selection - Code
======================
(explanation of this code follows. Note that this won't
directly compile, you have to add some stuff.
The
explanation of each numbered block follows...)
// [0x0]
GLuint select_buf[SELECT_BUFSIZE]; //selection buffer
GLuint hits; //number of hits
// [0x1]
glSelectBuffer(SELECT_BUFSIZE, select_buf);
glRenderMode(GL_SELECT);
glInitNames();
glPushName(0);
// [0x2]
//set up view matrix here - ie do whatever transformations
setupView();
glLoadIdentity();
// [0x3]
for(int i = 0; i < numpoints; i++){
glLoadName(i);
drawPoint(i);
}
// [0x4]
glFlush(); //flush the pipeline
hits = glRenderMode(GL_RENDER); //switch back to RENDER mode
// [0x5]
GLuint hitnames[10];
unsigned int hi = 0;
GLuint *bufp = selectbuffer;
GLuint name, numnames, z1, z2;
// [0x6]
for(unsigned int j = 0; j < hits; j++){
numnames = *bufp++;
z1 = *bufp++;
z2 = *bufp++;
while(numnames--){
name = *bufp++;
hitnames[hi++] = name;
}
}
=============================
Basic Selection - Explanation
=============================
0x0
=====
OpenGL fills select_buf with hit information. The structure
of the data is variable-length records, and the number of
records will be returned into hits. I'll explain these
when we use them. BTW, SELECT_BUFSIZE is a constant you
can define elsewhere.
0x1
=====
Here we do some basic setup. First we tell OpenGL where
the buffer it should fill with hit information is, by
calling glSelectBuffer(SELECT_BUFSIZE, select_buf). Next
we switch from RENDER rendering mode to SELECT rendering
mode with glRenderMode(GL_SELECT). In SELECT rendering
mode, nothing we draw will show up on the screen. Instead
the vertex information used to fill the selection buffer,
select_buf. Next we have to initialize the name stack by
calling glInitNames() and glPushName(0).
OpenGL uses the name stack to differentiate between the
different objects in the scene. Hits are discovered on
a per-vertex basis, OpenGL doesn't know that the next
8 vertices you draw are part of a cube. GL uses the
unsigned integer at the top of the Name stack
as the name of the current object. So for each new object
we start drawing, we push a new name on the stack. THE
NAMES HAVE TO BE UNIQUE OR WE WON'T BE ABLE TO TELL WHAT
WAS HIT LATER ON! (that's important...trust me)
0x2
=====
Here we have to set up our view of the scene. Basically,
this means you have to do all the transformations you
did to the currently drawn scene before you started
drawing it. Otherwise the objects you are trying to hit
are going to be in different positions internally then
they are on the screen. The call to setupView() is
something you have to fill in that does this. There
is an explanation of what should happen here later
in this file. The call to glLoadIdentity may not be
necessary in your program, depending on whether or
not you do any transformations to the MODELVIEW matrix
before you start drawing your objects. If you do, leave
that call out!
0x3
=====
Here we draw our objects, giving each one of them a
name. You'll probably have to change this for your
program, but what you need to do here should be clear.
In our curve-drawing program, we probably only want to be
selecting points. If you only have one curve, this
all becomes really easy. Just give each point in
the curve a number. Use that number as the name, ie
pass it as the argument to glLoadName(). Then draw
that point. If you have more than one curve you'll
need some sort of encoding scheme for names, I'll
give an example for that later. An important point to
make here is that the GL name stack has a depth limit,
at minimum it can hold 64 names. I'll also give
an example about how to get around this later.
0x4
=====
Now that we've drawn all our objects, we can figure out
which ones were hit. First we glFlush() the pipeline
so all our commands are actually carried out. Then
we change the rendering mode back to GL_RENDER. This
call returns the number of hits that occurred.
0x5
=====
Here we create some variables to hold hit information. We
make an array hitnames for the names of objects that
were clicked on. I'll assume that there are 10 max, but
you could dynamically size this array. Note that the
hits value returned from glRenderMode() is _NOT_ the
same as the number of names hit! This is explained below.
We index this array with hi. The pointer bufp points to
the selection buffer, which contains all the selection/hit
information. Finally, the last four GLuint's are fields
from the selection buffer.
0x6
====
Now we're going to examine the selection buffer and figure
out what objects were hit. The selection buffer is full
of variable-length records that look like this:
GLuint number_of_names_in_this_hit
GLuint z_buffer_1
GLuint z_buffer_2
GLuint name1
GLuint name2
...
GLuint nameN
So the first field is the number of names in the current
hit. You might wonder how you can have more then one name
per hit - think about what happens if two points are
at the same spot, or if one is 'behind' another. The two
z-buffer values define the closest and furthest z-values
for the names, you probably don't need these. Finally
we have a list of the names that were hit. The code
there is kind of cryptic pointer arithmetic, but it does
what we need. After this runs the hitnames array will
contain the unsigned integer names of any objects that
were clicked on.
Now that you know which points were clicked, you have
to pick one to be 'selected'. The easiest way is to
just select the first one returned. You could also
keep track of the current selection, and cycle through
the possibilities on each mouse click. Your point data
structure will probably need a 'selected' flag so you
can draw selected points differently from normal points.
==============================================
Setting up the PROJECTION matrix for selection
==============================================
The way selection works is we define some clipping boundaries
in our viewport, then draw all our objects. Objects outside
of these boundaries are discarded, objects inside are added
to the selection buffer as 'hits'. We use the name stack to
differentiate between objects. You can define these clipping
planes yourself, but GL already has calls to do it for you.
This has to be done before you create your PROJECTION matrix.
Right now the code in our setupView() function looks something
like this:
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
//create PROJECTION matrix
gluPerspective(...);
...
//create MODELVIEW matrix
glMatrixMode(GL_MODELVIEW);
//apply view-changing transformations here
...
If we are picking, we need to add a step before we create the
PROJECTION matrix:
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
//new code starts here
GLint viewport[4];
glGetIntegerv(GL_VIEWPORT, viewport);
gluPickMatrix((GLdouble)mousedownx,
(GLDouble)(viewport[3]-mousedowny),
PICK_REGION_SIZE, PICK_REGION_SIZE,
viewport);
//create PROJECTION matrix
gluPerspective(...);
...
//create MODELVIEW matrix
glMatrixMode(GL_MODELVIEW);
//apply view-changing transformations here
...
The important bit here is the gluPickMatrix function. This
creates the clip boundaries so that when you draw the
objects in SELECTION mode, only the ones within the picking
region are hit. PICK_REGION_SIZE is a float you need to define
that determines how big the hit region is. You might need to
do all this more then once if you are picking from objects of
non-uniform size. The hit region is essentially a rectangle that
stretches from +infin to -infin in the Z plane. The values
mousedownx and mousedowny need to be the coordinates of a mouse
click in the viewport, you should have these saved somehow from
your mousePressEvent method.
==================
Advanced Selection
==================
Here are a few things you might have to deal with...
>64 Names
==========
If you need more then 64 names, you have to loop through
the selection code more then once. Basically, you want an
algorithm that will let you iterate through all your points,
touching each point once. Then you loop through the selection
rendering code (numpoints mod 64) times. If you don't do this,
only the first 64 objects will ever be selectable. If the
name stack is full, any new pushed names are discarded.
More then one Curve
===================
If you have more then one curve, you might have some trouble
with point names. You can try to figure out some way to index
all your points, or you can number your curves and encode
the curve number into the object name. An easy way to do this
is to set the name as
glLoadName(( (curvenum) << 16) + pointnum );
then decode it as
curvenum = name >> 16; //index of curve that was hit
pointnum = name & 0xFFFF; //nth point
This limits a curve to 65536 points, which should be enough.
if not you could give more bits to the point number...
Comments? Suggestions? Flames? Flounder? let me know: ryansc@cpsc.ucalgary.ca
Back to my graphics page...