http://blog.sina.com.cn/liuzhongtu
本文源自于一篇英文《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 );