有时我们不仅仅是渲染场景,而且还要与渲染的场景进行交互。大多数情况下是使用鼠标进行交互。注:viewing volume(可视区域,视景体)
选择
OpenGL 的选择模式允许你通过鼠标点击屏幕,来选择鼠标下面的物体。使用 OpenGL 的选择特性,当你点击屏幕时就指定了一个可视区域,决定了哪些物体在这个可视区域中。基于你的屏幕坐标和你指定的像素大小,glu 库提供了一个有用的函数 gluPickMatrix 来产生一个矩阵,使用这个矩阵可以在你当前鼠标的位置产生更小的可视区域。然后你使用选择模式来测试这个可视区域,看哪些物体被包含在里面了。
在选择模式下,图像并不会被复制到帧缓冲区中(即帧缓冲区不会被修改)。反之,在可视区域中绘制的图元会在选择缓冲区中产生点击记录。这个缓冲区和其他的 OpenGL 缓冲区不同,它是个整型数组。
首先我们需要设置选择缓冲区,并为你的图元进行命名,这样才能在选择缓冲区中被标识。然后解析选择缓冲区得到哪些对象与可视区域相交。在可视区域外的物体将不会被绘制。为了挑选,我们会指定一个位于鼠标点下面一段小空间的可视区域,然后测试有哪些被命名的物体在这个区域内被绘制。
命名你的图元
图元的名称就像显示列表的名称一样是整型数组。图元的名称列表被存在名称栈中。在你初始化名称栈之后,你就可以往这个栈存放名称。你可以往栈顶压如新的名称,或者用当前的名称替换掉栈顶的名称。如果需要单个点击可以返回多个名称。命名图元的代码示例如下:
#define SUN 1
#define EARTH 2
#define MOON 3
void RenderSphere()
{
glPushMatrix();
glTranslatef(0.0f, 0.0f, -10.0f);
//初始化名称栈
glInitNames();
//往栈顶压栈,压如一个名称
glPushName(0);
glColor3f(1.0f, 0.0f, 0.0f);
//用当前名称SUN替换掉栈顶名称
glLoadName(SUN);
glutSolidSphere(1.0, 26, 26);
glRotatef(yRot, 0.0f, 1.0f, 0.0f);
glTranslatef(2.0f, 0.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);
//用EARTH替换掉栈顶名称
glLoadName(EARTH);
glutSolidSphere(0.3, 26, 26);
glTranslatef(1.0f, 0.0f, 0.0f);
glColor3f(0.25f, 0.25f, 0.75f);
//用当前名称MOON替换掉栈顶名称
glLoadName(MOON);
glutSolidSphere(0.1, 26, 26);
glPopMatrix();
}
注意只有在选择模式下glInitNames,glLoadName,glPushName才有效,
在GL_RENDER正常渲染模式下这些函数调用将被忽略
使用选择模式
OpenGL 有三种不同的渲染模式,默认的 GL_RENDER 模式,还有 GL_SELECTION 模式和 GL_FEEDBACK 模式。在使用选择模式之前,我们需要切换到选择模式。调用如下:
glRenderMode(GL_SELECTION);
在选择模式渲染完物体之后,调用 glRenderMode (GL_RENDER) 返回点击记录。注意当调用 glRenderMode (GL_RENDER) 时只有在之前的模式选择模式 GL_SELECTI 和反馈模式下才会有返回值。返回值是点击记录,即在当前可视区域内被命名的物体的个数。
在使用选择模式 glRenderMode (GL_SELECTION); 之前,要设置选择缓冲区:
void glSelectBuffer( GLsizei size, GLuint *buffer);
size 为缓冲区的大小,buffer 为缓冲区的指针。在进入选择模式时,OpenGl 会将一个指针初始化指向这个选择缓冲区,当有点击记录产生时,就往这个缓冲区中写记录。如果在填充这个选择缓冲区时溢出了,那么 OpenGL 并设置一个溢出标志。
选择缓冲区
在选择模式下,渲染的过程中选择缓冲区由点击记录填充。点击记录由在可视区域内有多少个被命名的物体来产生的。在默认情况下,即可视区域为整个窗口。
选择缓冲区是一组无符号的整型数组。每一个点击记录至少产生 4 个元素。这四个元素分别是名称栈中名称的个数,最小 z 值,最大 z 值,栈底的名称 1, 如果有更多继续往下添加。如下图:
挑选
许多情况下我们想用鼠标去挑选某个物体。要使用选择模式实现这个功能,首先是在鼠标点击附近的范围内创建一个裁剪区域(可视区域),然后测试有哪些物体在这个可视区域内。GLU 库中提供了一个函数 gluPickMatrix,我们可以用这个函数创建一个用于描述新的可视区域的矩阵,然后乘以当前的投影矩阵,就可以得到我们想要的可视区域。
void gluPickMatrix(GLdouble x, GLdouble y, GLdouble width, GLdouble height, GLint viewport[4]);
x,y 为窗口的坐标定义了可视区域的中心,我们可以设置为鼠标点击的位置。width 和 height 指定了可视区域的宽高(以窗口中的像素为单位)。viewport 是视口。我们可以通过 glGetIntegerv (GL_VIEWPORT, viewport); 来获得。
gluPickMatrix 的效果是把裁剪区域变换为单位立方体 - 1<= (x,y,z) <=1 (或 - w<=(wx,wy,wz)<=w, 挑选矩阵有效的执行一次正交变换,把裁剪区域映射到单位立方体上。
由于 OpenGL 的坐标原点在窗口的左下角,而 Windows 的坐标原点在窗口的左上角。
所以我们调用时,要注意把 Y 轴的值反转一下。如下:
gluPickMatrix(xPos, viewport[3] – yPos + viewport[1], 2,2, viewport);
我们可以用 glut 库提供的函数,设置回调函数来处理鼠标点击事件。
glutMouseFunc(MouseCallBack);
…
void MouseCallBack(int key, int state, int x, int y)
{
if (key == GLUT_LEFT_BUTTON && state == GLUT_DOWN)
{
ProcessSelection(x, y);
}
}
总体的步骤:
-
调用 glSelectBuffer 设置选择缓冲区
-
glRenderMode (GL_SELECTION) 切换到选择模式。
-
使用 glInitNames 和 glPushName 初始化名称栈
-
定义用于选择的可视区域
-
为每个图元命名,并绘制图元。
-
调用 glRenderMode (GL_RENDER);返回点击记录
因为只有在 GL_RENDER 模式下 glInitNames,glPushName,glLoadName 将被忽略,所以我们可以把这些函数和绘制图元的函数写在一个函数调用中。
示例代码片段:
void RenderSphere()
{
glPushMatrix();
glTranslatef(0.0f, 0.0f, -10.0f);
//初始化名称栈
glInitNames();
//往栈顶压栈,压如一个名称
glPushName(0);
glColor3f(1.0f, 0.0f, 0.0f);
//用当前名称SUN替换掉栈顶名称
glLoadName(SUN);
glutSolidSphere(1.0, 26, 26);
glRotatef(yRot, 0.0f, 1.0f, 0.0f);
glTranslatef(2.0f, 0.0f, 0.0f);
glColor3f(0.0f, 0.0f, 1.0f);
//用EARTH替换掉栈顶名称
glLoadName(EARTH);
//glPushName(EARTH);
glutSolidSphere(0.3, 26, 26);
glTranslatef(1.0f, 0.0f, 0.0f);
glColor3f(0.25f, 0.25f, 0.75f);
//用当前名称MOON替换掉栈顶名称
glLoadName(MOON);
//glPushName(MOON);
glutSolidSphere(0.1, 26, 26);
glPopMatrix();
}
void RenderScene()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
RenderSphere();
glutSwapBuffers();
}
void ChangeSize(GLsizei w, GLsizei h)
{
if(h == 0)
h = 1;
glViewport(0, 0, w, h);
GLfloat fAspect = (GLfloat)w / (GLfloat)h;
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
gluPerspective(35.0f, fAspect, 1.0f, 50.0f);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
}
void TimerFunc(int value)
{
yRot += 0.5;
if (yRot > 360.0f)
{
yRot = 0.0f;
}
glutPostRedisplay();
glutTimerFunc(50, TimerFunc, 1);
}
//处理点击记录
void ProcessHit(int hits, GLuint *buf)
{
for (int i = 1; i <= hits; ++i)
{
GLuint nameNum = *buf;
printf("hit number %d \n", i);
printf("name stack count is %d\n", *buf); buf++;
printf("min z value is %g\n", (float)*buf/0x7FFFFFFF); buf++;
printf("max z value is %g\n", (float)*buf/0x7FFFFFFF); buf++;
printf("name value is : ");
for (int j = 0; j < nameNum; ++j)
{
switch(*buf)
{
case SUN:
printf("SUN \t");
break;
case EARTH:
printf("EARTH \t");
break;
case MOON:
printf("MOON \t");
break;
default:
break;
}
buf++;
}
printf("\n");
}
}
void ProcessSelection(int x, int y)
{
GLint viewport[4], hits;
static GLuint selectBuffer[BUFFER_LENGTH];
//设置选择缓冲区
glSelectBuffer(BUFFER_LENGTH, selectBuffer);
//切换到投影矩阵,我们需要创建 可视区域
glMatrixMode(GL_PROJECTION);
//保留原先的 投影矩阵,以便恢复
glPushMatrix();
glLoadIdentity();
//获得视口
glGetIntegerv(GL_VIEWPORT, viewport);
//切换到选择模式
glRenderMode(GL_SELECT);
GLfloat aspect = (GLfloat)viewport[2]/(GLfloat)viewport[3];
//创建一个描述可视区域的矩阵
gluPickMatrix(x, viewport[3]-y+viewport[1], 2, 2, viewport);
//与投影矩阵相乘,得到可视区域
gluPerspective(35.0, aspect, 1.0, 200.0);
//在选择模式下 渲染图元
RenderSphere();
//返回点击记录数。
hits = glRenderMode(GL_RENDER);
ProcessHit(hits, selectBuffer);
glMatrixMode(GL_PROJECTION);
glPopMatrix();
glMatrixMode(GL_MODELVIEW);
}
当我只选择太阳时:
只有一个点击记录,一个物体在选择的可视区域内输出如下:
当太阳和地球重叠,然后我点击地球时:
有两个点击记录 输出如下: