OpenGL中的拾取模式( Picking)

1. Opengl中的渲染模式有三种:(1)渲染模式,默认的模式;(2)选择模式, (3)反馈模式。如下

GLint glRenderMode(GLenum mode)

 mode可以选取以下三种模式之一:绘制模式(GL_RENDER),选择模式(GL_SELECT),反馈模式(GL_FEEDBACK)。

  函数的返回值可以确定选择模式下的命中次数或反馈模式下的图元数量。

2. OpenGL进行图形编程的时候,通常要用鼠标进行交互操作,比如用鼠标点选择画面中的物体,我们称之为拾取(Picking).

  OpenGL中的拾取是对OpenGL图形管线的一个应用。所以OpenGL中的拾取并不是像D3D一样采用射线交叉测试来判断是否选中一个目标,而是在图形管线投影变换(Projection Transformation)阶段利用拾取矩阵来实现的。

  

  OpenGL的图形管线如上图所示。

  总的来说,OpenGL图形管线大体分为上面的五个阶段,在Opengl中做了下述内容的合并:

  1.   把模型变换与视图变换合并为模型视图变换,即GL_MODELVIEW
  2.   归一化视体并裁剪处理也整合进了投影变换处理中,Opengl统一为:投影变换

  因此,Opengl中可以由如下五大变换过程就简化为了三种过程模型视图变换 --> 投影变换 --> 视口变换

  在编程的时候使用glMatrixMode(GL_MODELVIEW),或者 glMatrixMode(GL_PROJECTION)就是告诉OpenGL我们是要在那个阶段进行操作。

3. 投影变换过程

  先来看看投影变换,因为理解投影变换是理解OpenGL拾取的前提条件。

  为了简单起见,这里以正交投影(Orthogonal Projection)为例。在OpenGL中,使用正交投影可以调用glOrtho (left, right, bottom, top, zNear, zFar),其中的六个参数分别对应正交投影视体的六个平面 到观察坐标系原点的距离。一旦在程序中调用了这个函数,OpenGL会马上创建根据给定的六个参数创建一个视体,并且把视体的大小归一化到-1到1之间, 也就是说,OpenGL会自动把你给的参数所对应的x,y,z值转换为-1到1之间的值,并且这个视体的中心就是观察坐标系的原点。

  要注意的是,当视体归 一化后,z轴的方向要反向,也就是说,这里OpenGL的右手坐标系要换成左手坐标系。原因很简单,z轴朝向显示器里的方向更符合我们的常识,越向里就离 我们越远,z的值也就越大

  也可以这样理解:世界坐标系中采用右手坐标系。但是,在Opengl中显示投影之后的坐标系采用左手坐标系

 

   当我们调用了glOrtho()这个函数后,OpenGL会建立一个矩阵,也就是投影矩阵。这个矩阵可以分解为 三个步骤

  1.   首先将我们设置的视体移动到观察坐标是的原点
  2.   然后在缩放为边长为2的单位视体。因为转化后的视体坐标都在-1和1之间,所以视体的边长就是 2。
  3.   然后再对z进行反方向。最后的投影矩阵我们用 来表示的话,那么有

  

  上面的矩阵虽然看起来很复杂,其实很简单。它就是进行移动,缩放,反号三个操作而已。

  现在我们在OpenGL中检查一下是不是进行了这样的操作,对程序代码作下述测试:

//进入投影变换过程
glMatrixMode(GL_PROJECTION); glLoadIdentity(); glOrtho(
-10, 10, -10, 10, -10, 10); GLfloat m[16]; glGetFloatv(GL_PROJECTION_MATRIX, m); //获取当前投影矩阵的内容

  首先进入投影变换阶段,然后我们使用glLoadIdentity()在矩阵栈中存入单位矩阵。设置视体为边长为20的正方体。这里我们把glortho中的6个参数带入上面公式计算,得到查看结果:

   

  

  可以看到,得到的数据和我们计算的一样。说明OpenGL的确是创建了这样的矩阵来进行计算。弄清楚了OpenGL中的投影变换,现在就开看看大家关心的拾取操作。

