PolygonSpriteBatch, where should it be used
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;
}
}







[...] integrating it after a quick design round via Skype/Google Docs. You can find more information at http://www.fainted.dk/?p=142 Comment (RSS) [...]