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

(limited to 'gfx-iso')

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