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. --- CMakeLists.txt | 1 + gfx-iso/CMakeLists.txt | 30 ++++ gfx-iso/demo/isogfx-demo.c | 213 ++++++++++++++++++++++++ gfx-iso/include/isogfx/isogfx.h | 66 ++++++++ gfx-iso/src/isogfx.c | 361 ++++++++++++++++++++++++++++++++++++++++ gfx/CMakeLists.txt | 4 +- 6 files changed, 673 insertions(+), 2 deletions(-) create mode 100644 gfx-iso/CMakeLists.txt create mode 100644 gfx-iso/demo/isogfx-demo.c create mode 100644 gfx-iso/include/isogfx/isogfx.h create mode 100644 gfx-iso/src/isogfx.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 2aac1ab..3a0cd5a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -2,4 +2,5 @@ cmake_minimum_required(VERSION 3.0) add_subdirectory(gfx) add_subdirectory(gfx-app) +add_subdirectory(gfx-iso) add_subdirectory(gltfview) diff --git a/gfx-iso/CMakeLists.txt b/gfx-iso/CMakeLists.txt new file mode 100644 index 0000000..8f95f7f --- /dev/null +++ b/gfx-iso/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 3.0) + +project(isogfx) + +set(CMAKE_C_STANDARD 11) +set(CMAKE_C_STANDARD_REQUIRED On) +set(CMAKE_C_EXTENSIONS Off) + +add_library(isogfx + src/isogfx.c) + +target_include_directories(isogfx PUBLIC + include) + +target_link_libraries(isogfx PRIVATE + mempool) + +target_compile_options(isogfx PRIVATE -Wall -Wextra -Wpedantic) + +# Demo + +project(isogfx-demo) + +add_executable(isogfx-demo + demo/isogfx-demo.c) + +target_link_libraries(isogfx-demo PRIVATE + gfx + gfx-app + isogfx) diff --git a/gfx-iso/demo/isogfx-demo.c b/gfx-iso/demo/isogfx-demo.c new file mode 100644 index 0000000..d6c1ab0 --- /dev/null +++ b/gfx-iso/demo/isogfx-demo.c @@ -0,0 +1,213 @@ +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +static const int SCREEN_WIDTH = 1408; +static const int SCREEN_HEIGHT = 960; +static const int TILE_WIDTH = 64; +static const int TILE_HEIGHT = TILE_WIDTH / 2; +static const int WORLD_WIDTH = 20; +static const int WORLD_HEIGHT = 20; + +static const Pixel BLACK = (Pixel){.r = 0x38, .g = 0x3b, .b = 0x46}; +static const Pixel WHITE = (Pixel){.r = 0xA5, .g = 0xb3, .b = 0xc0}; +static const Pixel RED = (Pixel){.r = 0xdc, .g = 0x76, .b = 0x84}; + +typedef struct State { + Gfx* gfx; + IsoGfx* iso; + Tile red; + int xpick; + int ypick; + Texture* screen_texture; + Scene* scene; +} State; + +static void make_checkerboard(IsoGfx* iso, Tile black, Tile white) { + assert(iso); + for (int y = 0; y < isogfx_world_height(iso); ++y) { + for (int x = 0; x < isogfx_world_width(iso); ++x) { + const int odd_col = x & 1; + const int odd_row = y & 1; + const Tile value = (odd_row ^ odd_col) == 0 ? black : white; + isogfx_set_tile(iso, x, y, value); + } + } +} + +static bool init(const GfxAppDesc* desc, void** app_state) { + State* state = calloc(1, sizeof(State)); + if (!state) { + return false; + } + + if (!(state->iso = isogfx_new(&(IsoGfxDesc){ + .screen_width = SCREEN_WIDTH, + .screen_height = SCREEN_HEIGHT, + .tile_width = TILE_WIDTH, + .tile_height = TILE_HEIGHT, + .world_width = WORLD_WIDTH, + .world_height = WORLD_HEIGHT}))) { + goto cleanup; + } + if (!(state->gfx = gfx_init())) { + goto cleanup; + } + RenderBackend* render_backend = gfx_get_render_backend(state->gfx); + + if (!(state->screen_texture = gfx_make_texture( + render_backend, &(TextureDesc){ + .width = SCREEN_WIDTH, + .height = SCREEN_HEIGHT, + .dimension = Texture2D, + .format = TextureRGB8, + .filtering = NearestFiltering, + .wrap = ClampToEdge, + .mipmaps = false}))) { + goto cleanup; + } + + ShaderProgram* shader = gfx_make_view_texture_shader(render_backend); + if (!shader) { + goto cleanup; + } + + Geometry* geometry = gfx_make_quad_11(render_backend); + if (!geometry) { + goto cleanup; + } + + MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1}; + material_desc.uniforms[0] = (ShaderUniform){ + .type = UniformTexture, + .value.texture = state->screen_texture, + .name = sstring_make("Texture")}; + Material* material = gfx_make_material(&material_desc); + if (!material) { + return false; + } + + const MeshDesc mesh_desc = + (MeshDesc){.geometry = geometry, .material = material, .shader = shader}; + Mesh* mesh = gfx_make_mesh(&mesh_desc); + if (!mesh) { + goto cleanup; + } + + SceneObject* object = gfx_make_object(); + if (!object) { + goto cleanup; + } + gfx_add_object_mesh(object, mesh); + + state->scene = gfx_make_scene(); + SceneNode* node = gfx_make_object_node(object); + SceneNode* root = gfx_get_scene_root(state->scene); + gfx_set_node_parent(node, root); + + const Tile black = isogfx_make_tile( + state->iso, &(TileDesc){.type = TileFromColour, .colour = BLACK}); + const Tile white = isogfx_make_tile( + state->iso, &(TileDesc){.type = TileFromColour, .colour = WHITE}); + state->red = isogfx_make_tile( + state->iso, &(TileDesc){.type = TileFromColour, .colour = RED}); + make_checkerboard(state->iso, black, white); + isogfx_render(state->iso); + + *app_state = state; + return true; + +cleanup: + if (state->gfx) { + gfx_destroy(&state->gfx); + } + free(state); + return false; +} + +static void shutdown(void* app_state) { + assert(app_state); + State* state = (State*)(app_state); + isogfx_del(&state->iso); + gfx_destroy(&state->gfx); + free(app_state); +} + +static void update(void* app_state, double t, double dt) { + assert(app_state); + State* state = (State*)(app_state); + + double mouse_x, mouse_y; + gfx_app_get_mouse_position(&mouse_x, &mouse_y); + + isogfx_pick_tile(state->iso, mouse_x, mouse_y, &state->xpick, &state->ypick); + + printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick); +} + +static void render(void* app_state) { + assert(app_state); + State* state = (State*)(app_state); + + isogfx_render(state->iso); + if ((state->xpick != -1) && (state->ypick != -1)) { + isogfx_draw_tile(state->iso, state->xpick, state->ypick, state->red); + } + + const Pixel* screen = isogfx_get_screen_buffer(state->iso); + assert(screen); + gfx_update_texture( + state->screen_texture, &(TextureDataDesc){.pixels = screen}); + + RenderBackend* render_backend = gfx_get_render_backend(state->gfx); + Renderer* renderer = gfx_get_renderer(state->gfx); + + gfx_start_frame(render_backend); + gfx_render_scene( + renderer, &(RenderSceneParams){ + .mode = RenderDefault, .scene = state->scene, .camera = 0}); + gfx_end_frame(render_backend); +} + +static void resize(void* app_state, int width, int height) { + assert(app_state); + State* state = (State*)(app_state); + + RenderBackend* render_backend = gfx_get_render_backend(state->gfx); + gfx_set_viewport(render_backend, width, height); +} + +int main(int argc, const char** argv) { + const int initial_width = SCREEN_WIDTH; + const int initial_height = SCREEN_HEIGHT; + const int max_fps = 60; + + gfx_app_run( + &(GfxAppDesc){ + .argc = argc, + .argv = argv, + .width = initial_width, + .height = initial_height, + .max_fps = max_fps, + .update_delta_time = max_fps > 0 ? 1.0 / (double)max_fps : 0.0, + .title = "Isometric Renderer"}, + &(GfxAppCallbacks){ + .init = init, + .update = update, + .render = render, + .resize = resize, + .shutdown = shutdown}); + + return 0; +} diff --git a/gfx-iso/include/isogfx/isogfx.h b/gfx-iso/include/isogfx/isogfx.h new file mode 100644 index 0000000..a5f7770 --- /dev/null +++ b/gfx-iso/include/isogfx/isogfx.h @@ -0,0 +1,66 @@ +/* + * Isometric rendering engine. + */ +#pragma once + +#include + +typedef struct IsoGfx IsoGfx; + +typedef uint8_t Tile; +typedef uint8_t Channel; + +typedef struct Pixel { + Channel r, g, b; +} Pixel; + +typedef enum TileDescType { + TileFromColour, + TileFromFile, + TileFromMemory +} TileDescType; + +typedef struct TileDesc { + TileDescType type; + union { + Pixel colour; + struct { + const char* path; + } file; + struct { + const void* data; + } mem; + }; +} TileDesc; + +typedef struct IsoGfxDesc { + int screen_width; + int screen_height; + int tile_width; + int tile_height; + int world_width; + int world_height; + int max_num_tiles; // 0 for an implementation-defined default. +} IsoGfxDesc; + +IsoGfx* isogfx_new(const IsoGfxDesc*); + +void isogfx_del(IsoGfx**); + +Tile isogfx_make_tile(IsoGfx*, const TileDesc*); + +void isogfx_set_tile(IsoGfx*, int x, int y, Tile); + +void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile); + +void isogfx_pick_tile( + const IsoGfx*, double xcart, double ycart, int* xiso, int* yiso); + +void isogfx_render(IsoGfx*); + +void isogfx_draw_tile(IsoGfx*, int x, int y, Tile); + +const Pixel* isogfx_get_screen_buffer(const IsoGfx*); + +int isogfx_world_width(const IsoGfx*); +int isogfx_world_height(const IsoGfx*); 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 + +#include + +#include +#include +#include +#include +#include + +/// 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; +} diff --git a/gfx/CMakeLists.txt b/gfx/CMakeLists.txt index f5ef44c..182e4e5 100644 --- a/gfx/CMakeLists.txt +++ b/gfx/CMakeLists.txt @@ -69,7 +69,8 @@ target_include_directories(gfx PRIVATE target_compile_options(gfx PRIVATE -std=gnu11 -Wall -Wextra -Wpedantic) target_link_libraries(gfx PUBLIC - cstring) + cstring + math) target_link_libraries(gfx PRIVATE cgltf @@ -78,7 +79,6 @@ target_link_libraries(gfx PRIVATE glad listpool log - math mempool shaders stb -- cgit v1.2.3