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

Sprite Sheets

Sprite Sheets screenshot

Last Updated: Aug 9th, 2012

In the previous tutorial, we were updating the vertex data in our VBO every frame despite the fact that our vertex data didn't change from frame to frame. If you have a set of sprite images that you reuse every frame, you can just preallocate your vertex data.
From LTexture.h
        virtual ~LTexture();
        /*
        Pre Condition:
         -None
        Post Condition:
         -Frees texture
        Side Effects:
         -None
        */
In this tutorial we're going to have a class that inherits from LTexture. To make sure our base class destructor gets called, we make it virtual.
From LTexture.h
        virtual void freeTexture();
        /*
        Pre Condition:
         -A valid OpenGL context
        Post Condition:
         -Deletes texture if it exists
         -Deletes member pixels if they exist
         -Sets texture ID to 0
        Side Effects:
         -None
        */
We're also going to override how textures are freed in the child class.
From LSpriteSheet.h
class LSpriteSheet : public LTexture
{
    public:
        LSpriteSheet();
        /*
        Pre Condition:
         -None
        Post Condition:
         -Initializes buffer ID
        Side Effects:
         -None
        */

        ~LSpriteSheet();
        /*
        Pre Condition:
         -None
        Post Condition:
         -Deallocates sprite sheet data
        Side Effects:
         -None
        */

        int addClipSprite( LFRect& newClip );
        /*
        Pre Condition:
         -None
        Post Condition:
         -Adds clipping rectangle to clip array
         -Returns index of clipping rectangle within clip array
        Side Effects:
         -None
        */

        LFRect getClip( int index );
        /*
        Pre Condition:
         -A valid index
        Post Condition:
         -Returns clipping clipping rectangle at given index
        Side Effects:
         -None
        */

        bool generateDataBuffer();
        /*
        Pre Condition:
         -A loaded base LTexture
         -Clipping rectangles in clip array
        Post Condition:
         -Generates VBO and IBO to render sprites with
         -Returns true on success
         -Reports to console is an error occured
        Side Effects:
         -Member buffers are bound
        */

        void freeSheet();
        /*
        Pre Condition:
         -None
        Post Condition:
         -Deallocates member VBO, IBO, and clip array
        Side Effects:
         -None
        */

        void freeTexture();
        /*
        Pre Condition:
         -None
        Post Condition:
         -Frees sprite sheet and base LTexture
        Side Effects:
         -None
        */

        void renderSprite( int index );
        /*
        Pre Condition:
         -Loaded base LTexture
         -Generated VBO
        Post Condition:
         -Renders sprite at given index
        Side Effects:
         -Base LTexture is bound
         -Member buffers are bound
        */

    protected:
        //Sprite clips
        std::vector<LFRect> mClips;

        //VBO data
        GLuint mVertexDataBuffer;
        GLuint* mIndexBuffers;
};
Here's the LSpriteSheet class, which inherits from the LTexture class. A sprite sheet in our case is a texture with a specialized use.

At the top we have the constructor and destructor like we usually do. Then we have addClipSprite(), which adds a clip rectangle for a sprite to the member array. The function getClip() gets a clip from the member array.

Once we have all the clip rectangles set, we'll call generateDataBuffer() to use our clip rectangles to generate our VBO and IBOs. Where we're done with our clip rectangles, we'll call freeSheet() to deallocate our clipping data.

LTexture is inherited publicly, so all the public base functions can still be accessed. We're going to make some changes to freeTexture() as you'll see later in the tutorial. Lastly, we have renderSprite() which of course renders a sprite.

In terms of new variables, we have "mClips" which is representing our array of clip rectangles. Then we have "mVertexDataBuffer" which we'll use to hold the vertex data for all of our sprites in one big VBO. Lastly we have "mIndexBuffers" which will be an array of IBOs. For this implementation of a sprite sheet, we'll have on big VBO and an IBO for each individual sprite.
From LSpriteSheet.cpp
LSpriteSheet::LSpriteSheet()
{
    //Initialize vertex buffer data
    mVertexDataBuffer = NULL;
    mIndexBuffers = NULL;
}

