理解OpenGL拾取模式(OpenGL Picking)_opengl 拾取模式 可以拾取点吗-程序员宅基地

技术标签: OpenGL  

在用OpenGL进行图形编程的时候,通常要用鼠标进行交互操作,比如用鼠标点选择画面中的物体,我们称之为拾取(Picking),在网上看了很多OpenGL拾取的文章,但大多是只是介绍在OpenGL中如何拾取,如何利用OpenGL提供的一系列函数来完成拾取,最多再简单介绍下OpenGL的名字栈(Name stack),拾取矩阵(Picking Matrix)等等,但是拾取真正的原理确没有提到。所以,我在这里为大家详细介绍下OpenGL中拾取是怎样实现的,以及其背后的真正原理。

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

总的来说,OpenGL图形管线大体分为上面的五个阶段。在编程的时候使用glMatrixMode(GL_MODELVIEW),或者 glMatrixMode(GL_PROJECTION)就是告诉OpenGL我们是要在那个阶段进行操作。先来看看投影变换,因为理解投影变换是理解OpenGL拾取的前提条件。为了简单起见,这里以正交投影(Orthogonal Projection)为例。在OpenGL中,使用正交投影可以调用glOrtho (left, right, bottom, top, zNear, zFar),其中的六个参数分别对应正交投影视体的六个平面到观察坐标系原点的距离。一旦在程序中调用了这个函数,OpenGL会马上创建根据给定的六个参数创建一个视体,并且把视体的大小归一化到-1到1之间,也就是说,OpenGL会自动把你给的参数所对应的x,y,z值转换为-1到1之间的值,并且这个视体的中心就是观察坐标系的原点。要注意的是,当视体归一化后,z轴的方向要反向,也就是说,这里OpenGL的右手坐标系要换成左手坐标系。原因很简单,z轴朝向显示器里的方向更符合我们的常识,越向里就离我们越远,z的值也就越大。

image

当我们调用了glOrtho()这个函数后,OpenGL会建立一个矩阵,也就是投影矩阵。这个矩阵可以分解为三个步骤,首先将我们设置的视体移动到观察坐标是的原点,然后在缩放为边长为2的单位视体。因为转化后的视体坐标都在-1和1之间,所以视体的边长就是2。然后再对z进行反方向。最后的投影矩阵我们用 来表示的话,那么有

e

上面的矩阵虽然看起来很复杂,其实很简单。它就是进行移动,缩放,反号三个操作而已。现在我们在OpenGL中检查一下是不是进行了这样的操作。添加下面的代码。

   
   
    
  1. glMatrixMode(GL_PROJECTION);    
  2.   
  3. glLoadIdentity();    
  4.   
  5. glOrtho(-10, 10, -10, 10, -10, 10);    
  6.   
  7.         
  8.   
  9. GLfloat m[16];    
  10.   
  11. glGetFloatv(GL_PROJECTION_MATRIX, m);    

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

d

代码中我们使用了glGetFloatv来获得当前操作矩阵,然后设定断点来检查一下。

image

可以看到,得到的数据和我们计算的一样。说明OpenGL的确是创建了这样的矩阵来进行计算。弄清楚了OpenGL中的投影变换,现在就开看看大家关心的拾取操作。OpenGL的拾取就是利用投影变换中归一化视体这个操作来实现的。拾取的时候,我们可以想象想用一个方框来选择我们要选择的物体。比如一个边长为2的正方形,我们用鼠标在窗口上点击的时候,一旦点到一个位置,那么就在这个位置生成一个边长为2的正方形,那么正方形内包围的物体就是我们要选择的物体,如果这个正方形内没有包围任何东西,那么就说明什么都没有选择到。这个过程就和我们归一化投影,然后再剪裁的过程是一样的。OpenGL会自动剪裁掉在归一化视体之外的物体,那么如果我们把选择物体用的方框转换为用投影时的视体,那么在这个方框外的东西,也就是我们没有选择的东西,OpenGL会自动的为我们扔掉。所以OpenGL提供了选择模式glRenderMode(GL_SELECT),当我们进行拾取前进入这个模式,然后设定好我们的选择框的大小,再为我们要选择的物体设定好名字,也就是我们说的名字栈。接下来的操作和投影变换就一样了,先把这个选择框移归一化为边长为-1到1的正方体,然后移动到原点,最后放大为我们窗口的大小。(这时OpenGL已经把在选择框外的东西剪裁掉了,如果这个时候我们显示投影矩阵中的内容的话,就会只看到我们选择到的东西,并且放大和窗口一样大。)然后OpenGL会把选中的物体信息记录在一个叫做SelectBuffer的缓冲中,这个缓冲就是个一维数组,里面保存了名字栈中名字的个数,选择到的物体的最小最大深度值,也就是z的值,这个值是个0到1之间的值,也就是里我们最近的为0,最远的为1。selectBuffer是个整型的数组,所以这里保存的深度值是乘以0xFFFFFFFF后的值。当然最重要的,其中还保存了我们选择到的物体的名字,这样只要在程序中判断选择到物体的名字,我们就可以判断是不是选择到了要选择的物体了。整个拾取的过程可如下。

