Skip to content

Instantly share code, notes, and snippets.

@Aphlax
Last active February 19, 2022 18:39
Show Gist options
  • Save Aphlax/3c2302541ed17fe5284b83f55e392518 to your computer and use it in GitHub Desktop.
Save Aphlax/3c2302541ed17fe5284b83f55e392518 to your computer and use it in GitHub Desktop.
Blender Add-on for creating sprite sheets in the composition editor.

Sprite Sheet

This blender Add-on facilitates the creation of sprite sheets.

Requirements

All the individual frames which should be combined into a sprite sheet are already rendered and they are stored as images in a single directory.

Workflow

Select the "Create sprite sheet" entry in the main "Render" menu to open the setup dialog. Select the files you want to combine into a sprite sheet and the number of images per row, then click the "Create sprite sheet" buttom. After that, make sure the output path is set correctly and hit Ctrl+F12!

Note: This Add-on will create a new scene for the sprite sheet composition, so it will not destroy the composition of the current scene.

bl_info = {
'name': 'Sprite Sheet',
'description': 'Composes a sprite sheet out of images.',
'author': 'Aphlax',
'version': (1, 0),
'blender': (3, 0, 1),
'location': 'Render > Create sprite sheet',
'category': 'Import-Export',
}
import bpy
import bpy_extras
import math
import os
import textwrap
from bpy_extras.io_utils import ImportHelper
from bpy.types import Operator, PropertyGroup
from bpy.props import CollectionProperty, IntProperty, EnumProperty
def createSpriteSheetComposition(scene, path, files, max_x):
"""Creates a sprite sheet using composition nodes.
Parameters:
scene (bpy.types.Scene): The scene to create the sprite sheet in.
path (str): Directory path containing all images.
files (bpy.types.File[]): Image files to use.
max_x (int): Maximal number of sprites per row.
"""
# Load images.
images = [bpy.data.images.load(os.path.join(path, file.name), check_existing = False) for file in files]
# Calculate sprite sheet properties.
width, height = images[0].size
x = min(len(files), max_x)
y = math.ceil(len(files) / x)
offset_x, offset_y = -width * (x - 1) / 2, height * (y - 1) / 2
# Set up the scene.
scene.render.resolution_x = width * x
scene.render.resolution_y = height * y
scene.render.resolution_percentage = 100
scene.render.image_settings.file_format = 'PNG'
scene.render.image_settings.color_mode = 'RGBA'
scene.frame_current = 0
scene.frame_start = 0
scene.frame_end = 0
scene.use_nodes = True
# Remove all composition nodes from the scene.
for node in scene.node_tree.nodes:
scene.node_tree.nodes.remove(node)
# Create an output node and store the input we can use to output the image.
composite_node = scene.node_tree.nodes.new('CompositorNodeComposite')
composite_node.inputs[1].hide = True
composite_node.inputs[2].hide = True
composite_node.location = 0, 0
composite_node.label = 'Sprite sheet'
last_output = composite_node.inputs[0]
# Put each image into the final output positioned correctly.
for i in range(0, len(images)):
image_node = scene.node_tree.nodes.new('CompositorNodeImage')
image_node.image = images[i]
image_node.outputs[1].hide = True
image_node.outputs[2].hide = True
image_node.location = -750, i * 180 - 80
image_node.hide = True
transform_node = scene.node_tree.nodes.new('CompositorNodeTransform')
transform_node.inputs[1].default_value = offset_x + (i % x) * width
transform_node.inputs[2].default_value = offset_y - (i // x) * height
transform_node.inputs[3].hide = True
transform_node.inputs[4].hide = True
transform_node.location = -500, i * 180
# Combine the images with Alpha over nodes (but we only need n-1 of them), then link everything up.
output = last_output
if i + 1 != len(files):
alpha_over_node = scene.node_tree.nodes.new('CompositorNodeAlphaOver')
alpha_over_node.inputs[0].hide = True
alpha_over_node.location = -250, i * 180
scene.node_tree.links.new(alpha_over_node.outputs[0], last_output)
last_output = alpha_over_node.inputs[1]
output = alpha_over_node.inputs[2]
scene.node_tree.links.new(image_node.outputs[0], transform_node.inputs[0])
scene.node_tree.links.new(transform_node.outputs[0], output)
def goToSpriteSheetScene(input_path):
"""Switches to the scene named "SPRITE SHEET" or creates it, if it does not exist.
Parameters:
input_path (str): Used to set the initial output path if a new scene is created.
"""
for scene in bpy.data.scenes.values():
if scene.name == 'SPRITE SHEET':
bpy.context.window.scene = scene
return
scene = bpy.data.scenes.new(name = 'SPRITE SHEET')
scene.render.filepath = os.path.join(os.path.abspath(os.path.join(input_path, os.pardir)), 'sprite_sheet#.png')
bpy.context.window.scene = scene
class SSC_OT_SpriteSheetDialog(Operator, ImportHelper):
"""Dialog for setting up the sprite sheet based on an open file dialog."""
bl_idname = 'ssc.create_sprite_sheet'
bl_label = 'Create sprite sheet'
filter_glob: bpy.props.StringProperty(
default = '*.jpg;*.jpeg;*.png;*.tif;*.tiff;*.bmp',
options = {'HIDDEN'}
)
files: CollectionProperty(type = PropertyGroup)
max_x: IntProperty(name = 'Sprites per row', default = 8, min = 1)
scene: EnumProperty(
description = 'Scene that should be modified',
items = {
('SPRITE_SHEET', 'use or create SPRITE SHEET scene',
'Uses the SPREITE SHEET scene or creates a new scene if it does not exist.'),
('_CURRENT', 'use current scene', 'Modifies the composition nodes of the current scene.')},
default = 'SPRITE_SHEET')
def draw(self, context):
self.layout.row(align = True).label(text = 'Create sprite sheet')
description_label = self.layout.column(align = True)
description = 'Select the image files in the file browser (use "A" to select all files in a directory)' \
' and set the sprites per row below. When you are happy, click the "Create sprite sheet" button.' \
' This will create a scene with a compositing setup for the sprite sheet. At this point, all that' \
' is left to do for you is to check the output path and then hit Ctrl+F12!'
wrapper = textwrap.TextWrapper(width = int(context.region.width / 5.5 + 1))
for line in wrapper.wrap(text = description):
description_label.label(text = line)
self.layout.separator(factor = 10)
self.layout.row(align = True).label(text = 'Sprite sheet properties')
self.layout.row(align = True).prop(self, 'max_x')
scene_property = self.layout.column(align = True)
scene_property.label(text = 'Scene')
scene_property.props_enum(self, 'scene')
def execute(self, context):
for file in self.files:
if not os.path.splitext(file.name)[1] in ['.jpg', '.jpeg', '.png', '.tif', '.tiff', '.bmp']:
self.report({'ERROR'}, file.name + ' is not an image. Please select image files only.')
return {'CANCELLED'}
input_path = os.path.dirname(self.filepath)
if self.scene == 'SPRITE_SHEET':
goToSpriteSheetScene(input_path)
createSpriteSheetComposition(bpy.context.scene, input_path, self.files, self.max_x)
self.report({'INFO'}, 'Created a composition sprite sheet! Check your output path and then hit Ctrl+F12.')
return {'FINISHED'}
def create_render_menu_entry(self, context):
self.layout.separator()
self.layout.operator(SSC_OT_SpriteSheetDialog.bl_idname, text = 'Create sprite sheet')
def register():
bpy.utils.register_class(SSC_OT_SpriteSheetDialog)
bpy.types.TOPBAR_MT_render.append(create_render_menu_entry)
def unregister():
bpy.utils.unregister_class(SSC_OT_SpriteSheetDialog)
bpy.types.TOPBAR_MT_render.remove(create_render_menu_entry)
if __name__ == '__main__':
bpy.utils.register_class(SSC_OT_SpriteSheetDialog)
bpy.ops.ssc.create_sprite_sheet('INVOKE_DEFAULT')
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment