OpenGL:Tutorials:Picking
From GPWiki
[edit] Picking with OpenGLSo 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. [edit] 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. [edit] 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:
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 ;). [edit] I get it, now explain to me how to code!To use picking in your program you have to understand a couple of things:
[edit] The procedureNow that you have a background about how picking works, let's see how to code it:
[edit] The selection bufferI 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
The record contains 4 info about the hit:
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
[edit] 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; }
[edit] Code, without SDLHere 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; } |


