One of the recurring compliments that Swivel gets is that it runs very smooth. This was not an accident. For a puzzle game there are surprisingly many things that are potentially rendered in a single frame: several layers of blocks, shadows, crossfading backgrounds, particles, explosions, double-layered fonts, etc. One of my early goals was to have rendering be very fast. I wanted the game to ooze smoothness. I wanted 60 frames per second whenever possible.
And that was when I still had an iPhone 3G.
It turns out the key to achieving this is quite simple, and is the subject of this post.
Since I was writing my own rendering engine, I had the advantage of direct control over OpenGL and could therefore optimize to my heart's content. One of the basic principles of optimizing with OpenGL is to eliminate state changes. OpenGL is essentially a state-based system. You set a bunch of states and render, set a bunch of states and render.
Naturally, one can only reduce rendering so much. But what may not be obvious to the OpenGL novice is that state changes can be more expensive than rendering in certain cases. This lends itself to the obvious optimization to group calls together that share similar rendering state. This is often not hard to do: render shadows at the same time, render particles at the same time, and so on.
But there is something even more important you can and should do.
One of the more expensive state changes, at least on mobile hardware, is texture binding.
If you can eliminate all texture binding, except for once — and only once — at the beginning of, say, a level change, or start of a game, or even better at the start of your application as a whole — well my friend, that's a big win.
Guess what Swivel does?
Yep. It loads everything at application launch. Every bit of graphics the game needs (with the exception of backgrounds) is squeezed into one giant texture. And here is what it looks like scaled down:
Swivel actually has two of these. One 1024×1024 “low resolution” texture, and one 2048×2048 texture for retina display devices.
Every image in there is packed with a one pixel gap in between to prevent bleeding when OpenGL looks up pixel values. The gray color is actually transparent — I simply filled it in to make things more visible for this illustration.
You'll note immediately that I have my fonts packed in there as sub-atlases. This is important. Fonts need textures too and, even if it is small, binding it for text rendering and then binding the main atlas for everything else alternately is expensive. Honestly, had space been in greater demand I could have had the font glyphs packed individually instead of in sub-atlases, but it worked out okay.
Another thing you'll notice is those placeholder textures. That's where I stick my backgrounds. I load those and draw them into the texture on level changes. This is a necessary evil because Swivel has twenty-some backgrounds and unfortunately loading them causes a slight delay (which the level change cleverly masks) but it ultimately pays off. It's better than having a separate atlas for every level which would take longer to load and waste precious storage bytes.
I have two background placeholders so I can crossfade between them. One is slightly larger than the other to hold the menu background.
It's surprising just how much you can fit into one texture if you get creative about reusing things. Several items are monochrome and get colored as they are rendered — the fonts for example. Fonts in Swivel have two layers to let the inner layer be whatever color I want while the outer layer stays white.
As it is, some of those images are a bit extravagant (four huge button images!) and I could have been more judicious, but I tended to only do so as necessary to let Swivel have as much gorgeous artwork as possible. In the end, there was still room left!
So here's the takeaway:
Texture atlases are awesome.
If you're not using texture atlases already, shame on you.
If you are, and if it is possible to use one and only one, you should.
The speed boost is great. Never needing to stall to load textures makes for a better user experience. And having every graphic at your fingertips, without concern about whether it is loaded or needs to be bound, is fantastic.
This post is part of iDevBlogADay, a group of blogs by indie iPhone developers featuring two posts per day. You can subscribe to iDevBlogADay through RSS or follow the #iDevBlogADay hash tag or @idevblogaday on Twitter.