Sprite Batching in OpenGL ES 2.0

Batching geometry is a basic and very common technique for optimizing OpenGL ES 2.0 code. It works by grouping objects together and drawing them in a single draw call instead of one by one. Having a large number of draw calls may imply a serious bottleneck and reducing that number may significantly increase performance. The reason why the number of draw calls matter is that the CPU has to prepare and configure the GL pipeline before each call, and that’s quite an expensive operation.

In this post I’m going to focus on batching sprites, due to their simplicity. A sprite consists of six vertices (two triangles) with a texture applied to it, so it’s used for all almost all graphics in 2D games. It’s also used frequently in 3D games for things such as as particle effects that would be very expensive to compute using real 3D geometry. Another advantage of sprites over 3D geometry is that since OpenGL is very good at interpolates textures, they usually appear smooth and don’t have the jagged edges that 3D objects may suffer from.

The method I’m using here is probably just one of several ways to accomplish batching. I didn’t really read about this method anywhere, I just figured that this is a good way of doing it. Feel free to suggest alternative methods. I’ll be happy to hear about them!

Basic sprite shader

We start by looking at a sprite shader which no support for batching. The vertex shader is very simply. All we need as input is the model-view-projection-matrix, position and texCoord. The gl_Position is calculated by transforming the position according to the mvp matrix, and the texCoord is interpolated for the fragment shader through it’s varying variable.

Vertex Shader:

uniform mat4 u_mvpMatrix;

attribute vec4 a_position;
attribute vec2 a_texCoord; 

varying vec2 v_texCoord;

void main() {
    v_texCoord = a_texCoord;
    gl_Position = u_mvpMatrix * a_position;
}

The fragment shader samples the given texture and the varying texCoord and sets the gl_FragColor.

Fragment Shader:

precision mediump float;

uniform sampler2D s_texture;
varying vec2 v_texCoord;

void main() {
    gl_FragColor = texture2D(s_texture, v_texCoord);
}

A limitation here in the vertex shader is that all vertex attributes defined in this draw call needs to use the same mvp matrix. The mvp matrix defines the position, rotation and scale of the vertices which means that if we want to draw multiple sprites at different positions using this shader, we have to redefine the uniform mvp matrix for each sprite. This can’t be done in the middle of a draw call so we have to use multiple draw calls, which we want to avoid!

Sprite shader with batching support

So, looking at the basic sprite shader above, how can we extend it to use different mvp matrices for each sprite? The answer is to define u_mvpMatrix as an array of matrices. The total space available for uniforms and attributes in OpenGL ES 2.0 is limited, so we have to limit the size of the array, how large you can make the array depends on how many additional attribues and uniforms are used. A size of 24 should be quite close to the minimum limit defined in the specification.

When using an array for the mvp matrix, we also need to define the index for each vertex. This is accomplished by using an additional attribute. OpenGL ES 2.0 has no support for integer attributes, so we’re bound to using a float data type that is rounded in the shader.

Our new shader looks like this:

uniform mat4 u_mvpMatrix[24]; 

attribute float a_mvpMatrixIndex;
attribute vec4 a_position;
attribute vec2 a_texCoord;

varying vec2 v_texCoord;

void main() {
    int mvpMatrixIndex = int(a_mvpMatrixIndex);

    v_texCoord = a_texCoord;
    gl_Position = u_mvpMatrix[mvpMatrixIndex] * a_position;
}

We don’t have to make any modifications to the fragment shader, so it stays the same as before:

precision mediump float;

uniform sampler2D s_texture; 

varying vec2 v_texCoord;

void main() {
    gl_FragColor = texture2D(s_texture, v_texCoord);;
}

This change requires you to send the a_mvpMatrixIndex attribute for each vertex. All six vertices belonging to the first sprite should have an index of zero, the second sprite should have index of one, and so on. The a_mvpMatrixIndex array should look something like this:
0.0,0.0,0.0,0.0,0.0,0.0,1.0,1.0,1.0,1.0,1.0,1.0,2.0,2.0,2.0,2.0,2.0,2.0,3.0…

Summary

By using the method above, you can draw 24 sprites with each draw call, each with a unique position, rotation and scale. All sprites have to use the same texture though, so if you want to use different images for each sprite, you will have to use a sprite sheet which is a single texture with multiple images in it.

If you want to use sprite batching to do draw even more than 24 sprites in a single call you could for instance decrease the used uniform space by requiring all sprites to have constant scale or rotation and only vary by position. In this way, you could use a vec4 u_position[96] to draw 96 sprites with each call. Another option would be to make the mvp matrix multiplication before sending it to the shader. In this way you could draw a virtually unlimited number of sprites with each call, but I’m not sure this is a very good idea performance wise since it increases the load on the CPU before each call, and the GPU is usually a lot better at handling matrix multiplications.

So that’s it. Feel free to comment with suggested improvements or alternatives to this method! This is the method I’ve implemented in Rend, my OpenGL ES 2.0 framework for iOS.

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

About Me

I am developer based in Stockholm. For as long as I can remember I have been fascinated by code. My best lines of code are written at night.
Co-founder of Monterosa. Learning node.js at invoise.com.