image

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

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

其中x,y是鼠标点击到窗口上的坐标width和height就是这个拾取框的长宽viewport是为了得到我们窗口的大小。一但调用了该函数,OpenGL就会创建一个拾取矩阵,分解这个矩阵的话,可以看到,这个矩阵就是上面的移动拾取框到原点,然后再放大为视体大小这两个步骤。所以即使我们不使用这个函数,也可以自己计算出这个拾取矩阵.

p

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


   
   
    
  1. void SelectObject(GLint x, GLint y)    
  2.   
  3. {    
  4.   
  5.   GLuint selectBuff[32]={0};//创建一个保存选择结果的数组    
  6.   
  7.   GLint hits, viewport[4];      
  8.   
  9.   
  10.   
  11.   glGetIntegerv(GL_VIEWPORT, viewport); //获得viewport    
  12.   
  13.   glSelectBuffer(64, selectBuff); //告诉OpenGL初始化  selectbuffer    
  14.   
  15.   glRenderMode(GL_SELECT);    //进入选择模式    
  16.   
  17.   
  18.   
  19.   glInitNames();  //初始化名字栈    
  20.   
  21.   glPushName(0);  //在名字栈中放入一个初始化名字,这里为‘0’    
  22.   
  23.   
  24.   
  25.   glMatrixMode(GL_PROJECTION);    //进入投影阶段准备拾取    
  26.   
  27.   glPushMatrix();     //保存以前的投影矩阵    
  28.   
  29.   glLoadIdentity();   //载入单位矩阵    
  30.   
  31.   
  32.   
  33.   float m[16];    
  34.   
  35.   glGetFloatv(GL_PROJECTION_MATRIX, m);    
  36.   
  37.   
  38.   
  39.   gluPickMatrix( x,           // 设定我们选择框的大小,建立拾取矩阵,就是上面的公式  viewport[3]-y,    // viewport[3]保存的是窗口的高度,窗口坐标转换为OpenGL坐标    
  40.   
  41.    2,2,              // 选择框的大小为2,2    
  42.   
  43.    viewport          // 视口信息,包括视口的起始位置和大小    
  44.   
  45. );        
  46.   
  47.                                                         
  48.   
  49.     glGetFloatv(GL_PROJECTION_MATRIX, m);    
  50.   
  51. glOrtho(-10, 10, -10, 10, -10, 10);     //拾取矩阵乘以投影矩阵,这样就可以让选择框放大为和视体一样大    
  52.   
  53.     glGetFloatv(GL_PROJECTION_MATRIX, m);    
  54.   
  55.   
  56.   
  57.     draw(GL_SELECT);    // 该函数中渲染物体,并且给物体设定名字    
  58.   
  59.         
  60.   
  61.     glMatrixMode(GL_PROJECTION);    
  62.   
  63.     glPopMatrix();  // 返回正常的投影变换    
  64.   
  65.     glGetFloatv(GL_PROJECTION_MATRIX, m);    
  66.   
  67.     hits = glRenderMode(GL_RENDER); // 从选择模式返回正常模式,该函数返回选择到对象的个数    
  68.   
  69.     if(hits > 0)    
  70.   
  71.         processSelect(selectBuff);  //  选择结果处理    
  72.   
  73.     }    
  74.   
  75.   
  76.   
  77.     void draw(GLenum model=GL_RENDER)    
  78.   
  79.     {    
  80.   
  81.         if(model==GL_SELECT)    
  82.   
  83.         {    
  84.   
  85.             glColor3f(1.0,0.0,0.0);    
  86.   
  87.             glLoadName(100);    
  88.   
  89.             glPushMatrix();    
  90.   
  91.         glTranslatef(-5, 0.0, 10.0);    
  92.   
  93.             glBegin(GL_QUADS);    
  94.   
  95.                 glVertex3f(-1, -1, 0);    
  96.   
  97.                 glVertex3f( 1, -1, 0);    
  98.   
  99.             glVertex3f( 1, 1, 0);    
  100.   
  101.                 glVertex3f(-1, 1, 0);    
  102.   
  103.             glEnd();    
  104.   
  105.             glPopMatrix();    
  106.   
  107.     
  108.   
  109.             glColor3f(0.0,0.0,1.0);    
  110.   
  111.             glLoadName(101);    
  112.   
  113.        glPushMatrix();    
  114.   
  115.             glTranslatef(5, 0.0, -10.0);    
  116.   
  117.         glBegin(GL_QUADS);    
  118.   
  119.            glVertex3f(-1, -1, 0);    
  120.   
  121.             glVertex3f( 1, -1, 0);    
  122.   
  123.            glVertex3f( 1, 1, 0);    
  124.   
  125.             glVertex3f(-1, 1, 0);    
  126.   
  127.         glEnd();    
  128.   
  129.             glPopMatrix();    
  130.   
  131.         }    
  132.   
  133.         else    
  134.   
  135.     {    
  136.   
  137.         glColor3f(1.0,0.0,0.0);    
  138.   
  139.         glPushMatrix();    
  140.   
  141.             glTranslatef(-5, 0.0, -5.0);    
  142.   
  143.             glBegin(GL_QUADS);    
  144.   
  145.             glVertex3f(-1, -1, 0);    
  146.   
  147.             glVertex3f( 1, -1, 0);    
  148.   
  149.             glVertex3f( 1, 1, 0);    
  150.   
  151.             glVertex3f(-1, 1, 0);    
  152.   
  153.         glEnd();    
  154.   
  155.             glPopMatrix();    
  156.   
  157.         
  158.   
  159.             glColor3f(0.0,0.0,1.0);    
  160.   
  161.         glPushMatrix();    
  162.   
  163.             glTranslatef(5, 0.0, -10.0);    
  164.   
  165.             glBegin(GL_QUADS);    
  166.   
  167.                 glVertex3f(-1, -1, 0);    
  168.   
  169.             glVertex3f( 1, -1, 0);    
  170.   
  171.             glVertex3f( 1, 1, 0);    
  172.   
  173.             glVertex3f(-1, 1, 0);    
  174.   
  175.             glEnd();    
  176.   
  177.         glPopMatrix();    
  178.   
  179.         }    
  180.   
  181. }   

