上一题下一题
跳转到
 
 
  世界如此多姿,发展如此迅速,窥一斑未必还能知全豹。但正如万花筒一样,每一个管窥都色彩斑斓。  
 
 
  知识通道 | 学习首页 | 教师首页 | PK首页 | 知识创造首页 | 企业首页 | 登录
 
本文对应知识领域
乱弹OpenGL选择-拾取机制(上)
作者:未知 申领版权
2010年10月25日 共有 2328 次访问 【添加到收藏夹】 【我要附加题目
受欢迎度:

    无论是游戏还是VR,三维世界总免不了与用户的交互。而这其中常也免不了“用户对场景中物件的选择(也就是,拾取)”这种需求。OpenGL本身就内置有一套拾取机制,这次就乱弹一下吧。
     
    OpenGL内置的拾取方法,应该是第三次要接触了。头次是课程作业,套例子糊里糊涂弄成功了;第二次是去年9月,做一个DEMO,实现起来终于遇到了诸多麻烦,好不容易通过艰辛调试得到了正确的结果,相关代码被我供奉捧信至今,但其实也不算真正理解了这套机制。这次则是跟老师做的一个项目需要,虽说把之前的代码弄进来这个选取逻辑就差不多了,但我觉得嘛,还是趁机作一次更深入的了解吧。
    假设用户是通过鼠标左键单击来选择场景物件的。先说说一般游戏引擎里的“射线检测”实现思路,也就是在用户拾取物件,点击渲染窗口(屏幕)上的相应的某一点的时候,激发一条从相机位置(眼睛)过该点的射线,这条射线反映在世界空间中,就是“穿过”视像平面发射到三维世界空间的光束。根据三维投影的知识(小孔成像原理)可知,该光速必然可以“射中”用户所点击处的物件,问题转化为线面相交检测——检测通过则意味着“射中”该面所属的三维物件。
    1.名字标识
    这“射线检测”比较好理解。好吧,请先认为OpenGL也遵循这种机制(可不要先入为主哦,后面或许会否定这个假设的),那么,从工序的后端往前看,要解决些什么问题呢?恩,三维物件知道自己身体某一部分的面被一条线叉中了,但它怎么通知应用程序“我被叉叉了”呢?简单的就是给上头打报告,报告内容首项写上自己的姓名。好吧,在三维世界里允许长一个样的但不允许有同样的识别码,换言之,你最好给场景中每样你在意的物件分配一个唯一的ID,好在它们打报告的时候不会弄混。于是,在这层意义上,ID就是名字。
    OpenGL拾取机制觉得ID这太俗了,于是用名字(Name)来唯一表示每个可拾取物件。但是我们作为应用者就该记住,这里的Name就是ID,即使它真是一个名字"Allan",那背后肯定也有这么一句"#define Allan 13"之类的。OpenGL拾取的名字机制也就是ID机制。当然,它里面没有什么排斥不唯一性的,但你让两个物件拥有相同Name的时候,你起码得知道自己在干嘛(是为了实现某些奇怪的效果吧,略谈~)。
    2.名字栈机制
    上头....不,应用程序怎么储存这些资料呢?啊,先说说应用程序怎么与那些被射中而打受伤报告的物件沟通吧,它怎么拿到报告呢?答案是名字栈。
    在OpenGL内部,有一种叫做HIT Record[击中记录]的数据,保存在一个特别的自设定缓存区——Select Buffer[选择缓存]里。我们可以通过以下这个函数来设定SelectBuffer,给予一个已经分配好空间的INT数组作为参数(数组指针和具体大小):
    glSelectBuffer (GLsizei size, GLuint *buffer);
    至于我们到底应该给它多大的空间,下面讲述HIT Record数据结构的时候你就明白了。记住,HIT Record数据的填充是应用程序(上头)的任务,在拾取过程中我们无必要对它动手脚。我们更应该关心的是之前所说的沟通问题。首先看看几个分配Name(名字)的函数:
     

  1. //初始化名字栈
  2. void  glInitNames ();
  3.  
  4. //向名字栈压入名字(ID)
  5. void glPushName(GLuint name); 
  6.  
  7. //从名字栈弹出名字(ID)
  8. void glPopName();
  9.  
  10. //直接把名字(ID)放在栈头位置上
  11. void glLoadName(GLunit name); 
  12. //相当于:
  13. //glPopName();
  14. //glPushName(name);
  15.  

