Why Games Still Use Sprite Sheets
Sprite sheets exist because of how GPUs batch work, not to save disk space — texture switches break batching, and the fixes all follow from that.
Loading 200 separate PNGs and loading one sheet containing all 200 images costs about the same in disk space and memory. So why does every 2D engine, from Unity to Godot to raw WebGL, push you toward packing sprites into a single sheet? The answer has nothing to do with file size. It's about how GPUs are fed.
Draw calls are the bottleneck, not pixels
A GPU renders in batches: the CPU submits a draw call — "draw these vertices with this texture and this shader" — and the GPU chews through it. The GPU side is absurdly fast; drawing 10,000 small quads in one call is trivial. The expensive part is the call itself: each one involves driver validation, state checks, and CPU-side bookkeeping. A frame budget that handles a handful of thousand-sprite draw calls comfortably can fall apart at thousands of one-sprite draw calls, even though the pixel count is identical.
So the engine's job is batching: collecting every sprite that can be drawn with the same state into one call.
Texture switches break batches
The catch is the word same state — and the piece of state that changes most often in a 2D game is which texture is bound. Every time the next sprite uses a different texture than the previous one, the engine has to flush the current batch, bind the new texture, and start a new call. Draw a player, then a coin, then a player, then a coin, each from its own PNG, and you get four draw calls for four sprites.
Pack player and coin into one sheet and the texture never changes: every sprite is just a different rectangle of UV coordinates into the same bound texture, and the whole scene collapses into one call. That is the entire reason sprite sheets (or texture atlases, the same idea generalized) exist.
Padding and extrusion: why sprites need breathing room
Packing images edge-to-edge creates a rendering bug that confuses everyone the first time: thin lines of the neighboring sprite flickering along edges. The cause is bilinear filtering — when the GPU samples a texture between texel centers, it blends the four nearest texels. At a sprite's border, some of those neighbors belong to the sprite packed next to it, and the blend drags foreign pixels in.
Two fixes, usually applied together:
- Padding — transparent gap (commonly 2px) between packed sprites, so the neighbors being blended are empty rather than wrong.
- Extrusion — duplicating each sprite's edge pixels outward into the gap, so edge samples blend with more of the same color instead of transparency (which otherwise darkens edges).
Mipmapping raises the stakes: each mip level averages larger and larger texture regions, so bleed that was 1px at full resolution can span a whole sprite a few levels down. Atlases meant for mipmapped rendering need more padding than atlases for pixel-perfect 2D.
Power-of-two sizes
Older GPU hardware — and the OpenGL ES 2 baseline that mobile and WebGL 1 inherited — restricts non-power-of-two textures: no mipmaps, no repeat wrapping, sometimes no support at all. GPU texture compression formats add their own alignment rules (some historically required square power-of-two inputs). Modern APIs have relaxed most of this, but atlas packers still default to 1024×1024, 2048×2048, and 4096×4096 because those sizes are safe everywhere and compress cleanly. The waste from rounding up to the next power of two is usually smaller than the compatibility problems from not doing it.
When not to atlas
Atlasing isn't free, and some textures don't belong in one:
- Full-screen art — a background that fills the screen alone gains nothing from sharing a texture, and it bloats the atlas past size limits for everything else.
- Tiling textures — repeat wrapping works per-texture, not per-region. A texture that needs to tile can't live inside an atlas rectangle.
- Memory grouping — everything in an atlas loads and stays resident together. Packing a menu screen's art with in-game sprites means both are always in memory.
- Frequently updated textures — anything re-uploaded at runtime (video frames, dynamic render targets) would force re-uploading the whole sheet.
The rule of thumb: atlas things that are drawn together, in the same scenes, at the same time.
If you're packing sprites by hand, Spellkit's sprite sheet generator handles the layout, padding, and frame metadata, and the sprite analyzer does the reverse — inspecting an existing sheet to recover its frame grid. Both run entirely in the browser.
