Lazy Foo' Productions

SDL Forums external link SDL Tutorials Articles OpenGL Tutorials OpenGL Forums external link
Follow BlueSky Follow Facebook Follow Twitter Follow Threads
Donate
News FAQs Contact Bugs

Textured Vertex Buffers

Textured Vertex Buffers screenshot

Last Updated: Aug 9th, 2012

Using with glBegin()/glEnd() with glVertex() (known as immediate mode rendering) is deprecated in modern OpenGL implementations. If you want to render a textured polygon in modern OpenGL implementations, you're going to have to do it with VBOs.
From LTexCoord.h
#ifndef LTEX_COORD_H
#define LTEX_COORD_H

#include "LOpenGL.h"

struct LTexCoord
{
    GLfloat s;
    GLfloat t;
};

#endif
Here we have a new file with a new data type. An LTexCoord is a data type we're going to use to make handling texture coordinates easier.
From LVertexData2D.h
#ifndef LVERTEX_DATA_2D_H
#define LVERTEX_DATA_2D_H

#include "LVertexPos2D.h"
#include "LTexCoord.h"

struct LVertexData2D
{
    LVertexPos2D position;
    LTexCoord texCoord;
};

#endif
Each one of the vertices for our textured quad has a vertex position and a texture coordinate, so we have another file with another data type to put them together. Each one of our textured quad corners will be represented by a LVertexData2D object.
From LTexture.h
        //Texture name
        GLuint mTextureID;

        //Current pixels
        GLuint* mPixels;

        //Texture dimensions
        GLuint mTextureWidth;
        GLuint mTextureHeight;

        //Unpadded image dimensions
        GLuint mImageWidth;
        GLuint mImageHeight;

        //VBO IDs
        GLuint mVBOID;
        GLuint mIBOID;
};
We have new member variables for our VBO and IBO.
From LTexture.h
    private:
        GLuint powerOfTwo( GLuint num );
        /*
        Pre Condition:
         -None
        Post Condition:
         -Returns nearest power of two integer that is greater
        Side Effects:
         -None
        */

        void initVBO();
        /*
        Pre Condition:
         -A valid OpenGL context
         -A loaded member texture
        Post Condition:
         -Generates VBO and IBO to use for rendering
        Side Effects:
         -Binds NULL VBO and IBO
        */

        void freeVBO();
        /*
        Pre Condition:
         -A generated VBO
        Post Condition:
         -Frees VBO and IBO
        Side Effects:
         -None
        */
We also have the new functions initVBO() and freeVBO() handle our VBO variables.
From LTexture.cpp
LTexture::LTexture()
{
    //Initialize texture ID and pixels
    mTextureID = 0;
    mPixels = NULL;

    //Initialize image dimensions
    mImageWidth = 0;
    mImageHeight = 0;

    //Initialize texture dimensions
    mTextureWidth = 0;
    mTextureHeight = 0;

    //Initialize VBO
    mVBOID = 0;
    mIBOID = 0;
}
As always, don't forget to initialize your variables.
From LTexture.cpp
void LTexture::initVBO()
{
    //If texture is loaded and VBO does not already exist
    if( mTextureID != 0 && mVBOID == 0 )
    {
        //Vertex data
        LVertexData2D vData[ 4 ];
        GLuint iData[ 4 ];

        //Set rendering indices
        iData[ 0 ] = 0;
        iData[ 1 ] = 1;
        iData[ 2 ] = 2;
        iData[ 3 ] = 3;

        //Create VBO
        glGenBuffers( 1, &mVBOID );
        glBindBuffer( GL_ARRAY_BUFFER, mVBOID );
        glBufferData( GL_ARRAY_BUFFER, 4 * sizeof(LVertexData2D), vData, GL_DYNAMIC_DRAW );

        //Create IBO
        glGenBuffers( 1, &mIBOID );
        glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mIBOID );
        glBufferData( GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), iData, GL_DYNAMIC_DRAW );

        //Unbind buffers
        glBindBuffer( GL_ARRAY_BUFFER, NULL );
        glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, NULL );
    }
}
Here's our VBO initializer. Notice how it will only run if a texture is already loaded. There's no point in a textured VBO with no texture.

First we declare our vertex data and index data we're going to send to the GPU. Then we set our indices. The reason we aren't setting our vertex data is because it's going to be set in the rendering function. The reason we're using "GL_DYNAMIC_DRAW" is because we're going to be updating the vertex data, as opposed to the previous tutorial where we sent the data once and reused it.

