CS 381 Fall 2013 > Lecture Notes for Monday, October 7, 2013 |
CS 381 Fall 2013
Lecture Notes for Monday, October 7, 2013
Vertex and Element Buffer Objects
Review
Recall that immediate mode
(sending data using
glBegin
-glEnd
)
is inefficient in three ways.
- It has high funcation-call overhead.
- Vertices cannot be reused in mutlitple primitives.
- Vertex data must be specified anew in each frame.
The first problem was addressed with vertex arrays.
- We store attribute data in one or more arrays.
- We describe the data to OpenGL, using
glVertexPoint
,glColorPointer
, etc. - We indicate what functionality our vertex array is to
provide, using
glEnableClientState
, etc. - We draw using
glDrawArrays
.
The second problem was addressed with element arrays.
- A vertex array is handled as above, without the drawing command.
- We store vertex-array indices (elements) in an array.
- We draw using
glDrawElements
.
The third problem is addressed by allowing vertex and element arrays to be stored in memory managed by the graphics hardware, in the form of vertex buffer objects (VBOs) and element buffer objects (EBOs).
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.
OpenGL Server-Side Data
Client-Server
In many subfields of computing, we deal with client-server models. A client is a module that needs some service performed. A server is a module that can provide the service. Often the client and server are on separate hardware, with separate processing power, storage, etc. And often the connection between the client and server is slow or unreliable. Thus, we try to minimize use of the connection.
A standard example involves the web. Someone runs a web client (usually a browser). This requests web-page data from web servers running on other machines. The Internet usually provides the connection between the two.
OpenGL Client-Server Model
OpenGL was designed around a client-server model from the beginning. The client is the application code. The server is the hardware that implements the pipeline and does the rendering. Today, the OpenGL server is likely to be your computer’s graphics hardware, which is actually a separate computer in the same box, with its own processor (GPU) and memory.
OpenGL makes a clear distinction between client-side data and server-side data. All the data we have dealt with so far have been client-side: stored in memory accessible to the normal processor and managed by the application. In particular our vertex and element arrays have been client-side data.
Recent versions of the OpenGL specification have also allowed for server-side data. We allocate space on the server for data storage, and then store data there. The data are subsequently managed by the server.
OpenGL Objects
An OpenGL object is a wrapper around server-side storage. (This is unrelated to the C++ usage of the word “object”, meaning a value of class type.)
To create an object,
we generate a name,
which is a GLuint
value, provided by OpenGL,
that uniquely identifies an allocated chunk of server-side storage.
When we are done with the object,
we delete the name.
Different kinds of objects have different generation
and deletion commands.
But they all work the same way.
For example, buffer objects,
which we will use here,
have names generated by
glGenBuffers
.
This takes two parameters:
a GLsizei
(something like a size_t
)
specifying how many names to generate,
and a (GLuint *)
pointing to memory
sufficient to hold the required number of names.
I prefer to generate names one at a time:
[C++]
GLuint mybuff; glGenBuffers(1, &mybuff);
Buffer-object names are deleted
using glDeleteBuffers
,
which is called the same way.
[C++]
glDeleteBuffers(1, &mybuff);
Between generation and deletion, the object exists and can be used. To use an object, we bind its name to a target—a predefined OpenGL constant corresponding to the way we are using the object. Once an object’s name is bound, we use the target as an alias for the object.
For example, the target for a
vertex buffer object—server-side
storage for a vertex array,
is GL_ARRAY_BUFFER
.
We bind with glBindBuffer
.
[C++]
glBindBuffer(GL_ARRAY_BUFFER, mybuff); // Now we can use GL_ARRAY_BUFFER as an alias for the object // whose name is stored in mybuff.
To use a different object of the same kind,
bind its name to the target.
To revert to client-side storage,
bind the value 0
to the target.
Vertex Buffer Objects
A vertex buffer object (VBO)
is server-side storage for a vertex array.
To use a VBO,
generate a buffer object name as above
(you will probably want to store it in a global variable)
and bind it to the target GL_ARRAY_BUFFER
.
Store the attribute data in an array as before.
Send the vertex-array data to the server using
glBufferData
.
This takes four parameters:
- Target: for a VBO, this is
GL_ARRAY_BUFFER
. - The length, in bytes, of the vertex array.
- A pointer to the vertex array.
- A hint about how often the VBO will be updated
and used.
For now, make this
GL_STATIC_DRAW
.
After sending the data, we have a vertex array stored on the server. We may alter the data in our array; it is no longer used.
We can describe the vertex array,
enable functionality,
and draw almost exactly as before.
There is one difference.
When we call glVertexPointer
and we specify a pointer to our data,
we need a pointer to the location of the data
on the server.
And on the server, the data starts at location zero:
[C++]
glVertexPointer(..., ..., ..., (GLvoid *)(0)); // Assumes array starts with vertex-position data
I have written a header file to make management of OpenGL
objects more convenient:
globj.h
.
This header requires the GLEW package
(discussed in more detail later);
function glewInit
must be called before
using functions—other than constructors—defined
in the file.
(See any posted GLEW-using application for an example of how
this is done.)
Header globj.h
defines several classes;
each manages generation, binding, and deletion
for a particular kind of OpenGL object.
The class that manages vertex buffer objects is
called VBO
.
Create a global varaible of type VBO
for each vertex buffer object you want to create.
Call member function bind
on a global
variable when you want to use its object.
Putting all this together, we can use a VBO like this.
[C++]
// Global variables VBO vbo1; // In initialization GLdouble vdata1[3*3] = { -1., -1., 0., // Vertex 0: position data (x, y, z) 1., -1., 0., // Vertex 1 0., 1., 0. // Vertex 2 }; vbo1.bind(); glBufferData(GL_ARRAY_BUFFER, // Target 3*3*sizeof(GLdouble), // Length (bytes) vdata1, // Pointer to data GL_STATIC_DRAW); // Usage hint glVertexPointer(3, // Components per vertex GL_DOUBLE, // Type of component 3*sizeof(GLdouble), // Stride (bytes) (GLvoid *)(0)); // Ptr to data (on server!) // In display function vbo1.bind(); glEnableClientState(GL_VERTEX_ARRAY); // OR do in initialization glDrawArrays(GL_TRIANGLES, 0, 3);
Now we can create several VBOs in the initialization and use any or all of them in the display function. We simply bind the one we want to use any any particular time.
See
multispinner.cpp
for an application that uses VBOs.
This application requires GLEW.
The header globj.h
must be in subdirectory lib381
.
Element Buffer Objects
An element buffer object (EBO)
is server-side storage for a vertex array.
To use an EBO,
generate a buffer object name as before
and bind it to the target GL_ELEMENT_ARRAY_BUFFER
.
Store the element data in an array as always.
Send the element-array data to the server using
glBufferData
.
But now the target is GL_ELEMENT_ARRAY_BUFFER
.
Other details of handling EBOs are as above.
In globj.h
the class that manages
an EBO is called EBO
.
Notes on VBOs & EBOs
The standard drawing functions for vertex and element arrays
do 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.
If you want range checking done,
then look into glDrawRangeElements
.
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 may affect the efficiency of operations,
but they do not change the operations we are allowed to perform.
Unit Overview—Shaders & Lighting
Now we begin our third unit: Shaders & Lighting.
Recall the three issues in 3-D CG:
- Transformations & viewing.
- HSR.
- Lighting.
We have covered the first two. Next, we look at the third.
We will cover lighting in the context of learning to use programmable graphics hardware.
Note: The FFP does include lighting capability. The relevant computations occur between the model/view and projection transformations. This is essentially why model/view and projection are separate: between the two transformations, we have placed vertices into a common 3-D space, which is exactly what we need for lighting.
We will not be using the FFP lighting functionality, but we will be doing some of our lighting computations in the same place in the pipeline.
Topics in this unit:
- Introduction to Shaders
- Vector Operations
- The Basics of GLSL
- Communicating with Shaders
- Basic Lighting
- Details of Lighting
- Phong Illumination
- Computing Normals
- Splines
- Front & Back of Polygons
- Fancy Lighting
Introduction to Shaders
Shaders & Shading Languages
A shader
is a program that is intended to be run by the graphics hardware (GPU)
as part of rendering.
As a complete program, a shader has its own source code,
its own functions,
global variables, and “main
”.
The GPU is a real processor. As such, it has an assembly language, and shaders can be written in this. However, we prefer to use higher-level languages, called shading languages. The main two:
- Cg/HLSL (nVidia/Microsoft).
- GLSL, a.k.a. GLslang (OpenGL).
These are very similar, both being based on “C”, with vector and matrix functionality added. We will use GLSL.
Since shaders are full-fledged separate programs,
with their own main
, etc.,
it becomes ambiguous to talk about “the program”.
We will therefore refer to our primary C or C++ program
as the application.
Each of our executable examples in the next few weeks,
will consist of a single C++ application,
along with two or more GLSL shaders.
Evolution
It all began with the fixed-function pipeline (FFP), which is what we have been using so far. Functionality was added until it became quite complex.
In 2001, programmable graphics hardware. Originally, there were two kinds of shaders: vertex shaders and fragment shaders. These are named according to where they execute in the pipeline. Later, a third was added: geometry shaders.
As more and more of the work is pushed into programs running on the GPU, the graphics hardware looks more and more like an ordinary computer. Perhaps we will eventually see systems that do not make any distinction between the GPU and the main processor.
Shaders in the Pipeline
A vertex shader is executed once for each vertex, in the Vertex Processing portion of the pipeline. Operations that it replaces include the model/view transformation, FFP lighting, and the projection transformation. It does not replace clipping. Thus, when we write shaders, if we want to use the model/view transformation, then we must write code to do so; fortunately, this is very easy.
A fragment shader is executed once for each fragment, in the Fragment Processing portion of the pipeline. It does not replace any of the operations we have seen. The depth test is done after the fragment shader.
One can also write a geometry shader, which deals with vertices in groups, on the level of geometric primitives. These can do things like subdivide polygons, adding new vertices. We will not be writing geometry shaders in this class.
GLEW
In order to use certain advanced OpenGL features in a system-independent manner, much of the application code from this point on—and all the applications that use shaders— will require a package called the OpenGL Extensions Wrangler, or GLEW. You will most likely need to download and install this.
Our system-independent #include
s become more
complicated with GLEW.
Here is what to put at the top of every program now.
[C++]
// OpenGL/GLUT includes - DO THESE FIRST #include <cstdlib> // Do this before GL/GLUT includes using std::exit; #ifndef __APPLE__ # include <GL/glew.h> # include <GL/glut.h> // Includes OpenGL headers as well #else # include <GLEW/glew.h> # include <GLUT/glut.h> // Apple puts glut.h in a different place #endif #ifdef _MSC_VER // Tell MS-Visual Studio about GLEW lib # pragma comment(lib, "glew32.lib") #endif
GLEW must be initialized.
Do this in function main
,
after initializing GLUT,
but before calling your own initialization function.
[C++]
... glutCreateWindow("..."); // Init GLEW & check status if (glewInit() != GLEW_OK) { cerr << "glewInit failed" << endl; exit(1); } // Initialize GL states & register GLUT callbacks init(); ...
Once all the above is done and working, we can pretty much forget about GLEW. It is not something we use directly; rather, it makes various OpenGL features available, even if our installed OpenGL implementation does not support them as-is.