Toolkitly

How to make a sprite sheet from individual images

Your animation software exports numbered PNGs. Your engine wants one texture. Combining them is easy; combining them so the engine can take them apart again is where the details are.

1. Why bother with a sheet at all

Two reasons, and only one of them is tidiness. The real one is draw calls: every time the renderer switches texture it has to issue a new batch to the GPU. Fifty sprites in fifty files means fifty switches per frame; the same fifty sprites in one sheet can be drawn in a single batch. On anything mobile that difference is the whole frame budget.

The second is loading. One HTTP request or one file read beats fifty, particularly on the web, where each request carries its own latency.

2. Frame order breaks more sheets than anything else

Your exporter wrote run_1.png through run_12.png. Sorted as text — which is what a file dialog, a shell glob and most packing tools do — that order is 1, 10, 11, 12, 2, 3… Pack that and the animation plays in nonsense order, and nothing about the sheet looks wrong until you hit play.

Two fixes, and you want both. Zero-pad the index when you export: run_01.png sorts correctly everywhere, forever. And use a tool that sorts numerically rather than alphabetically, so an unpadded set still comes out right. The Sprite Sheet Generator does the numeric sort on load and lets you drag any frame to a new position afterwards.

3. Cell size: the largest frame decides

In a grid sheet every cell is identical, and its size is that of the largest frame in the set. That is not laziness — it is what makes the sheet indexable. An animation player finds frame n by arithmetic: x = (n % columns) × cellWidth. The moment cells differ in size, that stops working and you need a manifest instead.

So the interesting question is where a smaller frame sits inside its cell. Centred is the default and right for effects, projectiles and anything symmetrical. Bottom-centred is what a character needs: if the frames of a walk cycle vary in height — and they will, once you add a bounce — centring them makes the sprite jitter vertically as it plays, because the feet land somewhere different in every frame. Aligning to the bottom of the cell pins them.

If the frames carry a lot of empty margin, trimming each one to its visible pixels before packing shrinks the cell size and often the sheet by a large factor. The trade is that each frame's origin moves, so only trim when the consumer reads per-frame offsets from a manifest.

4. Padding, and the stray line along your sprite

A sprite that shows a thin sliver of the neighbouring frame along one edge is suffering from texture bleeding. It happens when the GPU samples the sheet at a fractional coordinate — a scaled sprite, a rotated one, a camera sitting on half a pixel — and the sample strays past the frame boundary.

One or two transparent pixels of padding between cells fixes it outright: a stray sample picks up transparency instead of the wrong sprite. Add the same as a margin around the sheet if the sheet itself will be scaled. It costs a little area and saves an afternoon of confused debugging.

Power-of-two sheet dimensions are the other option people ask about. They matter much less than they used to — desktop and modern mobile GPUs take arbitrary sizes — but they are still worth setting for older mobile targets, some compressed texture formats, and any texture you intend to mipmap.

5. Grid or packed

Grid when the frames belong to an animation. Uniform cells, indexed by number, no manifest strictly required — this is what Godot's AnimatedSprite2D, Unity's Grid By Cell Size slicer and Phaser's load.spritesheet() all expect.

Packed when the sprites are unrelated and differently sized — UI icons, tiles, one-off props. A packer fits them together to waste as little space as possible, which means there is no grid to index and the JSON manifest carrying each frame's coordinates is not optional. Load it with this.load.atlas() or your engine's equivalent.

Do not pack an animation. Saving a few hundred kilobytes is not worth losing the ability to say "play frames 4 through 9".

6. Slicing it back apart in your engine

Godot: assign the PNG to an AnimatedSprite2D's SpriteFrames resource, open Select Frames, and enter the reported columns and rows. Set the import filter to Nearest for pixel art, or the engine will blur every frame the moment the sprite is scaled.

Unity: Sprite Mode Multiple, then Sprite Editor → Slice → Grid By Cell Size, entering the cell size and any padding you used. Filter Mode Point (no filter), Compression None.

Phaser: a grid sheet loads with this.load.spritesheet(key, url, { frameWidth, frameHeight }). If you used padding, pass spacing and margin in the same config object — leaving them out is the usual reason a padded sheet slices one pixel off.

Common problems

The animation plays in the wrong order

Alphabetical sorting of unpadded names. Re-sort numerically, or zero-pad the indices and export again.

Every frame has a sliver of the next one along an edge

Texture bleeding. Add one or two pixels of padding — and tell the engine about it, or the slicer will be off by that amount.

The character bobs up and down while walking

Frames of different heights centred in their cells. Switch to bottom-centre alignment.

The sheet is enormous for a handful of sprites

One oversized frame is setting the cell size for all of them. Trim the frames, or move the outlier to its own sheet.

The engine cannot find the frames in a packed sheet

There is no grid to find. Packed sheets need the JSON manifest and an atlas loader.

Build a sheet now