Skip to content

Sprite Batching

mattdesl edited this page Dec 20, 2012 · 14 revisions
start » Sprite Batching

If we were to try and render a game with the debugTexture method from the Textures tutorial, we would quickly run into performance problems. This is because we are only pushing one sprite at a time to the GPU. What we need is to "batch" many sprites into the same draw call; for this we use a SpriteBatch.

Intro

You can see a minimal implementation of a SpriteBatcher here -- it's modeled after the batcher in LibGDX.

As discussed in the Textures tutorial, a sprite is nothing more than a set of vertices that make up a rectangular shape. Each vertex can have any number of attributes, but generally SpriteBatchers use the following:

  • Position(x, y) - where the vertex lies on the screen
  • TexCoord(s, t) - what region of our Texture we want to render
  • Color(r, g, b, a) - to specify tinting or transparency

Most sprite batchers are fairly simple to use, and may look like this in your game:

//called on game creation
public void create() {
    //create a single batcher we will use throughout our application
    spriteBatch = new SpriteBatch();
}

//called on frame render
public void render() {
    //prepare the batch for rendering
    spriteBatch.begin(); 

    //draw all of our sprites
    spriteBatch.draw(mySprite1, x, y);
    spriteBatch.draw(mySprite2, x, y);
    ...

    //end the batch, flushing the data to GPU
    spriteBatch.end();
}

//called when the display is resized
public void resize(int width, int height) {
    //notify the sprite batcher whenever the screen changes 
    spriteBatch.resize(width, height);
}

When we call spriteBatch.draw(...), this simply pushes the sprite's vertex information (position, texcoord, color) onto a very large stack. The vertices aren't passed to the GPU until one of the following occurs:

  • The batch is forced to render with end() or another call that flushes the batch (like flush())
  • The user tries drawing a sprite that uses a different Texture than the last one. The batch needs to be flushed and the new texture bound before we can continue.
  • We have reached the capacity of our stack, so we need to flush to start over again

This is the basic idea behind a sprite batcher. As you can see, using many textures will lead to many draw calls (as the batch will need to flush for each new texture). This is why a texture atlas (AKA sprite sheet) is always recommended; it allows us to render many sprites in a single draw call.

Vertex Color

We can change the tinting and transparency of our sprites by setting the batch color, AKA "vertex color." The RGB will be multiplied by the texture color; so if our texture was white (1, 1, 1, 1) and we specified a vertex color of (1, 0, 0, 1), the result would be red. The Alpha component allows us to adjust the opacity of sprites rendered to screen.

spriteBatch.begin();

//draw calls will now use 50% opacity
spriteBatch.setColor(1f, 1f, 1f, 0.5f);
spriteBatch.draw(...);
spriteBatch.draw(...);

//draw calls will now use 100% opacity (default)
spriteBatch.setColor(1f, 1f, 1f, 1f);
spriteBatch.draw(...);

spriteBatch.end();

TextureRegion

As discussed, for best performance we should use a texture atlas, and draw regions of it (AKA sub-images) to make up our game's sprites. For this we have a utility class, TextureRegion. It allows us to specify in pixels the upper left position (x, y) and size (width, height) of our sub-image. Let's take our earlier example, where we want to render the highlighted tile:

VertexBreakdown

We can get a TextureRegion of the tile with the following:

//specify x, y, width, height of tile
region = new TextureRegion(64, 64, 64, 64);

As you can see, the TextureRegion utility allows us to get sub-images without worrying about calculating the texture coordinates. We can then render the individual tile with our sprite batch like so:

... inside SpriteBatch begin / end ...
spriteBatch.draw(region, x, y);

Triangles, not Quads

In the earlier series, we have been thinking of textures as quads, but in reality most sprite batchers will use two adjacent triangles to represent a rectangular sprite. The vertices may be ordered differently depending on the engine (LibGDX tends to use lower-left origin), but the basic idea looks like this:

Verts

A single sprite has 2 triangles -- or 6 vertices. Each vertex has 8 attributes (X, Y, S, T, R, G, B, A) which together make up position, texture coordinates and vertex color. This means that with every sprite, we are pushing 48 floats to the stack. A more optimized sprite batcher might pack the RGBA into a single float, or may forgo vertex colors altogether.

Now where?

Creating your own sprite batcher is not easy, and requires understanding of shaders, vertex buffers, and basic matrix math. Before diving into these advanced topics, I'd recommend getting comfortable with the SpriteBatcher provided for you by lwjgl-basics. Alternatively, you can use LibGDX as the implementation is very similar. You should also be comfortable with GLSL before attempting your own sprite batcher.

If you are still keen to learn about how it all works under the hood, see the ShaderProgram and SpriteBatcher articles.

Clone this wiki locally