轻轻松松做OpenGL鼠标拾取

http://blog.sina.com.cn/liuzhongtu

 

http://www.cnblogs.com/Yuri/

 

本文源自于一篇英文《OpenGL Picking Made Easy》,作者不详。

概述

在科学和工程的3维可视化应用当中,用户在屏幕上点击就可以让应用程序知道用户点击的是什么对象。我们将这一过程,称之为拾取。

想象一下,这一过程在软件中由自己编程来实现,将如何进行的。你得为此做各种变换,并找出各个对象最终在屏幕上的位置,然后判断究竟哪一下与鼠标最近。当然这样做是可行的,但没有人会真正去做这个事,因为过程实在是太繁琐了。

幸运的是,OpenGL让硬件提供了对拾取的支持。基本思路如下:

1. 让硬件处于拾取(或“选择”)模式;

2. 让硬件在拾取模式下,重新绘制场景,只不过不加入颜色;

3. 绘制场景时,为需要做拾取的对象命名;

4. 根据鼠标位置加上一定的误差范围,硬件返回选择的结果;

5. 让硬件重新回到绘图(“渲染”)模式。

设置

首先是进行设置,从一组#define和变量声明开始。PICK_TOL来定义拾取误差(按像素计),PICK_BUFFER_SIZE是拾取对象命名数组PickBuffer的大小。RenderMode记录绘图的模式。

?/P>

#define PICK_TOL 10.

?/P>

#define PICK_BUFFER_SIZE 256

unsigned int PickBuffer[PICK_BUFFER_SIZE];

int RenderMode;

InitGraphics()

在这个函数中,告诉硬件使用哪个数组作拾取名字数组,这个数组多大。它必须在创建窗口后完成。

?/P>

glutInitWindowSize( INIT_WINDOW_SIZE, INIT_WINDOW_SIZE );

glutInitWindowPosition( WIN_LEFT, WIN_BOTTOM );

GrWindow = glutCreateWindow( WINDOWTITLE );

glutSetWindowTitle( WINDOWTITLE );

. . .

?/P>

glSelectBuffer( PICK_BUFFER_SIZE, PickBuffer );

拾取对象命名

为需要做拾取的对象进行命名,名字实际上是32位无符号整数。

glLoadName( 0 );

glutWireSphere( 1.0, 15, 15 );

glLoadName( 1 );

glutWireCube( 1.5 );

glLoadName( 2 );

glutWireCone( 1.0, 1.5, 20, 20 );

glLoadName( 3 );

glutWireTorus( 0.5, 0.75, 20, 20 );

有一点,需要注意,函数glLoadName()不可置于glBegin()与glEnd()之间。下面的代码就是犯了这种错误:

glBegin( GL_TRIANGLES );

for( i = 0; i < NTRIS; i++ )

{

glLoadName( i );

glVertex3f( Tris[i].x0, Tris[i],y0, Tris[i].z0 );

glVertex3f( Tris[i].x1, Tris[i],y1, Tris[i].z1 );

glVertex3f( Tris[i].x2, Tris[i],y2, Tris[i].z2 );

}

glEnd();

应改作:

for( i = 0; i < NTRIS; i++ )

{

glLoadName( i );

glBegin( GL_TRIANGLES );

glVertex3f( Tris[i].x0, Tris[i],y0, Tris[i].z0 );

glVertex3f( Tris[i].x1, Tris[i],y1, Tris[i].z1 );

glVertex3f( Tris[i].x2, Tris[i],y2, Tris[i].z2 );

glEnd();

}

拾取名字的构成可以不是线性的,可形成层次结构的多重名字。因为拾取名字实际上是存放在栈中,多重名字结构可用如下形式生成:

glLoadName( JAGUAR );

glPushName( BODY );

glCallList( JagBodyList );

glPopName();

glPushName( FRONT_LEFT_TIRE );

glPushMatrix();

glTranslatef( ??, ??, ?? );

glCallList( TireList );

glPopMatrix();

glPopName();

glPushName( FRONT_RIGHT_TIRE );

glPushMatrix();

glTranslatef( ??, ??, ?? );

glCallList( TireList );

glPopMatrix();

glPopName();

……

glLoadName( YUGO );

glPushName( BODY );

glCallList( YugoBodyList );

……

拾取上面的对象将会返回两个名字,告诉你拾取了哪辆车,以及该辆车的哪个部件。

如果要进行绘图,需要将绘图模式从拾取模式转成渲染模式。因此在函数Reset()中:

RenderMode = GL_RENDER;