LSpriteSheet::~LSpriteSheet()
{
    //Clear sprite sheet data
    freeSheet();
}
The constructor initializes member variables and the destructor deallocates sprite sheet data. Remember that the LTexture destructor is virtual so the base LTexture gets deallocated after the LSpriteSheet gets deallocated.
From LSpriteSheet.cpp
int LSpriteSheet::addClipSprite( LFRect& newClip )
{
    //Add clip and return index
    mClips.push_back( newClip );
    return mClips.size() - 1;
}

LFRect LSpriteSheet::getClip( int index )
{
    return mClips[ index ];
}
The function addClipSprite() simply adds on the clipping rectangle at the end of the STL vector and returns the index of the last element. getClip() returns the requested clip rectangle. We don't check for array bounds because getting sprite dimensions can be used heavily during rendering, a performance critical part of the program.
From LSpriteSheet.cpp
bool LSpriteSheet::generateDataBuffer()
{
    //If there is a texture loaded and clips to make vertex data from
    if( getTextureID() != 0 && mClips.size() > 0 )
    {
        //Allocate vertex buffer data
        int totalSprites = mClips.size();
        LVertexData2D* vertexData = new LVertexData2D[ totalSprites * 4 ];
        mIndexBuffers = new GLuint[ totalSprites ];
After we loaded our texture and added all the clip rectangles for the sprites we want to render, it's time to generate our VBO data.

After making sure there's a base texture to render with and clip rectangles to generate data from, we allocate our vertex data (with 4 vertices per sprite) and an IBO per sprite.
From LSpriteSheet.cpp
        //Allocate vertex data buffer name
        glGenBuffers( 1, &mVertexDataBuffer );

        //Allocate index buffers names
        glGenBuffers( totalSprites, mIndexBuffers );

        //Go through clips
        GLfloat tW = textureWidth();
        GLfloat tH = textureHeight();
        GLuint spriteIndices[ 4 ] = { 0, 0, 0, 0 };

        for( int i = 0; i < totalSprites; ++i )
Next we generate our big VBO and the IBOs for each sprite.

Then we get the texture width/height so we can map our texture coordinates. After that we declare some sprite indices we'll use for our IBOs.

Now we're ready to go through the clip rectangles and set our index/vertex data.
From LSpriteSheet.cpp
        {
            //Initialize indices
            spriteIndices[ 0 ] = i * 4 + 0;
            spriteIndices[ 1 ] = i * 4 + 1;
            spriteIndices[ 2 ] = i * 4 + 2;
            spriteIndices[ 3 ] = i * 4 + 3;
At the top of our for loop we set our index data for current sprite.

If you're wondering how these indices are calculated, think of it this way:

If you have 3 sprites with 4 vertices each, you're going to have a total of 12 vertices with indices going from from 0 to 11. The first sprite will have indices 0, 1, 2, and 3. The second sprite will have indices 4, 5, 6, and 7. The third sprite will be 8, 9, 10, and 11.

Now let's take the third sprite which will have a clip index of 2 because arrays start counting from 0. This gives us:
  • 2 * 4 + 0 = 8
  • 2 * 4 + 1 = 9
  • 2 * 4 + 2 = 10
  • 2 * 4 + 3 = 11
From LSpriteSheet.cpp
            //Top left
            vertexData[ spriteIndices[ 0 ] ].position.x = -mClips[ i ].w / 2.f;
            vertexData[ spriteIndices[ 0 ] ].position.y = -mClips[ i ].h / 2.f;

            vertexData[ spriteIndices[ 0 ] ].texCoord.s =  (mClips[ i ].x) / tW;
            vertexData[ spriteIndices[ 0 ] ].texCoord.t =  (mClips[ i ].y) / tH;

            //Top right
            vertexData[ spriteIndices[ 1 ] ].position.x =  mClips[ i ].w / 2.f;
            vertexData[ spriteIndices[ 1 ] ].position.y = -mClips[ i ].h / 2.f;

            vertexData[ spriteIndices[ 1 ] ].texCoord.s =  (mClips[ i ].x + mClips[ i ].w) / tW;
            vertexData[ spriteIndices[ 1 ] ].texCoord.t =  (mClips[ i ].y) / tH;

            //Bottom right
            vertexData[ spriteIndices[ 2 ] ].position.x =  mClips[ i ].w / 2.f;
            vertexData[ spriteIndices[ 2 ] ].position.y =  mClips[ i ].h / 2.f;

            vertexData[ spriteIndices[ 2 ] ].texCoord.s =  (mClips[ i ].x + mClips[ i ].w) / tW;
            vertexData[ spriteIndices[ 2 ] ].texCoord.t =  (mClips[ i ].y + mClips[ i ].h) / tH;

            //Bottom left
            vertexData[ spriteIndices[ 3 ] ].position.x = -mClips[ i ].w / 2.f;
            vertexData[ spriteIndices[ 3 ] ].position.y =  mClips[ i ].h / 2.f;

            vertexData[ spriteIndices[ 3 ] ].texCoord.s =  (mClips[ i ].x) / tW;
            vertexData[ spriteIndices[ 3 ] ].texCoord.t =  (mClips[ i ].y + mClips[ i ].h) / tH;
After setting our indices for the current sprite, we set the vertex data for the current sprite. This time around the sprite's origin is at the center of the sprite.
From LSpriteSheet.cpp
            //Bind sprite index buffer data
            glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mIndexBuffers[ i ] );
            glBufferData( GL_ELEMENT_ARRAY_BUFFER, 4 * sizeof(GLuint), spriteIndices, GL_STATIC_DRAW );
        }
At the bottom of the for loop for the current sprite, we want to set the IBO data for the current sprite.
From LSpriteSheet.cpp
        //Bind vertex data
        glBindBuffer( GL_ARRAY_BUFFER, mVertexDataBuffer );
        glBufferData( GL_ARRAY_BUFFER, totalSprites * 4 * sizeof(LVertexData2D), vertexData, GL_STATIC_DRAW );

        //Deallocate vertex data
        delete[] vertexData;
    }
