The Developer’s Cry

Yet another blog by a hobbyist programmer

OpenGL sprite sheet aka texture atlas

For the past months we have been looking at OpenGL, how to make a shader, doing 3D rotations, working with colors, and finally something about line drawing. This time we will have a look at sprite sheets (also known as “texture atlas”). It basically is an ordered collection of images, all gathered together in one single image. This one single image we will use as a texture. When rendering an object, we will tell OpenGL to cut out only a small rectangle of the texture, effectively selecting a single image from the collection. Using this technique you can select the right wall texture in your Quake 9 engine, or use it for animation in a 2D sprite engine. Another obvious application is fast rendering of text using a texture font.

As you can see in the image above, the sprites are not positioned back-to-back; there is lots of whitespace around them. This is actually on purpose and very important. If you do not do this, you will get texture bleed, an effect where colors of adjacent pixels seem to bleed into the edges of the sprite. This happens because of a precision problem that occurs at the boundary of the sprite. Although the sprite is (for example) 64 pixels wide, to OpenGL the texture coordinates are a floating point value between zero and 1.0. Therefore the sprite boundary is at a floating point value, and OpenGL may sample from adjacent texels. To prevent this from happening we simply keep whitespace around the sprites. What I usually do is keep at least one row of blank pixels, and put the texture coordinate at half a pixel.

We will call the first sprite in the sheet “sprite #0”, and it is situated in the left lower bottom corner (a “mathematical” coordinate system). It is the smiling smiley. It’s important to note that its position in pixel coordinates is clearly not (0,0) because of the whitespace boundary around it. At the least we want to cut out the sprite 0.5 pixels from the edge. Since OpenGL maps in normalized coordinates, we calculate the texture coordinates of our sprite as follows, in pseudo-code:

px = 1.0 / img_w        # one pixel in texture space
half_px = px * 0.5      # 1/2 pixel in texture space
py = 1.0 / img_h        # one pixel height in texture space
half_py = py * 0.5      # 1/2 pixel in texture space

# make texture coords for sprite (x,y,w,h)
sprites[0].tx = sprites[0].x * px - half_px
sprites[0].ty = sprites[0].y * py - half_py
sprites[0].tw = sprites[0].w * px + px
sprites[0].th = sprites[0].h * py + py

First thing to note is that the texels in texture space are not square (!), at least they’re not if you use a rectangular image.

The sprite width and height are +1 pixel; one half pixel on either side. The sprite width and height are often constant and can thus be constants in code too. In the example given here however I use w and h members for every sprite, so they may vary. We cache the texture coordinates of the sprite so this calculation only needs to be done during initialization.

The second sprite, the middle smiley, does not start right after that as there is another boundary in between. It’s safe to do without this extra boundary, but I mention it because it is present in our example sprite sheet.

When rendering the sprite as two triangles in a strip, OpenGL wants to know four texture coordinates, which is now very simple:

tex[0].x = sprites[n].tx
tex[0].y = sprites[n].ty

tex[1].x = sprites[n].tx + sprites[n].tw
tex[1].y = sprites[n].ty

tex[2].x = sprites[n].tx
tex[2].y = sprites[n].ty + sprites[n].th

tex[3].x = sprites[n].tx + sprites[n].tw
tex[3].y = sprites[n].ty + sprites[n].th

This technique is not all that hard to implement, but you have to be precise about the details and work with the data structures. Moreover, you or the artist needs to prepare the sprite sheet in a pixel perfect way. But surely you can do that.

It’s easy to see how you might use spritesheets for animation in a 2D sprite engine. You can also apply this technique to render text using a texture font. Monospace fonts are straightforward to implement, if you want proportional fonts it gets more advanced. Before, I would use FTGL, but it uses OpenGL in immediate mode which does not mix with our shaders. On top of that, FTGL is really slow compared to this code, which honestly performs much better.