CS 381 Fall 2012 > Lecture Notes for Tuesday, October 2, 2012 |
We wish to draw a moving object with moving parts that have moving parts .... In order to do this, we adhere to the following two principles.
We have already seen the first principle. The second principle is new, but it is easily followed: whenever we modify a transformation, push first and then pop when we are done using the modified transformation.
Recall the OpenGL matrix-stack commands:
glPushMatrix
glMatrixMode
).glPopMatrix
Top of stack is the matrix affected by transformation commands and used in the pipeline.
Thus, a function that draws an object with 3 moving parts might look like this:
void drawObj() { // MAIN PART OF OBJECT // Draw main part of object // MOVING PART 1 glPushMatrix(); // Set up transformation for part 1 (no glLoadIdentity!) // Draw part 1 (call another function?) glPopMatrix(); // MOVING PART 2 glPushMatrix(); // Set up transformation for part 2 (no glLoadIdentity!) // Draw part 2 (call another function?) glPopMatrix(); // MOVING PART 3 glPushMatrix(); // Set up transformation for part 3 (no glLoadIdentity!) // Draw part 3 (call another function?) glPopMatrix(); }
Notes
See
arm.cpp
for code that draws and animates a hierarchical object.
glBegin
-glEnd
In the original OpenGL specification,
the only way to draw general-geometry primitives
was using glBegin
-glEnd
.
But this mechanism turned out to have three problems.
Later versions of OpenGL added mechanisms to deal with each of these problems.
For the first problem OpenGL now allows for data to be specified using a vertex array: a data structure holding coordinates and other attributes for arbitrarily large numbers of vertices. A large vertex array can be passed to OpenGL with just a few function calls.
For the second problem, a vertex array can be stored in memory managed by OpenGL, in the form of a vertex buffer objects (VBO). The vertex data stored in such an object can be reused repeatedly.
For the third problem, we can specify the structure of a primitive independently of the coordinates and other attributes of the vertices using an element array, stored in an element buffer object (EBO). (Note: alternative names are index array, index buffer object.)
Using the VBO/EBO mechanism is considerably more complicated
than glBegin
-glEnd
.
In this class, you may use whichever method you want.
Note, however, that the glBegin
and glEnd
commands have been officially removed from the latest OpenGL versions.
Further, OpenGL’s descendants,
OpenGL ES and WebGL, have never allowed
for the glBegin
-glEnd
mechanism.
In any number of computing contexts, the client-server paradigm is used. The idea is that some task needs to be done. The module that needs the task performed is the client. The module that can perform the task for the client is the server.
For example, a web browser is an example of a web client. It wants webpage data, and a web server can provide such data.
The client-server approach allows for:
OpenGL was designed using a client-server approach. The client is the application code. The server is whatever does the rendering; these days, it will be the GPU. These might be on the same computer, or they might be on different computers, different continents, or perhaps one day different planets. (Strangely, despite technological advances, rendering on a different machine seems to have become less common. In the early 1990s, I did it often; in the past five years, I don’t recall doing it at all. -GGC-)
This client-server design is what glFlush
is all about.
When the connection between the client and the server is slow,
we want to avoid using it.
We save up data to be sent in a single burst.
The glFlush
command directs the system to
send any saved-up data to the server.
OpenGL makes a clear distinction between client-side data and server-side data. The latter is managed by OpenGL, in the form of objects. An OpenGL object has nothing to do with C++ classes; it is a chunk of server-side storage.
When we use an OpenGL object:
glGenBuffers
.GL_ARRAY_BUFFER
;
we bind a name to this target using glBindBuffer
.
Once the name is bound,
we refer to the object using the target;
OpenGL knows that we are really referring to the last-bound
object.glDeleteBuffers
.VBOs and EBOs are examples of OpenGL objects. Later, when we discuss textures, we will see another kind: texture objects.
Recall that a vertex buffer object is a chunk of OpenGL server-side data that holds a vertex array: coordinates and other attributes of arbitrarily large numbers of vertices. Here is how we deal with such an object in our code.
glGenBuffers
.
This takes two parameters:
the number of names to generate,
and a pointer to GLuint
storage
for the required number of names.
I suggest generating one name at a time.
GLuint vboname; // Make this a global? glGenBuffers(1, &vboname);
GL_ARRAY_BUFFER
using glBindBuffer
.
glBindBuffer(GL_ARRAY_BUFFER, vboname);
glBufferData
.
This has four parameters:
target, how much data in bytes, pointer to the data,
and a hint for how to optimize the storage.
This last can be GL_STATIC_DRAW
for now;
this means that we do not intend to change the vertex data much
(but we still can, if we want).
GLdouble vbuff[SIZE]; // Put data into vbuff here glBufferData(GL_ARRAY_BUFFER, SIZE * sizeof(GLdouble), vbuff, GL_STATIC_DRAW);
We tell OpenGL where the vertex coordinates are,
using the command glVertexPointer
.
This takes four parameters:
components per vertex (e.g., \(3\) for a 3-D vertex),
type of component (e.g., GL_DOUBLE
),
stride (distance in bytes from one vertex to the next),
and the byte index of the start of the first vertex in the array.
This last, for some reason, is a pointer: (GLvoid *)
.
glVertexPointer(3, GL_DOUBLE, 3 * sizeof(GLdouble), (GLvoid *)(0));
We similarly describe color data using glColorPointer
.
When get to texture coordinates, we can describe them
using glTexCoordPointer
.
And when we get to surface normal vectors,
we can describe them with glNormalPointer
(note: this assumes \(3\) components; leave off the first parameter).
glEnableClientState
.
If we want OpenGL to do glVertex*
calls
using our vertex data, then pass GL_VERTEX_ARRAY
.
If we want it to do glColor*
calls,
using our color data,
then pass GL_COLOR_ARRAY
.
Similarly use GL_NORMAL_ARRAY
,
GL_TEXTURE_COORD_ARRAY
.
glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY);
We can also call glDisableClientState
to turn off selected functionality.
Functionality is disabled by default;
so we only need to disable things we have previously enabled.
glDrawArrays
.
This takes three parameters:
the primitive to render,
which vertex to begin with (probably \(0\)),
and how many vertices to use (probably all of them).
See the sample code, below, for an example.
glDeleteBuffers
.
This takes the same parameters as glGenBuffers
.
Note: We are not actually required to delete our names; this is done automatically when the application ends. But if we repeatedly generate names without deleting, then we may run out of server-side storage. If we write a class wrapper for a VBO, then it is a good idea to delete in the destructor. However, do not generate names in the constructor! We cannot doglDeleteBuffers(1, &vboname);
glGenBuffers
until OpenGL
has been initialized, and constructors for global objects are called
before that.Suppose we want to draw two triangles with multicolored vertices. We could do it like this.
glBegin(GL_TRIANGLES); glColor3d(1., 0., 0.); // First triangle glVertex2d(0., 0.); glColor3d(0., 1., 0.); glVertex2d(1., 0.); glColor3d(0., 0., 1.); glVertex2d(0., 1.); glColor3d(0., 1., 1.); // Second triangle glVertex2d(-1., -1.); glColor3d(1., 0., 1.); glVertex2d(0., -1.); glColor3d(1., 1., 0.); glVertex2d(-1., 0.); glEnd();
Here is complete code to use a VBO to draw the same triangles.
We declare a global GLuint
to hold the name.
GLuint trianglevbo; // Name of VBO holding data for triangles
In our initialization, generate, bind, set the data, send it to OpenGL, and describe it.
// Generate name glGenBuffers(1, &trianglevbo); // Bind name glBindBuffer(GL_ARRAY_BUFFER, trianglevbo); // Set data GLdouble triangledata[6*2+6*3] = { 0., 0., // Vertex 0 1., 0., // Vertex 1 0., 1., // Vertex 2 -1., -1., // Vertex 3 0., -1., // Vertex 4 -1., 0., // Vertex 5 1., 0., 0. // Color 0 0., 1., 0. // Color 1 0., 0., 1. // Color 2 0., 1., 1. // Color 3 1., 0., 1. // Color 4 1., 1., 0. // Color 5 }; // Send data glBufferData(GL_ARRAY_BUFFER, // Target (6*2+6*3) * sizeof(GLdouble), // Data size (bytes) triangledata, // Pointer to data GL_STATIC_DRAW); // Storage hint // Describe data glVertexPointer(2, // Components per vertex GL_DOUBLE, // Type of vertex component 2 * sizeof(GLdouble), // Stride (bytes) (GLvoid *)(0)); // Byte index of first vertex // (pointer, for some reason) glColorPointer(3, // Components per color GL_DOUBLE, // Type of color component 3 * sizeof(GLdouble), // Stride (bytes) (GLvoid *)(6*2)); // Byte index of first color
Note that, after the above call to glBufferData
,
the data are on the server,
and we can do whatever we want with array triangledata
:
destroy it, use it for something else, etc.
To render our triangles, bind, enable, and draw in the display function.
// Bind name glBindBuffer(GL_ARRAY_BUFFER, trianglevbo); // Enable functionality glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); // Render using VBO glDrawArrays(GL_TRIANGLES, // Primitive 0, // Index of start vertex 6); // How many vertices
Optionally, we can delete our VBO when we are sure we never want to use it again. This probably should not be part of the regular update-display cycle. Put it in a destructor, perhaps.
glDeleteBuffers(1, &trianglevbo);
The above code looks hideous, of course.
But there are advantages.
The next time we want to draw the same triangles,
we simply bind, enable, and call glDrawArrays
.
And this would work even for \(100,000\) triangles.
See
vbo.h
for a (hopefully) convenient wrapper class for VBOs.
See
vbodemo.cpp
for a simple application that uses this class.
An element buffer object holds integers, which are used as indices into whatever VBO is bound when we render. Here is how we deal with such an object in our code.
glGenBuffers
.
GLuint eboname; // Make this a global? glGenBuffers(1, &eboname);
GL_ELEMENT_ARRAY_BUFFER
using glBindBuffer
.
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, eboname);
glBufferData
.
I suggest that you store your data as GLuint
s.
GLuint ebuff[SIZE]; // Put data into ebuff here glBufferData(GL_ELEMENT_ARRAY_BUFFER, SIZE * sizeof(GLuint), ebuff, GL_STATIC_DRAW);
There is no need to describe the data; it always integers, using as indices into a VBO.
glDrawArrays
.
Then bind an EBO
and render with glDrawElements
.
This takes four parameters:
the primitive,
the number of elements (indices) to use,
the type of an element (probably GL_UNSIGNED_INT
),
and which element to start with (probably \(0\)).
As with glVertexPointer
,
the last parameter must be a pointer (GLvoid *)
,
for some reason.
See the sample code, below, for an example.
glDeleteBuffers
.
Suppose we have set up trianglevbo
, as above.
We want to draw a single triangle using vertices
\(0\), \(2\), \(5\).
In other words, we want to do this:
glBegin(GL_TRIANGLES); glColor3d(1., 0., 0.); // Color+vert 0 glVertex2d(0., 0.); glColor3d(0., 0., 1.); // Color+vert 2 glVertex2d(0., 1.); glColor3d(1., 1., 0.); // Color+vert 5 glVertex2d(-1., 0.); glEnd();
Here is code to use an EBO with our VBO to draw this triangle. We assume that the set up code for the VBO is done as above, except for the rendering.
We declare a global GLuint
to hold the name.
GLuint niftyebo; // Name of EBO holding indices for new triangle
In our initialization, generate, bind, set the data, and send it to OpenGL.
// Generate name glGenBuffers(1, &niftyebo); // Bind name glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, niftyebo); // Set data GLuint niftyindices[3] = { 0, 2, 5 }; // Send data glBufferData(GL_ELEMENT_ARRAY_BUFFER, // Target 3 * sizeof(GLuint), // Data size (bytes) niftyindices, // Pointer to data GL_STATIC_DRAW); // Storage hint
Once again, after the above call to glBufferData
,
the data are on the server,
and we can do whatever we want with array niftyindices
.
To render the triangle in the display function, bind the VBO and enable, then bind the EBO and draw.
// Bind VBO name glBindBuffer(GL_ARRAY_BUFFER, trianglevbo); // Enable VBO functionality glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_COLOR_ARRAY); // Bind EBO name glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, niftyebo); // Render using EBO glDrawElements(GL_TRIANGLES, // Primitive 3, // Number of elements GL_UNSIGNED_INT, // Type of element (GLvoid *)(0)); // Start element
Lastly, the optional delete.
glDeleteBuffers(1, &niftytebo);
See
ebo.h
for a (hopefully) convenient wrapper class for EBOs.
See
ebodemo.cpp
for a simple application that uses this class.
OpenGL does little or no range checking. It is your responsibility to make sure that you do not reference data that lies beyond the end of the stored data.
Each EBO handles a single primitive. To do multiple primitives, use multiple EBOs. You could use one gigantic VBO to hold all vertex data, then lots of EBOs, one for each primitive.
The hint GL_STATIC_DRAW
says that we do not intend to change the data,
and we intend to use it a lot.
We can also use GL_STREAM_DRAW
,
which indicates no changing and little use.
Lastly, there is GL_DYNAMIC_DRAW
,
which indicates frequent alterations in the data.
But these are merely hints;
they do not affect the operations we are allowed to perform.
You may wish to look into display lists, which allow you to store, not just vertex data, but arbitrary rendering commands.
ggchappell@alaska.edu