一般来说,栈这种数据结构,是一种前入前出的形式。因此一般也只允许访问栈首元素。现在不妨就假设场景中有三个物件好了,回头看看假设的OpenGL选择拾取逻辑:用户没有点击屏幕的时候一切如常,一旦用户进行了点选则触发拾取逻辑——在这里,我们给每个物件分配一个临时的名字(如前述,相当于ID):
     

  1. const int OBJECT1 = 1;
  2. const int OBJECT2 = 2;
  3. const int OBJECT3 = 3;
  4.  
  5. //渲染待选择物件的函数
  6. void RenderObjects(GLeum renderMode)
  7. {
  8.   if( renderMode == GL_SELECT )  //选择模式
  9.   {
  10.      glPushName(OBJECT1);
  11.      RenderObject1();
  12.      glPopName();
  13.  
  14.      glPushName(OBJECT2);
  15.      RenderObject2();
  16.      glPopName();
  17.  
  18.      glPushName(OBJECT3);
  19.      RenderObject3();
  20.      glPopName();
  21.  
  22.     RenderOtherRelated();
  23.   }
  24.   else   // ( renderMode == GL_RENDER ) 即正常渲染模式
  25.   {
  26.      RenderObject1();
  27.      RenderObject2();
  28.      RenderObject3();
  29.      RenderOtherRelated();
  30.    }
  31. }

我们用一个renderMode枚举值来分开两种逻辑,即选择模式和正常渲染模式,事实上这是必须的,后面会详述。在选择模式下,分配名字。诶?先PUSH后POP,那不是在栈头压一个然后又弹出,然后再压再弹,等于什么都没有?嘿,这种无意义的事情谁也不会干啦,关键是中间夹着的渲染过程,后面等全部代码亮出后你就能理解了。
    这里你要明白这种用名字栈分配名字的做法——它沟通了该名字所代表物件与应用程序内部的HIT Record。当某物件被“拾取”(被光束射中)的时候,对应的名字和相关信息便会被提交给HIT Record,存储在SelectBuffer里面。相关信息包括该物件离光束发射处(相机/眼睛)最近的点的深度值和最远的点的深度值等等,以下反映了当一个物件被拾取后,名字栈机制向HIT Record发送的信息(然后HIT Record把此信息存入SelectBuffer):

    
击中的物件的名字的数目
这个物件中最近的点的深度值
这个物件中最远的点的深度值
击中的物件的名字之一
击中的物件的名字之二 
    
    (若有多个名字,则如此类推...)

 
    上述为一般式,因为一个物件可以配两个或多个名字(这是可能用于某些特殊应用的,在参考文章Picking - LightHouse里提到,有兴趣的可以去看看)。更一般的应用,如上述例子,则会产生如下信息:

    
击中的物件的名字的数目(即:1)
这个物件中最近的点的深度值
这个物件中最远的点的深度值
这个物件的唯一的名字

例子代码中共会产生1条,2条,或3条这样的记录(第一项都是一样的,为1,因为它们的名字唯一),因为用户这一次点击啊,可能就击中了3个物件中的其中一个,也可能一次过击中两个或三个(如果物件靠得近,或者视点离物件很远导致物件在屏幕上显得距离近)。如果是一次多条记录的话,它们会按检测顺序(例子中即是渲染顺序)依次紧跟着存入SelectBuffer里。这样就完了吗?还没呢!还有可能出现第4条记录会发送到HIT Record上(或者单独发送或者一同):

    
0
这个物件中最近的点的深度值
这个物件中最远的点的深度值

