Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Gimp 2.8 Sprite Sheet from layer groups (python-fu)

A thing to convert a Gimp image of multiple layered sprites into a sprite sheet.

The way I make pixel art sprites is this: imagine you're drawing, say, a knight. I have all the frames for that knight in one great big Gimp XCF image, organised into groups.

So there's a layer group called "Running"; this defines a whole sprite, in multiple frames. In there, there's a set of layer groups, one per frame, called "Running 1" to "Running 9" (for example). In "Running 1", there's one or more layers which all go together to form that one single frame. This works out neatly because you can make an individual frame out of a few different parts, and edit those parts separately. This makes it easy to copy unchanging parts from one frame to the next, while just changing the bits that are different.

You then do the same thing for all the other sprites you want for this character; one for walking, one for dying, whatever. This means that your eventual XCF image looks like this:

Image
|- Running                     <--- this is a layer group which defines a sprite
|  |- Running 1                <--- this defines one frame within that sprite
|  |  |- Main body             /---
|  |  |- Left leg to left      |    these are all merged together to make that "Running 1" frame
|  |  |- Right leg to right    \---
|  |- Running 2
|  |  |- Main body
|  |  |- Left leg mostly left
|  |  |- Right leg mostly right
|  |- Running 3
|  |  |- Main body
|  |  |- Left leg central
|  |  |- Right leg central
...etc
|- Walking
|  |- Walking 1
|  |  |- Main body
|  |  |- Left leg bent
|  |  |- Right leg straight
...etc

If you have an image set up like this, then the Gimp Python-fu plugin I've created makes a sprite sheet for it. You get one sprite per row, one frame per column; the sublayers (so "Main body", "left leg to left" and "right leg to right") are all added together to make one frame. Extra bonus: layers (not layer groups) called "Spritesheet skip " are skipped, so you can add a "background" layer to each frame which is automatically not included in your sprite sheet, to avoid problems with frames below bleeding through if you don't want them to.

There's then another column, on the far right, which lists in the image in text how big a frame is, what the y-offset for each row is, and what the name of each sprite row is, so it's easy to see what's going on.

Install by adding to ~/.gimp-2.8/plug-ins/ and making executable, and then restarting the Gimp. (You might need to install Python-fu, but I think it comes out of the box. You need to restart the Gimp to pick up the new script, but after that any changes you make to it are picked up automatically.) Execute with Tools menu > Make sprite sheet. If there are errors, they'll show up in the Error Console (Windows > Dockable Dialogues > Error console); in particular, it checks whether your image matches the above nested layer group setup and whines if it doesn't.

#!/usr/bin/env python
# Make a sprite sheet from layer groups. Expects one top level layer group per sprite,
# with one layer GROUP per frame inside (which will contain many layers which are flattened into that one frame)
from gimpfu import *
import random, traceback
def real_sprite_sheet_from_layer_groups(image, drawable):
# confirm we have at least one grouplayer
if len(image.layers) < 1:
gimp.message("Image must have one group layer per animation at top level")
return
width = None; height = None
for gl in image.layers:
if not isinstance(gl, gimp.GroupLayer):
gimp.message("Layer '%s' is not a group layer" % (gl.name,))
return
for gl2 in gl.layers:
if not isinstance(gl2, gimp.GroupLayer):
gimp.message("Layer '%s/%s' is not a group layer" % (gl.name, gl2.name))
return
for l in gl2.layers:
if not isinstance(l, gimp.Layer):
gimp.message("Layer '%s/%s/%s' is not a standard layer" % (gl.name, gl2.name, l.name))
return
# confirm all internal layers are the same size
if width is None:
width = l.width; height = l.height
elif width == l.width and height == l.height:
pass
else:
gimp.message("Layer '%s/%s/%s' is not the expected size (%sx%s)" % (gl.name, gl2.name, l.name, width, height))
return
# We now know the image matches our expected format.
# Now calculate the image we need to create. It will have one row per grouplayer,
# and inside that one column per child group layer. Each "cell" will be the size
# of the internal images.
# We add one to columns to write into the final cell some indicator of which sprite it is
rows = len(image.layers)
cols = max([len(gl.layers) for gl in image.layers]) + 1
ss_width = cols * width
ss_height = rows * height
ss = gimp.Image(ss_width, ss_height, RGB)
all_ss = gimp.Layer(ss, "Sprite sheet", ss_width, ss_height,
RGBA_IMAGE, 100, NORMAL_MODE)
ss.add_layer(all_ss, 1)
notes_bg = gimp.Layer(ss, "Notes bg", width, ss_height,
RGBA_IMAGE, 100, NORMAL_MODE)
notes_bg.fill(BACKGROUND_FILL)
notes_bg.set_offsets((cols-1) * width, 0)
ss.add_layer(notes_bg, 1)
row = 0
for sprite in image.layers:
col = 0
for frame in sprite.layers[::-1]:
x = col * width
y = row * height
for layer in frame.layers[::-1]:
if layer.name.startswith("spritesheet skip"): continue
pdb.gimp_edit_copy(layer)
floating = pdb.gimp_edit_paste(all_ss, 1)
# new layers are pasted in the centre of the destination
pdb.gimp_layer_translate(floating, (-ss_width/2) + x + (width/2), (-ss_height/2) + y + (height/2))
pdb.gimp_floating_sel_anchor(floating)
col += 1
# Add note to end
txt = pdb.gimp_text_fontname(ss, None, 0, 0,
"%s\n%s frames\ny=%s" % (sprite.name, len(sprite.layers), row * height),
10, True, 8, PIXELS, "Sans")
txt.set_offsets((cols-1) * width + 2, y + 2)
row += 1
# Add note to bottom right
txt = pdb.gimp_text_fontname(ss, None, 0, 0, "Frames: %sx%s" % (width, height), 10, True, 8, PIXELS, "Sans")
txt.set_offsets((cols-1) * width + 2, rows * height - txt.height)
# merge all layers down
while len(ss.layers) > 1:
pdb.gimp_image_merge_down(ss, ss.layers[0], 1)
ss.layers[0].name = "Sprite sheet"
gimp.Display(ss)
gimp.displays_flush()
def sprite_sheet_from_layer_groups(image, drawable):
try:
real_sprite_sheet_from_layer_groups(image, drawable)
except:
gimp.message("PYTHON ERROR: %s" % traceback.format_exc())
register(
"python_fu_sil_sprite_sheet_from_layer_groups",
"Make a sprite sheet for nested layer groups",
"Expects one top level layer group per sprite, with one layer GROUP per frame inside",
"Stuart Langridge",
"Stuart Langridge",
"2018-04-10",
"<Image>/_Tools/Make sprite sheet",
"RGB*, GRAY*", # Create a new image, don't work on an existing one
[],
[],
sprite_sheet_from_layer_groups)
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment