From cef3385c2bee0b098a7795548345a9281ace008e Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Wed, 26 Jul 2023 08:39:37 -0700 Subject: Add support for paletted sprites. --- gfx-iso/asset/mkasset.py | 127 +++++++++++++++++++++++++++++++++-------------- gfx-iso/src/isogfx.c | 65 +++++++++++++++++------- 2 files changed, 136 insertions(+), 56 deletions(-) diff --git a/gfx-iso/asset/mkasset.py b/gfx-iso/asset/mkasset.py index b4e335f..3ca8a1d 100644 --- a/gfx-iso/asset/mkasset.py +++ b/gfx-iso/asset/mkasset.py @@ -165,44 +165,57 @@ def get_num_cols(image, sprite_width): return 0 -def get_sprite_sheet_rows(input_filepath, sprite_width, sprite_height): +def get_sprite_sheet_rows(im, 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. + Returns a list of lists [[sprite]], 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) + # 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 + 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 + 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) + + assert (len(cols) == num_cols) + rows.append(cols) + + return rows + + +def make_image_from_rows(rows, sprite_width, sprite_height): + """Concatenate the rows into a single RGBA image.""" + im_width = sprite_width * max(len(row) for row in rows) + im_height = len(rows) * sprite_height + im = Image.new('RGBA', (im_width, im_height)) + y = 0 + for row in rows: + x = 0 + for sprite in row: + im.paste(sprite.convert('RGBA'), (x, y)) + x += sprite_width + y += sprite_height + return im def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, @@ -217,25 +230,65 @@ def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, sprite sheets. """ rows = [] + for input_filepath in input_file_paths: + with Image.open(input_filepath) as sprite_sheet: + rows.extend( + get_sprite_sheet_rows(sprite_sheet, sprite_width, + sprite_height)) - for sprite_sheet in input_file_paths: - rows.extend( - get_sprite_sheet_rows(sprite_sheet, sprite_width, sprite_height)) + im = make_image_from_rows(rows, sprite_width, sprite_height) + im = im.convert(mode="P", palette=Image.ADAPTIVE, colors=256) + + # The sprite data in 'rows' is no longer needed. + # Keep just the number of columns per row. + rows = [len(row) for row in rows] 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))) + # Write palette. + # getpalette() returns 256 colors, but the palette might use less than + # that. getcolors() returns the number of unique colors. + # getpalette() also returns a flattened list, which is why we must *4. + num_colours = len(im.getcolors()) + colours = im.getpalette(rawmode="RGBA")[:4 * num_colours] + palette = [] + for i in range(0, 4 * num_colours, 4): + palette.append((colours[i], colours[i + 1], colours[i + 2], + colours[i + 3])) + + output.write(ctypes.c_uint16(len(palette))) + output.write(bytearray(colours)) + print(f"Sprite width: {sprite_width}") print(f"Sprite height: {sprite_height}") print(f"Rows: {len(rows)}") + print(f"Colours: {len(palette)}") + + # print("Palette") + # for i, colour in enumerate(palette): + # print(f"{i}: {colour}") - for sprites in rows: - output.write(ctypes.c_uint16(len(sprites))) - for sprite_bytes in sprites: + for row, num_columns in enumerate(rows): + output.write(ctypes.c_uint16(num_columns)) + upper = row * sprite_height + lower = (row + 1) * sprite_height + for col in range(num_columns): + left = col * sprite_width + right = (col + 1) * sprite_width + sprite = im.crop((left, upper, right, lower)) + sprite_bytes = sprite.tobytes() + + assert (len(sprite_bytes) == sprite_width * sprite_height) output.write(sprite_bytes) + # if (row == 0) and (col == 0): + # print(f"Sprite: ({len(sprite_bytes)})") + # print(list(sprite_bytes)) + # sprite.save("out.png") + def main(): parser = argparse.ArgumentParser() diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c index 9ba1bec..4568375 100644 --- a/gfx-iso/src/isogfx.c +++ b/gfx-iso/src/isogfx.c @@ -100,26 +100,49 @@ static inline const Ts_Tile* ts_tileset_get_next_tile( /// The pixels of the row follow a "sprite-major" order. It contains the /// 'sprite_width * sprite_height' pixels for the first column/sprite, then the /// second column/sprite, etc. +/// +/// Pixels are 8-bit indices into the sprite sheet's colour palette. typedef struct Ss_Row { uint16_t num_cols; /// Number of columns in this row. - Pixel pixels[1]; /// Count: num_cols * sprite_width * sprite_height. + uint8_t pixels[1]; /// Count: num_cols * sprite_width * sprite_height. } Ss_Row; +typedef struct Ss_Palette { + uint16_t num_colours; + Pixel colours[1]; /// Count: num_colors. +} Ss_Palette; + /// Sprite sheet top-level data definition. /// /// Sprite width and height are assumed constant throughout the sprite sheet. typedef struct Ss_SpriteSheet { - uint16_t sprite_width; /// Sprite width in pixels. - uint16_t sprite_height; /// Sprite height in pixels. - uint16_t num_rows; - Ss_Row rows[1]; /// Count: num_rows. + uint16_t sprite_width; /// Sprite width in pixels. + uint16_t sprite_height; /// Sprite height in pixels. + uint16_t num_rows; + Ss_Palette palette; /// Variable size. + Ss_Row rows[1]; /// Count: num_rows. Variable offset. } Ss_SpriteSheet; -const Ss_Row* get_sprite_sheet_row(const Ss_SpriteSheet* sheet, int row) { +static inline const Ss_Row* get_sprite_sheet_row( + const Ss_SpriteSheet* sheet, int row) { assert(sheet); assert(row >= 0); assert(row < sheet->num_rows); - return &sheet->rows[row]; + // Skip over the palette. + const Ss_Row* rows = + (const Ss_Row*)(&sheet->palette.colours[0] + sheet->palette.num_colours); + return &rows[row]; +} + +static inline const uint8_t* get_sprite_sheet_sprite( + const Ss_SpriteSheet* sheet, const Ss_Row* row, int col) { + assert(sheet); + assert(row); + assert(col >= 0); + assert(col < row->num_cols); + const int sprite_offset = col * sheet->sprite_width * sheet->sprite_height; + const uint8_t* sprite = &row->pixels[sprite_offset]; + return sprite; } // ----------------------------------------------------------------------------- @@ -732,11 +755,19 @@ static Pixel alpha_blend(Pixel src, Pixel dst) { /// /// The rectangle's pixels are assumed to be arranged in a linear, row-major /// fashion. +/// +/// If indices are given, then the image is assumed to be colour-paletted, where +/// 'pixels' is the palette and 'indices' the pixel indices. Otherwise, the +/// image is assumed to be in plain RGBA format. static void draw_rect( IsoGfx* iso, ivec2 origin, int rect_width, int rect_height, - const Pixel* pixels) { + const Pixel* pixels, const uint8_t* indices) { assert(iso); +#define rect_pixel(x, y) \ + (indices ? pixels[indices[py * rect_width + px]] \ + : pixels[py * rect_width + px]) + // Rect can exceed screen bounds, so we must clip it. #define max(a, b) (a > b ? a : b) const int py_offset = max(0, rect_height - origin.y); @@ -748,7 +779,7 @@ static void draw_rect( const int sy = origin.y + py - py_offset; for (int px = 0; (px < rect_width) && (origin.x + px < iso->screen_width); ++px) { - const Pixel colour = pixels[py * rect_width + px]; + const Pixel colour = rect_pixel(px, py); if (colour.a > 0) { const int sx = origin.x + px; const Pixel dst = screen_xy(iso, sx, sy); @@ -767,7 +798,7 @@ static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) { const Pixel* pixels = tile_xy_const_ref(iso, tile_data, 0, 0); - draw_rect(iso, origin, tile_data->width, tile_data->height, pixels); + draw_rect(iso, origin, tile_data->width, tile_data->height, pixels, 0); } static void draw(IsoGfx* iso) { @@ -807,15 +838,11 @@ static void draw_sprite( assert(sprite->animation < sheet->num_rows); assert(sprite->frame >= 0); - const SpriteSheetRow* ss_row = &sheet->rows[sprite->animation]; - assert(sprite->frame < ss_row->num_cols); - - const int sprite_offset = - sprite->frame * sheet->sprite_width * sheet->sprite_height; - - const Pixel* frame = &ss_row->pixels[sprite_offset]; - - draw_rect(iso, origin, sheet->sprite_width, sheet->sprite_height, frame); + const SpriteSheetRow* row = get_sprite_sheet_row(sheet, sprite->animation); + const uint8_t* frame = get_sprite_sheet_sprite(sheet, row, sprite->frame); + draw_rect( + iso, origin, sheet->sprite_width, sheet->sprite_height, + sheet->palette.colours, frame); } static void draw_sprites(IsoGfx* iso) { -- cgit v1.2.3