Tutorial: Animated Spritesheets with GIMP and Unity

This is a quick tutorial describing how to use a plugin I’ve written to create simple spritesheets with GIMP. These can be imported into Unity and used to create animated sprites for UI elements, 2D games, etc. Here’s an example of the end result (based on a free icon from the excellent game-icons.net):

Prerequisites

  • Unity: Tested with Unity 2017, but there’s nothing fancy going on so most recent versions should be fine.
  • GIMP 2.8 or later: This may work with earlier versions, but I haven’t tested any.
    • Python: Comes bundled with recent versions of GIMP, but if you’re stuck on an old copy you may need to reinstall it after installing Python for the first time.
  • The plugin: Right-click and save to your GIMP plugin folder. On Windows that’s: “C:\Users\YourUserName\.gimp-2.8\plug-ins”. Close GIMP first or restart afterwards.

Phase 1: Working in GIMP

  • Open GIMP and create a new image (File -> New) with a size of one animation frame (square dimensions work best, but aren’t required)
  • Create a layer for each frame of your animation and *insert actual art here* 
  • Line up the contents of each frame for smooth animation. This is easiest with a transparent background and liberal use of eye icons to toggle layers on/off in the Layers window as you go.
  • Order layers so that the first frame of the animation is at the bottom of the layer list, and the last frame is at the top. See below for an example setup.

  • You can now preview your animation in GIMP using the playback filter (Filters -> Animation -> Playback)
  • If you’re using a transparent background, make sure to add “(replace)” to each layer’s name so frames don’t overlay each other when animating.
  • Optional Export as GIF: You can also now export to an animated GIF by selecting (File -> Export As… GIF Image (*gif)) and ticking the ‘As Animation’ option on the dialog that appears.

  • If you installed the plugin as described in the Prerequisites section you should now be able to select (Filters -> Animation -> Create Spritesheet)
  • This will display a simple dialog with an option to output your animation on a single row or in a grid layout (the default). Hit OK to generate a spritesheet as a new image.
  • Voila! You have a spritesheet. Save this to your Unity project’s asset folder as a .jpg or .png (use the latter for transparent images)

Phase 2: Working in Unity

  • Select your spritesheet in Unity and under its Import Settings in the inspector make sure Texture Type is set to ‘Sprite (2D and UI)’
  • Open the spritesheet in the Sprite Editor, select Slice and change the Type to Grid by Cell Count
  • Adjust the number of columns and rows to match your spritesheet and then select Slice
  • You should now have a collection of sprites parented under your spritesheet, one per animation frame
  • These can be referenced individually by an Image (Source Image) component for UI or a Sprite Renderer (Sprite) component for 2D games

  • For full 2D games or other complex scenarios you’ll probably want to drive your animations (updating the sprite over time) from code, but for simple UI animations..
  • You can quickly test your animation by adding an Animator component to an Image object (GameObject -> UI -> Image) and animating the Image.Sprite value over time

That’s it! I hope you found this tutorial useful. I’ve released the GIMP plugin script under a Creative Commons – CC0 license which means its dedicated to the public domain and you’re free to do absolutely anything you like with it. No need to link back to me if you do either, but it’s always appreciated nonetheless.

