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.

  1. It has high funcation-call overhead.
  2. Vertices cannot be reused in mutlitple primitives.
  3. Vertex data must be specified anew in each frame.

The first problem was addressed with vertex arrays.

The second problem was addressed with element arrays.

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:

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:

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.

Image not displayed: detail of Vertex Processing portion of pipeline, including FFP 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

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:

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.

Image not displayed: diagram of the programmable pipeline, showing FFP operations that are replaced by shaders

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 #includes 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.


CS 381 Fall 2013: Lecture Notes for Monday, October 7, 2013 / Updated: 7 Oct 2013 / Glenn G. Chappell