OpenGL:Tutorials:Picking

From GPWiki
Jump to: navigation, search

Warning

This page describe the old method using deprecated OpenGL features that are now in the Compatibility Profile.

Picking with OpenGL

So you've chosen to interact with the world, right? If so, then you are in the right place ;) When I was young (about 14 I think), and learning OpenGL, I felt the necessity to select an object in the screen when the mouse was clicked on some point. To accomplish this, I tried to create an algorithm that, given the coordinates of the cursor, calculates the spatial coordinates in the world i was drawing to select an object. I never solved this problem, so for some years I never used the mouse to select objects.

Then I discovered picking, and life became easy.

Picking?

Yes, picking is the action that you do when you want to select something on the screen: you just move the pointer over the object, and click to select. This is done by picking.

Every time you need to interact with the world, everytime you have that mouse cursor to drag&drop, select and move something, you need to pick an object. So I think this is relevant in game programming.

How does it work?

Picking in OpenGL is actually a bit weird, since - as you probably know - OpenGL is raw (yeah, I love raw things); this gives us lots of power, but also it takes some time to understand well the concept and to get it working as we need.

As you may know - or if you don't know I'm explaining this right now - OpenGL has 3 rendering modes:

Render mode (GL_RENDER) 
This is the default mode, and is used to draw something on the framebuffer.
Select mode (GL_SELECT) 
This is used to select, we'll see this later.
Feedback mode (GL_FEEDBACK) 
This is used as feedback: instead of drawing something, this produces lots of info about the scene we are drawing.

To change the render mode you can use the glRenderMode function, which prototype is

 GLint glRenderMode (GLenum mode)

Actually we need only the render and select modes to accomplish picking. The trick of picking is in this function: glRenderMode returns a value, which doesn't depend on the new mode that you are passing, but depends on the previous render mode. Here's the trick: imagine that when you are in GL_SELECT mode, everytime you draw something that is visible, a counter is incremented. Then, when you call glRenderMode(GL_RENDER) to actually draw something on the framebuffer, the counter is returned to you. This counter is the number of objects drawn when in GL_SELECT mode. If you restrict the drawing area to half screen, the counter is incremented only for the object in the visible area. I think you got the trick :) If you restrict the area to a single pixel, which is where the mouse is, glRenderMode will return a counter that is the number of objects under the mouse. In this way you can determine which objects are under the mouse.

"Umm no" - you may think - "I can't determine which objects are drawn under the mouse only by knowing how many they are". This is true, infact to get the selected object we need something else: the selection buffer.

The selection buffer is a simple buffer managed by OpenGL, which is filled with some data about the object we drawn in the region of the screen. Using this buffer we can get a list of the objects under the cursor.

Of course this is only an explanation about how this works, but there is other. I hope this explanation is clear, because it took some time and some code before i can figure it out, since tutorials and manuals explain to use it, but not how it works (or it wasn't enough for me ;).

I get it, now explain to me how to code!

To use picking in your program you have to understand a couple of things:

How can OpenGL understand what is actually an object? (If you draw a cube and a triangle under the cursor, how can OpenGL count the number of objects drawn?)
OpenGL can't know if two triangles are part of the same object or not. So, OpenGL can't figure it out: you have to tell it. Before drawing an object (an entity that can be selected), you have to say to OpenGL that what you are about to draw is a new object by assigning a name to it. You do this with a naming function: glLoadName. This function pushes a name (which is a GLuint) at the top of the name stack.
So, what is a name stack?
A name stack is a structure used in selection mode, which stores unique identifiers for each of the drawn objects. The stack is initialized by the glInitNames function. So, when you switch to the GL_SELECT mode, you first have to initialize the name stack before telling OpenGL the name of the object you are about to draw.
The name stack can only be filled when in selection mode.
This means that glLoadName, glInitNames and related functions, do not work in modes that aren't GL_SELECT.

The procedure

Now that you have a background about how picking works, let's see how to code it:

  1. Define a drawing function which draws N objects. Before drawing each object you define it's name by calling glLoadName(Ith)
  2. Define a event handling function which can handle the mouse click.
    • When a click event is computed, you have to call a function which handles the selection
  3. Define a selection function, which does the following
    1. Initialize a selection buffer, which will contain data about selected objects
    2. Save somewhere the info about the current viewport
    3. Switch to GL_SELECT mode
    4. Initialize the name stack
    5. Restrict the drawing area around the mouse position
    6. Draw the objects with their names
    7. Get the number of hits
    8. Handle the hits, and get the picked object
  4. Launch your application and wait for some clicks ;)

The selection buffer