After we're done with going through the sprites with the for loop, we set the VBO data for our whole sprite sheet.

Remember that the vertex data was dynamically allocated. It's already on the GPU, so we can delete it on the client side.
From LSpriteSheet.cpp
    //Error
    else
    {
        if( getTextureID() == 0 )
        {
            printf( "No texture to render with!\n" );
        }

        if( mClips.size() <= 0 )
        {
            printf( "No clips to generate vertex data from!\n" );
        }

        return false;
    }

    return true;
}
At the bottom of the generateDataBuffer() function we report any errors if we need to and return the success of the function.
From LSpriteSheet.cpp
void LSpriteSheet::freeSheet()
{
    //Clear vertex buffer
    if( mVertexDataBuffer != NULL )
    {
        glDeleteBuffers( 1, &mVertexDataBuffer );
        mVertexDataBuffer = NULL;
    }

    //Clear index buffers
    if( mIndexBuffers != NULL )
    {
        glDeleteBuffers( mClips.size(), mIndexBuffers );
        delete[] mIndexBuffers;
        mIndexBuffers = NULL;
    }

    //Clear clips
    mClips.clear();
}
In freeSheet(), all we do is deallocate the sprite sheet data since we may want to reuse the base LTexture. Notice how "mIndexBuffers" is deleted because the IBO names we dynamically allocated.
From LSpriteSheet.cpp
void LSpriteSheet::freeTexture()
{
    //Get rid of sprite sheet data
    freeSheet();

    //Free texture
    LTexture::freeTexture();
}
Here's the reason we made freeTexture() function in LTexture virtual. Because LSpriteSheet inherits LTexture publicly, all the base public functions are exposed. We don't want it to happen where there's vertex data with no texture. With this overidden function, the LSpriteSheet will deallocate both the sprite sheet data and the base texture.
From LSpriteSheet.cpp
void LSpriteSheet::renderSprite( int index )
{
    //Sprite sheet data exists
    if( mVertexDataBuffer != NULL )
    {
        //Set texture
        glBindTexture( GL_TEXTURE_2D, getTextureID() );

        //Enable vertex and texture coordinate arrays
        glEnableClientState( GL_VERTEX_ARRAY );
        glEnableClientState( GL_TEXTURE_COORD_ARRAY );

            //Bind vertex data
            glBindBuffer( GL_ARRAY_BUFFER, mVertexDataBuffer );

            //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 ) );

            //Draw quad using vertex data and index data
            glBindBuffer( GL_ELEMENT_ARRAY_BUFFER, mIndexBuffers[ index ] );
            glDrawElements( GL_QUADS, 4, GL_UNSIGNED_INT, NULL );

        //Disable vertex and texture coordinate arrays
        glDisableClientState( GL_TEXTURE_COORD_ARRAY );
        glDisableClientState( GL_VERTEX_ARRAY );
    }
}
Finally in our renderSprite() function, we bind our monolothic VBO with our vertex and texture coordinates and render using the sprite's specific IBO.
From LUtil.cpp
#include "LUtil.h"
#include <IL/il.h>
#include <IL/ilu.h>
#include "LSpriteSheet.h"