没错,它就是RenderOtherRelated()所代表的物件。从上面的记录也可看出,显然我们没有给它分配名字,但它确实参与了拾取过程(更准确地说,因为它也在renderMode == GL_SELECT 这个选择项里)。为什么允许这样的情况出现呢?根据参考文章Picking - LightHouse所说,譬如我们遇上了这种情况:场景由多个房间组成(并且不进行场景划分),每个房间里有不少魔法石待玩家拾取。那么如果我们单单就让这些魔法石都在选择模式里分配名字,然后拾取,“射线光束”就很容易穿过墙壁,“不小心地”把隔壁房间的魔法石给捡了- -。如果墙壁也作为待拾取物,则射线在碰到墙壁的时候就停止了,但是墙壁没有分配名字,因此只有一条首项为0的记录被发送,这样很好,后期判断处理很简捷。但是缺点是……
    缺点是它只有三项。同样,具有两个名字的物件的HIT Record记录有五项,然后拥有多个名字的物件则是更多项……问题在于我们在SelectBuffer里读取的时候怎么办呢?(譬如一次拾取过程中获得含几条记录的HIT Record,读取各名字来确定物件时。)我们最怕不规则的数组……
    那么,用glSelectBuffer设定SelectBuffer的时候,你现在应该知道了,其大小应该根据你认为一次点选可能击中的物件数的最大值,结合那些在GL_SELECT下的三项,多项的非正常物件数来决定。
    本篇最后说一下那两个深度值:
    1. 它们是物件对应Z-BUFFER中的深度值,乘以2^32 -1后取整而得到;
    2. 它们的值不是线性连续的,因为Z-BUFFER中的深度值本来就不是线性连续的(倒是它们的倒数是线性连续的,并符合插值规则,具体的自己去找本3D图形学数学原理的书看去~)
    3.它们是在投影变换之后得到的,经过的视截体裁减过程(what? ),因此针对的是物件在视锥体内的部分的离眼最近最远点,而不一定就是实际世界空间中完整物件的最近最远点。
    好了,接下来的事项我留在了下篇日志中:
     
    那么,开头的假设——OpenGL拾取机制基于射线检测——哈,是错的。的确它们有相同处,至少目前探讨的名字栈机制与这个假设是否成立没有半点关系。注意,在真正的OpenGL拾取机制中,从相机(眼睛)发出的不是光束射线,而是“目光”~!
    呃……呃,哼,哼……嘛,诸君!请留意下篇:
    
    

    

 

相关新闻

战略:绩效管理的中心
债务大转嫁
新股破发不是病 利益链条才要命
中美研究型大学科技管理体制与运行机制比较研究与启示
1.3 Intel 系列CPU简介
Direct3D 实现鼠标拾取
理解OpenGL拾取模式(OpenGL Picking)
Picking Tutorial
3D数学:求点到直线的投影
全屏反锯齿 - 多重采样Ⅱ

您可能对这些感兴趣  

《VB程序设计基础》选择题
设计模式之Iterator
设计模式之Visitor
设计模式之Interpreter(解释器)
设计模式之Mediator(中介者)
设计模式之Strategy(策略)
设计模式之State
设计模式之Command
设计模式之Chain of Responsibility(职责链)
设计模式之Observer

题目筛选器
日期:
类型:
状态:
得分: <=
分类:
作者:
职业:
关键字:
搜索

 
 
 
  焦点事件
 
  知识体系
 
  职业列表
 
 
  最热文章
 
 
  最多引用文章
 
 
  最新文章
 
 
 
 
网站介绍 | 广告服务 | 招聘信息 | 保护隐私权 | 免责条款 | 法律顾问 | 意见反馈
版权所有 不得转载
沪ICP备 10203777 号 联系电话:021-54428255
  帮助提示    
《我的太学》是一种全新的应用,您在操作中遇到疑问或者问题,请拨打电话13564659895,15921448526。
《我的太学》