使用OpenGL实现三维坐标的鼠标拣选
Implementation of RIP(Ray-Intersection-Penetration)
3D Coordinates Mouse Selection Using OpenGL
顾 露 (武汉理工大学 计算机系 中科院智能设计与智能制造研究所 湖北武汉 430070)
摘要(Abstract):
本文提出并实现一种用于三维坐标拣选的RIP(Ray-Intersection-Penetration)方法。介绍了如何在已经渲染至窗口的三维场景
中,使用鼠标或者相关设备拣选特定三维对象的方法。此方法对于正交投影或透视投影均有效,相对于OpenGL自带的选择与反馈机制,本方法无论是拣选精度
还是算法实现效率均高出许多,是一种比较通用的解决方案。关键词(Keywords) 正交投影(Ortho-Projection)、透视投影(Perspective-Projection)
世界坐标系、屏幕坐标系、三维拣选、OpenGL
一、简介(Introduction)
OpenGL是一种比较“纯粹”的3D图形API,一般仅用于三维图形的渲染,对于特定领域的开发者(如游戏开发者)而言,如果选择使用
OpenGL进行开发,类似碰撞检测的机制就都需要自行编写了。但是由于鼠标在图形程序中的应用非常非常之广泛(例如现在已经很少有PC游戏能完全地脱离
鼠标),OpenGL在图形库的基础上添加了选择与反馈机制(Select &
Feedback)来满足用户使用鼠标实时操作三维图形的需要。但由于种种原因,我们需要更为特殊的选择机制以满足特定需求,在这里我们提出了一种简单迅
速的RIP(Ray-Intersection-Penetration)方法,可以满足绝大多数典型应用的需要。
二、相关研究(Related Work) 用过OpenGL选择与反馈机制的开发者,或多或少可能都会觉得它难以令人满意。大致表现在下面几个方面:
一、 编写程序比较繁琐。
想要使用选择反馈机制就需要切换渲染模式,操作命名堆栈,计算拣选矩阵,检查选中记录,这些繁琐的步骤很容易出错,而且非常不便于调试,只会降低工作效率和热情。二、 只能做基于图元的选定。
如
下图(1 – a),使用GL_TRIANGLES绘制了一个三角形,三个顶点分别为
P1、P2和P3。若使用该机制,你将只能判断是否在三维场景中选中了这个三角形(用户点击处是否在P1、P2和P3的范围内),而无法判断用户是点击了
这个三角形哪一部分(是左边的m区域内还是右边的n区域内),因为所绘制的P1、P2和P3本身构成的三角形就是一个基本图元,对于拣选机制而言是不可分
的。当然,把这个三角形拆成两个三角形再分别进行测试也是一个可行的方案,可是看看图(1 – b),这可怎么拆呢?还有图(1 –
c)呢?另外,如果n和m两个平面不共面呢?对于使用者而言,OpenGL提供的拣选机制功能的确有限。
三、降低了渲染效率。
OpenGL
中的选择和反馈是与普通渲染方式不同的一种特殊的渲染方式。我们使用时一般是先在帧缓存中渲染普通场景,然后进入选择模式重绘场景,此时帧缓存的内容并无
变化。也就是说,为了选择某些物体,我们需要在一帧中使用不同的渲染方式将其渲染两遍。我们知道对对象进行渲染是比较耗时的操作,当场景中需要选择的对象
多而杂的时候,采用这个机制是非常影响速度的。
另外在OpenGL红宝书中介绍了一种简便易行的办法:在后缓冲中使用不同的颜色重绘所有对象,每个对象用一个单色来标示其颜色,这样画好之后我们读取鼠
标所在点的颜色,就能够确定我们拣选了哪个物体。这种方法有一个缺陷,当场景中需要选择的对象的数目超出一定限度时,可能会出现标识数的溢出。对于这个问
题,红宝书给出的解决办法就是多次扫描。实践证明这种方法的确简便易行,但仍有不少局限性,而且做起来并不比第一种机制方便多少。限于篇幅,不再赘述。三、具体描述(Related Work) 看过了上面两种方法,我们会发现这两种方法都不是十分的方便,而且使用者不能对其进行完全的控制,不能精确地判定鼠标定位与实际的世界空间中三维坐标的关系。那么有什么更好的办法能够更简单更精确地对其加以控制呢? 实际上此处给出的解决方案十分简单,就是一个很普通也很有用的 GLU 函数 gluUnProject()。
此函数的具体用途是将一个OpenGL视区内的二维点转换为与其对应的场景中的三维坐标。
转换过程如下图所示(由点P在窗口中的XY坐标得到其在三维空间中的世界坐标):
这个函数在glu.h中的原型定义如下:int APIENTRY gluUnProject (
GLdouble winx,
GLdouble winy,
GLdouble winz,
const GLdouble modelMatrix[16],
const GLdouble projMatrix[16],
const GLint viewport[4],
GLdouble *objx,
GLdouble *objy,
GLdouble *objz); 其中前三个值表示窗口坐标,中间三个分别为模型视图矩阵(Model/View Matrix),投影矩阵(Projection Matrix)和视区(ViewPort),最后三个为输出的世界坐标值。 可能你会问:窗口坐标不是只有X轴和Y轴两个值么,怎么这里还有Z值?这就要从二维空间与三维空间的关系说起了。
众所周知,我们通过一个放置在三维世界中的摄像机,来观察当前场景中的对象。通过使用诸如gluPerspective()
这样的OpenGL函数,我们可以设置这个摄像机所能看到的视野的大小范围。这个视野的边界所围成的几何体是一个标准的平截头体(Frustum),可以
看做是金字塔状的几何体削去金字塔的上半部分后形成的一个台状物,如果还原成金字塔状,就得到了通常我们所说的视锥(View
Frustum)这个视锥的锥顶就是视点(View Point)也就是摄像机所在的位置。平截头体,视锥以及视点之间的关系,如下图所示:
在上面的图中,远裁剪面ABCD和近裁剪面A’B’C’D’构成了平截头体,加上虚线部分就是视锥,顶点O就是摄像机所在的视点。我们在窗口中所能看到的东东,全部都在此平截头体内。这跟前面的窗口坐标Z值有什么关系呢?看下图
如
此图所示,点P和点P’分别在远裁剪面ABCD和近裁剪面A’B’C’D’上。我们点击屏幕上的点P,反映到视锥中,就是选中了所有的从点P到点P’的
点。举个形象的例子,这就像是我们挽弓放箭,如果射出去的箭近乎笔直地飞出(假设力量非常之大近乎无穷),从挽弓的地点直至击中目标,在这条直线的轨迹上
任何物体都将被一穿而过。对应这里的情况,用户单击鼠标获得屏幕上的某一点,即是指定了从视点指向屏幕深处的某一方向,也就确定了屏幕上某条从O点出发的
射线(在图中即为OP)。在这里,我们称呼其为拣选射线。
因此,从窗口的XY坐标,我们仅仅只能获得一条出发自O点的拣选射线,并不能得到用户想要的点在这条射线上的确切位置。
这时候窗口坐标的Z值就能派上用场了。我们通过Z值,来指定我们想要的点在射线上的位置。假如用户点击了屏幕上的点(100,100)得到了这条射线OP,那么我们传入值1.0f就表示近裁剪面上的P点,而值0.0f则对应远裁剪面上的P’点。
这
样,我们通过引入一个窗口坐标的Z值,就能指定视锥内任意点的三维坐标。与此同时,我们还解决了前面红宝书给出的方法中存在的缺陷——同一位置上重叠物体
的选择问题。解决办法是:从屏幕坐标得到射线之后,分别让重叠的物体与该射线求交,得到的交点,然后根据这些与视点的远近确定选择的对象。如此我们就不必
受“仅仅只能选取屏幕中离观察者最近的物体”的限制了。这样一来,如果需要的话,我们甚至可以用代码来作一定的限定,通过判断交点与视点的距离,使得与该
拣选射线相交的物体中,离视点远的对象才能被选取,这样就能够对那些暂时被其他对象遮住的物体进行选取。
至于如何求拣选射线与对象的交点,在各种图形学的书中的数学部分均有讲述,在此不再赘述。
四、例程(Sample Code Fragment)
前面讲述了RIP方法,现在我们来看如何编写代码以实现之,以及一些需要注意的问题。
由于拣选射线以线段形式存储更加便于后面的计算,况且我们可以直接得到纵跨整个平截头体的线段(即前面图中的线段PP’),故我们直接计算出这条连接远近裁剪面的线段。我们将拣选射线的线段形式称之为拣选线段。
在下面的代码前方声明有两个类Point3f和LineSegment这分别表示由三个浮点数构成的三维空间中的点,以及由两个点构成的空间中的一条线段。
应注意代码中用到了类Point3f的一个需要三个浮点参数的构造函数,以及类LineSegment的一个需要两个点参数的构造函数。
获取拣选射线的例程如下所示(使用C++语言编写):class Point3f;
class LineSegment;
LineSegment GetSelectionRay(int mouse_x, int mouse_y) {
// 获取 Model-View、Projection 矩阵 & 获取Viewport视区
GLdouble modelview[16];
GLdouble projection[16];
GLint viewport[4];
glGetDoublev (GL_MODELVIEW_MATRIX, modelview);
glGetDoublev (GL_PROJECTION_MATRIX, projection);
glGetIntegerv (GL_VIEWPORT, viewport); GLdouble world_x, world_y, world_z; // 获取近裁剪面上的交点
gluUnProject( (GLdouble) mouse_x, (GLdouble) mouse_y, 0.0,
modelview, projection, viewport,
&world_x, &world_y, &world_z);
Point3f near_point(world_x, world_y, world_z); // 获取远裁剪面上的交点
gluUnProject( (GLdouble) mouse_x, (GLdouble) mouse_y, 1.0,
modelview, projection, viewport,
&world_x, &world_y, &world_z);
Point3f far_point(world_x, world_y, world_z); return LineSegment(near_point, far_point);
}
如果你是使用Win32平台进行开发,那么应当注意传入正确的参数。因为无论是使用Win32 API 还是DirectInput
来获取鼠标坐标,得到的Y值都应取反后再传入。因为OpenGL默认的原点在视区的左下角,Y轴从左下角指向左上角,而Windows默认的原点在窗口的
左上角,而Y轴方向与OpenGL相反,从左上角指向左下角。如下图所示:
我们可以看到代码被注释分为了三个部分:获取当前矩阵及视区,获取近裁剪面的交点,获取远裁剪面的交点。
我们通过OpenGL提供的查询函数轻松得到当前的ModelView和Projection矩阵,以及当前的Viewport(视区,也就是窗口的客户端区域,如果整个窗口区域用于OpenGL渲染的话)。
获得两个裁剪面上的交点的代码基本上是一样的,唯一的不同点是我们前面曾经详细地讨论过的窗口的Z坐标。不错,这个坐标表示的就是“深浅”的概念。它的值从点P’到点P的变化是从0.0f逐渐增至1.0f。此处类似于OpenGL的深度测试机制。
在得到两个交点之后,我们使用它们通过返回语句直接构建一条线段。在这里仅仅作为实例代码,故简捷清晰地直接返回线段对象,而没有通过引用参数来提高效率。
此
时用户可以使用这个函数来判断所选择的对象了。只需在需要的地方判断对象是否与此线段相交即可判断对象是否被选中,还可以通过进一步计算其交点位置来得到
详细的交点信息。这些计算均是常见的计算机图形学与三维数学计算,比如线段与三角形求交,线段与面求交,线段与球体求交,线段与柱体或锥体求交,等等。请
参考所列出的计算机图形学书籍。
五、结论(Conclusion)
在本文中,我们介绍了一种行之有效的三维坐标拾取方法,主要使用GLU库中的实用工具实现。这种方法速度快,效率高,能在不必重新绘制对象的前提下完成拣选工作。对比OpenGL自带的拣选机制来看,RIP的确在各种方面均有一定的优势。
六、参考文献(Reference) 【1】《OpenGL Programming Guide》
OpenGL ARB Mason Woo, Jackie Heider, Tom Davis, Dave Shreiner
【2】《OpenGL Reference Manual》
OpenGL ARB
【3】《Computer Graphics》
Donald Heam, M. Pauline Baker
【4】《Computer Graphics using OpenGL 2nd Edition》
F.S. Hill, JR.
posted on 2010-06-05 18:45
风轻云淡 阅读(3022)
评论(0) 编辑 收藏 引用 所属分类:
OpenGL