//Sprite sheet
LSpriteSheet gArrowSprites;
At the top of LUtil.cpp, we declare our sprite sheet.
From LUtil.cpp
bool loadMedia()
{
    //Load texture
    if( !gArrowSprites.loadTextureFromFile( "19_sprite_sheets/arrows.png" ) )
    {
        printf( "Unable to load sprite sheet!\n" );
        return false;
    }

    //Set clips
    LFRect clip = { 0.f, 0.f, 128.f, 128.f };

    //Top left
    clip.x = 0.f;
    clip.y = 0.f;
    gArrowSprites.addClipSprite( clip );

    //Top right
    clip.x = 128.f;
    clip.y = 0.f;
    gArrowSprites.addClipSprite( clip );

    //Bottom left
    clip.x = 0.f;
    clip.y = 128.f;
    gArrowSprites.addClipSprite( clip );

    //Bottom right
    clip.x = 128.f;
    clip.y = 128.f;
    gArrowSprites.addClipSprite( clip );

    //Generate VBO
    if( !gArrowSprites.generateDataBuffer() )
    {
        printf( "Unable to clip sprite sheet!\n" );
        return false;
    }

    return true;
}
In loadMedia(), we load our sprite sheet texture as we would with a plain LTexture. Then we the clip rectangles from each sprite. Lastly, we generate the data buffers from the clip rectangles.
From LUtil.cpp
void render()
{
    //Clear color buffer
    glClear( GL_COLOR_BUFFER_BIT );

    //Render top left arrow
    glLoadIdentity();
    glTranslatef( 64.f, 64.f, 0.f );
    gArrowSprites.renderSprite( 0 );

    //Render top right arrow
    glLoadIdentity();
    glTranslatef( SCREEN_WIDTH - 64.f, 64.f, 0.f );
    gArrowSprites.renderSprite( 1 );

    //Render bottom left arrow
    glLoadIdentity();
    glTranslatef( 64.f, SCREEN_HEIGHT - 64.f, 0.f );
    gArrowSprites.renderSprite( 2 );

    //Render bottom right arrow
    glLoadIdentity();
    glTranslatef( SCREEN_WIDTH - 64.f, SCREEN_HEIGHT - 64.f, 0.f );
    gArrowSprites.renderSprite( 3 );

    //Update screen
    glutSwapBuffers();
}
Finally in the render() function, we render each of the sprites in the 4 corners.