然后设点断点来检查一下。

image

上面看到我们的视口的起始位置就是窗口的原点,大小和窗口的大小一样,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之间的坐标得到

image

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

gif.latex

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

image

从上面可以看到,和我们计算的结果是一样的。在设置了拾取矩阵后,又调用了语句glOrtho(-10, 10, -10, 10, -10, 10),这表示让这个拾取框的大小铺满我们整个窗口,OpenGL会把刚才得到的拾取矩阵再乘以这个投影矩阵,我们可以得到

F

再在程序中设置断点来检查一下。

image

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

image

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

image

上图中可以看到红色正方形中有个白色的小方框,这个就是在设置拾取矩阵时的拾取框,拾取框在红色正方形内部,说明我们已经选中了红色的正方形了。如果这个时候把选择框中的东西显示出来的话,就可以看到我们选择到的部分铺满整个屏幕。

image

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

image 可以看到数组的前4个部分有值,第一个表示被选中物体的个数,当然现在是1。第二个表示和第三个表示物体的最小深度值和最大深度值,由于物体是个平面,所以这两个值是一样的。这里的深度值是整数,除以0xffffffff以后就得到了0到1之间的深度值了。第四个值,也就是我们选择到的物体的名字。这里就是红色的正方形。

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


版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/why_study/article/details/8204800

智能推荐

2023CCF中国开源大会丨开源供应链安全技术分论坛_程华 军科院-程序员宅基地

文章浏览阅读447次。2023 CCF 中国开源大会即将于10月21日-22日于湖南长沙举行,将举办近20场领域平行分论坛。由王千祥作为主席的开源供应链安全技术分论坛邀请您共同分享经验和见解,共同探索开源领域的无限未来!_程华 军科院

html文件引入其它html文件的几种方法:include方式-程序员宅基地

文章浏览阅读1.2k次。  可以在一个html的文件当中读取另一个html文件的内容吗?答案是确定的,而且方法不只一种,在以前我只会使用iframe来引用,后来发现了另外的几种方法,那今天就总结这几种方法让大家参考一下。  1.IFrame引入    a.html引入b.html的内容    <iframe name="content_iframe" marginwidth=0 marg..._引入html include

JAVA语言遗漏_java语音包 漏字-程序员宅基地

文章浏览阅读266次。IS-A关系:public class Animal{}public class Mammal extends Animal{}public class Reptile extends Animal{}public class Dog extends Mammal{}基于上面的例子,以下说法是正确的:Animal类是Mammal类的父类。Anim_java语音包 漏字