I know you want to code, but there is more to know before manage picking: the selection buffer. This buffer is a simple GLuint buffer which is used by OpenGL to store info about the hits

  • You define a buffer
  • In selection mode, everytime the name stack changes, or when glRenderMode is called a hit is generated
  • A hit record is copied into the buffer

The record contains 4 info about the hit:

  • Number of the hits selected
  • Min depth value (Z)
  • Max depth value (Z)
  • Object name (glLoadName)

So, to get info about the ith entry of the buffer, you've to:

 GLuint buffer[16];
 GLubyte *p;
 
 p = (GLuint*)(&buffer[i]);
 *p;        /* is Number of hits selected */
 *(p + 1);  /* is Min Z */
 *(p + 2);  /* is Max Z */
 *(p + 3);  /* is object name */

A last note, before the ending code

  • If you use the glLoadName function, the number of selected hits will be always one
  • You can push more names on the stack by using glPushName and glPopName

Code, finally!

Here an example code made in C, SDL and OpenGL.

 #include "SDL.h"
 #include "SDL_opengl.h"
 #include <gl/glut.h>
 
 #include <stdio.h>
 #include <stdlib.h>
 
 #define SW 400
 #define SH 400
 
 int selected = 0;
 
 void gl_draw();
 void gl_init(int w, int h);
 void gl_select(int x, int y);
 void gl_selall(GLint hits, GLuint *buff);
 void sdl_events();
 void sdl_mousedw(int x, int y, int but);
 void draw_block(float x, float y, float z);
 void list_hits(GLint hits, GLuint *names);
 
 void gl_draw()
 {
 
 	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 	glMatrixMode(GL_MODELVIEW);
 	glLoadIdentity();
 
 	glTranslatef(0.0, 0.0, -5.0);
 
 	glColor3f(1.0, 0.0, 0.0);
 	glLoadName(7); /* Overwrite the first name in the buffer */
 	draw_block(-0.3, 0, -2);
 
 	glColor3f(0.0, 1.0, 0.0);
 	glLoadName(14); /* Overwrite the first name in the buffer */
 	draw_block(0, 0, -4);
 
 	glColor3f(0.0, 0.0, 1.0);
 	glLoadName(21); /* Overwrite the first name in the buffer */
 	draw_block(0.3, 0, -6);
 }
 
 void gl_init(int w, int h)
 {
 	glClearColor(0.0, 0.0, 0.0, 1.0);
 	glViewport(0, 0, w, h);
 
 	glMatrixMode(GL_PROJECTION);
 	glLoadIdentity();
 
 	gluPerspective(60.0, 1.0, 0.0001, 1000.0);
 
 	glMatrixMode(GL_MODELVIEW);
 }
 
 void sdl_events()
 {
 	SDL_Event event;
 
 	while (SDL_PollEvent(&event))
 	{
 		switch (event.type)
 		{
 			case SDL_QUIT: exit(0);
 			case SDL_MOUSEBUTTONDOWN:
 			{
 				sdl_mousedw(event.button.x, event.button.y, event.button.button);
 			}; break;
 			case SDL_KEYDOWN:
 				switch (event.key.keysym.sym)
 				{
 					case SDLK_ESCAPE: exit(0);
 				}
 		}
 	}
 }
 
 void sdl_mousedw(int x, int y, int but)
 {
 	printf("Mouse button %d pressed at %d %d\n", but, x, y);
 	if (but == SDL_BUTTON_LEFT)
 		gl_select(x,SH-y); //Important: gl (0,0) ist bottom left but window coords (0,0) are top left so we have to change this!
 }
 
 void gl_select(int x, int y)
 {
 	GLuint buff[64] = {0};
 	GLint hits, view[4];
 	int id;
 
 	/*
 		This choose the buffer where store the values for the selection data
 	*/
 	glSelectBuffer(64, buff);
 
 	/*
 		This retrieves info about the viewport
 	*/
 	glGetIntegerv(GL_VIEWPORT, view);
 
 	/*
 		Switching in selecton mode
 	*/
 	glRenderMode(GL_SELECT);
 
 	/*
 		Clearing the names' stack
 		This stack contains all the info about the objects
 	*/
 	glInitNames();
 
 	/*
 		Now fill the stack with one element (or glLoadName will generate an error)
 	*/
 	glPushName(0);
 
 	/*
 		Now modify the viewing volume, restricting selection area around the cursor
 	*/
 	glMatrixMode(GL_PROJECTION);
 	glPushMatrix();
 		glLoadIdentity();
 
 		/*
 			restrict the draw to an area around the cursor
 		*/
 		gluPickMatrix(x, y, 1.0, 1.0, view);
 		gluPerspective(60, (float)view[2]/(float)view[3], 0.0001, 1000.0);
 
 		/*
 			Draw the objects onto the screen
 		*/
 		glMatrixMode(GL_MODELVIEW);
 
 		/*
 			draw only the names in the stack, and fill the array
 		*/
 		gl_draw();
 
 		/*
 			Do you remeber? We do pushMatrix in PROJECTION mode
 		*/
 		glMatrixMode(GL_PROJECTION);
 	glPopMatrix();
 
 	/*
 		get number of objects drawed in that area
 		and return to render mode
 	*/
 	hits = glRenderMode(GL_RENDER);
 
 	/*
 		Print a list of the objects
 	*/
 	list_hits(hits, buff);
 
 	/*
 		uncomment this to show the whole buffer
 	* /
 	gl_selall(hits, buff);
 	*/
 
 	glMatrixMode(GL_MODELVIEW);
 }
 
 void gl_selall(GLint hits, GLuint *buff)
 {
 	GLuint *p;
 	int i;
 
 	gl_draw();
 
 	p = buff;
 	for (i = 0; i < 6 * 4; i++)
 	{
 		printf("Slot %d: - Value: %d\n", i, p[i]);
 	}
 
 	printf("Buff size: %x\n", (GLbyte)buff[0]);
 }
 
 void draw_block(float x, float y, float z)
 {
 	glPushMatrix();
 		glTranslatef(x, y, z);
 		glutSolidCube(1.0);
 	glPopMatrix();
 }
 
 void list_hits(GLint hits, GLuint *names)
 {
 	int i;
 
 	/*
 		For each hit in the buffer are allocated 4 bytes:
 		1. Number of hits selected (always one,
 									beacuse when we draw each object
 									we use glLoadName, so we replace the
 									prevous name in the stack)
 		2. Min Z
 		3. Max Z
 		4. Name of the hit (glLoadName)
 	*/
 
 	printf("%d hits:\n", hits);
 
 	for (i = 0; i < hits; i++)
 		printf(	"Number: %d\n"
 				"Min Z: %d\n"
 				"Max Z: %d\n"
 				"Name on stack: %d\n",
 				(GLubyte)names[i * 4],
 				(GLubyte)names[i * 4 + 1],
 				(GLubyte)names[i * 4 + 2],
 				(GLubyte)names[i * 4 + 3]
 				);
 
 	printf("\n");
 }
 
 int main(int argc, char **argv)
 {
 	if (SDL_Init(SDL_INIT_VIDEO) < 0)
 	{
 		debug(SDL_GetError(), 0);
 		exit(1);
 	}
 
 	atexit(SDL_Quit);
 
 	SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 5);
 	SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 5);
 	SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 5);
 	SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16);
 	SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
 
 	if (!SDL_SetVideoMode(SW, SH, 0, SDL_OPENGL))
 	{
 		debug(SDL_GetError(), 0);
 		exit(1);
 	}
 
 	gl_init(SW, SH);
 
 	while (1)
 	{
 		sdl_events();
 		gl_draw();
 		SDL_GL_SwapBuffers();
 	}
 
 	return EXIT_SUCCESS;
 }

