From bab4a8169135dfecb2e434cca6825dcb96e1b9ec Mon Sep 17 00:00:00 2001 From: 3gg <3gg@shellblade.net> Date: Tue, 2 Sep 2025 19:06:01 -0700 Subject: Rename isogfx -> gfx2d --- src/backend.c | 2 +- src/gfx2d.c | 708 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/isogfx.c | 708 ---------------------------------------------------------- 3 files changed, 709 insertions(+), 709 deletions(-) create mode 100644 src/gfx2d.c delete mode 100644 src/isogfx.c (limited to 'src') diff --git a/src/backend.c b/src/backend.c index 94f1728..80c5974 100644 --- a/src/backend.c +++ b/src/backend.c @@ -1,5 +1,5 @@ #include -#include +#include #include #include diff --git a/src/gfx2d.c b/src/gfx2d.c new file mode 100644 index 0000000..da265b0 --- /dev/null +++ b/src/gfx2d.c @@ -0,0 +1,708 @@ +#include + +#include + +#include +#include +#include + +#include +#include +#include +#include + +/// Maximum path length. +#define MAX_PATH 256 + +/// Default animation speed. +#define ANIMATION_FPS 10 + +/// Time between animation updates. +#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS) + +typedef struct ivec2 { + int x, y; +} ivec2; + +typedef struct vec2 { + double x, y; +} vec2; + +// ----------------------------------------------------------------------------- +// Renderer state. +// ----------------------------------------------------------------------------- + +typedef struct CoordSystem { + ivec2 o; // Origin. + ivec2 x; + ivec2 y; +} CoordSystem; + +typedef struct Screen { + int width; + int height; + Pixel* pixels; +} Screen; + +typedef struct SpriteInstance { + struct SpriteInstance* next; + const Ss_SpriteSheet* sheet; + ivec2 position; + int animation; // Current animation. + int frame; // Current frame of animation. +} SpriteInstance; + +typedef struct IsoGfx { + Screen screen; + CoordSystem iso_space; + ivec2 camera; + double last_animation_time; + Tile next_tile; // For procedurally-generated tiles. + Tm_Map* map; + Ts_TileSet* tileset; + SpriteInstance* head_sprite; // Head of sprites list. + memstack stack; + size_t watermark; +} IsoGfx; + +// ----------------------------------------------------------------------------- +// Math and world / tile / screen access. +// ----------------------------------------------------------------------------- + +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 ivec2_neg(ivec2 a) { return (ivec2){.x = -a.x, .y = -a.y}; } + +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 vec2_add(vec2 a, vec2 b) { + return (vec2){.x = a.x + b.x, .y = a.y + b.y}; +} + +static inline vec2 ivec2_to_vec2(ivec2 a) { return (vec2){a.x, a.y}; } + +// Method 1. +// static inline vec2 cart2iso(vec2 cart, int s, int t, int w) { +// const double x = cart.x - (double)(w / 2); +// const double xiso = (x * t + cart.y * s) / (double)(s * t); +// return (vec2){ +// .x = (int)(xiso), .y = (int)((2.0 / (double)t) * cart.y - xiso)}; +//} + +// Method 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 = (one_over_s * x + one_over_t * cart.y), + .y = (-one_over_s * x + one_over_t * cart.y)}; +} + +static inline const Pixel* screen_xy_const_ref( + const Screen* screen, int x, int y) { + assert(screen); + assert(x >= 0); + assert(y >= 0); + assert(x < screen->width); + assert(y < screen->height); + return &screen->pixels[y * screen->width + x]; +} + +static inline Pixel screen_xy(Screen* screen, int x, int y) { + return *screen_xy_const_ref(screen, x, y); +} + +static inline Pixel* screen_xy_mut(Screen* screen, int x, int y) { + return (Pixel*)screen_xy_const_ref(screen, x, y); +} + +/// Create the basis for the isometric coordinate system with origin and vectors +/// expressed in the Cartesian system. +static CoordSystem make_iso_coord_system( + const Tm_Map* const map, const Screen* const screen) { + assert(map); + assert(screen); + const ivec2 o = {screen->width / 2, 0}; + const ivec2 x = { + .x = map->base_tile_width / 2, .y = map->base_tile_height / 2}; + const ivec2 y = { + .x = -map->base_tile_width / 2, .y = map->base_tile_height / 2}; + return (CoordSystem){o, x, y}; +} + +// ----------------------------------------------------------------------------- +// Renderer, world and tile management. +// ----------------------------------------------------------------------------- + +IsoGfx* isogfx_new(const IsoGfxDesc* desc) { + assert(desc->screen_width > 0); + assert(desc->screen_height > 0); + // Part of our implementation assumes even widths and heights for precision. + assert((desc->screen_width & 1) == 0); + assert((desc->screen_height & 1) == 0); + + IsoGfx tmp = {0}; + if (!memstack_make(&tmp.stack, desc->memory_size, desc->memory)) { + goto cleanup; + } + IsoGfx* iso = + memstack_alloc_aligned(&tmp.stack, sizeof(IsoGfx), alignof(IsoGfx)); + *iso = tmp; + + const size_t screen_size_bytes = + desc->screen_width * desc->screen_height * sizeof(Pixel); + Pixel* screen = + memstack_alloc_aligned(&iso->stack, screen_size_bytes, alignof(Pixel)); + + iso->screen = (Screen){.width = desc->screen_width, + .height = desc->screen_height, + .pixels = screen}; + + iso->last_animation_time = 0.0; + iso->watermark = memstack_watermark(&iso->stack); + + return iso; + +cleanup: + isogfx_del(&iso); + return nullptr; +} + +void isogfx_clear(IsoGfx* iso) { + assert(iso); + iso->last_animation_time = 0.0; + iso->next_tile = 0; + iso->map = nullptr; + iso->tileset = nullptr; + iso->head_sprite = nullptr; + // The base of the stack contains the IsoGfx and the screen buffer. Make sure + // we don't clear them. + memstack_set_watermark(&iso->stack, iso->watermark); +} + +void isogfx_del(IsoGfx** ppIso) { + assert(ppIso); + IsoGfx* iso = *ppIso; + if (iso) { + memstack_del(&iso->stack); + *ppIso = nullptr; + } +} + +void isogfx_make_map(IsoGfx* iso, const MapDesc* desc) { + assert(iso); + assert(desc); + assert(desc->tile_width > 0); + assert(desc->tile_height > 0); + // Part of our implementation assumes even widths and heights for greater + // precision. + assert((desc->tile_width & 1) == 0); + assert((desc->tile_height & 1) == 0); + // World must be non-empty. + assert(desc->world_width > 0); + assert(desc->world_height > 0); + // Must have >0 tiles. + assert(desc->num_tiles > 0); + + // Handle recreation by destroying the previous world and sprites. + isogfx_clear(iso); + + const int world_size = desc->world_width * desc->world_height; + const size_t map_size_bytes = sizeof(Tm_Map) + (world_size * sizeof(Tile)); + + // This implies that all tiles are of the base tile dimensions. + // We could enhance the API to allow for supertiles as well. Take in max tile + // width and height and allocate enough space using those values. + const size_t tile_size = desc->tile_width * desc->tile_height; + const size_t tile_size_bytes = tile_size * sizeof(Pixel); + const size_t tile_data_size_bytes = desc->num_tiles * tile_size_bytes; + const size_t tileset_size_bytes = sizeof(Ts_TileSet) + + (desc->num_tiles * sizeof(Ts_Tile)) + + tile_data_size_bytes; + + iso->map = memstack_alloc_aligned(&iso->stack, map_size_bytes, 4); + *iso->map = (Tm_Map){ + .world_width = desc->world_width, + .world_height = desc->world_height, + .base_tile_width = desc->tile_width, + .base_tile_height = desc->tile_height, + .num_layers = 1, + }; + + iso->tileset = memstack_alloc_aligned(&iso->stack, tileset_size_bytes, 4); + *iso->tileset = (Ts_TileSet){ + .num_tiles = desc->num_tiles, + }; + + iso->iso_space = make_iso_coord_system(iso->map, &iso->screen); +} + +bool isogfx_load_map(IsoGfx* iso, const char* filepath) { + assert(iso); + assert(filepath); + + bool success = false; + + // Handle recreation by destroying the previous world and sprites. + isogfx_clear(iso); + + // Load the map. + printf("Load tile map: %s\n", filepath); + WITH_FILE(filepath, { + const size_t map_size = get_file_size_f(file); + iso->map = memstack_alloc_aligned(&iso->stack, map_size, 4); + success = read_file_f(file, iso->map); + }); + if (!success) { + goto cleanup; + } + Tm_Map* const map = iso->map; + + printf("Map orientation: %d\n", ((Tm_Flags*)&map->flags)->orientation); + + // Load the tile set. + // + // Tile set path is relative to the tile map file. Make it relative to the + // current working directory before loading. + const char* ts_path = map->tileset_path; + char ts_path_cwd[MAX_PATH] = {0}; + if (!path_make_relative(filepath, ts_path, ts_path_cwd, MAX_PATH)) { + goto cleanup; + } + printf("Load tile set: %s\n", ts_path_cwd); + WITH_FILE(ts_path_cwd, { + const size_t file_size = get_file_size_f(file); + iso->tileset = memstack_alloc_aligned(&iso->stack, file_size, 4); + success = read_file_f(file, iso->tileset); + }); + if (!success) { + // TODO: Log errors using the log library. + goto cleanup; + } + const Ts_TileSet* const tileset = iso->tileset; + printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd); + + // TODO: These assertions on input data should be library runtime errors. + assert(ts_validate_tileset(tileset)); + assert(tm_validate_map(map, tileset)); + + iso->iso_space = make_iso_coord_system(iso->map, &iso->screen); + + success = true; + +cleanup: + if (!success) { + isogfx_clear(iso); + } + return success; +} + +int isogfx_world_width(const IsoGfx* iso) { + assert(iso); + return iso->map->world_width; +} + +int isogfx_world_height(const IsoGfx* iso) { + assert(iso); + return iso->map->world_height; +} + +static void make_tile_from_colour( + Pixel colour, const Ts_Tile* tile, Pixel* tile_pixels) { + assert(tile); + assert(tile_pixels); + + const int width = tile->width; + const int height = tile->height; + const int r = width / height; + for (int y = 0; y < height / 2; ++y) { + const int mask_start = width / 2 - r * y - 1; + const int mask_end = width / 2 + r * y + 1; + for (int x = 0; x < width; ++x) { + const bool mask = (mask_start <= x) && (x <= mask_end); + const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0}; + + // Top half. + *ts_tile_xy_mut(tile_pixels, tile, x, y) = val; + + // Bottom half reflects the top half. + const int y_reflected = height - y - 1; + *ts_tile_xy_mut(tile_pixels, tile, x, y_reflected) = val; + } + } +} + +Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) { + assert(iso); + assert(desc); + // Client must create a world first. + assert(iso->map); + assert(iso->tileset); + // Currently, procedural tiles must match the base tile size. + assert(desc->width == iso->map->base_tile_width); + assert(desc->height == iso->map->base_tile_height); + // Cannot exceed max tiles. + assert(iso->next_tile < iso->tileset->num_tiles); + + const Tile tile = iso->next_tile++; + + const size_t tile_size_bytes = desc->width * desc->height * sizeof(Pixel); + + switch (desc->type) { + case TileFromColour: { + assert(desc->width > 0); + assert(desc->height > 0); + + Ts_Tile* const ts_tile = ts_tileset_get_tile_mut(iso->tileset, tile); + + *ts_tile = (Ts_Tile){ + .width = iso->map->base_tile_width, + .height = iso->map->base_tile_height, + .pixels = tile * tile_size_bytes, + }; + + Pixel* const tile_pixels = + ts_tileset_get_tile_pixels_mut(iso->tileset, tile); + make_tile_from_colour(desc->colour, ts_tile, tile_pixels); + break; + } + case TileFromFile: + assert(false); // TODO + break; + case TileFromMemory: { + assert(desc->width > 0); + assert(desc->height > 0); + assert(false); // TODO + break; + } + } + + return tile; +} + +void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) { + assert(iso); + + Tm_Layer* const layer = tm_map_get_layer_mut(iso->map, 0); + Tile* map_tile = tm_layer_get_tile_mut(iso->map, layer, x, y); + + *map_tile = tile; +} + +void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) { + assert(iso); + for (int y = y0; y < y1; ++y) { + for (int x = x0; x < x1; ++x) { + isogfx_set_tile(iso, x, y, tile); + } + } +} + +SpriteSheet isogfx_load_sprite_sheet(IsoGfx* iso, const char* filepath) { + assert(iso); + assert(filepath); + + bool success = false; + SpriteSheet spriteSheet = 0; + const size_t watermark = memstack_watermark(&iso->stack); + + // Load sprite sheet file. + printf("Load sprite sheet: %s\n", filepath); + Ss_SpriteSheet* ss_sheet = nullptr; + WITH_FILE(filepath, { + const size_t file_size = get_file_size_f(file); + ss_sheet = + memstack_alloc_aligned(&iso->stack, file_size, alignof(Ss_SpriteSheet)); + success = read_file_f(file, ss_sheet); + }); + if (!success) { + goto cleanup; + } + assert(ss_sheet); + + spriteSheet = (SpriteSheet)ss_sheet; + +cleanup: + if (!success) { + if (ss_sheet) { + memstack_set_watermark(&iso->stack, watermark); + } + } + return spriteSheet; +} + +Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) { + assert(iso); + assert(sheet); + + // TODO: Remove memstack_alloc() and replace it with a same-name macro that + // calls memstack_alloc_aligned() with sizeof/alignof. No real point in + // having unaligned allocations. + SpriteInstance* sprite = memstack_alloc_aligned( + &iso->stack, sizeof(SpriteInstance), alignof(SpriteInstance)); + + sprite->sheet = (const Ss_SpriteSheet*)sheet; + sprite->next = iso->head_sprite; + iso->head_sprite = sprite; + + return (Sprite)sprite; +} + +void isogfx_set_sprite_position(IsoGfx* iso, Sprite hSprite, int x, int y) { + assert(iso); + SpriteInstance* sprite = (SpriteInstance*)hSprite; + sprite->position.x = x; + sprite->position.y = y; +} + +void isogfx_set_sprite_animation(IsoGfx* iso, Sprite hSprite, int animation) { + assert(iso); + SpriteInstance* sprite = (SpriteInstance*)hSprite; + sprite->animation = animation; +} + +void isogfx_update(IsoGfx* iso, double t) { + assert(iso); + + // If this is the first time update() is called after initialization, just + // record the starting animation time. + if (iso->last_animation_time == 0.0) { + iso->last_animation_time = t; + return; + } + + if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) { + // TODO: Consider linking animated sprites in a separate list so that we + // only walk over those here and not also the static sprites. + for (SpriteInstance* sprite = iso->head_sprite; sprite; + sprite = sprite->next) { + const Ss_SpriteSheet* sheet = sprite->sheet; + const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation); + sprite->frame = (sprite->frame + 1) % row->num_cols; + } + + iso->last_animation_time = t; + } +} + +// ----------------------------------------------------------------------------- +// Rendering and picking. +// ----------------------------------------------------------------------------- + +/// Get the screen position of the top diamond-corner of the tile at world +/// (x,y). +static ivec2 GetTileScreenOrigin( + const CoordSystem iso_space, ivec2 camera, int world_x, int world_y) { + const ivec2 vx_offset = ivec2_scale(iso_space.x, world_x); + const ivec2 vy_offset = ivec2_scale(iso_space.y, world_y); + const ivec2 screen_origin = + ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset)); + const ivec2 origin_view_space = ivec2_add(screen_origin, ivec2_neg(camera)); + return origin_view_space; +} + +static Pixel alpha_blend(Pixel src, Pixel dst) { + if ((src.a == 255) || (dst.a == 0)) { + return src; + } + const uint16_t one_minus_alpha = 255 - src.a; +#define blend(s, d) \ + (Channel)( \ + (double)((uint16_t)s * (uint16_t)src.a + \ + (uint16_t)d * one_minus_alpha) / \ + 255.0) + return (Pixel){.r = blend(src.r, dst.r), + .g = blend(src.g, dst.g), + .b = blend(src.b, dst.b), + .a = src.a}; +} + +/// Draw a rectangle (tile or sprite). +/// +/// The rectangle's top-left corner is mapped to the screen space position given +/// by 'top_left'. +/// +/// 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( + Screen* screen, ivec2 top_left, int rect_width, int rect_height, + const Pixel* pixels, const uint8_t* indices) { + assert(screen); + +#define rect_pixel(X, Y) \ + (indices ? pixels[indices[Y * rect_width + X]] : pixels[Y * rect_width + X]) + + // Rect origin can be outside screen bounds, so we must offset accordingly to + // draw only the visible portion. +#define max(a, b) (a > b ? a : b) + const int px_offset = max(0, -top_left.x); + const int py_offset = max(0, -top_left.y); + + // Rect can exceed screen bounds, so clip along Y and X as we draw. + for (int py = py_offset; + (py < rect_height) && (top_left.y + py < screen->height); ++py) { + const int sy = top_left.y + py; + for (int px = px_offset; + (px < rect_width) && (top_left.x + px < screen->width); ++px) { + const Pixel colour = rect_pixel(px, py); + if (colour.a > 0) { + const int sx = top_left.x + px; + const Pixel dst = screen_xy(screen, sx, sy); + const Pixel final = alpha_blend(colour, dst); + *screen_xy_mut(screen, sx, sy) = final; + } + } + } +} + +/// Draw a tile. +/// +/// 'screen_origin' is the screen coordinates of the top diamond-corner of the +/// tile (the base tile for super tiles). +/// World (0, 0) -> (screen_width / 2, 0). +static void draw_tile(IsoGfx* iso, ivec2 screen_origin, Tile tile) { + assert(iso); + assert(iso->tileset); + + const Ts_Tile* pTile = ts_tileset_get_tile(iso->tileset, tile); + const Pixel* pixels = ts_tileset_get_tile_pixels(iso->tileset, tile); + + // Move from the top diamond-corner to the top-left corner of the tile image. + // For regular tiles, tile height == base tile height, so the y offset is 0. + // For super tiles, move as high up as the height of the tile. + const ivec2 offset = { + -(iso->map->base_tile_width / 2), + pTile->height - iso->map->base_tile_height}; + const ivec2 top_left = ivec2_add(screen_origin, offset); + + draw_rect( + &iso->screen, top_left, pTile->width, pTile->height, pixels, nullptr); +} + +static void draw_map(IsoGfx* iso) { + assert(iso); + + const int W = iso->screen.width; + const int H = iso->screen.height; + + memset(iso->screen.pixels, 0, W * H * sizeof(Pixel)); + + const Tm_Layer* layer = tm_map_get_layer(iso->map, 0); + + // TODO: Culling. + // Ex: map the screen corners to tile space to cull. + // Ex: walk in screen space and fetch the tile. + // The tile-centric approach might be more cache-friendly since the + // screen-centric approach would juggle multiple tiles throughout the scan. + for (int wy = 0; wy < iso->map->world_height; ++wy) { + for (int wx = 0; wx < iso->map->world_width; ++wx) { + const Tile tile = tm_layer_get_tile(iso->map, layer, wx, wy); + const ivec2 screen_origin = + GetTileScreenOrigin(iso->iso_space, iso->camera, wx, wy); + draw_tile(iso, screen_origin, tile); + } + } +} + +static void draw_sprite( + IsoGfx* iso, ivec2 origin, const SpriteInstance* sprite, + const Ss_SpriteSheet* sheet) { + assert(iso); + assert(sprite); + assert(sheet); + assert(sprite->animation >= 0); + assert(sprite->animation < sheet->num_rows); + assert(sprite->frame >= 0); + + const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation); + const uint8_t* frame = ss_get_sprite_sheet_sprite(sheet, row, sprite->frame); + draw_rect( + &iso->screen, origin, sheet->sprite_width, sheet->sprite_height, + sheet->palette.colours, frame); +} + +static void draw_sprites(IsoGfx* iso) { + assert(iso); + + for (const SpriteInstance* sprite = iso->head_sprite; sprite; + sprite = sprite->next) { + const Ss_SpriteSheet* sheet = sprite->sheet; + assert(sheet); + + const ivec2 screen_origin = GetTileScreenOrigin( + iso->iso_space, iso->camera, sprite->position.x, sprite->position.y); + draw_sprite(iso, screen_origin, sprite, sheet); + } +} + +void isogfx_set_camera(IsoGfx* iso, int x, int y) { + assert(iso); + iso->camera = (ivec2){x, y}; +} + +void isogfx_render(IsoGfx* iso) { + assert(iso); + draw_map(iso); + draw_sprites(iso); +} + +void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { + assert(iso); + assert(x >= 0); + assert(y >= 0); + assert(x < iso->map->world_width); + assert(y < iso->map->world_height); + + const ivec2 screen_origin = + GetTileScreenOrigin(iso->iso_space, iso->camera, x, y); + draw_tile(iso, screen_origin, tile); +} + +void isogfx_get_screen_size(const IsoGfx* iso, int* width, int* height) { + assert(iso); + assert(width); + assert(height); + *width = iso->screen.width; + *height = iso->screen.height; +} + +const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) { + assert(iso); + return iso->screen.pixels; +} + +void isogfx_pick_tile( + const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) { + assert(iso); + assert(xiso); + assert(yiso); + + const vec2 camera = ivec2_to_vec2(iso->camera); + const vec2 xy_cart = vec2_add(camera, (vec2){xcart, ycart}); + + const vec2 xy_iso = cart2iso( + xy_cart, iso->map->base_tile_width, iso->map->base_tile_height, + iso->screen.width); + + if ((0 <= xy_iso.x) && (xy_iso.x < iso->map->world_width) && + (0 <= xy_iso.y) && (xy_iso.y < iso->map->world_height)) { + *xiso = (int)xy_iso.x; + *yiso = (int)xy_iso.y; + } else { + *xiso = -1; + *yiso = -1; + } +} diff --git a/src/isogfx.c b/src/isogfx.c deleted file mode 100644 index fcb4b19..0000000 --- a/src/isogfx.c +++ /dev/null @@ -1,708 +0,0 @@ -#include - -#include - -#include -#include -#include - -#include -#include -#include -#include - -/// Maximum path length. -#define MAX_PATH 256 - -/// Default animation speed. -#define ANIMATION_FPS 10 - -/// Time between animation updates. -#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS) - -typedef struct ivec2 { - int x, y; -} ivec2; - -typedef struct vec2 { - double x, y; -} vec2; - -// ----------------------------------------------------------------------------- -// Renderer state. -// ----------------------------------------------------------------------------- - -typedef struct CoordSystem { - ivec2 o; // Origin. - ivec2 x; - ivec2 y; -} CoordSystem; - -typedef struct Screen { - int width; - int height; - Pixel* pixels; -} Screen; - -typedef struct SpriteInstance { - struct SpriteInstance* next; - const Ss_SpriteSheet* sheet; - ivec2 position; - int animation; // Current animation. - int frame; // Current frame of animation. -} SpriteInstance; - -typedef struct IsoGfx { - Screen screen; - CoordSystem iso_space; - ivec2 camera; - double last_animation_time; - Tile next_tile; // For procedurally-generated tiles. - Tm_Map* map; - Ts_TileSet* tileset; - SpriteInstance* head_sprite; // Head of sprites list. - memstack stack; - size_t watermark; -} IsoGfx; - -// ----------------------------------------------------------------------------- -// Math and world / tile / screen access. -// ----------------------------------------------------------------------------- - -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 ivec2_neg(ivec2 a) { return (ivec2){.x = -a.x, .y = -a.y}; } - -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 vec2_add(vec2 a, vec2 b) { - return (vec2){.x = a.x + b.x, .y = a.y + b.y}; -} - -static inline vec2 ivec2_to_vec2(ivec2 a) { return (vec2){a.x, a.y}; } - -// Method 1. -// static inline vec2 cart2iso(vec2 cart, int s, int t, int w) { -// const double x = cart.x - (double)(w / 2); -// const double xiso = (x * t + cart.y * s) / (double)(s * t); -// return (vec2){ -// .x = (int)(xiso), .y = (int)((2.0 / (double)t) * cart.y - xiso)}; -//} - -// Method 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 = (one_over_s * x + one_over_t * cart.y), - .y = (-one_over_s * x + one_over_t * cart.y)}; -} - -static inline const Pixel* screen_xy_const_ref( - const Screen* screen, int x, int y) { - assert(screen); - assert(x >= 0); - assert(y >= 0); - assert(x < screen->width); - assert(y < screen->height); - return &screen->pixels[y * screen->width + x]; -} - -static inline Pixel screen_xy(Screen* screen, int x, int y) { - return *screen_xy_const_ref(screen, x, y); -} - -static inline Pixel* screen_xy_mut(Screen* screen, int x, int y) { - return (Pixel*)screen_xy_const_ref(screen, x, y); -} - -/// Create the basis for the isometric coordinate system with origin and vectors -/// expressed in the Cartesian system. -static CoordSystem make_iso_coord_system( - const Tm_Map* const map, const Screen* const screen) { - assert(map); - assert(screen); - const ivec2 o = {screen->width / 2, 0}; - const ivec2 x = { - .x = map->base_tile_width / 2, .y = map->base_tile_height / 2}; - const ivec2 y = { - .x = -map->base_tile_width / 2, .y = map->base_tile_height / 2}; - return (CoordSystem){o, x, y}; -} - -// ----------------------------------------------------------------------------- -// Renderer, world and tile management. -// ----------------------------------------------------------------------------- - -IsoGfx* isogfx_new(const IsoGfxDesc* desc) { - assert(desc->screen_width > 0); - assert(desc->screen_height > 0); - // Part of our implementation assumes even widths and heights for precision. - assert((desc->screen_width & 1) == 0); - assert((desc->screen_height & 1) == 0); - - IsoGfx tmp = {0}; - if (!memstack_make(&tmp.stack, desc->memory_size, desc->memory)) { - goto cleanup; - } - IsoGfx* iso = - memstack_alloc_aligned(&tmp.stack, sizeof(IsoGfx), alignof(IsoGfx)); - *iso = tmp; - - const size_t screen_size_bytes = - desc->screen_width * desc->screen_height * sizeof(Pixel); - Pixel* screen = - memstack_alloc_aligned(&iso->stack, screen_size_bytes, alignof(Pixel)); - - iso->screen = (Screen){.width = desc->screen_width, - .height = desc->screen_height, - .pixels = screen}; - - iso->last_animation_time = 0.0; - iso->watermark = memstack_watermark(&iso->stack); - - return iso; - -cleanup: - isogfx_del(&iso); - return nullptr; -} - -void isogfx_clear(IsoGfx* iso) { - assert(iso); - iso->last_animation_time = 0.0; - iso->next_tile = 0; - iso->map = nullptr; - iso->tileset = nullptr; - iso->head_sprite = nullptr; - // The base of the stack contains the IsoGfx and the screen buffer. Make sure - // we don't clear them. - memstack_set_watermark(&iso->stack, iso->watermark); -} - -void isogfx_del(IsoGfx** ppIso) { - assert(ppIso); - IsoGfx* iso = *ppIso; - if (iso) { - memstack_del(&iso->stack); - *ppIso = nullptr; - } -} - -void isogfx_make_map(IsoGfx* iso, const MapDesc* desc) { - assert(iso); - assert(desc); - assert(desc->tile_width > 0); - assert(desc->tile_height > 0); - // Part of our implementation assumes even widths and heights for greater - // precision. - assert((desc->tile_width & 1) == 0); - assert((desc->tile_height & 1) == 0); - // World must be non-empty. - assert(desc->world_width > 0); - assert(desc->world_height > 0); - // Must have >0 tiles. - assert(desc->num_tiles > 0); - - // Handle recreation by destroying the previous world and sprites. - isogfx_clear(iso); - - const int world_size = desc->world_width * desc->world_height; - const size_t map_size_bytes = sizeof(Tm_Map) + (world_size * sizeof(Tile)); - - // This implies that all tiles are of the base tile dimensions. - // We could enhance the API to allow for supertiles as well. Take in max tile - // width and height and allocate enough space using those values. - const size_t tile_size = desc->tile_width * desc->tile_height; - const size_t tile_size_bytes = tile_size * sizeof(Pixel); - const size_t tile_data_size_bytes = desc->num_tiles * tile_size_bytes; - const size_t tileset_size_bytes = sizeof(Ts_TileSet) + - (desc->num_tiles * sizeof(Ts_Tile)) + - tile_data_size_bytes; - - iso->map = memstack_alloc_aligned(&iso->stack, map_size_bytes, 4); - *iso->map = (Tm_Map){ - .world_width = desc->world_width, - .world_height = desc->world_height, - .base_tile_width = desc->tile_width, - .base_tile_height = desc->tile_height, - .num_layers = 1, - }; - - iso->tileset = memstack_alloc_aligned(&iso->stack, tileset_size_bytes, 4); - *iso->tileset = (Ts_TileSet){ - .num_tiles = desc->num_tiles, - }; - - iso->iso_space = make_iso_coord_system(iso->map, &iso->screen); -} - -bool isogfx_load_map(IsoGfx* iso, const char* filepath) { - assert(iso); - assert(filepath); - - bool success = false; - - // Handle recreation by destroying the previous world and sprites. - isogfx_clear(iso); - - // Load the map. - printf("Load tile map: %s\n", filepath); - WITH_FILE(filepath, { - const size_t map_size = get_file_size_f(file); - iso->map = memstack_alloc_aligned(&iso->stack, map_size, 4); - success = read_file_f(file, iso->map); - }); - if (!success) { - goto cleanup; - } - Tm_Map* const map = iso->map; - - printf("Map orientation: %d\n", ((Tm_Flags*)&map->flags)->orientation); - - // Load the tile set. - // - // Tile set path is relative to the tile map file. Make it relative to the - // current working directory before loading. - const char* ts_path = map->tileset_path; - char ts_path_cwd[MAX_PATH] = {0}; - if (!path_make_relative(filepath, ts_path, ts_path_cwd, MAX_PATH)) { - goto cleanup; - } - printf("Load tile set: %s\n", ts_path_cwd); - WITH_FILE(ts_path_cwd, { - const size_t file_size = get_file_size_f(file); - iso->tileset = memstack_alloc_aligned(&iso->stack, file_size, 4); - success = read_file_f(file, iso->tileset); - }); - if (!success) { - // TODO: Log errors using the log library. - goto cleanup; - } - const Ts_TileSet* const tileset = iso->tileset; - printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd); - - // TODO: These assertions on input data should be library runtime errors. - assert(ts_validate_tileset(tileset)); - assert(tm_validate_map(map, tileset)); - - iso->iso_space = make_iso_coord_system(iso->map, &iso->screen); - - success = true; - -cleanup: - if (!success) { - isogfx_clear(iso); - } - return success; -} - -int isogfx_world_width(const IsoGfx* iso) { - assert(iso); - return iso->map->world_width; -} - -int isogfx_world_height(const IsoGfx* iso) { - assert(iso); - return iso->map->world_height; -} - -static void make_tile_from_colour( - Pixel colour, const Ts_Tile* tile, Pixel* tile_pixels) { - assert(tile); - assert(tile_pixels); - - const int width = tile->width; - const int height = tile->height; - const int r = width / height; - for (int y = 0; y < height / 2; ++y) { - const int mask_start = width / 2 - r * y - 1; - const int mask_end = width / 2 + r * y + 1; - for (int x = 0; x < width; ++x) { - const bool mask = (mask_start <= x) && (x <= mask_end); - const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0}; - - // Top half. - *ts_tile_xy_mut(tile_pixels, tile, x, y) = val; - - // Bottom half reflects the top half. - const int y_reflected = height - y - 1; - *ts_tile_xy_mut(tile_pixels, tile, x, y_reflected) = val; - } - } -} - -Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) { - assert(iso); - assert(desc); - // Client must create a world first. - assert(iso->map); - assert(iso->tileset); - // Currently, procedural tiles must match the base tile size. - assert(desc->width == iso->map->base_tile_width); - assert(desc->height == iso->map->base_tile_height); - // Cannot exceed max tiles. - assert(iso->next_tile < iso->tileset->num_tiles); - - const Tile tile = iso->next_tile++; - - const size_t tile_size_bytes = desc->width * desc->height * sizeof(Pixel); - - switch (desc->type) { - case TileFromColour: { - assert(desc->width > 0); - assert(desc->height > 0); - - Ts_Tile* const ts_tile = ts_tileset_get_tile_mut(iso->tileset, tile); - - *ts_tile = (Ts_Tile){ - .width = iso->map->base_tile_width, - .height = iso->map->base_tile_height, - .pixels = tile * tile_size_bytes, - }; - - Pixel* const tile_pixels = - ts_tileset_get_tile_pixels_mut(iso->tileset, tile); - make_tile_from_colour(desc->colour, ts_tile, tile_pixels); - break; - } - case TileFromFile: - assert(false); // TODO - break; - case TileFromMemory: { - assert(desc->width > 0); - assert(desc->height > 0); - assert(false); // TODO - break; - } - } - - return tile; -} - -void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) { - assert(iso); - - Tm_Layer* const layer = tm_map_get_layer_mut(iso->map, 0); - Tile* map_tile = tm_layer_get_tile_mut(iso->map, layer, x, y); - - *map_tile = tile; -} - -void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) { - assert(iso); - for (int y = y0; y < y1; ++y) { - for (int x = x0; x < x1; ++x) { - isogfx_set_tile(iso, x, y, tile); - } - } -} - -SpriteSheet isogfx_load_sprite_sheet(IsoGfx* iso, const char* filepath) { - assert(iso); - assert(filepath); - - bool success = false; - SpriteSheet spriteSheet = 0; - const size_t watermark = memstack_watermark(&iso->stack); - - // Load sprite sheet file. - printf("Load sprite sheet: %s\n", filepath); - Ss_SpriteSheet* ss_sheet = nullptr; - WITH_FILE(filepath, { - const size_t file_size = get_file_size_f(file); - ss_sheet = - memstack_alloc_aligned(&iso->stack, file_size, alignof(Ss_SpriteSheet)); - success = read_file_f(file, ss_sheet); - }); - if (!success) { - goto cleanup; - } - assert(ss_sheet); - - spriteSheet = (SpriteSheet)ss_sheet; - -cleanup: - if (!success) { - if (ss_sheet) { - memstack_set_watermark(&iso->stack, watermark); - } - } - return spriteSheet; -} - -Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) { - assert(iso); - assert(sheet); - - // TODO: Remove memstack_alloc() and replace it with a same-name macro that - // calls memstack_alloc_aligned() with sizeof/alignof. No real point in - // having unaligned allocations. - SpriteInstance* sprite = memstack_alloc_aligned( - &iso->stack, sizeof(SpriteInstance), alignof(SpriteInstance)); - - sprite->sheet = (const Ss_SpriteSheet*)sheet; - sprite->next = iso->head_sprite; - iso->head_sprite = sprite; - - return (Sprite)sprite; -} - -void isogfx_set_sprite_position(IsoGfx* iso, Sprite hSprite, int x, int y) { - assert(iso); - SpriteInstance* sprite = (SpriteInstance*)hSprite; - sprite->position.x = x; - sprite->position.y = y; -} - -void isogfx_set_sprite_animation(IsoGfx* iso, Sprite hSprite, int animation) { - assert(iso); - SpriteInstance* sprite = (SpriteInstance*)hSprite; - sprite->animation = animation; -} - -void isogfx_update(IsoGfx* iso, double t) { - assert(iso); - - // If this is the first time update() is called after initialization, just - // record the starting animation time. - if (iso->last_animation_time == 0.0) { - iso->last_animation_time = t; - return; - } - - if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) { - // TODO: Consider linking animated sprites in a separate list so that we - // only walk over those here and not also the static sprites. - for (SpriteInstance* sprite = iso->head_sprite; sprite; - sprite = sprite->next) { - const Ss_SpriteSheet* sheet = sprite->sheet; - const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation); - sprite->frame = (sprite->frame + 1) % row->num_cols; - } - - iso->last_animation_time = t; - } -} - -// ----------------------------------------------------------------------------- -// Rendering and picking. -// ----------------------------------------------------------------------------- - -/// Get the screen position of the top diamond-corner of the tile at world -/// (x,y). -static ivec2 GetTileScreenOrigin( - const CoordSystem iso_space, ivec2 camera, int world_x, int world_y) { - const ivec2 vx_offset = ivec2_scale(iso_space.x, world_x); - const ivec2 vy_offset = ivec2_scale(iso_space.y, world_y); - const ivec2 screen_origin = - ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset)); - const ivec2 origin_view_space = ivec2_add(screen_origin, ivec2_neg(camera)); - return origin_view_space; -} - -static Pixel alpha_blend(Pixel src, Pixel dst) { - if ((src.a == 255) || (dst.a == 0)) { - return src; - } - const uint16_t one_minus_alpha = 255 - src.a; -#define blend(s, d) \ - (Channel)( \ - (double)((uint16_t)s * (uint16_t)src.a + \ - (uint16_t)d * one_minus_alpha) / \ - 255.0) - return (Pixel){.r = blend(src.r, dst.r), - .g = blend(src.g, dst.g), - .b = blend(src.b, dst.b), - .a = src.a}; -} - -/// Draw a rectangle (tile or sprite). -/// -/// The rectangle's top-left corner is mapped to the screen space position given -/// by 'top_left'. -/// -/// 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( - Screen* screen, ivec2 top_left, int rect_width, int rect_height, - const Pixel* pixels, const uint8_t* indices) { - assert(screen); - -#define rect_pixel(X, Y) \ - (indices ? pixels[indices[Y * rect_width + X]] : pixels[Y * rect_width + X]) - - // Rect origin can be outside screen bounds, so we must offset accordingly to - // draw only the visible portion. -#define max(a, b) (a > b ? a : b) - const int px_offset = max(0, -top_left.x); - const int py_offset = max(0, -top_left.y); - - // Rect can exceed screen bounds, so clip along Y and X as we draw. - for (int py = py_offset; - (py < rect_height) && (top_left.y + py < screen->height); ++py) { - const int sy = top_left.y + py; - for (int px = px_offset; - (px < rect_width) && (top_left.x + px < screen->width); ++px) { - const Pixel colour = rect_pixel(px, py); - if (colour.a > 0) { - const int sx = top_left.x + px; - const Pixel dst = screen_xy(screen, sx, sy); - const Pixel final = alpha_blend(colour, dst); - *screen_xy_mut(screen, sx, sy) = final; - } - } - } -} - -/// Draw a tile. -/// -/// 'screen_origin' is the screen coordinates of the top diamond-corner of the -/// tile (the base tile for super tiles). -/// World (0, 0) -> (screen_width / 2, 0). -static void draw_tile(IsoGfx* iso, ivec2 screen_origin, Tile tile) { - assert(iso); - assert(iso->tileset); - - const Ts_Tile* pTile = ts_tileset_get_tile(iso->tileset, tile); - const Pixel* pixels = ts_tileset_get_tile_pixels(iso->tileset, tile); - - // Move from the top diamond-corner to the top-left corner of the tile image. - // For regular tiles, tile height == base tile height, so the y offset is 0. - // For super tiles, move as high up as the height of the tile. - const ivec2 offset = { - -(iso->map->base_tile_width / 2), - pTile->height - iso->map->base_tile_height}; - const ivec2 top_left = ivec2_add(screen_origin, offset); - - draw_rect( - &iso->screen, top_left, pTile->width, pTile->height, pixels, nullptr); -} - -static void draw_map(IsoGfx* iso) { - assert(iso); - - const int W = iso->screen.width; - const int H = iso->screen.height; - - memset(iso->screen.pixels, 0, W * H * sizeof(Pixel)); - - const Tm_Layer* layer = tm_map_get_layer(iso->map, 0); - - // TODO: Culling. - // Ex: map the screen corners to tile space to cull. - // Ex: walk in screen space and fetch the tile. - // The tile-centric approach might be more cache-friendly since the - // screen-centric approach would juggle multiple tiles throughout the scan. - for (int wy = 0; wy < iso->map->world_height; ++wy) { - for (int wx = 0; wx < iso->map->world_width; ++wx) { - const Tile tile = tm_layer_get_tile(iso->map, layer, wx, wy); - const ivec2 screen_origin = - GetTileScreenOrigin(iso->iso_space, iso->camera, wx, wy); - draw_tile(iso, screen_origin, tile); - } - } -} - -static void draw_sprite( - IsoGfx* iso, ivec2 origin, const SpriteInstance* sprite, - const Ss_SpriteSheet* sheet) { - assert(iso); - assert(sprite); - assert(sheet); - assert(sprite->animation >= 0); - assert(sprite->animation < sheet->num_rows); - assert(sprite->frame >= 0); - - const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation); - const uint8_t* frame = ss_get_sprite_sheet_sprite(sheet, row, sprite->frame); - draw_rect( - &iso->screen, origin, sheet->sprite_width, sheet->sprite_height, - sheet->palette.colours, frame); -} - -static void draw_sprites(IsoGfx* iso) { - assert(iso); - - for (const SpriteInstance* sprite = iso->head_sprite; sprite; - sprite = sprite->next) { - const Ss_SpriteSheet* sheet = sprite->sheet; - assert(sheet); - - const ivec2 screen_origin = GetTileScreenOrigin( - iso->iso_space, iso->camera, sprite->position.x, sprite->position.y); - draw_sprite(iso, screen_origin, sprite, sheet); - } -} - -void isogfx_set_camera(IsoGfx* iso, int x, int y) { - assert(iso); - iso->camera = (ivec2){x, y}; -} - -void isogfx_render(IsoGfx* iso) { - assert(iso); - draw_map(iso); - draw_sprites(iso); -} - -void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { - assert(iso); - assert(x >= 0); - assert(y >= 0); - assert(x < iso->map->world_width); - assert(y < iso->map->world_height); - - const ivec2 screen_origin = - GetTileScreenOrigin(iso->iso_space, iso->camera, x, y); - draw_tile(iso, screen_origin, tile); -} - -void isogfx_get_screen_size(const IsoGfx* iso, int* width, int* height) { - assert(iso); - assert(width); - assert(height); - *width = iso->screen.width; - *height = iso->screen.height; -} - -const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) { - assert(iso); - return iso->screen.pixels; -} - -void isogfx_pick_tile( - const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) { - assert(iso); - assert(xiso); - assert(yiso); - - const vec2 camera = ivec2_to_vec2(iso->camera); - const vec2 xy_cart = vec2_add(camera, (vec2){xcart, ycart}); - - const vec2 xy_iso = cart2iso( - xy_cart, iso->map->base_tile_width, iso->map->base_tile_height, - iso->screen.width); - - if ((0 <= xy_iso.x) && (xy_iso.x < iso->map->world_width) && - (0 <= xy_iso.y) && (xy_iso.y < iso->map->world_height)) { - *xiso = (int)xy_iso.x; - *yiso = (int)xy_iso.y; - } else { - *xiso = -1; - *yiso = -1; - } -} -- cgit v1.2.3