Texture atlases for 2D games is a great optimization for batching together tons of different sprites (especially quads) with a very few number of draw calls. Making them is a pain. For pixel art or other 2D art, DXT compression is often not a great choice due to the lossyness. One good way to make atlases for 2D games is with the PNG format. Single-file loaders and savers can be used to load and save PNGs with a single function call each.

Here’s an example with the popular stb_image, along with stb_image_write:

#define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" int w; int h; int comp; unsigned char* image = stbi_load( path, &w, &h, &comp, STBI_rgb_alpha ); ... stbi_write_png( path, w, h, comp, image, 4 ); 1 2 3 4 5 6 7 8 9 10 11 12 13 #define STB_IMAGE_IMPLEMENTATION #include "stb_image.h" #define STB_IMAGE_WRITE_IMPLEMENTATION #include "stb_image_write.h" int w ; int h ; int comp ; unsigned char * image = stbi_load ( path , &w , &h , &comp , STBI_rgb _ alpha ) ; . . . stbi_write_png ( path , w , h , comp , image , 4 ) ;

This requires two different headers, one for loading and one for saving. Additionally the author of these headers also has a bin packing header, which could be used to make a texture atlas compiler.

However I have created a single-file header called tinydeflate. It does PNG loading, saving, and can create a texture atlas given an array of images. Internally it contains its own bin packing algorithm for sorting textures and creating atlases. Here’s an example:

#define TINYDEFLATE_IMPL #include "tinydeflate.h" const char* names = { "path/image0.png", "path/image1.png" }; tdImage img0 = tdLoadPNG( names[ 0 ] ); tdImage img1 = tdLoadPNG( names[ 1 ] ); ... int w = 64; int h = 64; int count = 2; tdMakeAtlas( "atlas.png", "atlas.txt", w, h, pngs, count, names ); 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #define TINYDEFLATE_IMPL #include "tinydeflate.h" const char * names = { "path/image0.png" , "path/image1.png" } ; tdImage img0 = tdLoadPNG ( names [ 0 ] ) ; tdImage img1 = tdLoadPNG ( names [ 1 ] ) ; . . . int w = 64 ; int h = 64 ; int count = 2 ; tdMakeAtlas ( "atlas.png" , "atlas.txt" , w , h , pngs , count , names ) ;

The above example (if given a few more images) could output an atlas like so:

Followed by a very easy to parse text formatted file containing uv information for each atlas:

atlas.png 8 { "imgs/1x1.png", w = 16, h = 32, u = { 0.0000000000, 0.4998779297 }, v = { 0.2498779297, 0.0000000000 } } { "imgs/4x4.png", w = 16, h = 32, u = { 0.0000000000, 0.9998779297 }, v = { 0.2498779297, 0.5000000000 } } { "imgs/debug_tile.png", w = 16, h = 32, u = { 0.2500000000, 0.4998779297 }, v = { 0.4998779297, 0.0000000000 } } { "imgs/default.png", w = 18, h = 20, u = { 0.5000000000, 0.3123779297 }, v = { 0.7811279297, 0.0000000000 } } { "imgs/house_blue.png", w = 16, h = 16, u = { 0.2500000000, 0.7498779297 }, v = { 0.4998779297, 0.5000000000 } } { "imgs/house_red.png", w = 4, h = 4, u = { 0.2500000000, 0.8123779297 }, v = { 0.3123779297, 0.7500000000 } } { "imgs/house_yellow.png", w = 2, h = 2, u = { 0.2500000000, 0.8436279297 }, v = { 0.2811279297, 0.8125000000 } } { "imgs/squinkle.png", w = 1, h = 1, u = { 0.2812500000, 0.8280029297 }, v = { 0.2967529297, 0.8125000000 } } 1 2 3 4 5 6 7 8 9 10 11 atlas.png 8 { "imgs/1x1.png", w = 16, h = 32, u = { 0.0000000000, 0.4998779297 }, v = { 0.2498779297, 0.0000000000 } } { "imgs/4x4.png", w = 16, h = 32, u = { 0.0000000000, 0.9998779297 }, v = { 0.2498779297, 0.5000000000 } } { "imgs/debug_tile.png", w = 16, h = 32, u = { 0.2500000000, 0.4998779297 }, v = { 0.4998779297, 0.0000000000 } } { "imgs/default.png", w = 18, h = 20, u = { 0.5000000000, 0.3123779297 }, v = { 0.7811279297, 0.0000000000 } } { "imgs/house_blue.png", w = 16, h = 16, u = { 0.2500000000, 0.7498779297 }, v = { 0.4998779297, 0.5000000000 } } { "imgs/house_red.png", w = 4, h = 4, u = { 0.2500000000, 0.8123779297 }, v = { 0.3123779297, 0.7500000000 } } { "imgs/house_yellow.png", w = 2, h = 2, u = { 0.2500000000, 0.8436279297 }, v = { 0.2811279297, 0.8125000000 } } { "imgs/squinkle.png", w = 1, h = 1, u = { 0.2812500000, 0.8280029297 }, v = { 0.2967529297, 0.8125000000 } }

The nice thing about tinydeflate is the function to create atlases has a little bit of anti UV bleeding code inside of it. Most places on the internet will tell you to solve texture atlas bleeding by adding padding pixels around your textures. This might be important for 3D applications or when mip-mapping is important, however, I don’t have this kind of 3D experience. For 2D pixel art games it seems squeezing UVs ever so gently inside the borders of a texture works quite well, and so tinydeflate takes this approach. No complicated padding necessary.

And finally here’s an older video I did with some more information on writing the code yourself to do some of the above tasks.