There are plenty of ways in which content – specifically textures in this case – can be optimized for memory and performance. While on a small team every developer should have a good idea of what kind of impact their craft has on others and be able to account for that in their work, it is also important that preemptively optimizing content does not take up time that could be better spent working on its quality.
On Backworlds, we accept any kind of format textures and when memory or performance is a concern we optimize them in a build step – normally I would save this kind of work until late in the project but since we have already released a prototype, a demo and run playtest builds the build system has grown to be pretty big. Read on for some of the things we do to automatically optimize textures.
Probably the simplest part of our texture optimizer is the channel removal. Some of this is content-agnostic and applicable everywhere, for instance if an RGB texture is only using grayscale it only needs a single luminance channel and textures that only have full alpha do not need to store an alpha channel. Our content optimizer analyzes all of the levels in the game though, so it can also determine what channels are actually used for a given texture – if an alpha channel has valid information but is unused in the game, we can still eliminate it. In addition, for Backworlds-specific uses like the HSLE mask texture we know that only two channels are used and can simplify. Looping over all the game content also makes sure that we do not include any unused files in a build.
Note that so far we have not used texture compression in Backworlds – I might rewrite it to evaluate the best option in these cases but chances are we will want to pass in the compressed format (DXT, most likely) directly rather than evaluate it automatically since it has an impact on the visuals.
Another simple part is the automatic image cropping – again, since we look at how all the textures are used we know if a texture can be cropped by looking at the alpha channel, the luminance or not at all. When necessary, we simply cut the borders of an image as far as we can without removing any pixels that are above a certain threshold (currently 7/255).
Emil “Humus” Persson presented a nice technique for optimizing billboarded geometry at Siggraph 2012, we have not had any real problems with fillrate in Backworlds just yet but I may decide to implement it when we get a better sense of how many particles we end up with.
Non-power-of-two textures are less of a problem today than they were ten years ago, but it’s still something of a performance concern and in order to fix it without having to care about the size of our sprites we have the option of packing our textures into atlases. There are freeware tools for doing this – some more efficient than our methods, I’m sure – but if you are using only axis-aligned boxes (which we are to avoid filtering issues) it is simple enough to get good results that I wrote a binpacker integrated into our optimizer.
The technique is fairly straightforward – start with a texture large enough to fit any individual texture in it. Preferably at least twice as wide and twice as high as your largest texture so you’ll see less waste. Then add the textures one by one to the atlas, dividing it up as you go in binary chunks until you are done – create more atlases as needed if you run out of space, but do not forget to check earlier atlases when inserting subsequent textures in case they fit where others did not.
The binpacker, like the name suggests, works by dividing up the atlas into a binary tree. For the first texture we add, we split the atlas into two nodes using line A into 0 and 1- 0 being the one containing the texture. Then we recurse and split up the 0 node again along line B and we should end up with a 00 node with the texture and two empty nodes in 01 and 1, we want the 1 node to be larger than the 01 one so we always do the first split along the axis that will give us the greatest size difference. For the next texture we then search the tree for all the leafnodes starting with the 0 nodes and insert it using the same method in the first node that can fit it – in the example, the 01 node is not big enough so we have to use 1. Again, we cut the node twice – preserving as large a space as possible intact in 11. For the third texture we start by searching the 0-side of the tree again, only this time the texture fits in node 01 and we split it. By always putting the filled nodes on 0 in the tree we make sure that the smallest nodes are searched first which tends to give us a better fill on the texture atlas.
In addition to this, the Backworlds binpacker tries to rotate textures 90 degrees before determining that they do not fit.
The order in which the textures are added to the atlases matter – I add larger textures first in order to not leave narrow slivers of atlas space early on. This means sorting textures by either largest side or area – binpacking is fairly fast so I just run both and use the result that gives me the least amount of unused space.
Texture atlases can’t be used without issue for textures that need clamping or unique settings, and since we bake our backgrounds the performance loss of using non-power-of-two textures is largely irrelevant for static art. In the end it might not be used on enough assets to be necessary for Backworlds, but it is an interesting solution to a tricky problem.