From 48cef82988d6209987ae27fe29b72d7d5e402b3c Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Wed, 19 Jul 2023 08:35:00 -0700 Subject: Add sprites. --- gfx-iso/asset/mkasset.py | 128 ++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 122 insertions(+), 6 deletions(-) (limited to 'gfx-iso/asset') diff --git a/gfx-iso/asset/mkasset.py b/gfx-iso/asset/mkasset.py index 15f7912..b4e335f 100644 --- a/gfx-iso/asset/mkasset.py +++ b/gfx-iso/asset/mkasset.py @@ -1,15 +1,24 @@ -# Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine. +# Converts assets to binary formats (.ts, .tm, .ss) for the engine. # -# Currently handles Tiled's .tsx and .tmx file formats. +# Input file formats: +# - Tiled tile set (.tsx) +# - Tiled tile map (.tmx) +# - Sprite sheets (.jpg, .png, etc), 1 row per animation. +# +# Output file formats: +# - Binary tile set file (.ts) +# - Binary tile map file (.tm) +# - Binary sprite sheet file (.ss) # -# The output is a binary tile set file (.TS) or a binary tile map file (.TM). import argparse import ctypes +import os from PIL import Image import sys from xml.etree import ElementTree # Maximum length of path strings in .TS and .TM files. +# Must match the engine's value. MAX_PATH_LENGTH = 128 @@ -133,20 +142,127 @@ def convert_tmx(input_filepath, output_filepath): output.write(ctypes.c_uint16(int(tile_id))) +def get_num_cols(image, sprite_width): + """Return the number of non-empty columns in the image. + + Assumes no gaps in the columns. + """ + assert (image.width % sprite_width == 0) + num_cols = image.width // sprite_width + + # Start the search from right to left. + for col in reversed(range(1, num_cols)): + left = (col - 1) * sprite_width + right = col * sprite_width + rect = image.crop((left, 0, right, image.height)) + min_max = rect.getextrema() + for (channel_min, channel_max) in min_max: + if channel_min != 0 or channel_max != 0: + # 'col' is the rightmost non-empty column. + # Assuming no gaps, col+1 is the number of non-empty columns. + return col + 1 + + return 0 + + +def get_sprite_sheet_rows(input_filepath, sprite_width, sprite_height): + """Gets the individual rows of a sprite sheet. + + The input sprite sheet can have any number of rows. + + Returns a list of lists [[sprite bytes]], one inner list for the columns in + each row. + """ + with Image.open(input_filepath) as im: + # Sprite sheet's width and height must be integer multiples of the + # sprite's width and height. + assert (im.width % sprite_width == 0) + assert (im.height % sprite_height == 0) + + num_rows = im.height // sprite_height + + rows = [] + for row in range(num_rows): + # Get the number of columns. + upper = row * sprite_height + lower = (row + 1) * sprite_height + whole_row = im.crop((0, upper, im.width, lower)) + num_cols = get_num_cols(whole_row, sprite_width) + assert (num_cols > 0) + + # Crop the row into N columns. + cols = [] + for i in range(num_cols): + left = i * sprite_width + right = (i + 1) * sprite_width + sprite = im.crop((left, upper, right, lower)) + cols.append(sprite) + + sprite_bytes = [sprite.convert('RGBA').tobytes() for sprite in cols] + assert (len(sprite_bytes) == num_cols) + rows.append(sprite_bytes) + + return rows + + +def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, + output_filepath): + """Converts a set of sprite sheet images into a binary sprite sheet file + (.ss). + + The input sprite sheets can have any number of rows, one row per animation. + All rows from all sprite sheets are concatenated in the output file. + + The sprite's width and height is assumed constant throughout the input + sprite sheets. + """ + rows = [] + + for sprite_sheet in input_file_paths: + rows.extend( + get_sprite_sheet_rows(sprite_sheet, sprite_width, sprite_height)) + + with open(output_filepath, 'bw') as output: + output.write(ctypes.c_uint16(sprite_width)) + output.write(ctypes.c_uint16(sprite_height)) + output.write(ctypes.c_uint16(len(rows))) + + print(f"Sprite width: {sprite_width}") + print(f"Sprite height: {sprite_height}") + print(f"Rows: {len(rows)}") + + for sprites in rows: + output.write(ctypes.c_uint16(len(sprites))) + for sprite_bytes in sprites: + output.write(sprite_bytes) + + def main(): parser = argparse.ArgumentParser() - parser.add_argument("input", help="Input file (.tsx, .tmx)") + parser.add_argument("input", + nargs="+", + help="Input file (.tsx, .tmx) or path regex (sprite sheets)") + parser.add_argument("--width", type=int, help="Sprite width in pixels") + parser.add_argument("--height", type=int, help="Sprite height in pixels") + parser.add_argument("--out", help="Output file (sprite sheets)") args = parser.parse_args() - output_filepath_no_ext = drop_extension(args.input) if ".tsx" in args.input: + output_filepath_no_ext = drop_extension(args.input) output_filepath = output_filepath_no_ext + ".ts" convert_tsx(args.input, output_filepath) elif ".tmx" in args.input: + output_filepath_no_ext = drop_extension(args.input) output_filepath = output_filepath_no_ext + ".tm" convert_tmx(args.input, output_filepath) else: - print(f"Unhandled file format: {args.input}") + # Sprite sheets. + if not args.width or not args.height: + print("Sprite width and height must be given") + return 1 + output_filepath = args.out if args.out else "out.ss" + convert_sprite_sheet(args.input, args.width, args.height, + output_filepath) return 0 -- cgit v1.2.3