Sprite Sheets

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.
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.
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.
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:
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.
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.