As with previous the tutorial, we generate the buffers and send the data to them. After our buffers are created, we unbind the buffers.
From LTexture.h
bool LTexture::loadTextureFromPixels32( GLuint* pixels, GLuint imgWidth, GLuint imgHeight, GLuint texWidth, GLuint texHeight )
{
    //Free texture if it exists
    freeTexture();

    //Get image dimensions
    mImageWidth = imgWidth;
    mImageHeight = imgHeight;
    mTextureWidth = texWidth;
    mTextureHeight = texHeight;

    //Generate texture ID
    glGenTextures( 1, &mTextureID );

    //Bind texture ID
    glBindTexture( GL_TEXTURE_2D, mTextureID );

    //Generate texture
    glTexImage2D( GL_TEXTURE_2D, 0, GL_RGBA, mTextureWidth, mTextureHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels );

    //Set texture parameters
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, DEFAULT_TEXTURE_WRAP );
    glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, DEFAULT_TEXTURE_WRAP );

    //Unbind texture
    glBindTexture( GL_TEXTURE_2D, NULL );

    //Check for error
    GLenum error = glGetError();
    if( error != GL_NO_ERROR )
    {
        printf( "Error loading texture from %p pixels! %s\n", pixels, gluErrorString( error ) );
        return false;
    }

    //Generate VBO
    initVBO();

    return true;
}
We call our VBO initializer whenever we load a texture so the VBO is ready to use for rendering our textured quad. As you can see, the rest of our texture loading is the same as before. The only part our VBO is going to affect is how the textured quad is rendered.
From LTexture.cpp
void LTexture::render( GLfloat x, GLfloat y, LFRect* clip )
{
    //If the texture exists
    if( mTextureID != 0 )
    {
        //Texture coordinates
        GLfloat texTop = 0.f;
        GLfloat texBottom = (GLfloat)mImageHeight / (GLfloat)mTextureHeight;
        GLfloat texLeft = 0.f;
        GLfloat texRight = (GLfloat)mImageWidth / (GLfloat)mTextureWidth;

        //Vertex coordinates
        GLfloat quadWidth = mImageWidth;
        GLfloat quadHeight = mImageHeight;

        //Handle clipping
        if( clip != NULL )
        {
            //Texture coordinates
            texLeft = clip->x / mTextureWidth;
            texRight = ( clip->x + clip->w ) / mTextureWidth;
            texTop = clip->y / mTextureHeight;
            texBottom = ( clip->y + clip->h ) / mTextureHeight;

            //Vertex coordinates
            quadWidth = clip->w;
            quadHeight = clip->h;
        }

        //Move to rendering point
        glTranslatef( x, y, 0.f );
At the top of the LTexture render() function, our vertex and texture coordinate calculation is pretty much the same as when we used immediate mode. The data itself isn't changing, it's how it's being sent to the GPU that's changing.
From LTexture.cpp
        //Set vertex data
        LVertexData2D vData[ 4 ];

        //Texture coordinates
        vData[ 0 ].texCoord.s =  texLeft; vData[ 0 ].texCoord.t =    texTop;
        vData[ 1 ].texCoord.s = texRight; vData[ 1 ].texCoord.t =    texTop;
        vData[ 2 ].texCoord.s = texRight; vData[ 2 ].texCoord.t = texBottom;
        vData[ 3 ].texCoord.s =  texLeft; vData[ 3 ].texCoord.t = texBottom;

        //Vertex positions
        vData[ 0 ].position.x =       0.f; vData[ 0 ].position.y =        0.f;
        vData[ 1 ].position.x = quadWidth; vData[ 1 ].position.y =        0.f;
        vData[ 2 ].position.x = quadWidth; vData[ 2 ].position.y = quadHeight;
        vData[ 3 ].position.x =       0.f; vData[ 3 ].position.y = quadHeight;
After calculating our texture coordinates and vertex coordinates, we set the data in the vertex data array.

If any of you are confused why the vertex data is set like this, this code should actually look familiar.
From old LTexture render()
        //Render textured quad
        glBegin( GL_QUADS );
            glTexCoord2f(  texLeft,    texTop ); glVertex2f(       0.f,        0.f );
            glTexCoord2f( texRight,    texTop ); glVertex2f( quadWidth,        0.f );
            glTexCoord2f( texRight, texBottom ); glVertex2f( quadWidth, quadHeight );
            glTexCoord2f(  texLeft, texBottom ); glVertex2f(       0.f, quadHeight );
        glEnd();
We're just specifying the same vertex/texture coordinates as we did before, only now the data is being put in an array.
From LTexture.cpp
        //Set texture ID
        glBindTexture( GL_TEXTURE_2D, mTextureID );

        //Enable vertex and texture coordinate arrays
        glEnableClientState( GL_VERTEX_ARRAY );
        glEnableClientState( GL_TEXTURE_COORD_ARRAY );
After setting our vertex/texture coordinates, it's time to render our textured quad. To do that we have to bind our texture, enable vertex coordinate arrays with "GL_VERTEX_ARRAY" and also enable texture coordinate arrays with "GL_TEXTURE_COORD_ARRAY".
From LTexture.cpp
            //Bind vertex buffer
            glBindBuffer( GL_ARRAY_BUFFER, mVBOID );

            //Update vertex buffer data
            glBufferSubData( GL_ARRAY_BUFFER, 0, 4 * sizeof(LVertexData2D), vData );
Before we can render our textured quad, we need to update the vertex data in the VBO with the vertex data we calculated at the top of the render() function. First we bind the VBO so we can do operations on it. Then we call glBufferSubData() to update the vertex data on the current VBO much in the same way you would use glTexSubImage2D() to update a texture.

The first argument specifies what type of data you're updating. The second argument is the byte offset you want to start updating the data, and because we're updating the whole VBO, we start from the beginning address 0. The third argument is the size of the data you're updating in bytes. We're sending 4 LVertexData2Ds, so it's 4 times the size of a LVertexData2D. The last argument is the pointer to the vertex data.
From LTexture.cpp
            //Set texture coordinate data
            glTexCoordPointer( 2, GL_FLOAT, sizeof(LVertexData2D), (GLvoid*)offsetof( LVertexData2D, texCoord ) );

            //Set vertex data
            glVertexPointer( 2, GL_FLOAT, sizeof(LVertexData2D), (GLvoid*)offsetof( LVertexData2D, position ) );
We have to set the vertex pointer like we did last time, and now we have to set the texture coordinate pointer since we're texturing. As with immediate mode, you want to send your texture coordinates before your vertex coordinates. Because we have our vertex coordinates and texture coordinates in the same VBO, setting the pointers is a bit more involved.

The first argument for the glPointer functions is how many axes per coordinate. The second argument is the data type for the pointer data. The third argument is the stride.

What is stride? Remember how I said in previous tutorials that since our structure is made up entirely of GLfloats, we can treat it like an array of floats? That still true:
parallel arrays

Stride is the space between each set of data. When say the stride is sizeof(LVertexData2D) for the vertex pointer, you'll see that the starting address of each vertex is a LVertexData2D apart in bytes:
stride

So the way we're setting up the vertex pointer is to have OpenGL read in two GLfloats, shift over sizeof(LVertexData2D) bytes, read another two GLfloats, and so on until all the vertices are read:
pos stride

You may have noticed that the stride for the texture coordinates is also sizeof(LVertexData2D), but the texture coordinates are placed differently:
tex stride

Using the offsetof macro, we can give the vertex and texture coordinate pointers different starting address. When we define the last argument of the glPointer functions, the starting address of the data, we give it the byte offset of the member within the LVertexData2D struct.

Remember: we have a VBO bound, so any address we give the function is going to be retrieved from VBO memory.
From LTexture.cpp
            //Draw quad using vertex data and index data
            glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mIBOID );
            glDrawElements( GL_QUADS, 4, GL_UNSIGNED_INT, NULL );
With our vertex coordinate and texture coordinate data set, we bind our IBO and render our textured VBO quad.

You may be wondering why didn't we update the IBO the same way we updated our VBO. Remember that our IBO is set so we render 4 vertices in order. While our vertex data may change, the fact that we will render 4 vertices in order will not change.
From LTexture.cpp
        //Disable vertex and texture coordinate arrays
        glDisableClientState( GL_TEXTURE_COORD_ARRAY );
        glDisableClientState( GL_VERTEX_ARRAY );
    }
}
After we're done rendering our VBO textured quad, we disable the vertex and texture coordinate arrays.
From LTexture.cpp
void LTexture::freeVBO()
{
    //Free VBO and IBO
    if( mVBOID != 0 )
    {
        glDeleteBuffers( 1, &mVBOID );
        glDeleteBuffers( 1, &mIBOID );
    }
}
When we're done with our VBO and IBO, we free them using glDeleteBuffers().
From LTexture.cpp
void LTexture::freeTexture()
{
    //Delete texture
    if( mTextureID != 0 )
    {
        glDeleteTextures( 1, &mTextureID );
        mTextureID = 0;
    }

    //Delete pixels
    if( mPixels != NULL )
    {
        delete[] mPixels;
        mPixels = NULL;
    }

    mImageWidth = 0;
    mImageHeight = 0;
    mTextureWidth = 0;
    mTextureHeight = 0;
}
We don't free the VBO when we free the texture. It could happen where we load one texture and then another. In this case we can reuse the VBO from texture to texture.
From LTexture.cpp
LTexture::~LTexture()
{
    //Free texture data if needed
    freeTexture();

    //Free VBO and IBO if needed
    freeVBO();
}
We free the VBO after the LTexture object itself is deallocated.
From LUtil.cpp
bool loadMedia()
{
    if( !gVBOTexture.loadTextureFromFile( "18_textured_vertex_buffers/opengl.png" ) )
    {
        printf( "Unable to load OpenGL texture!\n" );
        return false;
    }

    return true;
}

void update()
{

}

void render()
{
    //Clear color buffer
    glClear( GL_COLOR_BUFFER_BIT );

    //Initialize modelview matrix
    glLoadIdentity();

    //Render textured quad using VBOs
    gVBOTexture.render( ( SCREEN_WIDTH - gVBOTexture.imageWidth() ) / 2.f, ( SCREEN_HEIGHT - gVBOTexture.imageHeight() ) / 2.f );

    //Update screen
    glutSwapBuffers();
}
As you can see, all these VBO operations are invisible outside of the class.