From 0831d5bce79008bfa6404f8e8116ae8290442fde Mon Sep 17 00:00:00 2001
From: 3gg <3gg@shellblade.net>
Date: Sat, 24 Jun 2023 18:46:33 -0700
Subject: Isometric Renderer initial commit.

---
 gfx-iso/src/isogfx.c | 361 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 361 insertions(+)
 create mode 100644 gfx-iso/src/isogfx.c

(limited to 'gfx-iso/src')

diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c
new file mode 100644
index 0000000..27981f9
--- /dev/null
+++ b/gfx-iso/src/isogfx.c
@@ -0,0 +1,361 @@
+#include <isogfx/isogfx.h>
+
+#include <mempool.h>
+
+#include <assert.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdlib.h>
+#include <string.h>
+
+/// Maximum number of tiles unless the user chooses a non-zero value.
+#define DEFAULT_MAX_NUM_TILES 1024
+
+typedef struct TileData {
+  Pixel pixels[1]; // Dynamically allocated.
+} TileData;
+
+DEF_MEMPOOL_DYN(TilePool, TileData)
+
+typedef struct IsoGfx {
+  Tile*    world;
+  Pixel*   screen;
+  uint8_t* tile_mask;
+  TilePool tiles;
+  int      screen_width;
+  int      screen_height;
+  int      tile_width;
+  int      tile_height;
+  int      world_width;
+  int      world_height;
+  int      max_num_tiles;
+} IsoGfx;
+
+typedef struct ivec2 {
+  int x, y;
+} ivec2;
+
+typedef struct vec2 {
+  double x, y;
+} vec2;
+
+static inline ivec2 ivec2_add(ivec2 a, ivec2 b) {
+  return (ivec2){.x = a.x + b.x, .y = a.y + b.y};
+}
+
+static inline ivec2 ivec2_scale(ivec2 a, int s) {
+  return (ivec2){.x = a.x * s, .y = a.y * s};
+}
+
+static inline ivec2 iso2cart(ivec2 iso, int s, int t, int w) {
+  return (ivec2){
+      .x = (iso.x - iso.y) * (s / 2) + (w / 2), .y = (iso.x + iso.y) * (t / 2)};
+}
+
+static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
+  const double one_over_s = 1. / (double)s;
+  const double one_over_t = 1. / (double)t;
+  const double x          = cart.x - (double)(w / 2);
+
+  return (vec2){
+      .x = (int)(one_over_s * x + one_over_t * cart.y),
+      .y = (int)(-one_over_s * x + one_over_t * cart.y)};
+}
+
+Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) {
+  assert(iso);
+  assert(tile);
+  assert(tile->pixels);
+  assert(x >= 0);
+  assert(y >= 0);
+  assert(x < iso->tile_width);
+  assert(y < iso->tile_height);
+  return &tile->pixels[y * iso->tile_width + x];
+}
+
+Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) {
+  assert(iso);
+  assert(tile);
+  assert(tile->pixels);
+  assert(x >= 0);
+  assert(y >= 0);
+  assert(x < iso->tile_width);
+  assert(y < iso->tile_height);
+  return tile->pixels[y * iso->tile_width + x];
+}
+
+static inline Tile world_xy(IsoGfx* iso, int x, int y) {
+  assert(iso);
+  assert(x >= 0);
+  assert(y >= 0);
+  assert(x < iso->world_width);
+  assert(y < iso->world_height);
+  return iso->world[y * iso->world_width + x];
+}
+
+static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) {
+  assert(iso);
+  assert(x >= 0);
+  assert(y >= 0);
+  assert(x < iso->world_width);
+  assert(y < iso->world_height);
+  return &iso->world[y * iso->world_width + x];
+}
+
+static inline Pixel screen_xy(IsoGfx* iso, int x, int y) {
+  assert(iso);
+  assert(x >= 0);
+  assert(y >= 0);
+  assert(x < iso->screen_width);
+  assert(y < iso->screen_height);
+  return iso->screen[y * iso->screen_width + x];
+}
+
+static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) {
+  assert(iso);
+  assert(x >= 0);
+  assert(y >= 0);
+  assert(x < iso->screen_width);
+  assert(y < iso->screen_height);
+  return &iso->screen[y * iso->screen_width + x];
+}
+
+static void draw_tile(IsoGfx* iso, ivec2 so, Tile tile) {
+  assert(iso);
+
+  const TileData* data = mempool_get_block(&iso->tiles, tile);
+  assert(data);
+
+  for (int py = 0; py < iso->tile_height; ++py) {
+    for (int px = 0; px < iso->tile_width; ++px) {
+      const Pixel colour = tile_xy(iso, data, px, py);
+      const int   sx     = so.x + px;
+      const int   sy     = so.y + py;
+      if ((sx >= 0) && (sy >= 0) && (sx < iso->screen_width) &&
+          (sy < iso->screen_height)) {
+        const uint8_t mask = iso->tile_mask[py * iso->tile_width + px];
+        if (mask == 1) {
+          *screen_xy_mut(iso, sx, sy) = colour;
+        }
+      }
+    }
+  }
+}
+
+static void draw(IsoGfx* iso) {
+  assert(iso);
+
+  const int W = iso->screen_width;
+  const int H = iso->screen_height;
+
+  memset(iso->screen, 0, W * H * sizeof(Pixel));
+
+  const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0};
+  const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
+  const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
+
+  // TODO: Since the world will generally be larger than the screen, it
+  // would be best to walk in screen space and fetch the tile.
+  // The tile-centric approach might be more cache-friendly, however, since the
+  // screen-centric approach would juggle multiple tiles throughout the scan.
+  for (int ty = 0; ty < iso->world_height; ++ty) {
+    for (int tx = 0; tx < iso->world_width; ++tx) {
+      const Tile  tile = world_xy(iso, tx, ty);
+      const ivec2 so =
+          ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty)));
+      draw_tile(iso, so, tile);
+    }
+  }
+}
+
+/// Creates a tile mask procedurally.
+static void make_tile_mask(IsoGfx* iso) {
+  assert(iso);
+  assert(iso->tile_mask);
+
+  for (int y = 0; y < iso->tile_height / 2; ++y) {
+    const int mask_start = iso->tile_width / 2 - 2 * y - 1;
+    const int mask_end   = iso->tile_width / 2 + 2 * y + 1;
+    for (int x = 0; x < iso->tile_width; ++x) {
+      const bool    masked = (mask_start <= x) && (x <= mask_end);
+      const uint8_t val    = masked ? 1 : 0;
+
+      // Top half.
+      iso->tile_mask[y * iso->tile_width + x] = val;
+
+      // Bottom half reflects the top half.
+      const int y_reflected = iso->tile_height - y - 1;
+      iso->tile_mask[y_reflected * iso->tile_width + x] = val;
+    }
+  }
+}
+
+/// Creates a tile with a constant colour.
+static void make_tile_from_colour(
+    const IsoGfx* iso, Pixel colour, TileData* tile) {
+  assert(iso);
+  assert(tile);
+
+  for (int y = 0; y < iso->tile_height; ++y) {
+    for (int x = 0; x < iso->tile_width; ++x) {
+      *tile_xy_mut(iso, tile, x, y) = colour;
+    }
+  }
+}
+
+IsoGfx* isogfx_new(const IsoGfxDesc* desc) {
+  assert(desc->screen_width > 0);
+  assert(desc->screen_height > 0);
+  assert(desc->tile_width > 0);
+  assert(desc->tile_height > 0);
+  // Part of our implementation assumes even widths and heights for greater
+  // precision.
+  assert((desc->screen_width & 1) == 0);
+  assert((desc->screen_height & 1) == 0);
+  assert((desc->tile_width & 1) == 0);
+  assert((desc->tile_height & 1) == 0);
+
+  IsoGfx* iso = calloc(1, sizeof(IsoGfx));
+  if (!iso) {
+    return 0;
+  }
+
+  iso->screen_width  = desc->screen_width;
+  iso->screen_height = desc->screen_height;
+  iso->tile_width    = desc->tile_width;
+  iso->tile_height   = desc->tile_height;
+  iso->world_width   = desc->world_width;
+  iso->world_height  = desc->world_height;
+  iso->max_num_tiles =
+      desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES;
+
+  const int world_size  = desc->world_width * desc->world_height;
+  const int screen_size = desc->screen_width * desc->screen_height;
+  const int tile_size   = desc->tile_width * desc->tile_height;
+
+  const int tile_size_bytes = tile_size * (int)sizeof(Pixel);
+
+  if (!(iso->world = calloc(world_size, sizeof(Tile)))) {
+    goto cleanup;
+  }
+  if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) {
+    goto cleanup;
+  }
+  if (!(iso->tile_mask = calloc(tile_size, sizeof(uint8_t)))) {
+    goto cleanup;
+  }
+  if (!mempool_make_dyn(&iso->tiles, iso->max_num_tiles, tile_size_bytes)) {
+    goto cleanup;
+  }
+
+  make_tile_mask(iso);
+
+  return iso;
+
+cleanup:
+  isogfx_del(&iso);
+  return 0;
+}
+
+void isogfx_del(IsoGfx** pIso) {
+  assert(pIso);
+  IsoGfx* iso = *pIso;
+  if (iso) {
+    if (iso->world) {
+      free(iso->world);
+    }
+    if (iso->screen) {
+      free(iso->screen);
+    }
+    if (iso->tile_mask) {
+      free(iso->tile_mask);
+    }
+    mempool_del(&iso->tiles);
+    free(iso);
+  }
+}
+
+Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) {
+  assert(iso);
+  assert(desc);
+
+  TileData* tile = mempool_alloc(&iso->tiles);
+  assert(tile); // TODO: Make this a hard assert.
+
+  switch (desc->type) {
+  case TileFromColour:
+    make_tile_from_colour(iso, desc->colour, tile);
+    break;
+  case TileFromFile:
+    assert(false); // TODO
+    break;
+  case TileFromMemory:
+    assert(false); // TODO
+    break;
+  }
+
+  return (Tile)mempool_get_block_index(&iso->tiles, tile);
+}
+
+void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) {
+  assert(iso);
+  *world_xy_mut(iso, x, y) = tile;
+}
+
+void isogfx_pick_tile(
+    const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) {
+  assert(iso);
+  assert(xiso);
+  assert(yiso);
+
+  const vec2 xy_iso = cart2iso(
+      (vec2){.x = xcart, .y = ycart}, iso->tile_width, iso->tile_height,
+      iso->screen_width);
+
+  const int x = (int)xy_iso.x;
+  const int y = (int)xy_iso.y;
+
+  if ((0 <= x) && (x < iso->world_width) && (0 <= y) &&
+      (y < iso->world_height)) {
+    *xiso = x;
+    *yiso = y;
+  } else {
+    *xiso = -1;
+  }
+}
+
+void isogfx_render(IsoGfx* iso) {
+  assert(iso);
+  draw(iso);
+}
+
+void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) {
+  assert(iso);
+  assert(x >= 0);
+  assert(y >= 0);
+  assert(x < iso->world_width);
+  assert(y < iso->world_height);
+
+  const ivec2 o  = {(iso->screen_width / 2) - (iso->tile_width / 2), 0};
+  const ivec2 vx = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
+  const ivec2 vy = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
+  const ivec2 so =
+      ivec2_add(o, ivec2_add(ivec2_scale(vx, x), ivec2_scale(vy, y)));
+
+  draw_tile(iso, so, tile);
+}
+
+const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) {
+  assert(iso);
+  return iso->screen;
+}
+
+int isogfx_world_width(const IsoGfx* iso) {
+  assert(iso);
+  return iso->world_width;
+}
+
+int isogfx_world_height(const IsoGfx* iso) {
+  assert(iso);
+  return iso->world_height;
+}
-- 
cgit v1.2.3