Code, without SDL

Here the same code without SDL. Simplified as much as possible. Worked with devcpp with glut package loaded.

 #include <gl/glut.h>
 
 #include <stdio.h>
 #include <stdlib.h>
 
 #define SW 400
 #define SH 400
 
 int selected = 0;
 
 void gl_draw();
 void gl_init(int w, int h);
 void gl_select(int x, int y);
 void gl_selall(GLint hits, GLuint *buff);
 void mouseClick();
 void mousedw(int x, int y, int but);
 void draw_block(float x, float y, float z);
 void list_hits(GLint hits, GLuint *names);
 
 
 void gl_draw()
 {
 	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
 	glMatrixMode(GL_MODELVIEW);
 	glLoadIdentity();
 
 	glTranslatef(0.0, 0.0, -5.0);
 
 	glColor3f(1.0, 0.0, 0.0);
 	glLoadName(7); /* Overwrite the first name in the buffer */
 	draw_block(-0.3, 0, -2);
 
 	glColor3f(0.0, 1.0, 0.0);
 	glLoadName(14); /* Overwrite the first name in the buffer */
 	draw_block(0, 0, -4);
 
 	glColor3f(0.0, 0.0, 1.0);
 	glLoadName(21); /* Overwrite the first name in the buffer */
 	draw_block(0.3, 0, -6);
 
    glutSwapBuffers();
 }
 
 void gl_init(int w, int h)
 {
 	glClearColor(0.0, 0.0, 0.0, 1.0);
 	glViewport(0, 0, w, h);
 
 	glMatrixMode(GL_PROJECTION);
 	glLoadIdentity();
 
 	gluPerspective(60.0, 1.0, 0.0001, 1000.0);
 
 	glMatrixMode(GL_MODELVIEW);
 }
 
 void mouseClick(int button, int state, int x, int y)
 {
    if 	((button == GLUT_LEFT_BUTTON) && (state == GLUT_DOWN))
	{
		mousedw(x, y, button);
	}
 }
 
 void mousedw(int x, int y, int but)
 {
 	printf("Mouse button %d pressed at %d %d\n", but, x, y);
 	gl_select(x,SH-y); //Important: gl (0,0) ist bottom left but window coords (0,0) are top left so we have to change this!
 }
 
 void gl_select(int x, int y)
 {
 	GLuint buff[64] = {0};
 	GLint hits, view[4];
 	int id;
 
 	/*
 		This choose the buffer where store the values for the selection data
 	*/
 	glSelectBuffer(64, buff);
 
 	/*
 		This retrieve info about the viewport
 	*/
 	glGetIntegerv(GL_VIEWPORT, view);
 
 	/*
 		Switching in selecton mode
 	*/
 	glRenderMode(GL_SELECT);
 
 	/*
 		Clearing the name's stack
 		This stack contains all the info about the objects
 	*/
 	glInitNames();
 
 	/*
 		Now fill the stack with one element (or glLoadName will generate an error)
 	*/
 	glPushName(0);
 
 	/*
 		Now modify the vieving volume, restricting selection area around the cursor
 	*/
 	glMatrixMode(GL_PROJECTION);
 	glPushMatrix();
 		glLoadIdentity();
 
 		/*
 			restrict the draw to an area around the cursor
 		*/
 		gluPickMatrix(x, y, 1.0, 1.0, view);
 		gluPerspective(60, 1.0, 0.0001, 1000.0);
 
 		/*
 			Draw the objects onto the screen
 		*/
 		glMatrixMode(GL_MODELVIEW);
 
 		/*
 			draw only the names in the stack, and fill the array
 		*/
 		glutSwapBuffers();
 		gl_draw();
 
 		/*
 			Do you remeber? We do pushMatrix in PROJECTION mode
 		*/
 		glMatrixMode(GL_PROJECTION);
 	glPopMatrix();
 
 	/*
 		get number of objects drawed in that area
 		and return to render mode
 	*/
 	hits = glRenderMode(GL_RENDER);
 
 	/*
 		Print a list of the objects
 	*/
 	list_hits(hits, buff);
 
 	/*
 		uncomment this to show the whole buffer
 	* /
 	gl_selall(hits, buff);
 	*/
 
 	glMatrixMode(GL_MODELVIEW);
 }
 
 void gl_selall(GLint hits, GLuint *buff)
 {
 	GLuint *p;
 	int i;
 
 	gl_draw();
 
 	p = buff;
 	for (i = 0; i < 6 * 4; i++)
 	{
 		printf("Slot %d: - Value: %d\n", i, p[i]);
 	}
 
 	printf("Buff size: %x\n", (GLbyte)buff[0]);
 }
 
 void draw_block(float x, float y, float z)
 {
 	glPushMatrix();
 		glTranslatef(x, y, z);
 		glutSolidCube(1.0);
 	glPopMatrix();
 }
 
 void list_hits(GLint hits, GLuint *names)
 {
 	int i;
 
 	/*
 		For each hit in the buffer are allocated 4 bytes:
 		1. Number of hits selected (always one,
 									beacuse when we draw each object
 									we use glLoadName, so we replace the
 									prevous name in the stack)
 		2. Min Z
 		3. Max Z
 		4. Name of the hit (glLoadName)
 	*/
 
 	printf("%d hits:\n", hits);
 
 	for (i = 0; i < hits; i++)
 		printf(	"Number: %d\n"
 				"Min Z: %d\n"
 				"Max Z: %d\n"
 				"Name on stack: %d\n",
 				(GLubyte)names[i * 4],
 				(GLubyte)names[i * 4 + 1],
 				(GLubyte)names[i * 4 + 2],
 				(GLubyte)names[i * 4 + 3]
 				);
 
 	printf("\n");
 }
 
 int main(int argc, char **argv)
 {
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB|GLUT_DOUBLE);
    glutInitWindowSize(SW, SH);
    glutInitWindowPosition(0, 0);
    glutCreateWindow(argv[0]);
    glutDisplayFunc(gl_draw);
    glutMouseFunc(mouseClick);
 
    gl_init(SW, SH);
 
    glutMainLoop();
 
 	return EXIT_SUCCESS;
 }