Picking Tutorial
拾取教程
The Name Stack
名字栈
The OpenGL API provides a mechanism for picking objects in a 3D scene. This tutorial will show you how to detect which objects are bellow the mouse or in a square region of the OpenGL window. The steps involved in detecting which objects are at the location where the mouse was clicked are:
OpenGL API提供一种机制在3D场景中拾取对象.这篇教程将会向你展示在OpenGL窗口中如何来检测一个对象在鼠标下方或是在一个矩形区域内.检测哪一个对象在鼠标点击区域包含以下步骤:
- 1. Get the window coordinates of the mouse
- 1. 得到鼠标的窗口坐标
- 2. Enter selection mode
- 2. 进入选择模式
- 3. Redefine the viewing volume so that only a small area of the window around the cursor is rendered
- 3. 重新定义视口的大小,这样只有窗口中围绕光标的一小块区域被绘制
- 4. Render the scene, either using all primitives or only those relevant to the picking operation
- 4. 重绘场景,要么使用所有基本元素或是只是那些和选择操作有关的元素
- 5. Exit selection mode and identify the objects which were rendered on that small part of the screen.
- 5. 退出选择模式,标明被绘制在屏幕一小区域内的对象.
In order to identify the rendered objects using the OpenGL API you must name all relevant objects in your scene. The OpenGL API allows you to give names to primitives, or sets of primitives (objects). When in selection mode, a special rendering mode provided by the OpenGL API, no objects are actually rendered in the framebuffer. Instead the names of the objects (plus depth information) are collected in an array. For unnamed objects, only depth information is collected.
为了使用OpenGL API来标明被绘制的对象,你必须将你场景中的所有相关对象命名.OpenGL API允许你命名一些基本元或基本单元的集合(对象).当处于选择模式时,OpenGL API提供一个特别的绘制模式,实际上没有对象被绘制在帧缓冲区内,相反,所有对象的名字(以及尝试信息)被收集在一个数组中.对于没有命名的对象,只有收集深度信息.
Using the OpenGL terminology, a hit occurs whenever a primitive is rendered in selection mode. Hit records are stored in the selection buffer. Upon exiting the selection mode OpenGL returns the selection buffer with the set of hit records. Since OpenGL provides depth information for each hit the application can then easily detect which object is closer to the user.
使用OpenGL术语, 当一个基本单元在选择模式下被绘制时点击事件就发生了.点击记录被存储在选择缓冲区内.一旦退出选择模式,OpenGL返回选择缓冲区以及点击记录集.因为OpenGL为每次点击提供深度信息,应用程序能够轻松的探测到哪个对象离用户更近.
Introducing the Name Stack
名字栈的介绍
As the title suggests, the names you assign to objects are stored in a stack. Actually you don't give names to objects, as in a text string. Instead you number objects. Nevertheless, since in OpenGL the term name is used, the tutorial will also use the term name instead of number.
如标题提示的那样,你赋于对象的名字被存储在一个栈中.事实上你并没有以字符串的形式给对象一个名字,相反你对它编号.不管怎样,因为在OpenGL中使用名字这个名字,本教程也使用名字而不是编号.
When an object is rendered, if it intersects the new viewing volume, a hit record is created. The hit record contains the names currently on the name stack plus the minimum and maximum depth for the object. Note that a hit record is created even if the name stack is empty, in which case it only contains depth information. If more objects are rendered before the name stack is altered or the application leaves the selection mode, then the depth values stored on the hit record are altered accordingly.
当一个对象被绘制时,如果它与新的视口区域相交,那么一次命中记录就产生了.命中记录包含在名字栈中的当前名字以及最小最大深度值.
A hit record is stored on the selection buffer only when the current contents of the name stack are altered or when the application leaves the selection mode .The rendering function for the selection mode therefore is responsible for the contents of the name stack as well as the rendering of primitives.
只有当命名栈被改变或是离开选择模式时,一个命中记录才被存储在选择缓冲区.因此选择模式下的绘制函数负责命名栈的内容就像绘制基本元一样.
OpenGL provides the following functions to manipulate the Name Stack:
OpenGL提供如下的函数下操纵名字栈:
void glInitNames(void);
This function creates an empty name stack. You are required to call this function to initialize the stack prior to pushing names.
这个函数创建一个空的名字栈.在向名字栈压入名字之前你必须调用这个函数来初始化这个栈.
void glPushName(GLuint name);
Adds name to the top of the stack. The stacks maximum dimension is implementation dependent, however according to the specs it must contain at least 64 names which should prove to be more than enough for the vast majority of applications. Nevertheless if you want to be sure you may query the state variable GL_NAME_STACK_DEPTH (use glGetIntegerv(GL_NAME_STACK_DEPTH)). Pushing values onto the stack beyond its capacity causes an overflow error GL_STACK_OVERFLOW.
将名字加入到栈顶.栈的最大容量依赖于实现,然而根据说明,它必须至少包含64个名字,它们应该足够大量的应用程序.不管怎样,如果你需要确认,你可以查询变量GL_NAME_STACK_DEPTH的状态(使用 glGetIntegerv(GL_NAME_STACK_DEPTH)).向名字栈压入值时,如果超出它的容量,会导致一个溢出错误GL_STACK_OVERFLOW.
void glPopName();
Removes the name from top of the stack. Popping a value from an empty stack causes an underflow, error GL_STACK_UNDERFLOW.
从名字栈的顶部移除名字.从一个空栈弹出一个值将导致一个下溢错误GL_STACK_UNDERFLOW.
void glLoadName(GLunit name);
This function replaces the top of the stack with name. It is the same as calling
这个函数替换名字栈顶部的名字.它和下面的调用等价:
glPopName();
glPushName(name);
This function is basically a short cut for the above snippet of code. Loading a name on an empty stack causes the error GL_INVALID_OPERATION.
这个函数基本上是下面代码片断的简写.从一个空栈取名字将导致GL_INVALID_OPERATION错误.
Note: Calls to the above functions are ignored when not in selection mode. This means that you may have a single rendering function with all the name stack functions inside it. When in the normal rendering mode the functions are ignored and when in selection mode the hit records will be collected.
注意:如果不在选择模式下,以下函数的调用将被忽略.这意味着你可以有一个单独的绘制函数,它里面有所有名字栈操作函数.当在普通绘制模式时,这些函数被忽略,当在选择模式时,命中记录将被收集.
Note: You can't place these functions inside a glBegin glEnd construction, which is kind of annoying since that will require a new rendering function for the selection mode in some cases. For instance if you have a set of points inside a glBegin glEnd and you want to name them in such a way that you can tell them apart, then you must create one glBegin glEnd block for each name.
注意:你不能在glBegin glEnd结构之间放置这些函数.这令人很讨厌同,因为在某些情况下,需要为选择模式准备一个新的绘制函数.例如,你有一系列的点在一个glBegin glEnd结构中,并且你想要命名它们,这样你可以区分它们,然后你必须为每个名字创建一个glBegin glEnd块.
Rendering for the Selection Mode
选择模式的绘制
An example of a rendering function is now presented.
下面展示一个绘制函数的例子:
#define BODY 1
#define HEAD 2
...
void renderInSelectionMode() {
1 glInitNames();
2 glPushName(BODY);
3 drawBody();
4 glPopName();
5 glPushName(HEAD);
6 drawHead();
7 drawEyes();
8 glPopName();
9 drawGround();
}
OK, lets go line by line.
好了,让我们一行一行的看:
- 1 glInitNames(); - This function creates an empty stack. This is required before any other operation on the stack such as Load, Push or Pop.
- 1 glInitNames(); - 这个函数创建一个空的名字栈.它必须在其它任何栈的操作之前,如压入,弹出等.
- 2 glPushName(BODY); - A name is pushed onto the stack. The stack now contains a single name.
- 2 glPushName(BODY); - 将一人名字压入栈.现在栈包含一个名字.
- 3 drawBody(); - A function which calls OpenGL primitives to draw something. If any of the primitives called in here intersects the viewing volume a hit record is created. The contents of the hit record will be the name currently on the name stack, BODY, plus the minimum and maximum depth values for those primitives that intersect the viewing volume
- 3 drawBody(); - 这个函数调用OpenGL基本绘制函数来画一些东西.如果在这里调用的任何元素与视口区域相交,那么一个命中记录就被创建了.命中记录的内容将是当前栈顶的名字,BODY,加上与视口相交的那些元素的最小最大深度值
- 4 glPopName(); - Removes the name of the top of the stack. Since the stack had a single item, it will now be empty. The name stack has been altered so if a hit record was created in 2 it will be saved in the selection buffer
- 4 glPopName(); -移除栈顶的名字.因为栈本来有一项,现在将会是空.名字栈已经被修改,所以一个命中记录被创建,它将会保存在选择缓冲区.
- 5 glPushName(HEAD); - A name is pushed onto the stack. The stack now contains a single name again. The stack has been altered, but there is no hit record so nothing goes into the selection buffer
- 5 glPushName(HEAD); - 一个名字被压入栈.栈现在再次只包含一个名字.栈被修改,但是现在没有命中记录,所以没有记录进入选择缓冲区.
- 6 drawHead(); - Another function that renders OpenGL primitives. Again if any of the primitives intersects the viewing volume a hit record is created
- 6 drawHead(); - 另一个函数来绘制OpenGL基本图元. 同样,如果有任何图元与视口区域相交,一个命中记录被会被创建.
- 7 drawEyes(); - Yet another function which renders OpenGL primitives. If any of the primitives in here intersects the viewing volume and a hit record already exists from 6, the hit record is updated accordingly. The names currently in the hit record are kept, but if any of the primitives in drawEyes() has a smaller minimum, or a larger maximum depth, then these new values are stored in the hit record. If the primitives in drawEyes() do intersect the viewing volume but there was no hit record from drawHead then a new one is created.
- 7 drawEyes(); - 同样,另一个函数来绘制OpenGL基本图元.如果有任何图元与视口区域相交,并且一个命中记录已经在6中存在,那么这个命中记录会相应的更新.在命中记录中的名字将会保留,但是如果在drawEyes()中有一个更小或是更大的深度,那么这些新的值就被存储在命中记录中.如果drawEyes()中的基本图元确实与视口区域相交,但是在drawHead()中没有产生命中记录,那么一个新的命中记录被会被创建.
- 8 glPopName(); - The name on the top of the stack is removed. Since the stack had a single name the stack is now empty. Again the stack has been altered so if a hit record was created it will be stored in the selection buffer
- 8 glPopName(); - 移除栈顶的名字.因为栈本来只有一个名字,现在则为空.栈再次被修改,所以如果曾有一个命中记录被创建,那么它将会被存储在选择缓冲区.
- 9 drawGround(); - If any if the primitives called in here intersects the viewing volume a hit record is created. The stack is empty, so no names will be stored in the hit record, only depth information. If no alteration to the name stack occurs after this point, then the hit record created will only be stored in the selection buffer when the application leaves the selection mode. This will be covered later on the tutorial.
- 9 drawGround(); - 如果在这里调用的基本绘制图元与视口区域相交,那么一个命中记录就会创建.栈是空的,所以没有名字被存储在命中记录中,只有深度信息.如果在此处之后没有修改名字栈,那么只有当应用程序退出选择模式时此命中记录才被存储在选择缓冲区中.这将在教程后面讲到.
Note: lines 4 and 5 could have been replaced by glLoadName(HEAD);
注意:第4,5行可以用glLoadName(HEAD)替代;
Note that you can push a dummy name (some unused value) right at the start, and afterwards just use glLoadName, instead of Pushing and Popping names. However it is faster to disregard objects that don't have a name than to check if the name is the dummy name. On the other hand it is probably faster to call glLoadName than it is to Pop followed by Push.
注意:你可以在一开始压入一个假的名字(某些无用的值),并且后来仅使用glLoadName而不是Pop和Push.然而忽略掉没有名字的对象比检查某个名字是否是假名字更快.另一方面,调用glLoadName可能比先Pop再Push要快些.
Using multiple names for an object
一个对象多个名字
There is no rule that says that an object must have a single name. You can give multiple names to an object. Suppose you have a number of snowmen disposed on a grid. Instead of naming them as 1,2,3,... you could name each of them with the row and column where they are placed. In this case each snowman will have two names describing its position: the row and column of the grid.
没有规定说一个对象只能有一个名字.你可以对一个对象给多个名字.假设你有一些表演者安排在一个格子里,与其用1,2,3...来命名他们,你可以用他们每个人在格子中所处的行和列来命名它们.在这种情况下,每个表演者有两个名字来描述他们的位置:格子的行和列.
The following two functions show the two different approaches. First using a single name for each snowman.
下面的两个函数展示了两种不同的方法.第一个为每个表演者使用一个名字.
for(int i = -3; i < 3; i++)
for(int j = -3; j < 3; j++) {
glPushMatrix();
glPushName(i*6+j);
glTranslatef(i*10.0,0,j * 10.0);
glCallList(snowManDL);
glPopName();
glPopMatrix();
}
The following function gives two names to each snowman. In this latter function if a hit occurs the hit record will have two names.
接下来的函数为每个表演者给两个名字.在此函数后,如果一个命中发生,此命中记录将会有两个名字.
for(int i = -3; i < 3; i++) {
glPushName(i);
for(int j = -3; j < 3; j++) {
glPushMatrix();
glPushName(j);
glTranslatef(i*10.0,0,j * 10.0);
glCallList(snowManDL);
glPopName();
glPopMatrix();
}
glPopName();
}
Hierarchical Naming
分层命名
This is a natural extension to multiple naming. It may be of interest to find out where in a particular object you have clicked. For instance you may want to know not only in which snowman you clicked but also if you clicked on the head or the body. The following function provides this information.
这个多命名的自然扩展.找出你点击过的一个特别对象的位置将很有意思.例如,你可能不仅想知道哪个表演者被点击,也想知道你点击的是头还是身体.下面的函数可以提供这些信息.
for(int i = -3; i < 3; i++) {
glPushName(i);
for(int j = -3; j < 3; j++) {
glPushMatrix();
glPushName(j);
glTranslatef(i*10.0,0,j * 10.0);
glPushName(HEAD);
glCallList(snowManHeadDL);
glLoadName(BODY);
glCallList(snowManBodyDL);
glPopName();
glPopName();
glPopMatrix();
}
glPopName();
}
In this case you'll have three names when you click on either the body or head of a snowman: the row, the column and the value BODY or HEAD respectively.
在这种情况下,当你点击一个表演者的身体或是头时,你将有三个名字:行,列和HEAD或BODY值.
A Note about Rendering on Selection Mode
选择模式下的绘制注意事项
As mentioned before, all calls to stack functions are ignored when not in selection mode, i.e. when rendering to the frame buffer. This means that you can have a single rendering function for both modes. However this can result in a serious waste of time. An application may have only a small number of pickable objects. Using the same function for both modes will require to render a lot of non-pickable objects when in selection mode. Not only the rendering time will be longer than required as will the time required to process the hit records.
像前面提到的那样,当不在选择模式时,所有调用栈的函数都被忽略.例如,当绘制到帧缓冲区时.这意味着你可以在这两种模式下只使用一个绘制函数.然而,这将会导致严重的时间浪费.一个应用程序可能只有极少的可选择对象.在两各模式下使用相同的函数将要求在选择模式下时也绘制大量的非可选择对象.不公绘制时间将比要求的长,处理命中记录的时间也会变长.
Therefore it makes sense to consider writing a special rendering function for the selection mode in these cases. However you should take extra care when doing so. Consider for instance an application where you have several rooms, each room with a potential set of pickable objects. You must prevent the user from selecting objects through walls, i.e. objects that are in other rooms and are not visible. If you use the same rendering function then the walls will cause a hit, and therefore you can use depth information to disregard objects that are behind the wall. However if you decide to build a function just with the pickable objects then there is no way to tell if an object is in the current room, or behind a wall.
因此,在这种情况下,最好考虑为选择模式下的绘制写特别的的绘制函数.然而,当你这样做时你应该特别小心.考虑这种情况,在一个应用程序中,你有几个房间,每个房间可能有几个可选择对象,你必须阻止用户透过墙来选择对象,如在另一个房间且不可见的对象.如果你使用相应的绘制函数,那么墙将会产生一个命中, 因此你可以利用深度信息来丢弃在墙背后的对象.然而,如果你决定写一个函数只绘制可选择的对象,那么在这种情况下,你没有办法区分哪个对象是在当前房间中或是在墙后面.
Therefore, when building a special rendering function for the selection mode you should include not only the pickable objects, but also all the objects that can cause occlusions, such as walls. For highly interactive real time applications it is probably a good option to build simplified representations of the objects that may cause occlusions. For instance a single polygon may replace a complex wall, or a box may replace a table.
因此,当为选择模式创建一个特别的绘制函数时,你应该不仅要包含可选择的对象,也要包含所有可以导致遮闭的对象.例如墙.对一些高度可交互实时的应用程序来说,用来些简单的对象来产生遮闭是一个不错的主意.例如,一个多边形可以替换一个复杂的墙,或是一个盒子来替换一个桌子.
原文:http://www.lighthouse3d.com/opengl/picking/index.php3?openglway