OpenGL 鼠标拾取漫谈

OpenGL 有多种方法可以实现鼠标拾取物体,其中的两种方法比较常用。第一种通常被认为是标准的选择方法,它跟踪被选中像素或包括其周围的一小片区域的所有对象,接下来具体介绍这种方法。第二种是选取一个颜色集,用互不相同的颜色绘制可以被选中的对象,通过检查选中点的颜色缓存的颜色来识别出选择对象。

对象

我们抽象地认为在 glBegin(…)glEnd() 之间绘制的图形为一个对象,它存在于世界坐标系中。通过投影和变换将其转化为图像绘制在屏幕上后,如何通过鼠标选中的屏幕上的像素点来选中一个存在于三维世界坐标系的物体呢,OpenGL 提出了一种强大的解决方案供我们使用。

名称

首先我们区分出可以被选中的物体,将它赋予一个名称(name),在绘制过程中,如果有名称的对象覆盖了选中的像素点,则将名称保存到一个选择缓存中。有趣的是,我们还可以使用层次结构的名称来命名一个物体,对于一辆汽车,我们可以说选中了一个车门,或者选中了一辆汽车,这为我们调整选中的精细度提供了可能。

虽然说是一个 name,但实际上是一个无符号整形(GLuint),用枚举来表示的话确实和名称差不多,嘛~

名称栈

OpenGL 使用栈来保存名称,称其为名称栈(name stack),当绘制到选中的像素点时,则将栈中的全部信息都保存到选择缓存内,理解这个操作对理解拾取的实现是至关重要的。上面所说的层次结构就是使用压栈、出栈的方法实现的。

选择缓存

选择缓存是一个 GLuint 类型的数组,由用户创建,通过 glSelectBuffer(GLsizei size, GLuint *buffer) 函数完成缓存设置。设置完毕后,API 在绘制时将会自动完成名称的记录工作,选择缓存的结构大致如下:

选择缓存

当对象绘制过程中覆盖到选中点时,名称栈中的信息将自动添加到选择缓存中,名称表的长度由名称数确定,若多于一个名称的,按名称添加的层次顺序排序,即先添加(入栈)的名称排在前;zminzmax 记录该对象在深度缓冲中的高度,离视点越远数值越大。

选择缓存中的每一个对象的相关信息可由 [3+N] 个无符号整型值表示,当我们不使用层次结构的名称来命名时,可以认为第 i 个对象的名称为 buf[i][3]。

绘制

OpenGL 使用 glRenderMode(mode) 设置绘制模式/光栅化模式,它的返回值由上一次调用的 mode 参数确定,不同的模式下进行绘图操作会有不同的行为。当设置为 GL_RENDER (默认模式)时,绘制的物体将会被显示出来,而设置为 GL_SELECT 时,帧缓冲(Frame buffer)不会更新,重绘的内容相当于不可见的,若先前设置好了选择缓存,切换到 GL_RENDER 模式时返回值即选择缓存中的对象数。

通常情况下,在选择模式中我们重新设置透视矩阵和模视矩阵,在设置透视之前调用 glPickMatrix(…) 设置拾取矩阵完成裁剪,这样可以将绘图的工作量减小;进一步考虑,可以使用一些简单的几何对象来近似表示模型,甚至可以让用户选择一些看不到的物体(隐藏道具?);对于 OpenGL 渲染的不可选择的 GUI 文本对象,也可以使用类似的思路使其表现为可选的。

交互

得到所选对象的名称后就可以在绘制时操纵对象。我见到过一个很炫的用例,它绘制一个贝塞尔曲面,用户可以通过鼠标选中并移动锚点,随着锚点移动,曲面也随着变化。

代码

移除掉无关代码后,一个简单示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
#include <stdio.h>
#include <math.h>
#include <GLUT/GLUT.h>

#define N_ROWS 10
#define N_COLS 10
#define CUBE_SIZE .5
#define BUFF_SIZE 100
#define PICK_TOL 10

GLuint hit;
GLuint selectBuffer[BUFF_SIZE];
GLint viewport[4];

void display(void);
void render(GLenum mode);
GLuint doSelect(GLint x, GLint y);

int main(int argc, const char * argv[]) {


glutMainLoop();
return 0;
}

void init(void) {


hit = -1;
}

void display(void) {



render(GL_RENDER);
glutSwapBuffers();
}

void animate(void) {

glutPostRedisplay();
}

void render(GLenum mode) {
int i, j;
int name = 0;



for (i = 0; i < N_ROWS; i++) {
for (j = 0; j < N_COLS; j++) {
if (mode == GL_SELECT) {
glLoadName(name++);
}
glPushMatrix();
{
glTranslatef(2*i*CUBE_SIZE, 0.0, 2*j*CUBE_SIZE);
if (hit == i*N_ROWS + j%N_COLS) {
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, redColor);
glutSolidCube(CUBE_SIZE);
glMaterialfv(GL_FRONT_AND_BACK, GL_AMBIENT_AND_DIFFUSE, greenColor);
} else {
glutSolidCube(CUBE_SIZE);
}
glPopName();
}
glPopMatrix();
}
}
glPopMatrix();
}

GLuint doSelect(GLint x, GLint y) {
int i;
GLint dx, dy;
GLint hits, tempHit;
GLuint zVal;

dx = glutGet(GLUT_WINDOW_WIDTH);
dy = glutGet(GLUT_WINDOW_HEIGHT);

glSelectBuffer(BUFF_SIZE, selectBuffer);
glRenderMode(GL_SELECT);
glInitNames();
glPushName(0);

// Set up view model
glPushMatrix();
{
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPickMatrix(x, dy - y, PICK_TOL, PICK_TOL, viewport);
gluPerspective(60.0, (GLfloat)windowW/(GLfloat)windowH, 1.0, 30.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// eye point center of view up
gluLookAt(camX, camY, camZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

render(GL_SELECT);

}
glPopMatrix();

hits = glRenderMode(GL_RENDER);

glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(60.0, (GLfloat)windowW/(GLfloat)windowH, 1.0, 30.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
// eye point center of view up
gluLookAt(camX, camY, camZ, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);

if (hits <= 0) {
return -1;
} else {
zVal = selectBuffer[1];
tempHit = selectBuffer[3];
for (i = 1; i < hits; i++) {
if (selectBuffer[4*i+1] < zVal) {
zVal = selectBuffer[4*i+1];
tempHit = selectBuffer[4*i+3];
}
}
}

return tempHit;
}

void mouse(int button, int state, int x, int y) {

if (state == GLUT_DOWN) {
hit = doSelect((GLint) x, (GLint)y);
}


glutPostRedisplay();
}

可以看到,在鼠标点击事件中调用了 doSelect 方法:切换到选择模式后重画视图,接着切换回来,得到选中的名称并返回。在绘制函数中对选中对象设置不同的材质便得到了下面的效果。

Select Demo

通过改变绘制的分支语句,也可以实现多选或选择一行的功能。在命名时如果仔细小心,也可以实现分层选择的功能。

一个参考资料: