Just before I left on my small holiday, a couple of users on the #badlogic channel on IRC, myself included, started talking about optimizing the way sprites are rendered. As you probably already know, sprites consist of 4 vertices, which make up a rectangle. The square nature of a sprite can often lead to a lot of white space in your texture, which is then rendered transparent and this is a waste of fillrate. Ruben Garat of Gemserk then posted an interesting link, and quickly the interest was sparked in a few more people.

So Stefan Bachmann and I started talking, and after a quick meeting with Mario Zechner on skype and some even quicker pseudocode with google docs, we had a plan for how we would do this. Stefan got started on the API, and I got started on making a quick exporter in softimage so we could test this stuff out. However Aurelien Ribon has already created a small editor to make this a smooth ride. In any case, it wasn’t long before we had a test running, and we’ve had some great benchmarking results. Notably Stefan managed to draw 1600, 128×128 sprites at 30fps with PolygonSprite, compared to 600 the regular sprites at the same framerate on his Samsung Galaxy S2.

But even with those numbers, PolygonSpriteBatch should not be used everywhere, and I’ll try to explain where to use them and where NOT to use them.

 

First let’s look at where you should not use it.

For the first tests I’ll be using a tree texture. This texture will be scaled to different sizes, but all of them will use the same shape. Below you can see the tree texture and the shape created in the polygon editor.

 

I’ll be switching between SpriteBatch and PolygonSpriteBatch to compare the two at the various use cases. All tests will be done on a Samsung Galaxy Nexus.

Case one
1000 sprites at 32×32 pixels.

  • PolygonSpriteBatch : 26 fps.
  • SpriteBatch : 57 fps.

Not exactly the best use case. The sprites are too small, and suddenly the distance between the vertices on the polygon shape is smaller than the pixels, so you end up having lots of data that is pretty much wasted, also since the size is so small, the white space areas are suddenly much smaller.
 

Case two
1000 sprites at 64×64 pixels.

  • PolygonSpriteBatch : 26 fps.
  • SpriteBatch : 30 fps.

Notice how PolygonSpriteBatch is the same as it was in Case One, but SpriteBatch has suddenly dropped to half. Still, the sprites are only 64×64 and SpriteBatch pulls out ahead.

Case three
500 sprites at 128×128 pixels.

  • PolygonSpriteBatch : 30 fps.
  • SpriteBatch : 15 fps.

I lowered the sprite count in this test on purpose so I could get the fps result a little more stable, even if I’m only rendering half the amount of sprites, pixel density is a lot higher than when the texture was 64×64, so it’s far heavier in this test. The interesting thing is how big the difference in fps is.

Now these three cases have shown, that you want to use PolygonSpriteBatch only if your textures are relatively large, and that the gain from doing this can be quite big. It will vary from device to device, and some will be better suited for it than others, but so far the results have been positive on any device we have tested.
There are however a few cases where it will also be beneficial to use this for small sprites. I won’t do benchmark tests for those, but just list them.

Use if

  • Large texture with a good amount* of white space.
  • Large texture with a small amount of white space, but the polygon shape has either 3 or 4 vertices.
  • Small texture with a good amount of white space, and the polygon shape has either 3 or 4 vertices.

Do not use if

  • Large texture with little white space.
  • Small texture with a good amount of whitespace that can not be shaped with less than 3 or 4 vertices.

*A Good amount is probably 35-40% or more, haven’t done extensive testing yet. As soon as I know a good ratio for this I’ll make sure to do another post.

I’ll leave you with the code for the last test, and the asset files.

Also you can find Aurelion Ribon’s polygon editor here.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.FPSLogger;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.PolygonRegion;
import com.badlogic.gdx.graphics.g2d.PolygonSprite;
import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;
 
public class PolygonSpriteBatchTest implements ApplicationListener, InputProcessor {
 
    OrthographicCamera camera;
    Texture texture;
    TextureRegion textureRegion;
    PolygonRegion pregion;
    PolygonSpriteBatch pbatch;
 
    boolean switcher = false;
 
    SpriteBatch batch;
 
    FPSLogger fps = new FPSLogger();
 
    Array<PolygonSprite> psprites = new Array<PolygonSprite>();
    Array<Sprite> sprites = new Array<Sprite>();
 
    @Override
    public void create() {      
 
        Gdx.input.setInputProcessor(this);
        camera = new OrthographicCamera(800, 480);
        texture = new Texture(Gdx.files.internal("tree128x128.png"));
        textureRegion = new TextureRegion(texture);
        pregion = new PolygonRegion(textureRegion, Gdx.files.internal("tree.psh"));
 
        camera.position.x = 400;
        camera.position.y = 240;
 
        pbatch = new PolygonSpriteBatch();
        batch = new SpriteBatch();
 
        int count = 500;
 
        for(int i=0; i<count; i++){
            PolygonSprite sprite = new PolygonSprite(pregion);
            sprite.setPosition(MathUtils.random(-10, 700), MathUtils.random(0, 360));
            sprite.setColor(MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), 1.0f);
 
            psprites.add(sprite);
        }
 
        for(int i=0; i<count; i++) {
            Sprite sprite = new Sprite(textureRegion);
            sprite.setPosition(MathUtils.random(-20, 740), MathUtils.random(0, 400));
            sprite.setColor(MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), 1.0f);
 
            sprites.add(sprite);
        }
    }
 
    @Override
    public void dispose() {
 
    }
 
    @Override
    public void render() {
 
        Gdx.gl.glClearColor(0.2f, 0.2f, 0.2f, 1);
        Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
        camera.apply(Gdx.gl10);
        camera.update();
 
        pbatch.setProjectionMatrix(camera.combined);
        batch.setProjectionMatrix(camera.combined);
 
        if(switcher == false) {
            pbatch.begin();
            for(int i=0;i<psprites.size;i++) {
                PolygonSprite sprite = psprites.get(i);
 
                sprite.draw(pbatch);
 
            }
            pbatch.end();
        } else {
 
            batch.begin();
            for(int i=0;i<sprites.size;i++) {
                Sprite sprite = sprites.get(i);
 
                sprite.draw(batch);
            }
            batch.end();
 
        }
        fps.log();
    }
 
    @Override
    public void resize(int width, int height) {
    }
 
    @Override
    public void pause() {
    }
 
    @Override
    public void resume() {
    }
 
    @Override
    public boolean keyDown(int keycode) {
        // TODO Auto-generated method stub
        return false;
    }
 
    @Override
    public boolean keyUp(int keycode) {
        // TODO Auto-generated method stub
        return false;
    }
 
    @Override
    public boolean keyTyped(char character) {
        // TODO Auto-generated method stub
        return false;
    }
 
    @Override
    public boolean touchDown(int x, int y, int pointer, int button) {
        //if(Gdx.input.justTouched()) {
            switcher = !switcher;
        //}
        return false;
    }
 
    @Override
    public boolean touchUp(int x, int y, int pointer, int button) {
        // TODO Auto-generated method stub
        return false;
    }
 
    @Override
    public boolean touchDragged(int x, int y, int pointer) {
        // TODO Auto-generated method stub
        return false;
    }
 
    @Override
    public boolean touchMoved(int x, int y) {
        // TODO Auto-generated method stub
        return false;
    }
 
    @Override
    public boolean scrolled(int amount) {
        // TODO Auto-generated method stub
        return false;
    }
}
import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.graphics.FPSLogger;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.PolygonRegion;
import com.badlogic.gdx.graphics.g2d.PolygonSprite;
import com.badlogic.gdx.graphics.g2d.PolygonSpriteBatch;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.utils.Array;

public class PolygonSpriteBatchTest implements ApplicationListener, InputProcessor {

	OrthographicCamera camera;
	Texture texture;
	TextureRegion textureRegion;
	PolygonRegion pregion;
	PolygonSpriteBatch pbatch;

	boolean switcher = false;

	SpriteBatch batch;

	FPSLogger fps = new FPSLogger();

	Array<PolygonSprite> psprites = new Array<PolygonSprite>();
	Array<Sprite> sprites = new Array<Sprite>();

	@Override
	public void create() {		

		Gdx.input.setInputProcessor(this);
		camera = new OrthographicCamera(800, 480);
		texture = new Texture(Gdx.files.internal("tree128x128.png"));
		textureRegion = new TextureRegion(texture);
	    pregion = new PolygonRegion(textureRegion, Gdx.files.internal("tree.psh"));

	    camera.position.x = 400;
	    camera.position.y = 240;

		pbatch = new PolygonSpriteBatch();
		batch = new SpriteBatch();

		int count = 500;

		for(int i=0; i<count; i++){
	        PolygonSprite sprite = new PolygonSprite(pregion);
	        sprite.setPosition(MathUtils.random(-10, 700), MathUtils.random(0, 360));
	        sprite.setColor(MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), 1.0f);

	        psprites.add(sprite);
		}

		for(int i=0; i<count; i++) {
			Sprite sprite = new Sprite(textureRegion);
			sprite.setPosition(MathUtils.random(-20, 740), MathUtils.random(0, 400));
	        sprite.setColor(MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), MathUtils.random(0.6f, 1), 1.0f);

	        sprites.add(sprite);
		}
	}

	@Override
	public void dispose() {

	}

	@Override
	public void render() {

		Gdx.gl.glClearColor(0.2f, 0.2f, 0.2f, 1);
		Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
		camera.apply(Gdx.gl10);
		camera.update();

		pbatch.setProjectionMatrix(camera.combined);
		batch.setProjectionMatrix(camera.combined);

		if(switcher == false) {
			pbatch.begin();
			for(int i=0;i<psprites.size;i++) {
				PolygonSprite sprite = psprites.get(i);

				sprite.draw(pbatch);

			}
			pbatch.end();
		} else {

			batch.begin();
			for(int i=0;i<sprites.size;i++) {
				Sprite sprite = sprites.get(i);

				sprite.draw(batch);
			}
			batch.end();

		}
		fps.log();
	}

	@Override
	public void resize(int width, int height) {
	}

	@Override
	public void pause() {
	}

	@Override
	public void resume() {
	}

	@Override
	public boolean keyDown(int keycode) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean keyUp(int keycode) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean keyTyped(char character) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean touchDown(int x, int y, int pointer, int button) {
		//if(Gdx.input.justTouched()) {
			switcher = !switcher;
		//}
		return false;
	}

	@Override
	public boolean touchUp(int x, int y, int pointer, int button) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean touchDragged(int x, int y, int pointer) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean touchMoved(int x, int y) {
		// TODO Auto-generated method stub
		return false;
	}

	@Override
	public boolean scrolled(int amount) {
		// TODO Auto-generated method stub
		return false;
	}
}
Keep in mind the PolygonSpriteBatch is still work in progress, there are a couple of things that will most likely be refined to make it even better, and some of the code will be moved to native eventually.