Accelerated 2D graphics with OpenGL and SDL: part 1

As some of you know, I’m working on an old-school puzzle game with Hryx. This game makes use of OpenGL’s graphics capabilities, but not for 3D graphics. Nope, the game is entirely two dimensional.

So why OpenGL? Because it’s really, really fast on most systems. Much faster than the slowpoke graphics in SDL.

This is going to be part one of a two part series on doing 2D graphics using OpenGL. And step one? Getting some 2D sprites loaded.

Loading the sprite with SDL_image
Now since we’re using SDL for our game, we decided on the excellent SDL_image to do the image loading.

Using SDL_image to load a sprite into memory is absurdly easy.

string filename = "myImage.png";
SDL_Surface* surface = IMG_Load( filename.c_str() );

That’s it!

But wait… that loads the graphic into an SDL surface. We need it to be an OpenGL texture! Damn.

OpenGL textures
In OpenGL, there’s no such thing as a “sprite.” You just have “textures.” So we’ll need to take our sprite and map it to a texture. The texture will be referenced with an integer (a GLuint) rather than a pointer.

But there’s an interesting restriction we have to work around that SDL simply doesn’t have. On most systems, OpenGL textures must have dimensions are are powers of two: 2, 4, 8, 16, and so on.

So once we load our SDL_Surface, we’ll make a new surface where we’ve rounded up the dimensions to the next highest power of two. The new surface will be filled with the alpha “color” and then we can blit (copy) over the existing image to the top left corner of our new image.

SDL_PixelFormat *format = surface->format;
Uint32 width = surface->w;
Uint32 height = surface->h;
Uint32 widthPow = (unsigned) pow( 2, ceil( log( width ) / log( 2 ) ) );
Uint32 heightPow = (unsigned) pow( 2, ceil( log( height ) / log( 2 ) ) );

// Create new empty surface.
SDL_Surface* newSurface = SDL_CreateRGBSurface( SDL_SRCALPHA,
	   widthPow, heightPow, 32,
	   rmask, bmask, gmask, amask );

// Fill sprite with alpha.
Uint32 alpha = 0;
alpha = SDL_MapRGBA( format, 0, 0, 0, amask );
SDL_Rect rect;
rect.x = 0;
rect.y = 0;
rect.h = heightPow;
rect.w = widthPow;
int ret = SDL_FillRect( newSurface, &rect, alpha);
surface->flags &= !SDL_SRCALPHA;

SDL_SetAlpha( newSurface, SDL_SRCALPHA, SDL_ALPHA_TRANSPARENT );

// Copy image data to our new surface.
ret = SDL_BlitSurface( surface, 0, newSurface, 0 );

Great, we got our surface loaded. Now it’s time to create an OpenGL texture, then take our SDL surface and copy it to the texture.

// This will be our OpenGL texture.
GLuint texture = 0;

// Bind the texture.
glPixelStorei(GL_UNPACK_ALIGNMENT,4);
glGenTextures(1, &texture );
glBindTexture(GL_TEXTURE_2D, texture);

// Convert surface to Open GL format.
gluBuild2DMipmaps(GL_TEXTURE_2D, 4,
	widthPow, heightPow, GL_RGBA,GL_UNSIGNED_BYTE, newSurface->pixels);

// Free our temporary SDL buffers.
SDL_FreeSurface( surface );
SDL_FreeSurface( newSurface );

Easy, huh?

Dealing with 24-bit surfaces
Up until now we’ve only dealt with is 32-bit images. One thing I hadn’t dealt with until now is 24-bit color-keyed images. A color-keyed image means you have RGB but no alpha or “A” channel to tell you what should be invisible. Instead, you just take one color (often black) and just say that it’s 100% transparent.

What we’re going to have to do is make the SDL_surface color keyed before we copy it to the OpenGL texture.

First, we’re going to need some functions for plotting individual pixels on an SDL texture. Unfortunately there’s no built in functions for this, so I stole some from this site.

Uint32 GetPixel( SDL_Surface *surface, int x, int y )
{
	Uint32 color = 0;
	char* pPosition = ( char* ) surface->pixels;
	pPosition += ( surface->pitch * y );
	pPosition += ( surface->format->BytesPerPixel * x );
	memcpy ( &color , pPosition , surface->format->BytesPerPixel );
	return color;
}

void SetPixel( SDL_Surface *surface, int x, int y, Uint32 color )
{
	char* pPosition = ( char* ) surface->pixels;
	pPosition += ( surface->pitch * y );
	pPosition += ( surface->format->BytesPerPixel * x );
	memcpy ( pPosition , &color , surface->format->BytesPerPixel );
}

