This approach works well for small sites with low overhead, but for games or other high-load websites, using droves of single image elements leads to long load times and slow performance, resulting in a poor end-user experience.
In an ecosystem where three seconds may cause you to lose half your users, it's important to use the proper tool to address this issue: texture atlasing.
What is texture atlasing?
In real-time graphics, a texture atlas is a single large image that contains many smaller sub-images, each of which is referenced independently. Texture atlases sprang up with the advent of 3D games, and have been around as long as we've had such games (for instance, here's one of the original lightmaps used in Quake 2).
Atlasing was originally used to accelerate the performance of 3D games, where there is significant overhead when swapping out or referencing new textures during the rasterization stages of a 3D pipeline. By combining all the smaller textures into one larger one, the graphics pipeline incurs less overhead from swapping, resulting in better performance.
A texture atlas from the HTML5 game GRITS. This single texture holds all the smaller images from the game that contain alpha values. Loading these images in a single texture reduces load time and improves runtime performance.
A side note on terminology: Many of you fancy HTML5 folks out there might say that a texture atlas is the same thing as a sprite sheet, but I don't believe that's correct. A sprite sheet is an atlas that contains only sprites.
In contrast, an atlas can contain many charts; each chart is a large image that holds a single type of graphic resource – e.g., sprites (for animation), UI textures (which are not animated), glyphs (for font-processing), and so on. An atlas is a technical concept, while a sprite sheet has a specific functional notion.
The benefits of using atlases in HTML5 games
Using a texture atlas in your HTML5 game can help your game load faster and run faster, and also reduces your bandwidth cost.
Faster HTTP load times
As I explained in my Google I/O talk, a 4k x 4k texture fetched from a server can take around 241ms to download, which is pretty fast. If you were to chop up that single download into 4096 separate requests, at 64×64 pixels each, the total load time changes drastically for the same number of pixels: It would increase from 241 ms to 4.3 seconds, an increase by a factor of 17x.
The timing charts below illustrate this difference graphically:
Fetching a 4096×4096 texture from a server requires a single HTTP request, and the request resolves quickly.
Fetching assets individually with multiple HTTP requests takes a long time. The long, lighter-shaded left-hand portion of the duration bars represent time where the browser blocked the connection because too many requests were being issued.
It's important to understand that there exists an upper limit on the number of requests that a browser can make to a single server. The browser itself sets this upper limit, and when the limit is reached, the browser blocks subsequent requests until an open connection becomes available.
This is the primary reason for the performance difference we see with individual assets versus atlased assets. If you have 4,000 pending HTTP requests, and only six connections are available, all the requests get stacked. In the figure above, the lighter-shaded portion of each duration bar is where Chrome blocked, waiting for an active connection to come along.
In addition to reducing overall load time, atlasing also helps reduce the number of HTTP requests from your app. This is a hyper-critical issue that the developers of HTML5 version of Command & Conquer found out the hard way: During development their app went viral, and their hosting service suspended their account until their request load dropped.
Reduced browser runtime overhead
Using individual texture assets can also have a very large impact on your game's runtime performance.
To use the cache properly, WebKit can detect when the system is resource-constrained (i.e., when the amount of available RAM is low), and instantiate a cache-eviction process to remove unneeded resources in an effort to improve performance.
For applications with a small number of DOM elements, the cache-eviction process is generally not an issue, but it can become a problem as the number of cacheable objects increases and the garbage collection algorithm spends more time looking for dead objects to reclaim.
For 2D, image-based games this can be a considerable problem, e.g., when 2,000+ animated sprites and background textures are loaded and referenced individually in a browser.
For these apps, images are generally the biggest offenders, and atlasing can really help: By combining the individual images into larger atlases, the number of unique cacheable resources decreases, allowing WebKit to spend less time in its cache-eviction process in resource-constrained environments.
Reduced GPU overhead
In Chrome, the 2D canvas element on a page has access to hardware acceleration if it's available. That means all of your draws, images, and transforms are handled by the GPU, which significantly improves performance. The catch, however, is that there are a few abstraction layers between your API calls and what the hardware is doing.
Working with a hardware-accelerated canvas is no different. Each texture must be bound to the GPU before the primitive quad can be drawn, and with a limited number of texture slots for a given GPU, there's quite a bit of time spent swapping textures in and out of the proper sampling units.
Atlasing reduces this overhead by allowing the GPU to bind a single texture (or a smaller number of textures) to the graphics driver, eliminating the extra overhead of the swap.
Reduced Memory footprint
With the exception of dumping the pixel data directly (ie, a .RAW file) most file formats used for game development come with additional header data that needs to be transferred with the file. This data is there, regardless of the dimensions of the file, or type of data inside it.
For instance, a DDS file, typically used for DirectX implementations of compressed textures, has a general overhead of 128 bytes for the header, which may not seem huge when compared to the data within it; However consider that 2000 loose texture files (not uncommon for a AAA game) would incur an extra overhead of 250k that needs to be transferred across the wire for each user loading your game.
Packing these textures into a single atlas removes the redundant overhead of these header bytes, reducing the overall size of your program.
Packing charts into an atlas
Creating a texture atlas is a tricky engineering task. Texture packing is a type of bin packing problem, which has been proven to be NP-hard. The problem is so challenging that I frequently use a variant of this algorithm as an interview question to evaluate the skills of potential hires.
TexturePacker is a great off-the-shelf tool that fits into most content pipelines quickly; it will generate the atlas data given a list of textures, alongside a data file that maps the individual source images to their final locations in the atlas.
If you're going to roll your own texture packer, I suggest you begin with some research. Here are some great places to start:
If you're looking to do highly complex packing (e.g., font glyph packing), most font rasterization libraries come with very aggressive atlas packers. Freetype-gl has one of the best ones I've seen; it uses the Rectangle Bin Pack algorithm.
Once your data has been packed into charts, you'll also need to output a mapping file that lists where each leaf-image (individual image) is in the atlas.
Using atlases in your game
Use of atlases in HTML5 games varies by game type:
A pure DOM game can use a CSS sprite sheet, and set CSS properties on a DOM image after loading the atlas data file.
A canvas game can use the canvas.drawImage API, specifying a subsection of an image to be drawn to the canvas location.
Web browsers are amazing programs: They abstract out the underlying complexities of loading and rendering web pages, hiding the under-the-hood machinery from the average web developer. For high-performance web games, however, this is less than ideal.
The layers of abstraction make it generally unclear how to organize data for maximum performance, and can lead to increased overhead. As with most development platforms, the key to high performance is a thoughtful understanding of what's really going on, and knowing what tools are available to aid in developing real-time applications.
Stepping back a bit, it's clear that HTML5 games have great promise. The ability to iterate quickly and deploy content across the Internet is a powerful incentive for developers. But it's important to approach HTML5 game development with the proper perspective.
Regardless of the unicorn dust of the web, working in HTML5 has many of the same pitfalls as console, mobile, and desktop development. Game developers need to continually explore and find great solutions for difficult problems.
For a take on HTML5 game development from the point of view of a traditional game developer, take a look at the following videos:
[This piece was reprinted from #AltDevBlogADay, a shared blog initiative started by @mike_acton devoted to giving game developers of all disciplines a place to motivate each other to write regularly about their personal game development passions.]