一量鼠标按钮触发了拾取,则进行如下一系列动作:

1. 设置拾取模式(GL_SELECT);

2. 调用Display()函数进行重绘;

3. 设置回渲染模式(GL_RENDER);

4. 检视拾取名字数组。

MouseButton

该函数执行前述的过程。

if( ( ActiveButton & LEFT ) && ( status == GLUT_DOWN ) )

{

RenderMode = GL_SELECT;

glRenderMode( GL_SELECT );

Display();

RenderMode = GL_RENDER;

Nhits = glRenderMode( GL_RENDER );

if( Debug )

fprintf( stderr, "# pick hits = %d/n", Nhits );

for( i = 0, index = 0; i < Nhits; i++ )

{

nitems = PickBuffer[index++];

zmin = PickBuffer[index++];

zmax = PickBuffer[index++];

if( Debug )

{

fprintf( stderr,

"Hit # %2d: found %2d items on the name stack/n",

i, nitems );

fprintf( stderr, "/tZmin = 0x%0x, Zmax = 0x%0x/n",

zmin, zmax );

}

for( j = 0; j < nitems; j++ )

{

item = PickBuffer[index++];

<< item is one of your pick names >>

<< do something with it >>

if( Debug )

fprintf( stderr, "/t%2d: %6d/n", j, item );

}

}

ActiveButton &= ~LEFT;

glutSetWindow( GrWindow );

glutPostRedisplay();

}

if( Nhits == 0 )

{

 

 

. . .

}

拾取发生生,拾取数组有什么变化?

拾取数组以下图形式组织:

zmin和zmax为internal无符号整数,表示哪个对象最近。zmin或zmax的较小值对应的对象要近于较大值对应的对象。

Display()

在Display()函数中,需要根据拾取和渲染的需要来要调整:

int viewport[4];

" " "

dx = glutGet( GLUT_WINDOW_WIDTH );

dy = glutGet( GLUT_WINDOW_HEIGHT );

" " "

glMatrixMode( GL_PROJECTION );

glLoadIdentity();

if( RenderMode == GL_SELECT )

{

viewport[0] = xl;

viewport[1] = yb;

viewport[2] = d;

viewport[3] = d;

gluPickMatrix( (double)Xmouse, (double)(dy - Ymouse),

PICK_TOL, PICK_TOL, viewport );

}

<< the call to glOrtho(), glFrustum(), or gluPerspective() goes here >>

" " "

 

if( RenderMode == GL_SELECT )

{

glInitNames();

glPushName( 0xffffffff );

}

 

<< your graphics drawing and pick name calls go here >>

 

if( AxesOnOff == ON && RenderMode == GL_RENDER )

glCallList( AxesList );

 

if( RenderMode == GL_RENDER )

glutSwapBuffers();

这一段唯一需要额外注意的地方是蓝色字体的部分。它检视拾取的视区大小和位置,拾取误差,调整投影矩阵使拾取盒所在区域占据整个窗口(原文如此,费解)。硬件接下来判断是否有对象穿过拾取盒空间(利用clip算法),如果有则拾取成功。

注意:gluPickMatrix()一行在程序中所有变换之前,这是因为我们希望它所定义的变换在所有变换完成之后进行。

拾取技巧

1. 不要将拾取时间浪费在你不会做拾取的对象上,例如

if( AxesOnOff == ON && RenderMode == GL_RENDER )

glCallList( AxesList );

因为轴不会用于拾取,就不必要在拾取模式下绘制。

2. 不要对光栅化字符作拾取,不会起作用的。因此在绘制场景时,建议这样写:

if( RenderMode == GL_RENDER )

{

glDisable( GL_DEPTH_TEST );

glMatrixMode( GL_PROJECTION );

glLoadIdentity();

gluOrtho2D( 0., 100., 0., 100. );

glMatrixMode( GL_MODELVIEW );

glLoadIdentity();

glColor3f( 1., 1., 1. );

sprintf( str, " Nhots = %d", Nhits );

DoRasterString( 1., 1., 0., str );

}

3. 你在拾取模式下绘制的场景用户根本看不到,你可以按你的拾取需要绘制而不是按用户看见的内容绘制。

比如说,一般情况下,线框对象线与线之间的空白区域是拾取不到对象的。但对象如果用实体方式来画,就可以在相同区域拾取到该对象。因此,你可以在渲染模式下来用线框方式绘制对象,在拾取模式下绘制用实体方式。

比如,

if( RenderMode == GL_SELECT )

glCallList( SolidTorusList );

else

glCallList( WireTorusList );

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值