Skip to content
AI Sprite Maker

Formats

Sprite Sheet Formats Explained: PNG + JSON, Aseprite, TexturePacker and Godot SpriteFrames

Published November 6, 202610 min read
On this page
  1. Two kinds of sheet
  2. Grid sheets (strips)
  3. Packed atlases
  4. The common JSON atlas format (TexturePacker style)
  5. Aseprite’s JSON
  6. Godot 4 SpriteFrames (.tres)
  7. Unity
  8. GIF, APNG and WebM are previews, not engine formats
  9. Which engine reads what
  10. Padding, extrusion and texture size
  11. Converting between formats

A sprite sheet is one image with many frames on it. The image is the easy part. The hard part is the data that says where each frame is, how long it lasts, and which frames make up “walk”. Every tool has its own way of writing that down, and most import problems come from a mismatch between what one tool wrote and what the next one expects.

This is a field guide to the formats you’ll actually run into, with minimal real examples of each, so you can recognise a file when you open it and know what your engine needs.

Two kinds of sheet

Grid sheets (strips)

Every frame is the same size and sits in a fixed grid, often one animation per row. The only metadata you need is:

  • frame width and height,
  • columns and rows (or frame count),
  • optionally margin (around the sheet) and spacing (between frames),
  • the frame rate, and which row is which animation.

That’s little enough to type into an import dialog, which is why grids work everywhere: Godot’s “Add frames from sprite sheet”, Unity’s Grid By Cell Size, Phaser’s load.spritesheet, GameMaker’s strip import, Aseprite’s Import Sprite Sheet.

The cost is wasted space: every frame takes the size of the largest one, so a sword swing that sticks out on two frames makes all frames wide. For a few characters that doesn’t matter. For hundreds of animations on mobile it might.

A real grid strip, ten equal 63 × 84 cells:

A 10-frame attack as a horizontal strip. The sword sticks out on a few frames, so every cell has to be wide enough for the widest one; that's the space an atlas would save.

Packed atlases

Frames are packed tightly in any arrangement, often trimmed (transparent borders cut away) and sometimes rotated 90° to fit better. Because nothing can be inferred from position, an atlas always comes with a data file listing every frame’s rectangle, plus enough information to put trimmed frames back in the right place.

Atlases save memory and let you put many characters, UI pieces and effects in one texture, which means fewer draw calls. They need tooling: you don’t edit an atlas by hand.

Grid sheet equal cells · frame size + columns is enough downleftrightup Packed atlas trimmed frames · needs JSON with every rect
Left: a grid sheet, where position alone tells you which frame is which. Right: a packed atlas, where frames of different sizes are fitted together and only the JSON knows where each one is.

The common JSON atlas format (TexturePacker style)

TexturePacker’s JSON became a de facto standard. Phaser, PixiJS and many other tools read it, and Aseprite writes it. It comes in two variants that differ only in how frames is stored.

JSON Hash: frames is an object keyed by frame name.

{
  "frames": {
    "walk_down_000.png": {
      "frame": { "x": 2, "y": 2, "w": 38, "h": 60 },
      "rotated": false,
      "trimmed": true,
      "spriteSourceSize": { "x": 13, "y": 4, "w": 38, "h": 60 },
      "sourceSize": { "w": 64, "h": 64 },
      "pivot": { "x": 0.5, "y": 1 }
    }
  },
  "meta": {
    "app": "https://www.codeandweb.com/texturepacker",
    "version": "1.0",
    "image": "hero.png",
    "format": "RGBA8888",
    "size": { "w": 512, "h": 256 },
    "scale": "1"
  }
}

JSON Array: frames is a list, and each entry carries its own filename.

{
  "frames": [
    {
      "filename": "walk_down_000.png",
      "frame": { "x": 2, "y": 2, "w": 38, "h": 60 },
      "rotated": false,
      "trimmed": true,
      "spriteSourceSize": { "x": 13, "y": 4, "w": 38, "h": 60 },
      "sourceSize": { "w": 64, "h": 64 }
    }
  ],
  "meta": { "image": "hero.png", "size": { "w": 512, "h": 256 }, "scale": "1" }
}

What the fields mean:

