|
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') |