summaryrefslogtreecommitdiff
path: root/gfx-iso/asset
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2023-07-19 08:35:00 -0700
committer3gg <3gg@shellblade.net>2023-07-19 08:35:00 -0700
commit48cef82988d6209987ae27fe29b72d7d5e402b3c (patch)
treefe5df57729a61839322ae8c1226d134e317b049f /gfx-iso/asset
parent2c668763a1d6e645dcfaa713b924de26260542d0 (diff)
Add sprites.
Diffstat (limited to 'gfx-iso/asset')
-rw-r--r--gfx-iso/asset/mkasset.py128
1 files changed, 122 insertions, 6 deletions
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 @@
1# Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine. 1# Converts assets to binary formats (.ts, .tm, .ss) for the engine.
2# 2#
3# Currently handles Tiled's .tsx and .tmx file formats. 3# Input file formats:
4# - Tiled tile set (.tsx)
5# - Tiled tile map (.tmx)
6# - Sprite sheets (.jpg, .png, etc), 1 row per animation.
7#
8# Output file formats:
9# - Binary tile set file (.ts)
10# - Binary tile map file (.tm)
11# - Binary sprite sheet file (.ss)
4# 12#
5# The output is a binary tile set file (.TS) or a binary tile map file (.TM).
6import argparse 13import argparse
7import ctypes 14import ctypes
15import os
8from PIL import Image 16from PIL import Image
9import sys 17import sys
10from xml.etree import ElementTree 18from xml.etree import ElementTree
11 19
12# Maximum length of path strings in .TS and .TM files. 20# Maximum length of path strings in .TS and .TM files.
21# Must match the engine's value.
13MAX_PATH_LENGTH = 128 22MAX_PATH_LENGTH = 128
14 23
15 24
@@ -133,20 +142,127 @@ def convert_tmx(input_filepath, output_filepath):
133 output.write(ctypes.c_uint16(int(tile_id))) 142 output.write(ctypes.c_uint16(int(tile_id)))
134 143
135 144
145def get_num_cols(image, sprite_width):
146 """Return the number of non-empty columns in the image.
147
148 Assumes no gaps in the columns.
149 """
150 assert (image.width % sprite_width == 0)
151 num_cols = image.width // sprite_width
152
153 # Start the search from right to left.
154 for col in reversed(range(1, num_cols)):
155 left = (col - 1) * sprite_width
156 right = col * sprite_width
157 rect = image.crop((left, 0, right, image.height))
158 min_max = rect.getextrema()
159 for (channel_min, channel_max) in min_max:
160 if channel_min != 0 or channel_max != 0:
161 # 'col' is the rightmost non-empty column.
162 # Assuming no gaps, col+1 is the number of non-empty columns.
163 return col + 1
164
165 return 0
166
167
168def get_sprite_sheet_rows(input_filepath, sprite_width, sprite_height):
169 """Gets the individual rows of a sprite sheet.
170
171 The input sprite sheet can have any number of rows.
172
173 Returns a list of lists [[sprite bytes]], one inner list for the columns in
174 each row.
175 """
176 with Image.open(input_filepath) as im:
177 # Sprite sheet's width and height must be integer multiples of the
178 # sprite's width and height.
179 assert (im.width % sprite_width == 0)
180 assert (im.height % sprite_height == 0)
181
182 num_rows = im.height // sprite_height
183
184 rows = []
185 for row in range(num_rows):
186 # Get the number of columns.
187 upper = row * sprite_height
188 lower = (row + 1) * sprite_height
189 whole_row = im.crop((0, upper, im.width, lower))
190 num_cols = get_num_cols(whole_row, sprite_width)
191 assert (num_cols > 0)
192
193 # Crop the row into N columns.
194 cols = []
195 for i in range(num_cols):
196 left = i * sprite_width
197 right = (i + 1) * sprite_width
198 sprite = im.crop((left, upper, right, lower))
199 cols.append(sprite)
200
201 sprite_bytes = [sprite.convert('RGBA').tobytes() for sprite in cols]
202 assert (len(sprite_bytes) == num_cols)
203 rows.append(sprite_bytes)
204
205 return rows
206
207
208def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height,
209 output_filepath):
210 """Converts a set of sprite sheet images into a binary sprite sheet file
211 (.ss).
212
213 The input sprite sheets can have any number of rows, one row per animation.
214 All rows from all sprite sheets are concatenated in the output file.
215
216 The sprite's width and height is assumed constant throughout the input
217 sprite sheets.
218 """
219 rows = []
220
221 for sprite_sheet in input_file_paths:
222 rows.extend(
223 get_sprite_sheet_rows(sprite_sheet, sprite_width, sprite_height))
224
225 with open(output_filepath, 'bw') as output:
226 output.write(ctypes.c_uint16(sprite_width))
227 output.write(ctypes.c_uint16(sprite_height))
228 output.write(ctypes.c_uint16(len(rows)))
229
230 print(f"Sprite width: {sprite_width}")
231 print(f"Sprite height: {sprite_height}")
232 print(f"Rows: {len(rows)}")
233
234 for sprites in rows:
235 output.write(ctypes.c_uint16(len(sprites)))
236 for sprite_bytes in sprites:
237 output.write(sprite_bytes)
238
239
136def main(): 240def main():
137 parser = argparse.ArgumentParser() 241 parser = argparse.ArgumentParser()
138 parser.add_argument("input", help="Input file (.tsx, .tmx)") 242 parser.add_argument("input",
243 nargs="+",
244 help="Input file (.tsx, .tmx) or path regex (sprite sheets)")
245 parser.add_argument("--width", type=int, help="Sprite width in pixels")
246 parser.add_argument("--height", type=int, help="Sprite height in pixels")
247 parser.add_argument("--out", help="Output file (sprite sheets)")
139 args = parser.parse_args() 248 args = parser.parse_args()
140 249
141 output_filepath_no_ext = drop_extension(args.input)
142 if ".tsx" in args.input: 250 if ".tsx" in args.input:
251 output_filepath_no_ext = drop_extension(args.input)
143 output_filepath = output_filepath_no_ext + ".ts" 252 output_filepath = output_filepath_no_ext + ".ts"
144 convert_tsx(args.input, output_filepath) 253 convert_tsx(args.input, output_filepath)
145 elif ".tmx" in args.input: 254 elif ".tmx" in args.input:
255 output_filepath_no_ext = drop_extension(args.input)
146 output_filepath = output_filepath_no_ext + ".tm" 256 output_filepath = output_filepath_no_ext + ".tm"
147 convert_tmx(args.input, output_filepath) 257 convert_tmx(args.input, output_filepath)
148 else: 258 else:
149 print(f"Unhandled file format: {args.input}") 259 # Sprite sheets.
260 if not args.width or not args.height:
261 print("Sprite width and height must be given")
262 return 1
263 output_filepath = args.out if args.out else "out.ss"
264 convert_sprite_sheet(args.input, args.width, args.height,
265 output_filepath)
150 266
151 return 0 267 return 0
152 268