FieldMeaning
frameWhere the frame is in the atlas image (after trimming), in pixels from the top-left
rotatedtrue if the packer turned the frame 90° clockwise to fit; frame.w/h are then the unrotated size
trimmedtrue if transparent borders were removed
spriteSourceSizeWhere the trimmed rectangle sits inside the original frame. x: 13, y: 4 means “draw this 13 px right and 4 px down from the original frame’s corner”
sourceSizeThe original, untrimmed frame size. Use this for layout so trimmed frames don’t jitter
pivotOptional normalised anchor (0–1). {x: 0.5, y: 1} is bottom centre
meta.imageThe atlas image file name, relative to the JSON
meta.scaleScale the atlas was exported at; used for @2x variants

Array preserves frame order explicitly; Hash relies on the JSON object’s key order, which most parsers keep but don’t strictly guarantee. If a tool offers both, Array is the safer choice for animations.

Trimming and jitter

If your engine ignores spriteSourceSize and sourceSize, trimmed frames are drawn from their trimmed corner, so the character shifts a few pixels every frame. If you see that, either make the importer respect those fields or export without trimming.

Aseprite’s JSON

Aseprite’s File → Export Sprite Sheet writes the same structure (Hash or Array), with additions that matter for animation:

{
  "frames": [
    {
      "filename": "hero 0.aseprite",
      "frame": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "rotated": false,
      "trimmed": false,
      "spriteSourceSize": { "x": 0, "y": 0, "w": 64, "h": 64 },
      "sourceSize": { "w": 64, "h": 64 },
      "duration": 100
    }
  ],
  "meta": {
    "app": "https://www.aseprite.org/",
    "version": "1.3.x",
    "image": "hero.png",
    "format": "RGBA8888",
    "size": { "w": 512, "h": 256 },
    "scale": "1",
    "frameTags": [
      { "name": "walk_down", "from": 0, "to": 7, "direction": "forward" },
      { "name": "walk_left", "from": 8, "to": 15, "direction": "forward" }
    ],
    "layers": [{ "name": "Layer 1", "opacity": 255, "blendMode": "normal" }],
    "slices": []
  }
}
  • duration is per frame, in milliseconds. Aseprite lets each frame have its own timing (a long anticipation frame before a fast swing), which a single fps value can’t express.
  • frameTags name ranges of frames. direction is forward, reverse or pingpong (newer versions also have pingpong_reverse). Tags are how one file holds a whole character.
  • layers and slices appear when you enable them in the export options. Slices are named rectangles, often used for hitboxes or 9-slice UI.

From the command line, the same export looks like this (useful in a build script):

aseprite -b hero.aseprite \
  --sheet hero.png --data hero.json \
  --format json-array --sheet-type packed \
  --list-tags --list-slices

Add --split-tags with a {tag} placeholder in the output name to get one file per tag, or --trim to trim frames. Check aseprite --help for your version, since flags are added over time.

Phaser can load this directly with this.load.aseprite() and create every tagged animation with this.anims.createFromAseprite(); see Phaser 3 sprite sheet animation. Other engines need a plugin or a small script that turns frameTags into animations.

Godot 4 SpriteFrames (.tres)

Godot doesn’t import JSON atlases natively. It stores animations in a SpriteFrames resource, played by AnimatedSprite2D. You can build one in the editor, or ship it as a text resource (.tres) alongside the sheet. A minimal Godot 4 SpriteFrames with one looping animation referencing regions of a single sheet:

[gd_resource type="SpriteFrames" load_steps=4 format=3]

[ext_resource type="Texture2D" path="res://sprites/hero_walk.png" id="1_sheet"]

[sub_resource type="AtlasTexture" id="AtlasTexture_0"]
atlas = ExtResource("1_sheet")
region = Rect2(0, 0, 64, 64)

[sub_resource type="AtlasTexture" id="AtlasTexture_1"]
atlas = ExtResource("1_sheet")
region = Rect2(64, 0, 64, 64)

[resource]
animations = [{
"frames": [{
"duration": 1.0,
"texture": SubResource("AtlasTexture_0")
}, {
"duration": 1.0,
"texture": SubResource("AtlasTexture_1")
}],
"loop": true,
"name": &"walk_down",
"speed": 10.0
}]