Cool, now we can plot pixels! Feel free to go crazy and write a routine to draw circles or something!

But all we really need to do here is to take pixels that correspond with our color key and set the alpha channel to the alpha mask channel color.

if ( 24 == format->BitsPerPixel )
{
   Uint32 colorKey = format->colorkey;

   for ( int y = 0; y < surface->h; ++y )
   {
	   for (int x = 0; x < surface->w; x++)
	   {
		   Uint32 color = GetPixel( surface, x, y );
		   if ( color == colorKey )
		   {
			   SetPixel( newSurface, x, y, 0 );
		   }
	   }
   }
}

Putting it all together
Here is the final code to load an image file to a texture using SDL.

#include 
#include 
#include 
#include 
#include 

#if SDL_BYTEORDER == SDL_LIL_ENDIAN
   static const Uint32 rmask = 0x000000FF;
   static const Uint32 bmask = 0x0000FF00;
   static const Uint32 gmask = 0x00FF0000;
   static const Uint32 amask = 0xFF000000;
#else
   static const Uint32 rmask = 0xFF000000;
   static const Uint32 bmask = 0x00FF0000;
   static const Uint32 gmask = 0x0000FF00;
   static const Uint32 amask = 0x000000FF;
#endif



Uint32 GetPixel( SDL_Surface *surface, int x, int y )
{
	Uint32 color = 0;
	char* pPosition = ( char* ) surface->pixels;
	pPosition += ( surface->pitch * y );
	pPosition += ( surface->format->BytesPerPixel * x );
	memcpy ( &color , pPosition , surface->format->BytesPerPixel );
	return color;
}


void SetPixel( SDL_Surface *surface, int x, int y, Uint32 color )
{
	char* pPosition = ( char* ) surface->pixels;
	pPosition += ( surface->pitch * y );
	pPosition += ( surface->format->BytesPerPixel * x );
	memcpy ( pPosition , &color , surface->format->BytesPerPixel );
}


GLuint loadTexture( std::string filename )
{
	SDL_Surface* surface = IMG_Load( filename.c_str() );

	if ( NULL == surface )
	{
	   // Error!
	   return 0;
	}


	SDL_PixelFormat *format = surface->format;
	Uint32 width = surface->w;
	Uint32 height = surface->h;
	Uint32 widthPow = (unsigned) pow( 2, ceil( log( width ) / log( 2 ) ) );
	Uint32 heightPow = (unsigned) pow( 2, ceil( log( height ) / log( 2 ) ) );

	// Create new empty surface.
	SDL_Surface* newSurface = SDL_CreateRGBSurface( SDL_SRCALPHA,
		   widthPow, heightPow, 32,
		   rmask, bmask, gmask, amask );

	// Fill sprite with alpha.
	Uint32 alpha = 0;
	alpha = SDL_MapRGBA( format, 0, 0, 0, amask );
	SDL_Rect rect;
	rect.x = 0;
	rect.y = 0;
	rect.h = heightPow;
	rect.w = widthPow;
	int ret = SDL_FillRect( newSurface, &rect, alpha);
	surface->flags &= !SDL_SRCALPHA;

	SDL_SetAlpha( newSurface, SDL_SRCALPHA, SDL_ALPHA_TRANSPARENT );

	// Copy image data to our new surface.
	ret = SDL_BlitSurface( surface, 0, newSurface, 0 );

	// Change color key to alpha transparency.
	// Used for 24-bit images.
	if ( 24 == format->BitsPerPixel )
	{
	   Uint32 colorKey = format->colorkey;

	   for ( int y = 0; y < surface->h; ++y )
	   {
		   for (int x = 0; x < surface->w; x++)
		   {
			   Uint32 color = GetPixel( surface, x, y );
			   if ( color == colorKey )
			   {
				   SetPixel( newSurface, x, y, 0 );
			   }
		   }
	   }
	}

	// This will be our OpenGL texture.
	GLuint texture = 0;

	// Bind the texture.
	glPixelStorei(GL_UNPACK_ALIGNMENT,4);
	glGenTextures(1, &texture );
	glBindTexture(GL_TEXTURE_2D, texture);

	// Convert surface to Open GL format.
	gluBuild2DMipmaps(GL_TEXTURE_2D, 4,
		widthPow, heightPow, GL_RGBA,GL_UNSIGNED_BYTE, 
                newSurface->pixels);

	// Free our temporary SDL buffers.
	SDL_FreeSurface( surface );
	SDL_FreeSurface( newSurface );

	return texture;
}

That’s it! Next time: displaying our new OpenGL texture on the screen as a 2d sprite.