4. Opengl中的选择模式拾取Pick操作的原理

  OpenGL的拾取就是利用投影变换归一化视体这个处理中来实现的

  拾取的时候,我们可以想象想用一个方框来选择我们要选择的物体。比如一个边长为2的正方形,我们用鼠标在窗口上点击的时候,一旦点到一个位置,那么就在这个位置生成一个边长为2的正方形,那么正方形内包围的物体就是我们要选择的物体,如果这个正方形内没有包围任何东西,那么就说明什么都没有选择到。这个过程就和我们归一化投影,然后再剪裁的过程是一样的。

  OpenGL会自动剪裁掉在归一化视体之外的物体,那么如果我们把选择物体用的方框转换为用投影时的视体,那么在这个方框外的东西,也就是我们没有选择的东西,OpenGL会自动的为我们扔掉。所以OpenGL提供了选择模式glRenderMode(GL_SELECT)当 我们进行拾取前进入这个模式,然后设定好我们的选择框的大小,再为我们要选择的物体设定好名字,也就是我们说的名字栈。

  接下来的操作和投影变换就一样了,就是归一化视体的处理过程。即 先把这个选择框移归一化为边长为-1到1的正方体,然后移动到原点,最后放大为我们窗口的大小。(这时OpenGL已经把在选择框外的东西剪裁掉了,如果 这个时候我们显示投影矩阵中的内容的话,就会只看到我们选择到的东西,并且放大和窗口一样大。)然后OpenGL会把选中的物体信息记录在一个叫做SelectBuffer的缓冲中,这个缓冲就是个一维数组,里面保存了名字栈中名字的个数,选择到的物体的最小最大深度值,也就是z的值,这个值是个0到1之间的值,也就是里我们最近的为0,最远的为1。selectBuffer是个整型的数组,所以这里保存的深度值是乘以0xFFFFFFFF后的值。当然最重要的,其中还保存了我们选择到的物体的名字,这样只要在程序中判断选择到物体的名字,我们就可以判断是不是选择到了要选择的物体了

  整个拾取的过程可如下:

   

   上图中左边的正方体是我们归一化的视体,拾取的时候就是在这个空间中拾取的。红色的小框是我们的选择框,即鼠标所在位置的选择矩形。里面的红色就是我们选择到的物体的一部分。现在要做的就是把这个小框转变为视体,这样OpenGL才能为我们把不要的东西扔掉。所以,首先还是把这个小框移动到观察坐标系的原点,然后再放大为我们归一化视体的大小,这样整个视体中就只有我们选中的东西了,上图中间显示了这个过程。视体外的东西已经被OpenGL剪裁掉选中的记录会保存到selectbuffer中。因为这些操作是在选择模式下完成的,所以看不到我们选择的过程,但是如果我们把选择的过程显示出来的话,就会看到上图右边的样子。整个窗口就铺满了我们选择的部分。

  在OpenGL中,提供了这个设置拾取框的函数。

   gluPickMatrix (x, y, width, height, viewport[4]);

  其中x,y是鼠标点击到窗口上的坐标width和height就是这个拾取框的长宽viewport是为了得到我们窗口的大小

   一但调用了该函数,OpenGL就会创建一个拾取矩阵,分解这个矩阵的话,可以看到,这个矩阵就是上面的移动拾取框到原点,然后再放大为视体大小这两个步骤。

  所以即使我们不使用这个函数,也可以自己计算出这个拾取矩阵.该矩阵的形式如下:

  

  同样,我们还是在OpenGL中代码测试,检查一下,是不是做了这样的操作。在OpenGL中添加下面的代码。

  1 void SelectObject(GLint x, GLint y)    
  2   
  3 {    
  4   GLuint selectBuff[32]={0};//创建一个保存选择结果的数组    
  5   GLint hits, viewport[4];      
  6   
  7   glGetIntegerv(GL_VIEWPORT, viewport); //获得viewport    
  8   glSelectBuffer(64, selectBuff); //告诉OpenGL初始化  selectbuffer    
  9 
 10    //进入选择模式    
 11   glRenderMode(GL_SELECT);   
 12   
 13   glInitNames();  //初始化名字栈    
 14   glPushName(0);  //在名字栈中放入一个初始化名字,这里为‘0’    
 15   
 16   glMatrixMode(GL_PROJECTION);    //进入投影阶段准备拾取    
 17   
 18   glPushMatrix();     //保存以前的投影矩阵    
 19   glLoadIdentity();   //载入单位矩阵    
 20   
 21   float m[16];    
 22   glGetFloatv(GL_PROJECTION_MATRIX, m);  //监控当前的投影矩阵 
 23   
 24   gluPickMatrix( x,           // 设定我们选择框的大小,建立拾取矩阵,就是上面的公式  
 25    viewport[3]-y,    // viewport[3]保存的是窗口的高度,窗口坐标转换为OpenGL坐标(OPengl窗口坐标系)   
 26    2,2,              // 选择框的大小为2,2    
 27    viewport          // 视口信息,包括视口的起始位置和大小    
 28    );        
 29   
 30     glGetFloatv(GL_PROJECTION_MATRIX, m);//查看当前的拾取矩阵  
 31     //投影处理,并归一化处理
 32     glOrtho(-10, 10, -10, 10, -10, 10);     //拾取矩阵乘以投影矩阵,这样就可以让选择框放大为和视体一样大    
 33     glGetFloatv(GL_PROJECTION_MATRIX, m);    
 34   
 35     draw(GL_SELECT);    // 该函数中渲染物体,并且给物体设定名字    
 36   
 37     glMatrixMode(GL_PROJECTION);    
 38 
 39     glPopMatrix();  // 返回正常的投影变换    
 40   
 41 
 42 
 43     glGetFloatv(GL_PROJECTION_MATRIX, m);//即还原在选择操作之前的投影变换矩阵 
 44   
 45     hits = glRenderMode(GL_RENDER); // 从选择模式返回正常模式,该函数返回选择到对象的个数    
 46   
 47     if(hits > 0)    
 48   
 49         processSelect(selectBuff);  //  选择结果处理    
 50   
 51 }    
 52   
 53   
 54   
 55     void draw(GLenum model=GL_RENDER)    
 56   
 57     {    
 58          if(model==GL_SELECT)    
 59         {    
 60             glColor3f(1.0,0.0,0.0);    
 61             glLoadName(100);  //第一个矩形命名
 62             glPushMatrix();    
 63             glTranslatef(-5, 0.0, 10.0);    
 64             glBegin(GL_QUADS);    
 65                 glVertex3f(-1, -1, 0);    
 66                 glVertex3f( 1, -1, 0);    
 67                 glVertex3f( 1, 1, 0);    
 68                 glVertex3f(-1, 1, 0);    
 69             glEnd();    
 70             glPopMatrix();    
 71 
 72 
 73 
 74             glColor3f(0.0,0.0,1.0);    
 75             glLoadName(101); //第二个矩形命名
 76             glPushMatrix();    
 77             glTranslatef(5, 0.0, -10.0);    
 78             glBegin(GL_QUADS);    
 79                 glVertex3f(-1, -1, 0);   
 80                 glVertex3f( 1, -1, 0);    
 81                 glVertex3f( 1, 1, 0);    
 82                 glVertex3f(-1, 1, 0);    
 83             glEnd();    
 84             glPopMatrix();    
 85   
 86     }    
 87      else //正常渲染  
 88     {    
 89         glColor3f(1.0,0.0,0.0);    
 90         glPushMatrix();    
 91             glTranslatef(-5, 0.0, -5.0);    
 92             glBegin(GL_QUADS);    
 93             glVertex3f(-1, -1, 0);    
 94             glVertex3f( 1, -1, 0);    
 95             glVertex3f( 1, 1, 0);    
 96             glVertex3f(-1, 1, 0);    
 97         glEnd();    
 98         glPopMatrix();    
 99   
100         
101   
102        glColor3f(0.0,0.0,1.0);    
103        glPushMatrix();   
104        glTranslatef(5, 0.0, -10.0);   
105        glBegin(GL_QUADS);    
106        glVertex3f(-1, -1, 0);    
107             glVertex3f( 1, -1, 0);    
108             glVertex3f( 1, 1, 0);    
109             glVertex3f(-1, 1, 0);    
110             glEnd();    
111         glPopMatrix();    
112    }    
113   
114 }

 然后设点断点来检查一下

  

  上面看到我们的视口的起始位置就是窗口的原点,大小和窗口的大小一样,500×500。然后在gluPickMatrix( x,  viewport[3]-y,    2,2,  viewport )函数调用后,会马上建立一个拾取矩阵,根据提供的参数,在屏幕上点击的坐标为 x=136,y=261,这是屏幕坐标,即单位是(像素),且原点在左上角

  转换成openGL坐标为x=136,y=500-261=239,即在归一化后的窗口坐标,因为窗口坐标是左手坐标系,因此原点在左下角。拾取框的另一个坐标就是138,242,因为这个给的长,宽都是2。现在就得到了拾取框的2个坐标了,,然后把这2个坐标再转换为-1到1之间的坐标得到:

  

  把这个坐标带入拾取矩阵中计算,可以得到

  

  然后,在程序中设置断点,获取当前操作矩阵检查一下:

  

  的确和我们计算的结果一样。然后代码中使用了glPopMatrix(),由于我们的拾取操作已经完成,到里位置,拾取的结果信息已经被OpenGL保存到了selectbuffer中了,所以这时我们就要不在需要这个拾取矩阵,由于之前使用了glPushMatrix()保存了原来的投影矩阵,现在只要glPopMatrix()就可以了。glPopMatrix()后,我们可以再设置断点看一下是不是真的返回到原来的投影矩阵。

  

  同样我们可以从上图中看到,的确在PopMatrix后,返回了原来的投影变换,所以现在从选择模式返回到正常模式的时候我们就可以看到正常的画面。

  

  在前面的代码中,我们已经为红色正方形命名为100,蓝色的为101,现在我们来看看selectbuffer里被选中的物体。

   

  可以看到数组结果中:每4个一组作为一个被选中的item的信息:该解析如下:

  第一个表示被选中物体的个数,当然现在是1。

  第二个表示和第三个表示物体的最小深度值和最大深度值,由于物体是个平面,所以这两个值是一样的。这里的深度值是整数,除以0xffffffff以后就得到了0到1之间的深度值了。

  第四个值,也就是我们选择到的物体的名字。这里就是红色的正方形的Name。

   OpenGL的整个拾取过程就是这样的。它是利用了图形管线中投影变换阶段来实现拾取操作的。对于OpenGL图形管线不了解的朋友可能对这种方法会感到困难。但是一旦理解了图形管线后,再来理解拾取就很容易了。

endl;

参考网址:

  http://blog.csdn.net/archielau/article/details/7646739

  

原文地址:https://www.cnblogs.com/icmzn/p/7373663.html