diff options
Diffstat (limited to 'gfx-iso/asset/mkasset.py')
-rw-r--r-- | gfx-iso/asset/mkasset.py | 128 |
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). | ||
6 | import argparse | 13 | import argparse |
7 | import ctypes | 14 | import ctypes |
15 | import os | ||
8 | from PIL import Image | 16 | from PIL import Image |
9 | import sys | 17 | import sys |
10 | from xml.etree import ElementTree | 18 | from 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. | ||
13 | MAX_PATH_LENGTH = 128 | 22 | MAX_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 | ||
145 | def 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 | |||
168 | def 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 | |||
208 | def 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 | |||
136 | def main(): | 240 | def 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 | ||