Picking Tutorial

http://www.lighthouse3d.com/opengl/picking/index.php?openglway 

实现鼠标拾取功能 

Picking Tutorial


The Name Stack


The OpenGL API provides a mechanism for picking objects in a 3D scene. This tutorial will show you how to detect which objects are bellow the mouse or in a square region of the OpenGL window.  <此教程告诉你如何检查出在鼠标下或渲染窗口中的矩形区域下的对象> The steps involved in detecting which objects are at the location where the mouse was clicked are:
  • 1. Get the window coordinates of the mouse
  • 2. Enter selection mode
  • 3. Redefine the viewing volume so that only a small area of the window around the cursor is rendered <重新定义视景体, 大小为鼠标下的小区域>
  • 4. Render the scene, either using all primitives or only those relevant to the picking operation
  • 5. Exit selection mode and identify the objects which were rendered on that small part of the screen.<退出选择模式, 识别在屏幕上的一小块区域内对象 >
  •     In order to identify the rendered objects using the OpenGL API you must name all relevant objects in your scene. The OpenGL API allows you to give names to primitives, or sets of primitives (objects).

       <为了使用OpenGL API 识别渲染对象,你必须为在你场景中的相对应的渲染对象命名。OpenGL API允许你为primitives或primitives命名>

        When in selection mode, a special rendering mode provided by the OpenGL API, no objects are actually rendered in the framebuffer. Instead the names of the objects (plus depth information) are collected in an array. For unnamed objects, only depth information is collected.

     <处于选择模式时(由OpenGL API提供了专门的渲染模式),没有对象在framebuffer中渲染(注:即不会被显示).相反,对象的名字及深度值被放在一个数组中。没有命名的对象中有深度信息被放在数组中>

        Using the OpenGL terminology, a hit occurs whenever a primitive is rendered in selection modeHit records are stored in the selection buffer. Upon exiting the selection mode OpenGL returns the selection buffer with the set of hit records. Since OpenGL provides depth information for each hit the application can then easily detect which object is closer to the user.

    <当使用OpenGL选择模式时,当对象被渲染时hit便被触发了。hit记录储存在选择缓冲区中。当退出选择模式时OpenGL返回选择缓冲区。缓冲区内保存了hit记录集。由于OpenGL提供了每一个hit记录的深度

    值,所以应用程序可以很容易地探测出哪一个对象离用户近。>

    Introducing the Name Stack

        As the title suggests, the names you assign to objects are stored in a stack. Actually you don't give names to objects, as in a text string. Instead you number objects. Nevertheless, since in OpenGL the term name is used, the tutorial will also use the term name instead of number.

        When an object is rendered, if it intersects the new viewing volume, a hit record is created. The hit record contains the names currently on the name stack plus the minimum and maximum depth for the object. Note that a hit record is created even if the name stack is empty, in which case it only contains depth information. If more objects are rendered before the name stack is altered or the application leaves the selection mode, then the depth values stored on the hit record are altered accordingly.

        A hit record is stored on the selection buffer only when the current contents of the name stack are altered or when the application leaves the selection mode.

       The rendering function for the selection mode therefore is responsible for the contents of the name stack as well as the rendering of primitives.

       OpenGL provides the following functions to manipulate the Name Stack:  

       void glInitNames(void);

       This function creates an empty name stack. You are required to call this function to initialize the stack prior to pushing names. 

        void glPushName(GLuint name); 

       Adds name to the top of the stack. The stacks maximum dimension is implementation dependent, however according to the specs it must contain at least 64 names which should prove to be more than enough for the vast majority of applications. Nevertheless if you want to be sure you may query the state variable GL_NAME_STACK_DEPTH (useglGetIntegerv(GL_NAME_STACK_DEPTH)). Pushing values onto the stack beyond its capacity causes an overflow error GL_STACK_OVERFLOW

        void glPopName(); 

       Removes the name from top of the stack. Popping a value from an empty stack causes an underflow, error GL_STACK_UNDERFLOW

        void glLoadName(GLunit name); 

       This function replaces the top of the stack with name. It is the same as calling

       glPopName();

        glPushName(name);

        This function is basically a short cut for the above snippet of code.   <此函数(glLoadName)是上面那一小段代码(glPopName glPushName)的简化>Loading a name on an empty stack causes the error GL_INVALID_OPERATION.

        Note: Calls to the above functions are ignored when not in selection mode. This means that you may have a single rendering function with all the name stack functions inside it.<?>

    When in the normal rendering mode the functions are ignored and when in selection mode the hit records will be collected.

        Note: You can't place these functions inside a glBegin glEnd construction,  <你不能把这些函数放在glBegin glEnd之间>.which is kind of annoying since that will require a new rendering function for the selection mode in some cases. For instance if you have a set of points inside a glBegin glEnd and you want to name them in such a way that you can tell them apart, then you must create one glBegin glEnd block for each name.

    Rendering for the Selection Mode

    An example of a rendering function is now presented.  


    #define BODY    1 

    #define HEAD    2

    void renderInSelectionMode() {
        glInitNames();

        glPushName(BODY);
        drawBody();
        glPopName();

        glPushName(HEAD);
        drawHead();
        drawEyes();
        glPopName();

        drawGround();
    }

     OK, lets go line by line.<让我们逐行解释>

  • 1 glInitNames(); - This function creates an empty stack. This is required before any other operation on the stack such as Load, Push or Pop.
  •  

  • 2 glPushName(BODY); - A name is pushed onto the stack. The stack now contains a single name.
  •  

  • 3 drawBody(); - A function which calls OpenGL primitives to draw something. If any of the primitives called in here intersects the viewing volume a hit record is created. The contents of the hit record will be the name currently on the name stack, BODY, plus the minimum and maximum depth values for those primitives that intersect the viewing volume<此时,任何一个被渲染的对象与视景体相交,则会产生hit记录。hit记录的内容是名字堆栈的当前名字BODY和所有与视景体相交的渲染单元的最小和最大值>
  •  

  • 4 glPopName(); - Removes the name of the top of the stack. Since the stack had a single item, it will now be empty. The name stack has been altered so if a hit record was created in 2 it will be saved in the selection buffer
  •  

  • 5 glPushName(HEAD); - A name is pushed onto the stack. The stack now contains a single name again. The stack has been altered, but there is no hit record so nothing goes into theselection buffer
  •  

  • 6 drawHead(); - Another function that renders OpenGL primitives. Again if any of the primitives intersects the viewing volume a hit record is created
  •  

  • 7 drawEyes(); - Yet another function which renders OpenGL primitives. If any of the primitives in here intersects the viewing volume and a hit record already exists from 6, the hit record is updated accordingly. The names currently in the hit record are kept, but if any of the primitives in drawEyes() has a smaller minimum, or a larger maximum depth, then these new values are stored in the hit record. If the primitives in drawEyes() do intersect the viewing volume but there was no hit record from drawHead then a new one is created.
  •  

  • 8 glPopName(); - The name on the top of the stack is removed. Since the stack had a single name the stack is now empty. Again the stack has been altered so if a hit record was created it will be stored in the selection buffer
  •  

  • 9 drawGround(); - If any if the primitives called in here intersects the viewing volume a hit record is created. The stack is empty, so no names will be stored in the hit record, only depth information. If no alteration to the name stack occurs after this point, then the hit record created will only be stored in the selection buffer when the application leaves the selection mode. This will be covered later on the tutorial.
  •  

    Note: lines 4 and 5 could have been replaced by glLoadName(HEAD);

    Note that you can push a dummy name (some unused value) right at the start, and afterwards just use glLoadName, instead of Pushing and Popping names. However it is faster to disregard objects that don't have a name than to check if the name is the dummy name. On the other hand it is probably faster to call glLoadName than it is to Pop followed by Push.

    Using multiple names for an object

    There is no rule that says that an object must have a single name. You can give multiple names to an object. Suppose you have a number of snowmen disposed on a grid. Instead of naming them as 1,2,3,... you could name each of them with the row and column where they are placed. In this case each snowman will have two names describing its position: the row and column of the grid.

    The following two functions show the two different approaches. First using a single name for each snowman. 

     

    for(int i = -3; i < 3; i++)
        for(int j = -3; j < 3; j++) {
            glPushMatrix();
            glPushName(i*6+j);
            glTranslatef(i*10.0,0,j * 10.0);
            glCallList(snowManDL);
            glPopName();
            glPopMatrix();
        }

     

     The following function gives two names to each snowman. In this latter function if a hit occurs the hit record will have two names. 

    <注:使用堆栈的原因:可为一个对象添加多个名字> 

    for(int i = -3; i < 3; i++) {
        glPushName(i); 
        for(int j = -3; j < 3; j++) {
            glPushMatrix();
            glPushName(j);
            glTranslatef(i*10.0,0,j * 10.0);
            glCallList(snowManDL);
            glPopName();
            glPopMatrix();
        }
        glPopName();

     } 

     Hierarchical Naming

    This is a natural extension to multiple naming. It may be of interest to find out where in a particular object you have clicked. For instance you may want to know not only in which snowman you clicked but also if you clicked on the head or the body. The following function provides this information. 

    <注:使用堆栈的原因:可为一个对象添加有层次的名字>  

     

    for(int i = -3; i < 3; i++) {
        glPushName(i); 
        for(int j = -3; j < 3; j++) {
            glPushMatrix();
            glPushName(j);
            glTranslatef(i*10.0,0,j * 10.0);
                glPushName(HEAD);
                glCallList(snowManHeadDL);
                glLoadName(BODY);
                glCallList(snowManBodyDL);
                glPopName();
            glPopName();
            glPopMatrix();
        }
        glPopName();
    }

     

    In this case you'll have three names when you click on either the body or head of a snowman: the row, the column and the value BODY or HEAD respectively.

    A Note about Rendering on Selection Mode

    As mentioned before, all calls to stack functions are ignored when not in selection mode, i.e. when rendering to the frame buffer. This means that you can have a single rendering function for both modes.

    <你可以单单使用渲染函数在这两种模式之中>

    However this can result in a serious waste of time. An application may have only a small number of pickable objects. Using the same function for both modes will require to render a lot of non-pickable objects when in selection mode. Not only the rendering time will be longer than required as will the time required to process the hit records in these cases.   <然而,这样做的结果是严重浪费时间。 一个程序可能只有很少的可拾取对象。在两种模式下使用同样的函数需要渲染大量不用选择的对象。渲染时间要比处理hit记录的时间要长。> However you should take extra care when doing so. Consider for instance an application where you have several rooms, each room with a potential set of pickable objects. You must prevent the user from selecting objects through walls, i.e. objects that are in other rooms and are not visible. If you use the same rendering function then the walls will cause a hit, and therefore you can use depth information to disregard objects that are behind the wall. However if you decide to build a function just with the pickable objects then there is no way to tell if an object is in the current room, or behind a wall.

    Therefore it makes sense to consider writing a special rendering function for the selection mode

     

        Therefore, when building a special rendering function for the selection mode you should include not only the pickable objects, but also all the objects that can cause occlusions, such as walls. For highly interactive real time applications it is probably a good option to build simplified representations of the objects that may cause occlusions. For instance a single polygon may replace a complex wall, or a box may replace a table. <因此,当为选择模式编写专门的渲染函数时,你可以仅包含可拾取的对象,还可以包含一个封装了的对象,如一片墙。实时交互性高的程序有个很好的主意:创建简单的对象。用多边形代替复杂的墙,或都用盒子代替桌子>

     

                                                         The Selection Mode

     

     

    So far the OpenGL naming scheme has been presented. This section will show you how to enter the selection mode for picking purposes. The first step is to tell OpenGL where to store the hit records. This is accomplished with the following function: 


    void glSelectBuffer(GLsizei size, GLuint *buffer); 

    Parameters:
    buffer : An array of unsigned integers. This array is where OpenGL will store the hit records.
    size : The size of the array.


    You are required to call this function before entering the selection mode. Next you enter the selection mode with a call to 


    void glRenderMode(GLenum mode); 

    Parameters:
    mode - use GL_SELECT to enter the rendering mode and GL_RENDER to return to normal rendering. This later value is the default.


    Now comes the tricky part. The application must redefine the viewing volume so that it renders only a small area around the place where the mouse was clicked.  <程序必须重新定义一个视景体,用它来渲染鼠标下的一小片区域> In order to do that it is necessary to set the matrix mode to GL_PROJECTION. <为此需要设置矩形模式为GL_PROJECTION>Afterwards, the application should push the current matrix to save the normal rendering mode settings.  <而后,程序将当前矩阵压入栈中来保存渲染模式下的设置。Next initialise the matrix. The following snippet of code shows how to do this: 

    glMatrixMode(GL_PROJECTION); 

    glPushMatrix();

    glLoadIdentity();

    OK so now you have a clean projection matrix. All that is required now is to define the viewing volume so that rendering is done only in a small area around the cursor. This can be accomplished using the following function: 

    <现在你可以清除投影矩阵了。现在需要定义一个视景体以便只渲染鼠标下一个块区域内的对象> 

     

    void gluPickMatrix(GLdouble x, GLdouble y, GLdouble witdth, GLdouble height, GLint viewport[4]); 

    Parameters:
    x,y : These two values represent the cursor location. They define the centre of the picking area. This value is specified in window coordinates. However note that window coordinates in OpenGL have the origin at the bottom left corner of the viewport, whereas for the Operating system is the upper left corner.
    width, height : The size of the picking region, too small and you may be hard pressed to pick small objects, too large and you may end up with too many hits.
    viewport : The current viewport


    Before you call the above function you have to get the current viewport. This can be done querying OpenGL for the state variable GL_VIEWPORT (use the function glGetIntegerv). Then you call gluPickMatrix, and finally set your projection (perspective or orthogonal) just as you did for the normal rendering mode. Finally get back to the modelview matrix mode, and initialise the Name Stack to start rendering. The following code shows the sequence of function calls to do just that using a perspective projection. 


     glGetIntegerv(GL_VIEWPORT,viewport); 
    gluPickMatrix(cursorX,viewport[3]-cursorY,5,5,viewport); 
    gluPerspective(45,ratio,0.1,1000); 
    glMatrixMode(GL_MODELVIEW); 
    glInitNames();  


    Note the second parameter of gluPickMatrix. As mentioned before, OpenGL has a different origin for its window coordinates than the operation system. The second parameter provides for the conversion between the two systems, i.e. it transforms the origin from the upper left corner, as provided by GLUT for example, into the bottom left corner.

    The picking region in this case is a 5x5 window. You may find that it is not appropriate for your application. Do some tests to find an appropriate value if you're finding it hard to pick the right objects.

    The following function does all operations required to enter the selection mode and start picking, assuming that cursorX and cursorY are the operating system window coordinates for the location of the mouse click.  

     

    #define BUFSIZE 512 

    GLuint selectBuf[BUFSIZE]

    void startPicking(int cursorX, int cursorY) {

        GLint viewport[4];

        glSelectBuffer(BUFSIZE,selectBuf);
        glRenderMode(GL_SELECT);

        glMatrixMode(GL_PROJECTION);
        glPushMatrix();
        glLoadIdentity();

        glGetIntegerv(GL_VIEWPORT,viewport);
        gluPickMatrix(cursorX,viewport[3]-cursorY,
                5,5,viewport);
        gluPerspective(45,ratio,0.1,1000);
        glMatrixMode(GL_MODELVIEW);
        glInitNames();
    }

     You may copy and paste this function to your application, with the appropriate modifications regarding your projection. If you do that then just call this function when entering the rendering mode and before rendering any graphic primitives.

     

                                                                            Processing the Hit Records 

     In order to process the hit records the application must first return to the normal rendering mode. <为了处理hit记录,程序必须先回到正常的渲染模式>This is done calling glRenderMode with GL_RENDER. This function returns the number of hit records that were created during rendering in the selection mode. <此函数(glRenderMode)返回hit记录的数量>After this step the application can process the selection buffer. Note that before calling glRender with GL_RENDER there is no guarantee that the hit records have been saved into the selection buffer.

    Furthermore, it is necessary to restore the original projection matrix. Since this matrix was saved when entering the selection mode with glPushMatrix, all that is required is to pop the matrix. The following excerpt of code shows the required steps: 

    void stopPicking() {

        int hits;
        
        // restoring the original projection matrix
        glMatrixMode(GL_PROJECTION);
        glPopMatrix();
        glMatrixMode(GL_MODELVIEW);
        glFlush();
        
        // returning to normal rendering mode
        hits = glRenderMode(GL_RENDER);
        
        // if there are hits process them
        if (hits != 0)
            processHits(hits,selectBuf);

    }

    So the final issue is related to the selection buffer structure. The selection buffer stores the hit records sequentially by the order they occurred, i.e. by the order that the primitives we're drawn. Note that primitives that wouldn't be drawn due to Z buffer depth culling still produce hit records.

    The hit records are potentially variable size records due to the number of names they contain.
  • The first field of the hit record is the number of names it contains.
  • The second and third fields represent the minimum and maximum depth of the hit. Note that only the vertices of the primitives that intersect the viewing volume are taken into account. For polygons that get clipped OpenGL reconstructs the vertices. So basically you get the maximum and minimum depths of the rendered segment of the primitives that intersects the viewing volume, not the minimum and maximum depths of the entire primitive. The depth is taken from the Z buffer (where it lies in the range [0,1]), it gets multiplied by 2^32 -1 and it is rounded to the nearest integer. Note that the depths you get are not linearly proportional to the distance to the viewpoint due to the nonlinear nature of the z buffer.
  • The sequence of names recorded for the hit. These names are the contents of the name stack when the hit was recorded. Note that since the number of names can be zero this sequence can be the empty sequence.
  • An example of a selection buffer with 3 hit records is presented next:

    Hit Record ContentsDescription
    0No names have been stored for the first hit
    4.2822e+009Minimum depth for first hit
    4.28436e+009Maximum depth for first hit
    1Number of names for the second hit
    4.2732e+009Minimum depth for second hit
    4.27334e+009Maximum depth for second hit
    6A single name for the second hit
    2Number of names for the third hit
    4.27138e+009Minimum depth for third hit
    4.27155e+009Maximum depth for third hit
    2First name for third hit
    5Second name for third hit

    In order to detect which object was closest to the viewpoint you use the depth information. For instance you can select the object with the smalest minimum depth has the one which the user intended to click on. In the above example the relevant hit is the third one. The following function, adapted from the one in the Red Book, prints out the names for the closest object. < 为了检测出哪一个对象离视口最近,你要使用尝试信息。你要选出深度值最小的那个对象,即用户点击的那个。在上面的例子中,单点了第三个对象。下面的函数,改编于红宝书,输出最近对象的名字>

     

    void processHits2 (GLint hits, GLuint buffer[]) 

    {
       unsigned int i, j;
       GLuint names, *ptr, minZ,*ptrNames, numberOfNames;

       printf ("hits = %d\n", hits);
       ptr = (GLuint *) buffer;
       minZ = 0xffffffff;
       for (i = 0; i < hits; i++) {    
          names = *ptr;
          ptr++; //Minimum depth 
          if (*ptr < minZ) {
              numberOfNames = names;
              minZ = *ptr;
              ptrNames = ptr+2;
          }
          
          ptr += names+2;
        }
      printf ("The closest hit names are ");
      ptr = ptrNames;
      for (j = 0; j < numberOfNames; j++,ptr++) {
         printf ("%d ", *ptr);
      }
      printf ("\n");
       

    源码

    source

     

    posted @ 2012-05-28 17:08  thinkpore  阅读(282)  评论(0)    收藏  举报