| CS 381 Fall 2012 > Lecture Notes for Tuesday, September 18, 2012 |
Many user interfaces, particularly those involving a mouse, have some notion of pointing at something to select it. This is known as picking. The primary question we need to answer is this: when the mouse is clicked, what was it clicked on?
A 2-D interface has width (\(x\)) and height (\(y\)), but no depth. A 3-D interface adds a \(z\)-coordinate: depth. A 2½-D interface lies between these two; it gives objects a front-to-back ordering, but no actual depth. For example, the standard desktop GUI with overlapping windows, is a 2½-D interface. We will look at picking methods appropriate for a 2½-D interface.
The extent of an object is the set of pixels in the framebuffer that the object occupies, or would occupy if it were not (partially) hidden by some other object. Thus, picking requires us to determine the front-most object in whose extent the mouse position lies.
To determine what object has been selected, we can check the mouse position against the extent of each object, in front-to-back order. The first hit, if any, gives the object selected. If there are no hits, then no object was selected.
This method is easily implemented when objects are all screen-aligned rectangles, and there are not a huge number of them, but it can become tricky when there are irregularly shaped objects.
When you write code for click-and-drag, remember:
See
clickdrag.cpp
for an example of code that does click-and-drag using direct extent testing.
We look briefly at two more advanced picking methods that make use of information determined by the rendering pipeline.
Here, we draw each object in a different solid color; the color of the pixel at the mouse position tells which object was clicked on.
Of course, we usually do not want to draw our scene this way; however, if we do all our work in an off-screen framebuffer (for example, the back buffer), then this method can be quite convenient.
If we are checking a color against a list of colors,
it is a good idea to using integer color components
instead of floating-point,
since floating-point equality checks are generally to be avoided.
Function glColor3ub takes 3 parameters of type
GLubyte (= GL unsigned byte).
Instead of 0.0 to 1.0, values run from 0 to 255.
The main problem we need to deal with, is how to read the framebuffer.
In OpenGL, this is done with the glReadPixels command,
which reads data from a rectangular region in the framebuffer,
to an array that you provide.
(Note: Related commands for tossing around portions of the framebuffer
include glDrawPixels
and glCopyPixels.)
Function glReadPixels has 7 parameters.
GLint.
Coordinates of the lower-left corner
of the image, in pixels from the left & bottom.
(Note: GLUT gives the mouse position in pixels
from the left & top; be careful.)GLsizei (an integer type).
Width and height of the region to read.
Set these both to 1 to read a single pixel.GLenum (use one of the OpenGL predefined constants).
What data to read.
Use GL_RGB to read the R, G, and B
components of the color.GLenum (use one of the OpenGL predefined constants).
The type of data in your array.
Use GL_UNSIGNED_BYTE if you are using an array
of GLubyte values.GLvoid * (i.e., a pointer to any type).
A pointer to your array.
Your array needs to be large enough to hold all the data.
For example, if you pass GL_RGB,
and you are reading an \(s\times t\) rectangle,
then your array needs to hold at least \(3st\) items.
The data will be stored as R, G, B, R, G, B, R, G, B, etc.
See
readfb.cpp
for an example of code that reads the framebuffer
(but does no picking).
Our final picking method is the most interesting. The rendering pipeline computes quite a lot of information that is eventually thrown out (think about it: everything is thrown out, except for the pixel colors). What if we could access and use some of this information?
Recall that clipping takes place at the end of the Vertex Processing portion of the pipeline. If a primitive shows up in the viewport, then the clipper sends it—possibly in altered form—on to Rasterization; if a primitive is entirely outside the viewport, then the clipper discards it. What if we could ask the clipper which objects were sent on to the Rasterization? If we set the viewport to be a single pixel at the mouse position, then the answer to this questio would tell us what we clicked on.
OpenGL allows such querying of the clipper, in what it calls selection mode. In this mode, Rasterization is shut down, and the framebuffer is not written. Instead, information is made available about what comes out of Vertex Processing.
The information that comes out is in the form of “names”.
A name is a number that is sent through the pipeline
along with a primitive.
Use the command glLoadName to do this.
This command normally does nothing;
in selection mode, it specifies a label (name)
for the primitives that follow it.
The current name is another GL state;
load the proper name just before drawing the primitives making up
some object,
and then each of those primitives will be labeled with that name.
When OpenGL leaves selection mode, it returns a list of the names corresponding to primitives that were not discarded by the clipper.
See
select.cpp
for an example of code that queries the clipper using OpenGL’s selection mode.
The main function of interest in this code is doselect.
In order to use this, you must write two other functions for it to call.
draw_with_namesglLoadName call.
The idea is that this function is also called by your display function.set_up_coordsglLoadIdentity
call.
The idea is that this function is also called by your reshape function.
Call function doselect when the user clicks the mouse,
and you want to allow picking.
Pass it the mouse position, as GLUT gives it to you,
and a vector<int>,
which is passed by reference, and will be modified.
When the function returns,
the names of all objects clicked on will be in the vector;
if it is empty, then no object was clicked on.
We now begin our third unit, Transformations & Viewing, in which we start doing 3-D graphics.
Topics in this unit:
Now we begin 3-D graphics. Of course, this is just 2-D graphics with depth thrown in. However, this seemingly small change can complicate things. Here are three issues that come up, or become harder to deal with, when we move to 3-D.
The above list forms a rough outline of the next month or so of this class.
Here are the “Three Laws of 3-D CG” (my own contribution to the material of this class -GGC-).
Remember: we are not making 3-D objects; we are making 2-D images of 3-D objects. Everything we do is aimed at the human retina (which is not very thick). So, for example, we might not draw the back side of an object. From a purely 3-D point of view, this is ridiculous. But when you are only making an image of the front side, it does not matter.
Throughout this class, we will be taking shortcuts, with the aim of producing good images quickly. Sometimes we do unrealistic things, like making light sources illuminate at the same brightness no matter how far away they are. But if it looks good and runs fast, then that is fine.
*The asterisk is here because of two fields in which this law is problematic. The first is scientific imagery, where fudging things can lead to accusations of misconduct. The seconds is the legal system, where fudging things can put you in jail. So do be sensible about applying this law.
The graphics field is full of 2-way choices:
rotate clockwise or counterclockwise,
the front of a polygon or the back,
translate forward or backward, etc.
After being involved in the graphics field for two decades,
I have reached the point where I can get these decisions right
on the first try,
nearly 50% of the time.
So: Don’t waste time trying to figure out which direction a
glRotate command will move things;
try it,
and if it goes the wrong direction,
then flip the sign of the angle.
It is expected that much of this material will be review.
A vector is a mathematical object that has both magnitude and direction. The vectors we deal with will be drawn as arrows in 2-D or 3-D space.
In the context of vectors, an ordinary number is refered to as a scalar.
We represent 2-D vector by a pair of numbers, and a 3-D vector by a triple of numbers. The numbers are components. One way to write a vector is to its components, separated by commas, between angle brackets:
\[\langle2,-3\rangle.\]
To find the components of a 2-D or 3-D arrow, place its tail on the origin, and look at the coordinates of the point at the the head of the vector.
Vector variables are usually boldface (\(\mathbf{v}\)) in printed matter, and written with a small right-pointing arrow above them (\(\vec{v}\)) when handwritten.
Below, the vector \(\mathbf{v} = \langle 2, -3\rangle\)
is illustrated.
The vector with all components equal to zero is called the zero vector. It is written as \(\mathbf{0}\). Note that this is different from the scalar \(0\).
A matrix (Latin plural: matrices) is a rectangular array of numbers. We give the vertical dimension first, so that the following is a \(3\times 2\) matrix:
\[ A = \begin{pmatrix} 1&2\\ 3&1\\ -1&0 \end{pmatrix}. \]
We generally use upper-case letters to represent matrices.
We represent the entries of a matrix using the corresponding lower-case letter, with subscripts representing the row and then the column, both numbered started at \(1\). So, above, \(a_{21} = 3\), while \(a_{12} = 2\).
The main diagonal of a matrix starts at the top left entry, and goes down and right. In the above matrix, the main diagonal consists of entries \(a_{11}\), \(a_{22}\); both of these are \(1\).
The transpose of a matrix is obtained by reflecting the matrix about its main diagonal. The transpose of matrix \(A\) is written \(A^\mathrm{T}\):
\[ A^\mathrm{T} = \begin{pmatrix} 1&3&-1\\ 2&1&0 \end{pmatrix}. \]
When we deal with matrices and vectors, we usually write an \(n\)-dimensional vector as an \(n\times 1\) matrix, which we call a column vector:
\[ \mathbf{v} = \langle 3,4 \rangle = \begin{pmatrix} 3\\ 4 \end{pmatrix}. \]
An \(m\times n\) matrix can be multiplied by an \(n\)-dimensional vector. Or, to put it differently, an \(m\times n\) matrix can be multiplied by a \(k\)-dimensional vector, as long as \(n = k\). The result is an \(m\)-dimensional vector.
We compute each component of the result using the corresponding row of the matrix. For example, we compute the second component of the result using the second row of the matrix. We multiply each item in this row by the corresponding component of the original vector, and then add these up.
\[ A\mathbf{v} = \begin{pmatrix} 1&2\\ 3&1\\ -1&0 \end{pmatrix} \, \begin{pmatrix} 3\\ 4 \end{pmatrix} = \begin{pmatrix} 1\times 3+2\times 4\\ 3\times 3+1\times 4\\ (-1)\times 3+0\times 4 \end{pmatrix} = \begin{pmatrix} 11\\ 13\\ -1 \end{pmatrix}. \]
We can multiply two matrices if the horizontal dimension of the first equals the vertical dimension of the second. In other words, we can multiply an \(m\times n\) matrix by an an \(n\times k\) matrix. The result is an \(m\times k\) matrix. Each column of the result is the matrix-vector product of the first matrix and a column of the second matrix:
\[ \begin{pmatrix} 1&2\\ 3&1\\ -1&0 \end{pmatrix} \, \begin{pmatrix} 3&5\\ 4&6 \end{pmatrix} = \begin{pmatrix} 1\times 3+2\times 4&1\times 5+2\times 6\\ 3\times 3+1\times 4&3\times 5+1\times 6\\ (-1)\times 3+0\times 4&(-1)\times 5+0\times 6 \end{pmatrix} = \begin{pmatrix} 11&17\\ 13&21\\ -3&-5 \end{pmatrix}. \]
The \(n\times n\) identity matrix has ones on its main diagonal and zeroes everywhere else:
\[ I_2 = \begin{pmatrix} 1&0\\ 0&1 \end{pmatrix}. \]
It is called the identity matrix because \(I\,A = A\), as long as the product is defined. For example,
\[ \begin{pmatrix} 1&0\\ 0&1 \end{pmatrix} \, \begin{pmatrix} 3&8\\ 2&1 \end{pmatrix} = \begin{pmatrix} 3&8\\ 2&1 \end{pmatrix}. \]
Some (but not all!) matrices have an inverse. The inverse of matrix \(A\) is written \(A^{-1}\). This is a matrix whose product with \(A\) is an identity matrix. For example,
\[ \begin{pmatrix} 2&4\\ 1&1 \end{pmatrix} \, \begin{pmatrix} -1/2&2\\ 1/2&-1 \end{pmatrix} = \begin{pmatrix} 1&0\\ 0&1 \end{pmatrix}. \]
For matrices \(A\) and \(B\), typically \(A\,B \ne B\,A\); in other words, matrix multiplication is not commutative.
However matrix multiplication is associative: \((A\,B)\,C = A\,(B\,C)\). This fact turns out to be very important for our handling of transformations.
ggchappell@alaska.edu