高速串行总线系列(2)高速串行总线技术总览_cml和pcie-程序员宅基地

文章浏览阅读881次。文章目录嵌入式高速串行总线技术应用对比SRIO总线JESD204总线PCIESATA总线AuroraFC标准VPX架构嵌入式高速串行总线技术应用对比SRIO总..._cml和pcie

DML编程控制_global-config.db-config.id-type=assign_id-程序员宅基地

文章浏览阅读332次。MP会默认将模型类的类名名首字母小写作为表名使用,假如数据库表的名称都以tbl_开头,那么我们就需要将所有的模型类上添加。这种生成策略,不需要手动设置ID,如果手动设置ID,则会使用自己设置的值。AUTO的作用是使用数据库ID自增,在使用该策略的时候一定要确保对应的数据库表设置了ID主键自增,否则无效。可想而知,MP的逻辑删除会将所有的查询都添加一个未被删除的条件,也就是已经被删除的数据是不应该被查询出来的。设置表的前缀内容,这样MP就会拿 tbl_加上模型类的首字母小写,就刚好组装成数据库的表名。_global-config.db-config.id-type=assign_id

Introduction to Rotary Cement Wet Kilns-程序员宅基地

文章浏览阅读70次。Most Portland cement is produced in a rotary kiln. Basically, this can be a long cylinder rotating about its axis once every single minute or two. The axis is inclined at a slight angle, the end using..._new machines to produce cement

随便推点

ubuntu系统无法连接蓝牙设备_ubuntu蓝牙连不上-程序员宅基地

文章浏览阅读707次,点赞11次,收藏8次。选择需要连接的设备,右键点击连接,信任即可。直接点击 blueman。_ubuntu蓝牙连不上

Android Action Bar 详解篇_android actionbar使用场景-程序员宅基地

文章浏览阅读373次。作为Android 3.0之后引入的新的对象,ActionBar可以说是一个方便快捷的导航神器。它可以作为活动的标题,突出活动的一些关键操作(如“搜索”、“创建”、“共享”等)、作为菜单的灵活使用,还可以实现类似TabWidget的标签功能以及下拉导航的功能,系统能够很好根据不同的屏幕配置来适应ActionBar的外观,配合起Fragemtn可谓是十分强大。 那么,对于今_android actionbar使用场景

python爬虫图片加速_Python爬虫加速神器的小试-程序员宅基地

文章浏览阅读134次。大名鼎鼎的aiohttp,相信如果你学习Python或者爬虫的时候,肯定听说过这个东西。没听过也不要紧,今天看完文章,只要记住,aiohttp这个东西,在写爬虫的时候,很牛逼就行了。aiohttp 就是一个用 asyncio实现的 HTTP client/server。 你可以通过它来简单实现一个具有异步处理功能的 clients 和 servers。 aiohttp同时还支持 Server We..._python 加速图片下载

eclipse处理get请求得前端中文乱码_eclipse 普通 get请求中文乱码-程序员宅基地

文章浏览阅读459次。我这个方法是将所有的地方都改成utf-8不单单是能解决前端问题第一:去博主这改一下eclipse编码配置https://blog.csdn.net/lanmuhhh2015/article/details/79366872第二:然后再 servlet中doget请求中加上response.setContentType("text/html;charset=utf-8");第三:去你to..._eclipse 普通 get请求中文乱码

Hadoop-02_yapoopg-02-程序员宅基地

文章浏览阅读98次。1、Hadoop HDFS:一个高可靠、高吞吐量的分布式文件系统。Hadoop MapReduce:一个分布式的离线并行计算框架。(分布式和集群,想想)2、hadoop的apache 和 cloudera公司目录结构apache目录结构: bin : 对集群操作的命令 hdfs dfs和hadoop fs sbin: 对集群的启动和关闭的命令 etc/hadoop/ : hadoo..._yapoopg-02

上海毅速:3D打印正在为模具行业深度赋能-程序员宅基地

文章浏览阅读307次。模温是影响模具生产效率和产品品质的关键因素,传统的冷却水路设计受限于加工技术,往往只能制成直线型,与产品之间的距离较远且分布不均,导致冷却效果不佳。而3D打印随形水路技术则能打破这一局限,打印出的水路可以具有任意结构形状和截面形状,实现水路与产品之间距离的最小化和最均匀分布,从而达到最优的冷却效果。在模具制造领域,3D打印随形水路制在模具领域的应用越来越多,随形水路不仅能有效缩短注塑周期,还能解决因冷却不均匀、模具温度过高等导致的品质问题。3D打印技术的核心原理在于离散、堆积与成型。