6 Responses

  1. ll3v3ll
    | Reply

    Thank you for the excellent work.
    Please take a look at this version with some modifications:
    1) The new image mode is the same as the original (RGB, INDEXED, GREY).
    2) If the original has a filename, the new image will have a filename also (with _Sheet added).
    3) The sheet size calculation now handles an even and odd number of sprites.

    #!/usr/bin/env python
    # License: Public Domain – https://creativecommons.org/share-your-work/public-domain/cc0/
    # v1: Original by Karn Bianco
    # v2: Updated by ll3v3ll (Pixeljam)

    from gimpfu import *
    import math

    def create_spritesheet(image, singleRow):

    # Grab all the layers from the original image, each one of which will become an animation frame
    layers = image.layers
    numLayers = len(layers)

    # Work out how many rows and columns we need for each of our layers/animation frames
    numCols = numLayers if singleRow else int(math.floor(math.sqrt(numLayers)))
    numRows = 1 if singleRow else int(math.ceil(float(numLayers) / float(numCols)))

    # And then determine the size of our new image based on the number of rows and columns
    newImgWidth = image.width * numCols
    newImgHeight = image.height * numRows

    # Determine image type (RGB, INDEXED, etc)
    imageType = image.base_type
    imageIsIndexed = (imageType == INDEXED)
    imageIsGray = (imageType == GRAY)
    newImageType = INDEXED if imageIsIndexed else GRAY if imageIsGray else RGB
    newLayerType = INDEXEDA_IMAGE if imageIsIndexed else GRAYA_IMAGE if imageIsGray else RGBA_IMAGE

    # NOTE: [see gimp.h]
    # typedef enum {
    # RGB_IMAGE = 0,
    # RGBA_IMAGE,
    # GRAY_IMAGE,
    # GRAYA_IMAGE,
    # INDEXED_IMAGE,
    # INDEXEDA_IMAGE,
    # UNKNOWN_IMAGE,
    # } ImageType;

    # Get colormap if image is INDEXED
    if imageIsIndexed:
    (numBytes, colorMap) = pdb.gimp_image_get_colormap(image)

    # NOTE: [see pdb browser]
    # The number of entries is specified by the ‘num-bytes’ parameter and
    # corresponds to the number of INT8 triples that must be contained in the
    # ‘colormap’ array. The actual number of colors in the transmitted colormap
    # is ‘num-bytes’ / 3.

    # Create a new image
    newImage = gimp.Image(newImgWidth, newImgHeight, newImageType)

    # Set colormap to new image if image is INDEXED
    if imageIsIndexed:
    pdb.gimp_image_set_colormap(newImage, numBytes, colorMap)

    # Add a single layer that fills the entire canvas
    newLayer = gimp.Layer(newImage, “Spritesheet”, newImgWidth, newImgHeight, newLayerType, 100, NORMAL_MODE)
    newImage.add_layer(newLayer, 1)

    # Give the new image a filename that represents the original image
    if image.filename:
    newImage.filename = image.filename.rstrip(“.”) + “_Sheet”

    # Clear any selections on the original image to esure we copy each layer in its entirety
    pdb.gimp_selection_none(image)

    # Layers are in the reverse order we want them so start at the end of the list and work backwards
    layerIndex = (numLayers – 1)

    # Loop over our spritesheet grid filling each one row at a time
    for y in xrange(0, numRows):
    for x in xrange(0, numCols):

    # Copy the layer’s contents and paste it into a “floating” layer in the new image
    pdb.gimp_edit_copy(layers[layerIndex])
    floatingLayer = pdb.gimp_edit_paste(newLayer, TRUE)

    # This floating layer will default to the center of the new image so we first shift to the top left
    # corner (0, 0) and and then shift to correct grid position based on current row and column index
    xOffset = (-newImgWidth/2) + (image.width/2) + (x * image.width)
    yOffset = (-newImgHeight/2) + (image.height/2) + (y * image.height)

    # GIMP will only copy non-transparent pixels, so if our image contains transparency
    # the new floating layer may be smaller than we want which will cause animation issues.
    # To resolve this we adjust our position by the difference in layer size to ensure everything aligns
    xOffset += (image.width – floatingLayer.width) / 2
    yOffset += (image.height – floatingLayer.height) / 2

    # Move the floating layer into the correct position
    pdb.gimp_layer_translate(floatingLayer, xOffset, yOffset)

    # Move to the next layer, unless we’re all done in which case exit!
    layerIndex = (layerIndex – 1)
    if layerIndex < 0:
    break;

    # Merge the last floating layer into our final 'Spritesheet' layer
    pdb.gimp_image_merge_visible_layers(newImage, 0)

    # Create and show a new image window for our spritesheet
    gimp.Display(newImage)
    gimp.displays_flush()

    # Register the plugin with Gimp so it appears in the filters menu
    register(
    "python_fu_create_spritesheet",
    "Creates a spritesheet (in a new image) from the layers of the current image. (v2)",
    "Creates a spritesheet (in a new image) from the layers of the current image. (v2)",
    "Karn Bianco; ll3v3ll",
    "Karn Bianco; ll3v3ll",
    "2018",
    "Create Spritesheet",
    "*",
    [
    (PF_IMAGE, 'image', 'Input image:', None),
    (PF_BOOL, "singleRow", "Output to a single row?", FALSE)
    ],
    [],
    create_spritesheet, menu="/Filters/Animation/”)

    main()

  2. abso
    | Reply

    love you, was searching this from a while.
    I will gain a lot of time now

    if like me you are on gimp 2.10 copy that on
    C:\Program Files\GIMP 2\lib\gimp\2.0\plug-ins

  3. thecovfefeguy
    | Reply

    I’m using the latest version of GIMP with the plugin and the “Create Spritesheet” option is missing.

    • Jonas Perusquia
      | Reply

      If you are in Linux or MacOS, you have to give execute permission to the plugin file.

  4. Jhonny
    | Reply

    It works now, after some changes:
    Improved the script to allow custom input, like you want a spritesheet with specified columns and rows.
    Here’s the script:

    https://pastebin.com/TgJ6c1F5

    Here is the code: (mind you this website might format the indexed code, ll3v3ll I couldn’t run your script from the gecko because it lost the tabs, but I used your numRows code, works like a charm now. Thanks everyone.):

    #!/usr/bin/env python
    # License: Public Domain – https://creativecommons.org/share-your-work/public-domain/cc0/
    # This is a script for Gimp 2.10 to generate spritesheets, originally from Karn Bianco.
    # https://github.com/Spydarlee/scripts/blob/master/GIMP/create_spritesheet.py
    # This functionality should be natively inside Gimp, because it automates the process of generating
    # Spritesheets where many artists needed. I myself use this script A LOT, it’s a lifesaver.
    #########################################################################################################
    # ADDED: Custom input for rows and columns (2020) – In the case of a preference for a specific use case.#
    # for example, if you have 8 sprites, and want to have 4 rows with 2 colms, you can specify in the input#
    # box and also check the bool box to use custom values. #
    #########################################################################################################
    # NOTE: Do not use odd numbers, only pairs, in sprite resolution. Otherwise weird shift will occur. #####
    #########################################################################################################
    from gimpfu import *
    import math

    def create_spritesheet(image, singleRow, customNums, customRows, customCol):

    # Grab all the layers from the original image, each one of which will become an animation frame
    layers = image.layers
    numLayers = len(layers)

    #Use customs if allowed
    if customNums:
    numCols = customCol
    numRows = customRows
    else:
    # Work out how many rows and columns we need for each of our layers/animation frames
    numCols = numLayers if singleRow else int(math.floor(math.sqrt(numLayers)))
    numRows = 1 if singleRow else int(math.ceil(float(numLayers) / float(numCols)))

    # Maybe we could input these values by hand, then we were able to control say, a multiple sprite
    #break, if needed. This will automate a splitt if needed, for let’s say, 4 frames * 8 directions.
    #Would be 4 rows with 8 columns, right?

    #if singleRow:
    # numRows = 1
    #else:
    # numRows = int(math.ceil(float(numLayers) / float(numCols)))

    # And then determine the size of our new image based on the number of rows and columns
    newImgWidth = image.width * numCols
    newImgHeight = image.height * numRows

    # Create a new image and a single layer that fills the entire canvas
    newImage = gimp.Image(newImgWidth, newImgHeight, RGB)
    newLayer = gimp.Layer(newImage, “Spritesheet”, newImgWidth, newImgHeight, RGBA_IMAGE, 100, NORMAL_MODE)
    newImage.add_layer(newLayer, 1)

    # Clear any selections on the original image to esure we copy each layer in its entirety
    pdb.gimp_selection_none(image)

    # Layers are in the reverse order we want them so start at the end of the list and work backwards
    layerIndex = (numLayers – 1)

    # Loop over our spritesheet grid filling each one row at a time
    for y in xrange(0, numRows):
    for x in xrange(0, numCols):

    # Copy the layer’s contents and paste it into a “floating” layer in the new image
    pdb.gimp_edit_copy(layers[layerIndex])
    floatingLayer = pdb.gimp_edit_paste(newLayer, TRUE)

    # This floating layer will default to the center of the new image so we first shift to the top left
    # corner (0, 0) and and then shift to correct grid position based on current row and column index
    xOffset = (-newImgWidth/2) + (image.width/2) + (x * image.width)
    yOffset = (-newImgHeight/2) + (image.height/2) + (y * image.height)

    # GIMP will only copy non-transparent pixels, so if our image contains transparency
    # the new floating layer may be smaller than we want which will cause animation issues.
    # To resolve this we adjust our position by the difference in layer size to ensure everything aligns
    xOffset += (image.width – floatingLayer.width) / 2
    yOffset += (image.height – floatingLayer.height) / 2

    #yOffset += 1#BUG FIX: the sprite was always eating the first pixel up. Maybe it’s Y FOR loop the issue. Don’t know why.
    #Maybe it’s because of a version update some base code now considers Y as 1, well, this fixes it.
    #AHA it’s because of odd numbers in the sprite size.
    #Always use pair numbers in the sprite size. That’s it.

    # Move the floating layer into the correct position
    pdb.gimp_layer_translate(floatingLayer, xOffset, yOffset)

    # Move to the next layer, unless we’re all done in which case exit!
    layerIndex = (layerIndex – 1)
    if layerIndex < 0:
    break;

    # Merge the last floating layer into our final 'Spritesheet' layer
    pdb.gimp_image_merge_visible_layers(newImage, 0)

    # Create and show a new image window for our spritesheet
    gimp.Display(newImage)
    gimp.displays_flush()

    # Register the plugin with Gimp so it appears in the filters menu
    register(
    "python_fu_create_spritesheet",
    "Creates a spritesheet (in a new image) from the layers of the current image.",
    "Creates a spritesheet (in a new image) from the layers of the current image.",
    "Karn Bianco",
    "Karn Bianco",
    "2018",
    "Create Spritesheet",
    "*",
    [
    (PF_IMAGE, 'image', 'Input image:', None),
    (PF_BOOL, "singleRow", "Output to a single row?", FALSE),
    (PF_BOOL, "customNums", "Use Custom Rows and Columns?", FALSE),
    (PF_INT, "customRows", "Custom Rows (Vertical)", 2),
    (PF_INT, "customCol", "Custom Columns (Horizontal)", 2)
    ],
    [],
    create_spritesheet, menu="/Filters/Animation/”)

    main()

  5. Sherman
    | Reply

    Hey there!

    Ended up making my own script spritesheets2layers in order to help fix up a bunch of sheets I have where I couldn’t slice them up properly in Unity alone.

    I wanted to thank you though because I only was able to do this after seeing what you had done and learning how to from your script and a couple of others. Thank you very much!

    https://imgur.com/a/oqUkk7J
    https://github.com/hiniko/gimp_tools/blob/master/spritesheet2layers/spritesheet2layers.py

Leave a Reply to Sherman Cancel reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.