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.