Chapter 3. OpenGL's Moving Triangle

Table of Contents

Moving the Vertices
A Better Way
More Power to the Shaders
Multiple Shaders
On Vertex Shader Performance
In Review
Glossary

This tutorial is about how to move objects around. It will introduce new shader techniques.

Moving the Vertices

The simplest way one might think to move a triangle or other object around is to simply modify the vertex position data directly. From previous tutorials, we learned that the vertex data is stored in a buffer object. So the task is to modify the vertex data in the buffer object. This is what cpuPositionOffset.cpp does.

The modifications are done in two steps. The first step is to generate the X, Y offset that will be applied to each position. The second is to apply that offset to each vertex position. The generation of the offset is done with the ComputePositionOffset function:

Example 3.1. Computation of Position Offsets

void ComputePositionOffsets(float &fXOffset, float &fYOffset)
{
    const float fLoopDuration = 5.0f;
    const float fScale = 3.14159f * 2.0f / fLoopDuration;
    
    float fElapsedTime = glutGet(GLUT_ELAPSED_TIME) / 1000.0f;
    
    float fCurrTimeThroughLoop = fmodf(fElapsedTime, fLoopDuration);
    
    fXOffset = cosf(fCurrTimeThroughLoop * fScale) * 0.5f;
    fYOffset = sinf(fCurrTimeThroughLoop * fScale) * 0.5f;
}

This function computes offsets in a loop. The offsets produce circular motion, and the offsets will reach the beginning of the circle every 5 seconds (controlled by fLoopDuration). The function glutGet(GLUT_ELAPSED_TIME) retrieves the integer time in milliseconds since the application started. The fmodf function computes the floating-point modulus of the time. In lay terms, it takes the first parameter and returns the remainder of the division between that and the second parameter. Thus, it returns a value on the range [0, fLoopDuration), which is what we need to create a periodically repeating pattern.

The cosf and sinf functions compute the cosine and sine respectively. It is not important to know exactly how these functions work, but they effectively compute a circle of diameter 2. By multiplying by 0.5f, it shrinks the circle down to a circle with a diameter of 1.

Once the offsets are computed, the offsets have to be added to the vertex data. This is done with the AdjustVertexData function:

Example 3.2. Adjusting the Vertex Data

void AdjustVertexData(float fXOffset, float fYOffset)
{
    std::vector<float> fNewData(ARRAY_COUNT(vertexPositions));
    memcpy(&fNewData[0], vertexPositions, sizeof(vertexPositions));
    
    for(int iVertex = 0; iVertex < ARRAY_COUNT(vertexPositions); iVertex += 4)
    {
        fNewData[iVertex] += fXOffset;
        fNewData[iVertex + 1] += fYOffset;
    }
    
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glBufferSubData(GL_ARRAY_BUFFER, 0, sizeof(vertexPositions), &fNewData[0]);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
}

This function works by copying the vertex data into a std::vector, then applying the offset to the X and Y coordinates of each vertex. The last three lines are the OpenGL-relevant parts.

First, the buffer objects containing the positions is bound to the context. Then the new function glBufferSubData is called to transfer this data to the buffer object.

The difference between glBufferData and glBufferSubData is that the SubData function does not allocate memory. glBufferData specifically allocates memory of a certain size; glBufferSubData only transfers data to the already existing memory. Calling glBufferData on a buffer object that has already been allocated tells OpenGL to reallocate this memory, throwing away the previous data and allocating a fresh block of memory. Whereas calling glBufferSubData on a buffer object that has not yet had memory allocated by glBufferData is an error.

Think of glBufferData as a combination of malloc and memcpy, while glBufferSubData is just memcpy.

The glBufferSubData function can update only a portion of the buffer object's memory. The second parameter to the function is the byte offset into the buffer object to begin copying to, and the third parameter is the number of bytes to copy. The fourth parameter is our array of bytes to be copied into that location of the buffer object.

The last line of the function is simply unbinding the buffer object. It is not strictly necessary, but it is good form to clean up binds after making them.

Buffer Object Usage Hints.  Every time we draw something, we are changing the buffer object's data. OpenGL has a way to tell it that you will be doing something like this, and it is the purpose of the last parameter of glBufferData. This tutorial changed the allocation of the buffer object slightly, replacing:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STATIC_DRAW);

with this:

glBufferData(GL_ARRAY_BUFFER, sizeof(vertexPositions), vertexPositions, GL_STREAM_DRAW);

GL_STATIC_DRAW tells OpenGL that you intend to only set the data in this buffer object once. GL_STREAM_DRAW tells OpenGL that you intend to set this data constantly, generally once per frame. These parameters do not mean anything with regard to the API; they are simply hints to the OpenGL implementation. Proper use of these hints can be crucial for getting good buffer object performance when making frequent changes. We will see more of these hints later.

The rendering function now has become this:

Example 3.3. Updating and Drawing the Vertex Data

void display()
{
    float fXOffset = 0.0f, fYOffset = 0.0f;
    ComputePositionOffsets(fXOffset, fYOffset);
    AdjustVertexData(fXOffset, fYOffset);
    
    glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
    glClear(GL_COLOR_BUFFER_BIT);
    
    glUseProgram(theProgram);
    
    glBindBuffer(GL_ARRAY_BUFFER, positionBufferObject);
    glEnableVertexAttribArray(0);
    glVertexAttribPointer(0, 4, GL_FLOAT, GL_FALSE, 0, 0);
    
    glDrawArrays(GL_TRIANGLES, 0, 3);
    
    glDisableVertexAttribArray(0);
    glUseProgram(0);
    
    glutSwapBuffers();
    glutPostRedisplay();
}

The first three lines get the offset and set the vertex data. Everything but the last line is unchanged from the first tutorial. The last line of the function is there to tell FreeGLUT to constantly call display. Ordinarily, display would only be called when the window's size changes or when the window is uncovered. glutPostRedisplay causes FreeGLUT to call display again. Not immediately, but reasonably fast.

If you run the tutorial, you will see a smaller triangle (the size was reduced in this tutorial) that slides around in a circle.

Fork me on GitHub