Iterative "KIS(S)" - The Sprite Atlas
Have you ever played Tetris?
As was covered in an earlier article, Asteroid Arena was intended to have a very simple line-drawing based representation. When this changed, so did the way we dealt with graphics resources - in particular images. Ships, asteroids, menu items… well almost all you see in the game screen is from an image, made by the artist.
Why is this a bigger issue than a bunch of lines? A bunch of lines is basically an image, so why the different treatment?
A "bunch of lines" is usually tens or even houndreds of lines, a "bunch of pixels" is usually a couple of thousands or even tens of thousands of pixels. As the data, and amount of data, changed - so did the implementation.
The GPU doesn't like many images (i.e. textures), in a perfect world you would have at most one. So this is the starting point - have just one image. But the artist doesn't make "just one image", so how would this be possible? Easy, just take all images and copy-paste them to a single large one! How big? Well… 2048 x 2048 pixels seems to be a good number. This is how we started our first attempt, put all images in one big image.
How do you put multiple images in one? There are two ways, ask the artist to do it - or ask the programmer to do it. UNDER NO CIRCUMSTANCE SHOULD YOU ASK THE ARTIST TO DO THIS, an artist is way too busy doing actual art - there is no time to solve issues that could be solved with a bit of programming. Thankfully for the programmer (ah yea, that's me) this is a very studied problem called the bin packing problem. There's a nice overview of some solutions here by Jukka Jylänki.
Our initial Keep It Simple attempt to solve the issue for Asteroid Arena was to pack all images to a single 2048 x 2048 texture using the Skyline algorithm. This is done when building the game to avoid any startup delays for the player, nobody likes watching a loading screen after all, and it worked! For a very, very long time (over a year if I remember correctly) all images we had in the game fit a single 2048 x 2048 image.
This became a problem first when we took a closer look on the ships, they looked slightly lower quality when rotated than what was authored, an issue usually solved by oversampling the image. However, oversampling means increasing the size of the image, at this point something that wasn't possible as the 2048 x 2048 pixel limit was reached. For the art, scaling up the graphics is trivial as everything is done in vector graphics, but the rasterized graphics exceeded the texture size limit required by the software.
I've seen this problem many times before and it's usually solved by specifying what images will be used in what "scenes", then the scenes get their own texture and those are then pre-loaded and used when displayed. In practice this means you need to support multiple textures and deal with all the issues that come along with it. This also means somebody needs to make "scenes" and connect them together for pre-loading (are you going to ask the artist to do this?).
Asteroid Arena has very few scenes: main menu, the arena itself, tutorial and speed run. The problem is even the tutorial itself contains so many images it doesn't fit the 2048 x 2048 limit! So then the tutorial would have to be split up into multiple scenes, preloaded and… yea, this would've been a tremendously complex solution to the problem. Wouldn't it still be possible to fit everything that is actually drawn every frame to a single texture?
All images you see at any point on the screen when playing Asteroid Arena would fit in a 2048 x 2048 texture. I don't want to separate the game into a "scene"-like structure, and I'm not going to ask "the artist" to do the busy work - so why not just stream the image data into the texture? From file, to CPU and then to GPU. Erhm, sounds like a huge amount of work for the computer to do in the middle of gameplay. How much work? Well, so small it didn't even show up on the profiler!
Asteroid Arena uses a single atlas, streaming in and out what is needed based on what you see on the screen. This means a fixed amount of GPU memory is used, nobody needs to separate the game into scenes, and adding new images doesn't affect the memory footprint of the game. This makes it easy to add new images, both for the artist and the programmer.
Oh and if the artist adds an image wider or taller than 2048 pixels the game crashes 😂
- Jens the Programmer, February 2022