Things to know:

  • load_steps is the number of external plus sub resources plus one. Godot fixes it up when it saves the file, so an off-by-one just gets corrected.
  • Each frame is an AtlasTexture pointing at a region of the sheet, so the sheet is loaded once.
  • speed is frames per second; each frame’s duration is a multiplier on that, not milliseconds. A duration of 2.0 holds a frame twice as long. That’s how Aseprite-style variable timing maps onto Godot.
  • &"walk_down" is a StringName literal. Godot 3’s format (format=2) is different and not interchangeable.
  • The ext_resource path is res://-absolute. Move the PNG outside the editor and Godot asks you to fix the dependency.

AI Sprite Maker’s Godot export writes this format with one animation per direction. Setup and code are in Using our sheets in Godot and How to import a sprite sheet in Godot 4.

Unity

Unity keeps sprite rectangles in the texture’s .meta file, not in a separate data file. When you slice in the Sprite Editor, Unity writes each sprite’s rect, pivot and name into the meta. Animations are separate .anim clips that reference those sprites.

So “a Unity sprite sheet” is really the PNG, its .meta, and the clips. To import a JSON atlas you need an importer (TexturePacker ships one) or an editor script that reads the JSON and writes sprite rects through the 2D Sprite package’s data provider. Remember that Unity rects start at the bottom-left of the texture while JSON atlases start at the top-left. Details in Sprite sheets in Unity.

GIF, APNG and WebM are previews, not engine formats

Animated GIFs are great for showing an animation in a store page, a tweet or a Discord message. They’re a poor source format: 256 colours per frame, 1-bit transparency (edges get jaggies or halos), frame delays in hundredths of a second, and no metadata about directions or names. APNG and WebP fix colour and transparency but are still single animations with no game metadata. WebM with alpha is good for large effects in some engines, not for pixel-art sprites.

Keep the PNG sheet as your source of truth and generate GIFs from it. If all you have is a GIF, the free GIF to Sprite Sheet tool unpacks it into a clean sheet.

Which engine reads what

Engine / frameworkGrid sheetJSON atlas (TexturePacker style)Aseprite JSONNative animation data
Godot 4Yes (SpriteFrames editor, Sprite2D hframes/vframes)Via plugin or scriptVia plugin or scriptSpriteFrames .tres
UnityYes (Sprite Editor grid slice)Via TexturePacker importer or scriptVia Aseprite Importer for .aseprite files.meta + .anim
Phaser 3load.spritesheetload.atlas (Hash and Array)load.aseprite + createFromAsepriteAnimation Manager config
PixiJSVia a small scriptAssets.load spritesheet JSONMostly (it’s the same frame format; tags need code)animations key in the JSON
GameMakerStrip import (_stripN names)Not nativelyNot nativelySprite resources
DefoldTile sourceVia extensionVia extensionAtlas / tile source animations
Construct 3Import from sprite stripNot nativelyNot nativelyAnimations editor
LÖVEQuads from a grid (e.g. anim8)Via a libraryVia a libraryNone built in

The safe default is still a grid PNG with a small JSON sidecar (frame size, columns, rows, fps, row names). Every engine can load it, and every format above can be generated from it.

Padding, extrusion and texture size

Three settings in packers that matter more than they look:

  • Padding (or shape padding): empty pixels between frames. With nearest-neighbour filtering and integer scaling you can get away with 0. Add 2 px if your game ever scales by non-integer amounts, rotates sprites, or uses mip maps, or neighbouring frames bleed in as thin lines.
  • Extrude: repeats each frame’s edge pixels outward by 1 px. It fixes bleeding at the edges of tiles that touch, where padding alone shows a gap.
  • Max texture size and power of two: older GPUs and some mobile compression formats want power-of-two textures (256, 512, 1024…). Most modern targets don’t care, but staying at or under 2048 × 2048 per atlas is still a sensible default for mobile and web.

Converting between formats

You rarely need a paid tool for this:

If you generate sprites with AI Sprite Maker, every job can be exported as a PNG sheet, PNG + JSON, Godot SpriteFrames, Aseprite-style JSON with a tag per direction, a GIF preview or a frames ZIP, so you can pick the format for your engine instead of converting. More in Exporting sprites to your engine.

Sprites on this site were made with AI Sprite Maker

Tools used in this guide

All guides

Found a mistake or something out of date? Email hello@aispritemaker.com and we will fix it.