diff options
Diffstat (limited to 'gfx-iso')
-rw-r--r-- | gfx-iso/CMakeLists.txt | 42 | ||||
-rw-r--r-- | gfx-iso/demos/CMakeLists.txt | 2 | ||||
-rw-r--r-- | gfx-iso/demos/checkerboard/CMakeLists.txt | 16 | ||||
-rw-r--r-- | gfx-iso/demos/checkerboard/checkerboard.c | 166 | ||||
-rw-r--r-- | gfx-iso/demos/isomap/CMakeLists.txt | 16 | ||||
-rw-r--r-- | gfx-iso/demos/isomap/isomap.c | 105 | ||||
-rw-r--r-- | gfx-iso/include/isogfx/backend.h | 28 | ||||
-rw-r--r-- | gfx-iso/include/isogfx/isogfx.h | 136 | ||||
-rw-r--r-- | gfx-iso/src/backend.c | 199 | ||||
-rw-r--r-- | gfx-iso/src/isogfx.c | 952 | ||||
-rw-r--r-- | gfx-iso/tools/mkasset.py | 324 |
11 files changed, 0 insertions, 1986 deletions
diff --git a/gfx-iso/CMakeLists.txt b/gfx-iso/CMakeLists.txt deleted file mode 100644 index e4a677d..0000000 --- a/gfx-iso/CMakeLists.txt +++ /dev/null | |||
@@ -1,42 +0,0 @@ | |||
1 | cmake_minimum_required(VERSION 3.0) | ||
2 | |||
3 | project(isogfx) | ||
4 | |||
5 | set(CMAKE_C_STANDARD 17) | ||
6 | set(CMAKE_C_STANDARD_REQUIRED On) | ||
7 | set(CMAKE_C_EXTENSIONS Off) | ||
8 | |||
9 | # isogfx | ||
10 | |||
11 | add_library(isogfx | ||
12 | src/isogfx.c) | ||
13 | |||
14 | target_include_directories(isogfx PUBLIC | ||
15 | include) | ||
16 | |||
17 | target_link_libraries(isogfx PUBLIC | ||
18 | filesystem | ||
19 | mem | ||
20 | mempool) | ||
21 | |||
22 | target_compile_options(isogfx PRIVATE -Wall -Wextra -Wpedantic) | ||
23 | |||
24 | # Backend | ||
25 | |||
26 | add_library(isogfx-backend | ||
27 | src/backend.c) | ||
28 | |||
29 | target_include_directories(isogfx-backend PUBLIC | ||
30 | include) | ||
31 | |||
32 | target_link_libraries(isogfx-backend PUBLIC | ||
33 | isogfx) | ||
34 | |||
35 | target_link_libraries(isogfx-backend PRIVATE | ||
36 | gfx) | ||
37 | |||
38 | target_compile_options(isogfx-backend PRIVATE -Wall -Wextra -Wpedantic) | ||
39 | |||
40 | # Demos | ||
41 | |||
42 | add_subdirectory(demos) | ||
diff --git a/gfx-iso/demos/CMakeLists.txt b/gfx-iso/demos/CMakeLists.txt deleted file mode 100644 index c0a4101..0000000 --- a/gfx-iso/demos/CMakeLists.txt +++ /dev/null | |||
@@ -1,2 +0,0 @@ | |||
1 | add_subdirectory(checkerboard) | ||
2 | add_subdirectory(isomap) | ||
diff --git a/gfx-iso/demos/checkerboard/CMakeLists.txt b/gfx-iso/demos/checkerboard/CMakeLists.txt deleted file mode 100644 index d1691c6..0000000 --- a/gfx-iso/demos/checkerboard/CMakeLists.txt +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | cmake_minimum_required(VERSION 3.0) | ||
2 | |||
3 | project(checkerboard) | ||
4 | |||
5 | set(CMAKE_C_STANDARD 17) | ||
6 | set(CMAKE_C_STANDARD_REQUIRED On) | ||
7 | set(CMAKE_C_EXTENSIONS Off) | ||
8 | |||
9 | add_executable(checkerboard | ||
10 | checkerboard.c) | ||
11 | |||
12 | target_link_libraries(checkerboard PRIVATE | ||
13 | gfx-app | ||
14 | isogfx-backend) | ||
15 | |||
16 | target_compile_options(checkerboard PRIVATE -Wall -Wextra -Wpedantic) | ||
diff --git a/gfx-iso/demos/checkerboard/checkerboard.c b/gfx-iso/demos/checkerboard/checkerboard.c deleted file mode 100644 index dbc817c..0000000 --- a/gfx-iso/demos/checkerboard/checkerboard.c +++ /dev/null | |||
@@ -1,166 +0,0 @@ | |||
1 | #include <isogfx/backend.h> | ||
2 | #include <isogfx/isogfx.h> | ||
3 | |||
4 | #include <gfx/app.h> | ||
5 | |||
6 | #include <assert.h> | ||
7 | #include <stdbool.h> | ||
8 | #include <stdio.h> | ||
9 | |||
10 | static const int WINDOW_WIDTH = 1408; | ||
11 | static const int WINDOW_HEIGHT = 960; | ||
12 | static const int MAX_FPS = 60; | ||
13 | |||
14 | // Virtual screen dimensions. | ||
15 | static const int SCREEN_WIDTH = 704; | ||
16 | static const int SCREEN_HEIGHT = 480; | ||
17 | |||
18 | static const int TILE_WIDTH = 32; | ||
19 | static const int TILE_HEIGHT = TILE_WIDTH / 2; | ||
20 | static const int WORLD_WIDTH = 20; | ||
21 | static const int WORLD_HEIGHT = 20; | ||
22 | |||
23 | static const TileDesc tile_set[] = { | ||
24 | {.type = TileFromColour, | ||
25 | .width = TILE_WIDTH, | ||
26 | .height = TILE_HEIGHT, | ||
27 | .colour = (Pixel){.r = 0x38, .g = 0x3b, .b = 0x46, .a = 0xff}}, | ||
28 | {.type = TileFromColour, | ||
29 | .width = TILE_WIDTH, | ||
30 | .height = TILE_HEIGHT, | ||
31 | .colour = (Pixel){.r = 0xA5, .g = 0xb3, .b = 0xc0, .a = 0xff}}, | ||
32 | {.type = TileFromColour, | ||
33 | .width = TILE_WIDTH, | ||
34 | .height = TILE_HEIGHT, | ||
35 | .colour = (Pixel){.r = 0xdc, .g = 0x76, .b = 0x84, .a = 0xff}}, | ||
36 | }; | ||
37 | |||
38 | typedef enum Colour { | ||
39 | Black, | ||
40 | White, | ||
41 | Red, | ||
42 | } Colour; | ||
43 | |||
44 | typedef struct GfxAppState { | ||
45 | IsoBackend* backend; | ||
46 | IsoGfx* iso; | ||
47 | Tile red; | ||
48 | int xpick; | ||
49 | int ypick; | ||
50 | } GfxAppState; | ||
51 | |||
52 | static void make_checkerboard(IsoGfx* iso, Tile black, Tile white) { | ||
53 | assert(iso); | ||
54 | for (int y = 0; y < isogfx_world_height(iso); ++y) { | ||
55 | for (int x = 0; x < isogfx_world_width(iso); ++x) { | ||
56 | const int odd_col = x & 1; | ||
57 | const int odd_row = y & 1; | ||
58 | const Tile value = (odd_row ^ odd_col) == 0 ? black : white; | ||
59 | isogfx_set_tile(iso, x, y, value); | ||
60 | } | ||
61 | } | ||
62 | } | ||
63 | |||
64 | static bool init(GfxAppState* state, int argc, const char** argv) { | ||
65 | assert(state); | ||
66 | |||
67 | (void)argc; | ||
68 | (void)argv; | ||
69 | |||
70 | if (!(state->iso = isogfx_new(&(IsoGfxDesc){ | ||
71 | .screen_width = SCREEN_WIDTH, .screen_height = SCREEN_HEIGHT}))) { | ||
72 | return false; | ||
73 | } | ||
74 | IsoGfx* iso = state->iso; | ||
75 | |||
76 | isogfx_resize(iso, SCREEN_WIDTH, SCREEN_HEIGHT); | ||
77 | |||
78 | if (!isogfx_make_world( | ||
79 | iso, &(WorldDesc){ | ||
80 | .tile_width = TILE_WIDTH, | ||
81 | .tile_height = TILE_HEIGHT, | ||
82 | .world_width = WORLD_WIDTH, | ||
83 | .world_height = WORLD_HEIGHT})) { | ||
84 | return false; | ||
85 | } | ||
86 | |||
87 | const Tile black = isogfx_make_tile(iso, &tile_set[Black]); | ||
88 | const Tile white = isogfx_make_tile(iso, &tile_set[White]); | ||
89 | state->red = isogfx_make_tile(iso, &tile_set[Red]); | ||
90 | make_checkerboard(iso, black, white); | ||
91 | |||
92 | if (!(state->backend = IsoBackendInit(iso))) { | ||
93 | return false; | ||
94 | } | ||
95 | |||
96 | return true; | ||
97 | } | ||
98 | |||
99 | static void shutdown(GfxAppState* state) { | ||
100 | assert(state); | ||
101 | |||
102 | IsoBackendShutdown(&state->backend); | ||
103 | isogfx_del(&state->iso); | ||
104 | } | ||
105 | |||
106 | static void update(GfxAppState* state, double t, double dt) { | ||
107 | assert(state); | ||
108 | (void)dt; | ||
109 | |||
110 | IsoGfx* iso = state->iso; | ||
111 | |||
112 | isogfx_update(iso, t); | ||
113 | |||
114 | // Get mouse position in window coordinates. | ||
115 | double mouse_x, mouse_y; | ||
116 | gfx_app_get_mouse_position(&mouse_x, &mouse_y); | ||
117 | |||
118 | // Map from window coordinates to virtual screen coordinates. | ||
119 | IsoBackendGetMousePosition( | ||
120 | state->backend, mouse_x, mouse_y, &mouse_x, &mouse_y); | ||
121 | |||
122 | isogfx_pick_tile(iso, mouse_x, mouse_y, &state->xpick, &state->ypick); | ||
123 | |||
124 | printf("Picked tile: (%d, %d)\n", state->xpick, state->ypick); | ||
125 | } | ||
126 | |||
127 | static void render(GfxAppState* state) { | ||
128 | assert(state); | ||
129 | |||
130 | IsoGfx* iso = state->iso; | ||
131 | |||
132 | isogfx_render(iso); | ||
133 | |||
134 | if ((state->xpick != -1) && (state->ypick != -1)) { | ||
135 | isogfx_draw_tile(iso, state->xpick, state->ypick, state->red); | ||
136 | } | ||
137 | |||
138 | IsoBackendRender(state->backend, iso); | ||
139 | } | ||
140 | |||
141 | static void resize(GfxAppState* state, int width, int height) { | ||
142 | assert(state); | ||
143 | |||
144 | IsoBackendResizeWindow(state->backend, state->iso, width, height); | ||
145 | } | ||
146 | |||
147 | int main(int argc, const char** argv) { | ||
148 | GfxAppState state = {0}; | ||
149 | gfx_app_run( | ||
150 | &(GfxAppDesc){ | ||
151 | .argc = argc, | ||
152 | .argv = argv, | ||
153 | .width = WINDOW_WIDTH, | ||
154 | .height = WINDOW_HEIGHT, | ||
155 | .max_fps = MAX_FPS, | ||
156 | .update_delta_time = MAX_FPS > 0 ? 1.0 / (double)MAX_FPS : 0.0, | ||
157 | .title = "Isometric Renderer", | ||
158 | .app_state = &state}, | ||
159 | &(GfxAppCallbacks){ | ||
160 | .init = init, | ||
161 | .update = update, | ||
162 | .render = render, | ||
163 | .resize = resize, | ||
164 | .shutdown = shutdown}); | ||
165 | return 0; | ||
166 | } | ||
diff --git a/gfx-iso/demos/isomap/CMakeLists.txt b/gfx-iso/demos/isomap/CMakeLists.txt deleted file mode 100644 index 2dbfd32..0000000 --- a/gfx-iso/demos/isomap/CMakeLists.txt +++ /dev/null | |||
@@ -1,16 +0,0 @@ | |||
1 | cmake_minimum_required(VERSION 3.0) | ||
2 | |||
3 | project(isomap) | ||
4 | |||
5 | set(CMAKE_C_STANDARD 17) | ||
6 | set(CMAKE_C_STANDARD_REQUIRED On) | ||
7 | set(CMAKE_C_EXTENSIONS Off) | ||
8 | |||
9 | add_executable(isomap | ||
10 | isomap.c) | ||
11 | |||
12 | target_link_libraries(isomap PRIVATE | ||
13 | gfx-app | ||
14 | isogfx-backend) | ||
15 | |||
16 | target_compile_options(isomap PRIVATE -Wall -Wextra -Wpedantic) | ||
diff --git a/gfx-iso/demos/isomap/isomap.c b/gfx-iso/demos/isomap/isomap.c deleted file mode 100644 index a233659..0000000 --- a/gfx-iso/demos/isomap/isomap.c +++ /dev/null | |||
@@ -1,105 +0,0 @@ | |||
1 | #include <isogfx/backend.h> | ||
2 | #include <isogfx/isogfx.h> | ||
3 | |||
4 | #include <gfx/app.h> | ||
5 | |||
6 | #include <assert.h> | ||
7 | #include <stdbool.h> | ||
8 | |||
9 | static const int WINDOW_WIDTH = 1408; | ||
10 | static const int WINDOW_HEIGHT = 960; | ||
11 | static const int MAX_FPS = 60; | ||
12 | |||
13 | // Virtual screen dimensions. | ||
14 | static const int SCREEN_WIDTH = 704; | ||
15 | static const int SCREEN_HEIGHT = 480; | ||
16 | |||
17 | typedef struct GfxAppState { | ||
18 | IsoBackend* backend; | ||
19 | IsoGfx* iso; | ||
20 | int xpick; | ||
21 | int ypick; | ||
22 | SpriteSheet stag_sheet; | ||
23 | Sprite stag; | ||
24 | } GfxAppState; | ||
25 | |||
26 | static bool init(GfxAppState* state, int argc, const char** argv) { | ||
27 | assert(state); | ||
28 | (void)argc; | ||
29 | (void)argv; | ||
30 | |||
31 | if (!(state->iso = isogfx_new(&(IsoGfxDesc){ | ||
32 | .screen_width = SCREEN_WIDTH, .screen_height = SCREEN_HEIGHT}))) { | ||
33 | return false; | ||
34 | } | ||
35 | IsoGfx* iso = state->iso; | ||
36 | |||
37 | isogfx_resize(iso, SCREEN_WIDTH, SCREEN_HEIGHT); | ||
38 | |||
39 | if (!isogfx_load_world(iso, "/home/jeanne/assets/tilemaps/demo1.tm")) { | ||
40 | return false; | ||
41 | } | ||
42 | |||
43 | if (!isogfx_load_sprite_sheet( | ||
44 | iso, "/home/jeanne/assets/tilesets/scrabling/critters/stag/stag.ss", | ||
45 | &state->stag_sheet)) { | ||
46 | return false; | ||
47 | } | ||
48 | |||
49 | state->stag = isogfx_make_sprite(iso, state->stag_sheet); | ||
50 | isogfx_set_sprite_position(iso, state->stag, 5, 4); | ||
51 | |||
52 | if (!(state->backend = IsoBackendInit(iso))) { | ||
53 | return false; | ||
54 | } | ||
55 | |||
56 | return true; | ||
57 | } | ||
58 | |||
59 | static void shutdown(GfxAppState* state) { | ||
60 | assert(state); | ||
61 | // | ||
62 | } | ||
63 | |||
64 | static void update(GfxAppState* state, double t, double dt) { | ||
65 | assert(state); | ||
66 | (void)dt; | ||
67 | |||
68 | IsoGfx* iso = state->iso; | ||
69 | isogfx_update(iso, t); | ||
70 | } | ||
71 | |||
72 | static void render(GfxAppState* state) { | ||
73 | assert(state); | ||
74 | |||
75 | IsoGfx* iso = state->iso; | ||
76 | isogfx_render(iso); | ||
77 | IsoBackendRender(state->backend, iso); | ||
78 | } | ||
79 | |||
80 | static void resize(GfxAppState* state, int width, int height) { | ||
81 | assert(state); | ||
82 | |||
83 | IsoBackendResizeWindow(state->backend, state->iso, width, height); | ||
84 | } | ||
85 | |||
86 | int main(int argc, const char** argv) { | ||
87 | GfxAppState state = {0}; | ||
88 | gfx_app_run( | ||
89 | &(GfxAppDesc){ | ||
90 | .argc = argc, | ||
91 | .argv = argv, | ||
92 | .width = WINDOW_WIDTH, | ||
93 | .height = WINDOW_HEIGHT, | ||
94 | .max_fps = MAX_FPS, | ||
95 | .update_delta_time = MAX_FPS > 0 ? 1.0 / (double)MAX_FPS : 0.0, | ||
96 | .title = "Isometric Renderer", | ||
97 | .app_state = &state}, | ||
98 | &(GfxAppCallbacks){ | ||
99 | .init = init, | ||
100 | .update = update, | ||
101 | .render = render, | ||
102 | .resize = resize, | ||
103 | .shutdown = shutdown}); | ||
104 | return 0; | ||
105 | } | ||
diff --git a/gfx-iso/include/isogfx/backend.h b/gfx-iso/include/isogfx/backend.h deleted file mode 100644 index 172991d..0000000 --- a/gfx-iso/include/isogfx/backend.h +++ /dev/null | |||
@@ -1,28 +0,0 @@ | |||
1 | #pragma once | ||
2 | |||
3 | #include <stdbool.h> | ||
4 | |||
5 | typedef struct Gfx Gfx; | ||
6 | typedef struct IsoGfx IsoGfx; | ||
7 | |||
8 | typedef struct IsoBackend IsoBackend; | ||
9 | |||
10 | /// Initialize the backend. | ||
11 | IsoBackend* IsoBackendInit(const IsoGfx*); | ||
12 | |||
13 | /// Shut down the backend. | ||
14 | void IsoBackendShutdown(IsoBackend**); | ||
15 | |||
16 | /// Notify the backend of a window resize event. | ||
17 | /// This allows the backend to determine how to position and scale the iso | ||
18 | /// screen buffer on the graphics window. | ||
19 | void IsoBackendResizeWindow(IsoBackend*, const IsoGfx*, int width, int height); | ||
20 | |||
21 | /// Render the iso screen to the graphics window. | ||
22 | void IsoBackendRender(const IsoBackend*, const IsoGfx*); | ||
23 | |||
24 | /// Map window coordinates to iso space coordinates. | ||
25 | /// This takes into account any possible resizing done by the backend in | ||
26 | /// response to calls to IsoBackendResizeWindow(). | ||
27 | bool IsoBackendGetMousePosition( | ||
28 | const IsoBackend*, double window_x, double window_y, double* x, double* y); | ||
diff --git a/gfx-iso/include/isogfx/isogfx.h b/gfx-iso/include/isogfx/isogfx.h deleted file mode 100644 index 3421a7b..0000000 --- a/gfx-iso/include/isogfx/isogfx.h +++ /dev/null | |||
@@ -1,136 +0,0 @@ | |||
1 | /* | ||
2 | * Isometric rendering engine. | ||
3 | */ | ||
4 | #pragma once | ||
5 | |||
6 | #include <stdbool.h> | ||
7 | #include <stdint.h> | ||
8 | |||
9 | typedef struct IsoGfx IsoGfx; | ||
10 | |||
11 | /// Sprite sheet handle. | ||
12 | typedef uint16_t SpriteSheet; | ||
13 | |||
14 | /// Sprite handle. | ||
15 | typedef uint16_t Sprite; | ||
16 | |||
17 | /// Tile handle. | ||
18 | typedef uint16_t Tile; | ||
19 | |||
20 | /// Colour channel. | ||
21 | typedef uint8_t Channel; | ||
22 | |||
23 | typedef struct Pixel { | ||
24 | Channel r, g, b, a; | ||
25 | } Pixel; | ||
26 | |||
27 | typedef enum TileDescType { | ||
28 | TileFromColour, | ||
29 | TileFromFile, | ||
30 | TileFromMemory, | ||
31 | } TileDescType; | ||
32 | |||
33 | typedef struct TileDesc { | ||
34 | TileDescType type; | ||
35 | int width; /// Tile width in pixels. | ||
36 | int height; /// Tile height in pixels. | ||
37 | union { | ||
38 | Pixel colour; /// Constant colour tile. | ||
39 | struct { | ||
40 | const char* path; | ||
41 | } file; | ||
42 | struct { | ||
43 | const uint8_t* data; /// sizeof(Pixel) * width * height | ||
44 | } mem; | ||
45 | }; | ||
46 | } TileDesc; | ||
47 | |||
48 | typedef struct WorldDesc { | ||
49 | int tile_width; /// Base tile width in pixels. | ||
50 | int tile_height; /// Base tile height in pixels. | ||
51 | int world_width; /// World width in tiles. | ||
52 | int world_height; /// World height in tiles. | ||
53 | int max_num_tiles; /// 0 for an implementation-defined default. | ||
54 | } WorldDesc; | ||
55 | |||
56 | typedef struct IsoGfxDesc { | ||
57 | int screen_width; /// Screen width in pixels. | ||
58 | int screen_height; /// Screen height in pixels. | ||
59 | int max_num_sprites; /// 0 for an implementation-defined default. | ||
60 | int sprite_sheet_pool_size_bytes; /// 0 for an implementation-defined default. | ||
61 | } IsoGfxDesc; | ||
62 | |||
63 | /// Create a new isometric graphics engine. | ||
64 | IsoGfx* isogfx_new(const IsoGfxDesc*); | ||
65 | |||
66 | /// Destroy the isometric graphics engine. | ||
67 | void isogfx_del(IsoGfx**); | ||
68 | |||
69 | /// Create an empty world. | ||
70 | bool isogfx_make_world(IsoGfx*, const WorldDesc*); | ||
71 | |||
72 | /// Load a world from a tile map (.TM) file. | ||
73 | bool isogfx_load_world(IsoGfx*, const char* filepath); | ||
74 | |||
75 | /// Return the world's width. | ||
76 | int isogfx_world_width(const IsoGfx*); | ||
77 | |||
78 | /// Return the world's height. | ||
79 | int isogfx_world_height(const IsoGfx*); | ||
80 | |||
81 | /// Create a new tile. | ||
82 | Tile isogfx_make_tile(IsoGfx*, const TileDesc*); | ||
83 | |||
84 | /// Set the tile at position (x,y). | ||
85 | void isogfx_set_tile(IsoGfx*, int x, int y, Tile); | ||
86 | |||
87 | /// Set the tiles in positions in the range (x0,y0) - (x1,y1). | ||
88 | void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile); | ||
89 | |||
90 | /// Load a sprite sheet (.SS) file. | ||
91 | bool isogfx_load_sprite_sheet(IsoGfx*, const char* filepath, SpriteSheet*); | ||
92 | |||
93 | /// Create an animated sprite. | ||
94 | Sprite isogfx_make_sprite(IsoGfx*, SpriteSheet); | ||
95 | |||
96 | /// Destroy the sprite. | ||
97 | void isogfx_del_sprite(IsoGfx*, Sprite); | ||
98 | |||
99 | /// Destroy all the sprites. | ||
100 | void isogfx_del_sprites(IsoGfx*); | ||
101 | |||
102 | /// Set the sprite's position. | ||
103 | void isogfx_set_sprite_position(IsoGfx*, Sprite, int x, int y); | ||
104 | |||
105 | /// Set the sprite's current animation. | ||
106 | void isogfx_set_sprite_animation(IsoGfx*, Sprite, int animation); | ||
107 | |||
108 | /// Update the renderer. | ||
109 | /// | ||
110 | /// Currently this updates the sprite animations. | ||
111 | void isogfx_update(IsoGfx*, double t); | ||
112 | |||
113 | /// Render the world. | ||
114 | void isogfx_render(IsoGfx*); | ||
115 | |||
116 | /// Draw/overlay a tile at position (x,y). | ||
117 | /// | ||
118 | /// This function just renders a tile at position (x,y) and should be called | ||
119 | /// after isogfx_render() to obtain the correct result. To set the tile at | ||
120 | /// position (x,y) instead, use isogfx_set_tile(). | ||
121 | void isogfx_draw_tile(IsoGfx*, int x, int y, Tile); | ||
122 | |||
123 | /// Resize the virtual screen's dimensions. | ||
124 | bool isogfx_resize(IsoGfx*, int screen_width, int screen_height); | ||
125 | |||
126 | /// Get the virtual screen's dimensions. | ||
127 | void isogfx_get_screen_size(const IsoGfx*, int* width, int* height); | ||
128 | |||
129 | /// Return a pointer to the virtual screen's colour buffer. | ||
130 | /// | ||
131 | /// Call after each call to isogfx_render() to retrieve the render output. | ||
132 | const Pixel* isogfx_get_screen_buffer(const IsoGfx*); | ||
133 | |||
134 | /// Translate Cartesian to isometric coordinates. | ||
135 | void isogfx_pick_tile( | ||
136 | const IsoGfx*, double xcart, double ycart, int* xiso, int* yiso); | ||
diff --git a/gfx-iso/src/backend.c b/gfx-iso/src/backend.c deleted file mode 100644 index db91647..0000000 --- a/gfx-iso/src/backend.c +++ /dev/null | |||
@@ -1,199 +0,0 @@ | |||
1 | #include <isogfx/backend.h> | ||
2 | #include <isogfx/isogfx.h> | ||
3 | |||
4 | #include <gfx/core.h> | ||
5 | #include <gfx/gfx.h> | ||
6 | #include <gfx/renderer.h> | ||
7 | #include <gfx/scene.h> | ||
8 | #include <gfx/util/geometry.h> | ||
9 | #include <gfx/util/shader.h> | ||
10 | |||
11 | #include <assert.h> | ||
12 | #include <stdlib.h> | ||
13 | |||
14 | typedef struct IsoBackend { | ||
15 | Gfx* gfx; | ||
16 | Scene* scene; | ||
17 | /// The screen or "iso screen" refers to the colour buffer of the iso graphics | ||
18 | /// library. This texture is used to draw the iso screen onto the graphics | ||
19 | /// window. | ||
20 | Texture* screen_texture; | ||
21 | /// Window size. | ||
22 | int window_width; | ||
23 | int window_height; | ||
24 | /// The viewport refers to the area inside the window to which screen_texture | ||
25 | /// is drawn. It is a scaled version of the iso screen, scaled while | ||
26 | /// respecting the iso screen's aspect ratio to prevent distortion. | ||
27 | int viewport_x, viewport_y, viewport_width, viewport_height; | ||
28 | double stretch; // Stretch factor from iso screen dimensions to viewport | ||
29 | // dimensions. | ||
30 | } IsoBackend; | ||
31 | |||
32 | IsoBackend* IsoBackendInit(const IsoGfx* iso) { | ||
33 | assert(iso); | ||
34 | |||
35 | IsoBackend* backend = calloc(1, sizeof(IsoBackend)); | ||
36 | if (!backend) { | ||
37 | return 0; | ||
38 | } | ||
39 | |||
40 | if (!(backend->gfx = gfx_init())) { | ||
41 | goto cleanup; | ||
42 | } | ||
43 | GfxCore* gfxcore = gfx_get_core(backend->gfx); | ||
44 | |||
45 | int screen_width, screen_height; | ||
46 | isogfx_get_screen_size(iso, &screen_width, &screen_height); | ||
47 | |||
48 | if (!(backend->screen_texture = gfx_make_texture( | ||
49 | gfxcore, &(TextureDesc){ | ||
50 | .width = screen_width, | ||
51 | .height = screen_height, | ||
52 | .dimension = Texture2D, | ||
53 | .format = TextureSRGBA8, | ||
54 | .filtering = NearestFiltering, | ||
55 | .wrap = ClampToEdge, | ||
56 | .mipmaps = false}))) { | ||
57 | goto cleanup; | ||
58 | } | ||
59 | |||
60 | ShaderProgram* shader = gfx_make_view_texture_shader(gfxcore); | ||
61 | if (!shader) { | ||
62 | goto cleanup; | ||
63 | } | ||
64 | |||
65 | Geometry* geometry = gfx_make_quad_11(gfxcore); | ||
66 | if (!geometry) { | ||
67 | goto cleanup; | ||
68 | } | ||
69 | |||
70 | MaterialDesc material_desc = (MaterialDesc){.num_uniforms = 1}; | ||
71 | material_desc.uniforms[0] = (ShaderUniform){ | ||
72 | .type = UniformTexture, | ||
73 | .value.texture = backend->screen_texture, | ||
74 | .name = sstring_make("Texture")}; | ||
75 | Material* material = gfx_make_material(&material_desc); | ||
76 | if (!material) { | ||
77 | return false; | ||
78 | } | ||
79 | |||
80 | const MeshDesc mesh_desc = | ||
81 | (MeshDesc){.geometry = geometry, .material = material, .shader = shader}; | ||
82 | Mesh* mesh = gfx_make_mesh(&mesh_desc); | ||
83 | if (!mesh) { | ||
84 | goto cleanup; | ||
85 | } | ||
86 | |||
87 | SceneObject* object = | ||
88 | gfx_make_object(&(ObjectDesc){.num_meshes = 1, .meshes = {mesh}}); | ||
89 | if (!object) { | ||
90 | goto cleanup; | ||
91 | } | ||
92 | |||
93 | backend->scene = gfx_make_scene(); | ||
94 | SceneNode* node = gfx_make_object_node(object); | ||
95 | SceneNode* root = gfx_get_scene_root(backend->scene); | ||
96 | gfx_set_node_parent(node, root); | ||
97 | |||
98 | return backend; | ||
99 | |||
100 | cleanup: | ||
101 | if (backend->gfx) { | ||
102 | gfx_destroy(&backend->gfx); | ||
103 | } | ||
104 | free(backend); | ||
105 | return 0; | ||
106 | } | ||
107 | |||
108 | void IsoBackendShutdown(IsoBackend** ppApp) { | ||
109 | assert(ppApp); | ||
110 | |||
111 | IsoBackend* app = *ppApp; | ||
112 | if (!app) { | ||
113 | return; | ||
114 | } | ||
115 | |||
116 | gfx_destroy(&app->gfx); | ||
117 | } | ||
118 | |||
119 | void IsoBackendResizeWindow( | ||
120 | IsoBackend* app, const IsoGfx* iso, int width, int height) { | ||
121 | assert(app); | ||
122 | assert(iso); | ||
123 | |||
124 | app->window_width = width; | ||
125 | app->window_height = height; | ||
126 | |||
127 | // Virtual screen dimensions. | ||
128 | int screen_width, screen_height; | ||
129 | isogfx_get_screen_size(iso, &screen_width, &screen_height); | ||
130 | |||
131 | // Stretch the virtual screen onto the viewport while respecting the screen's | ||
132 | // aspect ratio to prevent distortion. | ||
133 | if (width > height) { // Wide screen. | ||
134 | app->stretch = (double)height / (double)screen_height; | ||
135 | app->viewport_width = (int)((double)screen_width * app->stretch); | ||
136 | app->viewport_height = height; | ||
137 | app->viewport_x = (width - app->viewport_width) / 2; | ||
138 | app->viewport_y = 0; | ||
139 | } else { // Tall screen. | ||
140 | app->stretch = (double)width / (double)screen_width; | ||
141 | app->viewport_width = width; | ||
142 | app->viewport_height = (int)((float)screen_height * app->stretch); | ||
143 | app->viewport_x = 0; | ||
144 | app->viewport_y = (height - app->viewport_height) / 2; | ||
145 | } | ||
146 | } | ||
147 | |||
148 | void IsoBackendRender(const IsoBackend* app, const IsoGfx* iso) { | ||
149 | assert(app); | ||
150 | assert(iso); | ||
151 | |||
152 | const Pixel* screen = isogfx_get_screen_buffer(iso); | ||
153 | assert(screen); | ||
154 | gfx_update_texture(app->screen_texture, &(TextureDataDesc){.pixels = screen}); | ||
155 | |||
156 | GfxCore* gfxcore = gfx_get_core(app->gfx); | ||
157 | Renderer* renderer = gfx_get_renderer(app->gfx); | ||
158 | |||
159 | // Clear the whole window. | ||
160 | gfx_set_viewport(gfxcore, 0, 0, app->window_width, app->window_height); | ||
161 | gfx_clear(gfxcore, vec4_make(0, 0, 0, 0)); | ||
162 | |||
163 | // Draw to the subregion where the virtual screen can stretch without | ||
164 | // distortion. | ||
165 | gfx_set_viewport( | ||
166 | gfxcore, app->viewport_x, app->viewport_y, app->viewport_width, | ||
167 | app->viewport_height); | ||
168 | |||
169 | // Render the iso screen. | ||
170 | gfx_start_frame(gfxcore); | ||
171 | gfx_render_scene( | ||
172 | renderer, &(RenderSceneParams){ | ||
173 | .mode = RenderDefault, .scene = app->scene, .camera = 0}); | ||
174 | gfx_end_frame(gfxcore); | ||
175 | } | ||
176 | |||
177 | bool IsoBackendGetMousePosition( | ||
178 | const IsoBackend* app, double window_x, double window_y, double* x, | ||
179 | double* y) { | ||
180 | assert(app); | ||
181 | |||
182 | // Translate from window coordinates to the subregion where the stretched | ||
183 | // iso screen is rendered. | ||
184 | const double screen_x = window_x - app->viewport_x; | ||
185 | const double screen_y = window_y - app->viewport_y; | ||
186 | |||
187 | // Position may be out of bounds. | ||
188 | if ((0 <= screen_x) && (screen_x < app->viewport_width) && (0 <= screen_y) && | ||
189 | (screen_y < app->viewport_height)) { | ||
190 | // Scale back from the stretched subregion to the iso screen dimensions. | ||
191 | *x = screen_x / app->stretch; | ||
192 | *y = screen_y / app->stretch; | ||
193 | return true; | ||
194 | } else { | ||
195 | *x = -1; | ||
196 | *y = -1; | ||
197 | return false; | ||
198 | } | ||
199 | } | ||
diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c deleted file mode 100644 index 52c4ae2..0000000 --- a/gfx-iso/src/isogfx.c +++ /dev/null | |||
@@ -1,952 +0,0 @@ | |||
1 | #include <isogfx/isogfx.h> | ||
2 | |||
3 | #include <filesystem.h> | ||
4 | #include <mem.h> | ||
5 | #include <mempool.h> | ||
6 | #include <path.h> | ||
7 | |||
8 | #include <linux/limits.h> | ||
9 | |||
10 | #include <assert.h> | ||
11 | #include <stdbool.h> | ||
12 | #include <stdint.h> | ||
13 | #include <stdio.h> | ||
14 | #include <stdlib.h> | ||
15 | #include <string.h> | ||
16 | |||
17 | /// Maximum number of tiles unless the user specifies a value. | ||
18 | #define DEFAULT_MAX_NUM_TILES 1024 | ||
19 | |||
20 | /// Maximum number of sprites unless the user specifies a value. | ||
21 | #define DEFAULT_MAX_NUM_SPRITES 128 | ||
22 | |||
23 | /// Size of sprite sheet pool in bytes unless the user specifies a value. | ||
24 | #define DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES (8 * 1024 * 1024) | ||
25 | |||
26 | /// Default animation speed. | ||
27 | #define ANIMATION_FPS 10 | ||
28 | |||
29 | /// Time between animation updates. | ||
30 | #define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS) | ||
31 | |||
32 | typedef struct ivec2 { | ||
33 | int x, y; | ||
34 | } ivec2; | ||
35 | |||
36 | typedef struct vec2 { | ||
37 | double x, y; | ||
38 | } vec2; | ||
39 | |||
40 | // ----------------------------------------------------------------------------- | ||
41 | // Tile set (TS) and tile map (TM) file formats. | ||
42 | // ----------------------------------------------------------------------------- | ||
43 | |||
44 | /// Maximum length of path strings in .TS and .TM files. | ||
45 | #define MAX_PATH_LENGTH 128 | ||
46 | |||
47 | typedef struct Ts_Tile { | ||
48 | uint16_t width; /// Tile width in pixels. | ||
49 | uint16_t height; /// Tile height in pixels. | ||
50 | Pixel pixels[1]; /// Count: width * height. | ||
51 | } Ts_Tile; | ||
52 | |||
53 | typedef struct Ts_TileSet { | ||
54 | uint16_t num_tiles; | ||
55 | uint16_t max_tile_width; /// Maximum tile width in pixels. | ||
56 | uint16_t max_tile_height; /// Maximum tile height in pixels. | ||
57 | Ts_Tile tiles[1]; /// Count: num_tiles. | ||
58 | } Ts_TileSet; | ||
59 | |||
60 | typedef struct Tm_Layer { | ||
61 | union { | ||
62 | char tileset_path[MAX_PATH_LENGTH]; // Relative to the Tm_Map file. | ||
63 | }; | ||
64 | Tile tiles[1]; /// Count: world_width * world_height. | ||
65 | } Tm_Layer; | ||
66 | |||
67 | typedef struct Tm_Map { | ||
68 | uint16_t world_width; /// World width in number of tiles. | ||
69 | uint16_t world_height; /// World height in number of tiles. | ||
70 | uint16_t base_tile_width; | ||
71 | uint16_t base_tile_height; | ||
72 | uint16_t num_layers; | ||
73 | Tm_Layer layers[1]; // Count: num_layers. | ||
74 | } Tm_Map; | ||
75 | |||
76 | static inline const Tm_Layer* tm_map_get_next_layer( | ||
77 | const Tm_Map* map, const Tm_Layer* layer) { | ||
78 | assert(map); | ||
79 | assert(layer); | ||
80 | return (const Tm_Layer*)((const uint8_t*)layer + sizeof(Tm_Layer) + | ||
81 | ((map->world_width * map->world_height - 1) * | ||
82 | sizeof(Tile))); | ||
83 | } | ||
84 | |||
85 | static inline const Ts_Tile* ts_tileset_get_next_tile( | ||
86 | const Ts_TileSet* tileset, const Ts_Tile* tile) { | ||
87 | assert(tileset); | ||
88 | assert(tile); | ||
89 | return (const Ts_Tile*)((const uint8_t*)tile + sizeof(Ts_Tile) + | ||
90 | ((tile->width * tile->height - 1) * sizeof(Pixel))); | ||
91 | } | ||
92 | |||
93 | // ----------------------------------------------------------------------------- | ||
94 | // Sprite sheet file format. | ||
95 | // ----------------------------------------------------------------------------- | ||
96 | |||
97 | /// A row of sprites in a sprite sheet. | ||
98 | /// | ||
99 | /// Each row in a sprite sheet can have a different number of columns. | ||
100 | /// | ||
101 | /// The pixels of the row follow a "sprite-major" order. It contains the | ||
102 | /// 'sprite_width * sprite_height' pixels for the first column/sprite, then the | ||
103 | /// second column/sprite, etc. | ||
104 | /// | ||
105 | /// Pixels are 8-bit indices into the sprite sheet's colour palette. | ||
106 | typedef struct Ss_Row { | ||
107 | uint16_t num_cols; /// Number of columns in this row. | ||
108 | uint8_t pixels[1]; /// Count: num_cols * sprite_width * sprite_height. | ||
109 | } Ss_Row; | ||
110 | |||
111 | typedef struct Ss_Palette { | ||
112 | uint16_t num_colours; | ||
113 | Pixel colours[1]; /// Count: num_colors. | ||
114 | } Ss_Palette; | ||
115 | |||
116 | /// Sprite sheet top-level data definition. | ||
117 | /// | ||
118 | /// Sprite width and height are assumed constant throughout the sprite sheet. | ||
119 | typedef struct Ss_SpriteSheet { | ||
120 | uint16_t sprite_width; /// Sprite width in pixels. | ||
121 | uint16_t sprite_height; /// Sprite height in pixels. | ||
122 | uint16_t num_rows; | ||
123 | Ss_Palette palette; /// Variable size. | ||
124 | Ss_Row rows[1]; /// Count: num_rows. Variable offset. | ||
125 | } Ss_SpriteSheet; | ||
126 | |||
127 | static inline const Ss_Row* get_sprite_sheet_row( | ||
128 | const Ss_SpriteSheet* sheet, int row) { | ||
129 | assert(sheet); | ||
130 | assert(row >= 0); | ||
131 | assert(row < sheet->num_rows); | ||
132 | // Skip over the palette. | ||
133 | const Ss_Row* rows = | ||
134 | (const Ss_Row*)(&sheet->palette.colours[0] + sheet->palette.num_colours); | ||
135 | return &rows[row]; | ||
136 | } | ||
137 | |||
138 | static inline const uint8_t* get_sprite_sheet_sprite( | ||
139 | const Ss_SpriteSheet* sheet, const Ss_Row* row, int col) { | ||
140 | assert(sheet); | ||
141 | assert(row); | ||
142 | assert(col >= 0); | ||
143 | assert(col < row->num_cols); | ||
144 | const int sprite_offset = col * sheet->sprite_width * sheet->sprite_height; | ||
145 | const uint8_t* sprite = &row->pixels[sprite_offset]; | ||
146 | return sprite; | ||
147 | } | ||
148 | |||
149 | // ----------------------------------------------------------------------------- | ||
150 | // Renderer state. | ||
151 | // ----------------------------------------------------------------------------- | ||
152 | |||
153 | typedef struct TileData { | ||
154 | uint16_t width; | ||
155 | uint16_t height; | ||
156 | uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool. | ||
157 | } TileData; | ||
158 | |||
159 | // File format is already convenient for working in memory. | ||
160 | typedef Ss_Row SpriteSheetRow; | ||
161 | typedef Ss_SpriteSheet SpriteSheetData; | ||
162 | |||
163 | typedef struct SpriteData { | ||
164 | SpriteSheet sheet; // Handle to the sprite's sheet. | ||
165 | ivec2 position; | ||
166 | int animation; // Current animation. | ||
167 | int frame; // Current frame of animation. | ||
168 | } SpriteData; | ||
169 | |||
170 | DEF_MEMPOOL_DYN(TilePool, TileData) | ||
171 | DEF_MEM_DYN(PixelPool, Pixel) | ||
172 | |||
173 | DEF_MEMPOOL_DYN(SpritePool, SpriteData) | ||
174 | DEF_MEM_DYN(SpriteSheetPool, SpriteSheetData) | ||
175 | |||
176 | typedef struct IsoGfx { | ||
177 | int screen_width; | ||
178 | int screen_height; | ||
179 | int tile_width; | ||
180 | int tile_height; | ||
181 | int world_width; | ||
182 | int world_height; | ||
183 | int max_num_sprites; | ||
184 | int sprite_sheet_pool_size_bytes; | ||
185 | double last_animation_time; | ||
186 | Tile* world; | ||
187 | Pixel* screen; | ||
188 | TilePool tiles; | ||
189 | PixelPool pixels; | ||
190 | SpritePool sprites; | ||
191 | SpriteSheetPool sheets; | ||
192 | } IsoGfx; | ||
193 | |||
194 | // ----------------------------------------------------------------------------- | ||
195 | // Math and world / tile / screen access. | ||
196 | // ----------------------------------------------------------------------------- | ||
197 | |||
198 | static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { | ||
199 | return (ivec2){.x = a.x + b.x, .y = a.y + b.y}; | ||
200 | } | ||
201 | |||
202 | static inline ivec2 ivec2_scale(ivec2 a, int s) { | ||
203 | return (ivec2){.x = a.x * s, .y = a.y * s}; | ||
204 | } | ||
205 | |||
206 | static inline ivec2 iso2cart(ivec2 iso, int s, int t, int w) { | ||
207 | return (ivec2){ | ||
208 | .x = (iso.x - iso.y) * (s / 2) + (w / 2), .y = (iso.x + iso.y) * (t / 2)}; | ||
209 | } | ||
210 | |||
211 | // Method 1. | ||
212 | // static inline vec2 cart2iso(vec2 cart, int s, int t, int w) { | ||
213 | // const double x = cart.x - (double)(w / 2); | ||
214 | // const double xiso = (x * t + cart.y * s) / (double)(s * t); | ||
215 | // return (vec2){ | ||
216 | // .x = (int)(xiso), .y = (int)((2.0 / (double)t) * cart.y - xiso)}; | ||
217 | //} | ||
218 | |||
219 | // Method 2. | ||
220 | static inline vec2 cart2iso(vec2 cart, int s, int t, int w) { | ||
221 | const double one_over_s = 1. / (double)s; | ||
222 | const double one_over_t = 1. / (double)t; | ||
223 | const double x = cart.x - (double)(w / 2); | ||
224 | return (vec2){ | ||
225 | .x = (one_over_s * x + one_over_t * cart.y), | ||
226 | .y = (-one_over_s * x + one_over_t * cart.y)}; | ||
227 | } | ||
228 | |||
229 | static const Pixel* tile_xy_const_ref( | ||
230 | const IsoGfx* iso, const TileData* tile, int x, int y) { | ||
231 | assert(iso); | ||
232 | assert(tile); | ||
233 | assert(x >= 0); | ||
234 | assert(y >= 0); | ||
235 | assert(x < tile->width); | ||
236 | assert(y < tile->height); | ||
237 | return &mem_get_chunk(&iso->pixels, tile->pixels_handle)[y * tile->width + x]; | ||
238 | } | ||
239 | |||
240 | // static Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) { | ||
241 | // return *tile_xy_const_ref(iso, tile, x, y); | ||
242 | // } | ||
243 | |||
244 | static Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) { | ||
245 | return (Pixel*)tile_xy_const_ref(iso, tile, x, y); | ||
246 | } | ||
247 | |||
248 | static inline const Tile* world_xy_const_ref(const IsoGfx* iso, int x, int y) { | ||
249 | assert(iso); | ||
250 | assert(x >= 0); | ||
251 | assert(y >= 0); | ||
252 | assert(x < iso->world_width); | ||
253 | assert(y < iso->world_height); | ||
254 | return &iso->world[y * iso->world_width + x]; | ||
255 | } | ||
256 | |||
257 | static inline Tile world_xy(const IsoGfx* iso, int x, int y) { | ||
258 | return *world_xy_const_ref(iso, x, y); | ||
259 | } | ||
260 | |||
261 | static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) { | ||
262 | return (Tile*)world_xy_const_ref(iso, x, y); | ||
263 | } | ||
264 | |||
265 | static inline const Pixel* screen_xy_const_ref( | ||
266 | const IsoGfx* iso, int x, int y) { | ||
267 | assert(iso); | ||
268 | assert(x >= 0); | ||
269 | assert(y >= 0); | ||
270 | assert(x < iso->screen_width); | ||
271 | assert(y < iso->screen_height); | ||
272 | return &iso->screen[y * iso->screen_width + x]; | ||
273 | } | ||
274 | |||
275 | static inline Pixel screen_xy(IsoGfx* iso, int x, int y) { | ||
276 | return *screen_xy_const_ref(iso, x, y); | ||
277 | } | ||
278 | |||
279 | static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) { | ||
280 | return (Pixel*)screen_xy_const_ref(iso, x, y); | ||
281 | } | ||
282 | |||
283 | static int calc_num_tile_blocks( | ||
284 | int base_tile_width, int base_tile_height, int tile_width, | ||
285 | int tile_height) { | ||
286 | const int base_tile_size = base_tile_width * base_tile_height; | ||
287 | const int tile_size = tile_width * tile_height; | ||
288 | const int num_blocks = tile_size / base_tile_size; | ||
289 | return num_blocks; | ||
290 | } | ||
291 | |||
292 | // ----------------------------------------------------------------------------- | ||
293 | // Renderer, world and tile management. | ||
294 | // ----------------------------------------------------------------------------- | ||
295 | |||
296 | IsoGfx* isogfx_new(const IsoGfxDesc* desc) { | ||
297 | assert(desc->screen_width > 0); | ||
298 | assert(desc->screen_height > 0); | ||
299 | // Part of our implementation assumes even widths and heights for precision. | ||
300 | assert((desc->screen_width & 1) == 0); | ||
301 | assert((desc->screen_height & 1) == 0); | ||
302 | |||
303 | IsoGfx* iso = calloc(1, sizeof(IsoGfx)); | ||
304 | if (!iso) { | ||
305 | return 0; | ||
306 | } | ||
307 | |||
308 | iso->screen_width = desc->screen_width; | ||
309 | iso->screen_height = desc->screen_height; | ||
310 | |||
311 | iso->last_animation_time = 0.0; | ||
312 | |||
313 | iso->max_num_sprites = desc->max_num_sprites == 0 ? DEFAULT_MAX_NUM_SPRITES | ||
314 | : desc->max_num_sprites; | ||
315 | iso->sprite_sheet_pool_size_bytes = desc->sprite_sheet_pool_size_bytes == 0 | ||
316 | ? DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES | ||
317 | : desc->sprite_sheet_pool_size_bytes; | ||
318 | |||
319 | const int screen_size = desc->screen_width * desc->screen_height; | ||
320 | if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) { | ||
321 | goto cleanup; | ||
322 | } | ||
323 | |||
324 | return iso; | ||
325 | |||
326 | cleanup: | ||
327 | isogfx_del(&iso); | ||
328 | return 0; | ||
329 | } | ||
330 | |||
331 | /// Destroy the world, its tile set, and the underlying pools. | ||
332 | static void destroy_world(IsoGfx* iso) { | ||
333 | assert(iso); | ||
334 | if (iso->world) { | ||
335 | free(iso->world); | ||
336 | iso->world = 0; | ||
337 | } | ||
338 | mempool_del(&iso->tiles); | ||
339 | mem_del(&iso->pixels); | ||
340 | } | ||
341 | |||
342 | /// Destroy all loaded sprites and the underlying pools. | ||
343 | static void destroy_sprites(IsoGfx* iso) { | ||
344 | assert(iso); | ||
345 | mempool_del(&iso->sprites); | ||
346 | mem_del(&iso->sheets); | ||
347 | } | ||
348 | |||
349 | void isogfx_del(IsoGfx** pIso) { | ||
350 | assert(pIso); | ||
351 | IsoGfx* iso = *pIso; | ||
352 | if (iso) { | ||
353 | destroy_world(iso); | ||
354 | destroy_sprites(iso); | ||
355 | if (iso->screen) { | ||
356 | free(iso->screen); | ||
357 | iso->screen = 0; | ||
358 | } | ||
359 | free(iso); | ||
360 | *pIso = 0; | ||
361 | } | ||
362 | } | ||
363 | |||
364 | bool isogfx_make_world(IsoGfx* iso, const WorldDesc* desc) { | ||
365 | assert(iso); | ||
366 | assert(desc); | ||
367 | assert(desc->tile_width > 0); | ||
368 | assert(desc->tile_height > 0); | ||
369 | // Part of our implementation assumes even widths and heights for greater | ||
370 | // precision. | ||
371 | assert((desc->tile_width & 1) == 0); | ||
372 | assert((desc->tile_height & 1) == 0); | ||
373 | |||
374 | // Handle recreation by destroying the previous world. | ||
375 | destroy_world(iso); | ||
376 | |||
377 | iso->tile_width = desc->tile_width; | ||
378 | iso->tile_height = desc->tile_height; | ||
379 | iso->world_width = desc->world_width; | ||
380 | iso->world_height = desc->world_height; | ||
381 | |||
382 | const int world_size = desc->world_width * desc->world_height; | ||
383 | const int tile_size = desc->tile_width * desc->tile_height; | ||
384 | const int tile_size_bytes = tile_size * (int)sizeof(Pixel); | ||
385 | const int tile_pool_size = | ||
386 | desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES; | ||
387 | |||
388 | if (!(iso->world = calloc(world_size, sizeof(Tile)))) { | ||
389 | goto cleanup; | ||
390 | } | ||
391 | if (!mempool_make_dyn(&iso->tiles, world_size, sizeof(TileData))) { | ||
392 | goto cleanup; | ||
393 | } | ||
394 | if (!mem_make_dyn(&iso->pixels, tile_pool_size, tile_size_bytes)) { | ||
395 | goto cleanup; | ||
396 | } | ||
397 | |||
398 | return true; | ||
399 | |||
400 | cleanup: | ||
401 | destroy_world(iso); | ||
402 | return false; | ||
403 | } | ||
404 | |||
405 | bool isogfx_load_world(IsoGfx* iso, const char* filepath) { | ||
406 | assert(iso); | ||
407 | assert(filepath); | ||
408 | |||
409 | bool success = false; | ||
410 | |||
411 | // Handle recreation by destroying the previous world. | ||
412 | destroy_world(iso); | ||
413 | |||
414 | // Load the map. | ||
415 | printf("Load tile map: %s\n", filepath); | ||
416 | Tm_Map* map = read_file(filepath); | ||
417 | if (!map) { | ||
418 | goto cleanup; | ||
419 | } | ||
420 | |||
421 | // Allocate memory for the map and tile sets. | ||
422 | const int world_size = map->world_width * map->world_height; | ||
423 | const int base_tile_size = map->base_tile_width * map->base_tile_height; | ||
424 | const int base_tile_size_bytes = base_tile_size * (int)sizeof(Pixel); | ||
425 | // TODO: Need to get the total number of tiles from the map. | ||
426 | const int tile_pool_size = DEFAULT_MAX_NUM_TILES; | ||
427 | |||
428 | if (!(iso->world = calloc(world_size, sizeof(Tile)))) { | ||
429 | goto cleanup; | ||
430 | } | ||
431 | if (!mempool_make_dyn(&iso->tiles, tile_pool_size, sizeof(TileData))) { | ||
432 | goto cleanup; | ||
433 | } | ||
434 | if (!mem_make_dyn(&iso->pixels, tile_pool_size, base_tile_size_bytes)) { | ||
435 | goto cleanup; | ||
436 | } | ||
437 | |||
438 | // Load the tile sets. | ||
439 | const Tm_Layer* layer = &map->layers[0]; | ||
440 | // TODO: Handle num_layers layers. | ||
441 | for (int i = 0; i < 1; ++i) { | ||
442 | const char* ts_path = layer->tileset_path; | ||
443 | |||
444 | // Tile set path is relative to the tile map file. Make it relative to the | ||
445 | // current working directory before loading. | ||
446 | char ts_path_cwd[PATH_MAX] = {0}; | ||
447 | if (!path_make_relative(filepath, ts_path, ts_path_cwd, PATH_MAX)) { | ||
448 | goto cleanup; | ||
449 | } | ||
450 | |||
451 | Ts_TileSet* tileset = read_file(ts_path_cwd); | ||
452 | if (!tileset) { | ||
453 | goto cleanup; | ||
454 | }; | ||
455 | |||
456 | // Load tile data. | ||
457 | const Ts_Tile* tile = &tileset->tiles[0]; | ||
458 | for (uint16_t j = 0; j < tileset->num_tiles; ++j) { | ||
459 | // Tile dimensions should be a multiple of the base tile size. | ||
460 | assert((tile->width % map->base_tile_width) == 0); | ||
461 | assert((tile->height % map->base_tile_height) == 0); | ||
462 | |||
463 | // Allocate N base tile size blocks for the tile. | ||
464 | const uint16_t tile_size = tile->width * tile->height; | ||
465 | const int num_blocks = tile_size / base_tile_size; | ||
466 | Pixel* pixels = mem_alloc(&iso->pixels, num_blocks); | ||
467 | assert(pixels); | ||
468 | memcpy(pixels, tile->pixels, tile_size * sizeof(Pixel)); | ||
469 | |||
470 | // Allocate the tile data. | ||
471 | TileData* tile_data = mempool_alloc(&iso->tiles); | ||
472 | assert(tile_data); | ||
473 | tile_data->width = tile->width; | ||
474 | tile_data->height = tile->height; | ||
475 | tile_data->pixels_handle = | ||
476 | (uint16_t)mem_get_chunk_handle(&iso->pixels, pixels); | ||
477 | |||
478 | tile = ts_tileset_get_next_tile(tileset, tile); | ||
479 | } | ||
480 | |||
481 | printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd); | ||
482 | |||
483 | free(tileset); | ||
484 | layer = tm_map_get_next_layer(map, layer); | ||
485 | } | ||
486 | |||
487 | // Load the map into the world. | ||
488 | layer = &map->layers[0]; | ||
489 | // TODO: Handle num_layers layers. | ||
490 | for (int i = 0; i < 1; ++i) { | ||
491 | memcpy(iso->world, layer->tiles, world_size * sizeof(Tile)); | ||
492 | |||
493 | // TODO: We need to handle 'firsgid' in TMX files. | ||
494 | for (int j = 0; j < world_size; ++j) { | ||
495 | iso->world[j] -= 1; | ||
496 | } | ||
497 | |||
498 | layer = tm_map_get_next_layer(map, layer); | ||
499 | } | ||
500 | |||
501 | iso->world_width = map->world_width; | ||
502 | iso->world_height = map->world_height; | ||
503 | iso->tile_width = map->base_tile_width; | ||
504 | iso->tile_height = map->base_tile_height; | ||
505 | |||
506 | success = true; | ||
507 | |||
508 | cleanup: | ||
509 | if (map) { | ||
510 | free(map); | ||
511 | } | ||
512 | if (!success) { | ||
513 | destroy_world(iso); | ||
514 | } | ||
515 | return success; | ||
516 | } | ||
517 | |||
518 | int isogfx_world_width(const IsoGfx* iso) { | ||
519 | assert(iso); | ||
520 | return iso->world_width; | ||
521 | } | ||
522 | |||
523 | int isogfx_world_height(const IsoGfx* iso) { | ||
524 | assert(iso); | ||
525 | return iso->world_height; | ||
526 | } | ||
527 | |||
528 | /// Create a tile mask procedurally. | ||
529 | static void make_tile_from_colour( | ||
530 | const IsoGfx* iso, Pixel colour, TileData* tile) { | ||
531 | assert(iso); | ||
532 | assert(tile); | ||
533 | |||
534 | const int width = tile->width; | ||
535 | const int height = tile->height; | ||
536 | const int r = width / height; | ||
537 | |||
538 | for (int y = 0; y < height / 2; ++y) { | ||
539 | const int mask_start = width / 2 - r * y - 1; | ||
540 | const int mask_end = width / 2 + r * y + 1; | ||
541 | for (int x = 0; x < width; ++x) { | ||
542 | const bool mask = (mask_start <= x) && (x <= mask_end); | ||
543 | const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0}; | ||
544 | |||
545 | // Top half. | ||
546 | *tile_xy_mut(iso, tile, x, y) = val; | ||
547 | |||
548 | // Bottom half reflects the top half. | ||
549 | const int y_reflected = height - y - 1; | ||
550 | *tile_xy_mut(iso, tile, x, y_reflected) = val; | ||
551 | } | ||
552 | } | ||
553 | } | ||
554 | |||
555 | Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) { | ||
556 | assert(iso); | ||
557 | assert(desc); | ||
558 | // Client must create world before creating tiles. | ||
559 | assert(iso->tile_width > 0); | ||
560 | assert(iso->tile_height > 0); | ||
561 | |||
562 | TileData* tile = mempool_alloc(&iso->tiles); | ||
563 | assert(tile); // TODO: Make this a hard assert. | ||
564 | |||
565 | const int num_blocks = calc_num_tile_blocks( | ||
566 | iso->tile_width, iso->tile_height, desc->width, desc->height); | ||
567 | |||
568 | Pixel* pixels = mem_alloc(&iso->pixels, num_blocks); | ||
569 | assert(pixels); // TODO: Make this a hard assert. | ||
570 | |||
571 | tile->width = desc->width; | ||
572 | tile->height = desc->height; | ||
573 | tile->pixels_handle = mem_get_chunk_handle(&iso->pixels, pixels); | ||
574 | |||
575 | switch (desc->type) { | ||
576 | case TileFromColour: | ||
577 | make_tile_from_colour(iso, desc->colour, tile); | ||
578 | break; | ||
579 | case TileFromFile: | ||
580 | assert(false); // TODO | ||
581 | break; | ||
582 | case TileFromMemory: | ||
583 | assert(false); // TODO | ||
584 | break; | ||
585 | } | ||
586 | |||
587 | return (Tile)mempool_get_block_index(&iso->tiles, tile); | ||
588 | } | ||
589 | |||
590 | void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) { | ||
591 | assert(iso); | ||
592 | *world_xy_mut(iso, x, y) = tile; | ||
593 | } | ||
594 | |||
595 | void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) { | ||
596 | assert(iso); | ||
597 | for (int y = y0; y < y1; ++y) { | ||
598 | for (int x = x0; x < x1; ++x) { | ||
599 | isogfx_set_tile(iso, x, y, tile); | ||
600 | } | ||
601 | } | ||
602 | } | ||
603 | |||
604 | bool isogfx_load_sprite_sheet( | ||
605 | IsoGfx* iso, const char* filepath, SpriteSheet* p_sheet) { | ||
606 | assert(iso); | ||
607 | assert(filepath); | ||
608 | assert(p_sheet); | ||
609 | |||
610 | bool success = false; | ||
611 | |||
612 | // Lazy initialization of sprite pools. | ||
613 | if (mempool_capacity(&iso->sprites) == 0) { | ||
614 | if (!mempool_make_dyn( | ||
615 | &iso->sprites, iso->max_num_sprites, sizeof(SpriteData))) { | ||
616 | return false; | ||
617 | } | ||
618 | } | ||
619 | if (mem_capacity(&iso->sheets) == 0) { | ||
620 | // Using a block size of 1 byte for sprite sheet data. | ||
621 | if (!mem_make_dyn(&iso->sheets, iso->sprite_sheet_pool_size_bytes, 1)) { | ||
622 | return false; | ||
623 | } | ||
624 | } | ||
625 | |||
626 | // Load sprite sheet file. | ||
627 | printf("Load sprite sheet: %s\n", filepath); | ||
628 | FILE* file = fopen(filepath, "rb"); | ||
629 | if (file == NULL) { | ||
630 | goto cleanup; | ||
631 | } | ||
632 | const size_t sheet_size = get_file_size(file); | ||
633 | SpriteSheetData* ss_sheet = mem_alloc(&iso->sheets, sheet_size); | ||
634 | if (!ss_sheet) { | ||
635 | goto cleanup; | ||
636 | } | ||
637 | if (fread(ss_sheet, sheet_size, 1, file) != 1) { | ||
638 | goto cleanup; | ||
639 | } | ||
640 | |||
641 | *p_sheet = mem_get_chunk_handle(&iso->sheets, ss_sheet); | ||
642 | success = true; | ||
643 | |||
644 | cleanup: | ||
645 | // Pools remain initialized since client may attempt to load other sprites. | ||
646 | if (file != NULL) { | ||
647 | fclose(file); | ||
648 | } | ||
649 | if (!success) { | ||
650 | if (ss_sheet) { | ||
651 | mem_free(&iso->sheets, &ss_sheet); | ||
652 | } | ||
653 | } | ||
654 | return success; | ||
655 | } | ||
656 | |||
657 | Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) { | ||
658 | assert(iso); | ||
659 | |||
660 | SpriteData* sprite = mempool_alloc(&iso->sprites); | ||
661 | assert(sprite); | ||
662 | |||
663 | sprite->sheet = sheet; | ||
664 | |||
665 | return mempool_get_block_index(&iso->sprites, sprite); | ||
666 | } | ||
667 | |||
668 | #define with_sprite(SPRITE, BODY) \ | ||
669 | { \ | ||
670 | SpriteData* data = mempool_get_block(&iso->sprites, sprite); \ | ||
671 | assert(data); \ | ||
672 | BODY; \ | ||
673 | } | ||
674 | |||
675 | void isogfx_set_sprite_position(IsoGfx* iso, Sprite sprite, int x, int y) { | ||
676 | assert(iso); | ||
677 | with_sprite(sprite, { | ||
678 | data->position.x = x; | ||
679 | data->position.y = y; | ||
680 | }); | ||
681 | } | ||
682 | |||
683 | void isogfx_set_sprite_animation(IsoGfx* iso, Sprite sprite, int animation) { | ||
684 | assert(iso); | ||
685 | with_sprite(sprite, { data->animation = animation; }); | ||
686 | } | ||
687 | |||
688 | void isogfx_update(IsoGfx* iso, double t) { | ||
689 | assert(iso); | ||
690 | |||
691 | // If this is the first time update() is called after initialization, just | ||
692 | // record the starting animation time. | ||
693 | if (iso->last_animation_time == 0.0) { | ||
694 | iso->last_animation_time = t; | ||
695 | return; | ||
696 | } | ||
697 | |||
698 | if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) { | ||
699 | // TODO: Consider linking animated sprites in a list so that we only walk | ||
700 | // over those here and not also the static sprites. | ||
701 | mempool_foreach(&iso->sprites, sprite, { | ||
702 | const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet); | ||
703 | assert(sheet); // TODO: Make this a hard assert inside the mem/pool. | ||
704 | const SpriteSheetRow* row = | ||
705 | get_sprite_sheet_row(sheet, sprite->animation); | ||
706 | sprite->frame = (sprite->frame + 1) % row->num_cols; | ||
707 | }); | ||
708 | |||
709 | iso->last_animation_time = t; | ||
710 | } | ||
711 | } | ||
712 | |||
713 | // ----------------------------------------------------------------------------- | ||
714 | // Rendering and picking. | ||
715 | // ----------------------------------------------------------------------------- | ||
716 | |||
717 | typedef struct CoordSystem { | ||
718 | ivec2 o; /// Origin. | ||
719 | ivec2 x; | ||
720 | ivec2 y; | ||
721 | } CoordSystem; | ||
722 | |||
723 | /// Create the basis for the isometric coordinate system with origin and vectors | ||
724 | /// expressed in the Cartesian system. | ||
725 | static CoordSystem make_iso_coord_system(const IsoGfx* iso) { | ||
726 | assert(iso); | ||
727 | const ivec2 o = {iso->screen_width / 2, 0}; | ||
728 | const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
729 | const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
730 | return (CoordSystem){o, x, y}; | ||
731 | } | ||
732 | |||
733 | /// Get the screen position of the top diamond-corner of the tile at world | ||
734 | /// (x,y). | ||
735 | static ivec2 GetTileScreenOrigin( | ||
736 | const CoordSystem iso_space, int world_x, int world_y) { | ||
737 | const ivec2 vx_offset = ivec2_scale(iso_space.x, world_x); | ||
738 | const ivec2 vy_offset = ivec2_scale(iso_space.y, world_y); | ||
739 | const ivec2 screen_origin = | ||
740 | ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset)); | ||
741 | |||
742 | return screen_origin; | ||
743 | } | ||
744 | |||
745 | static Pixel alpha_blend(Pixel src, Pixel dst) { | ||
746 | if ((src.a == 255) || (dst.a == 0)) { | ||
747 | return src; | ||
748 | } | ||
749 | const uint16_t one_minus_alpha = 255 - src.a; | ||
750 | #define blend(s, d) \ | ||
751 | (Channel)( \ | ||
752 | (double)((uint16_t)s * (uint16_t)src.a + \ | ||
753 | (uint16_t)d * one_minus_alpha) / \ | ||
754 | 255.0) | ||
755 | return (Pixel){ | ||
756 | .r = blend(src.r, dst.r), | ||
757 | .g = blend(src.g, dst.g), | ||
758 | .b = blend(src.b, dst.b), | ||
759 | .a = src.a}; | ||
760 | } | ||
761 | |||
762 | /// Draw a rectangle (tile or sprite). | ||
763 | /// | ||
764 | /// The rectangle's top-left corner is mapped to the screen space position given | ||
765 | /// by 'top_left'. | ||
766 | /// | ||
767 | /// The rectangle's pixels are assumed to be arranged in a linear, row-major | ||
768 | /// fashion. | ||
769 | /// | ||
770 | /// If indices are given, then the image is assumed to be colour-paletted, where | ||
771 | /// 'pixels' is the palette and 'indices' the pixel indices. Otherwise, the | ||
772 | /// image is assumed to be in plain RGBA format. | ||
773 | static void draw_rect( | ||
774 | IsoGfx* iso, ivec2 top_left, int rect_width, int rect_height, | ||
775 | const Pixel* pixels, const uint8_t* indices) { | ||
776 | assert(iso); | ||
777 | |||
778 | #define rect_pixel(X, Y) \ | ||
779 | (indices ? pixels[indices[Y * rect_width + X]] : pixels[Y * rect_width + X]) | ||
780 | |||
781 | // Rect origin can be outside screen bounds, so we must offset accordingly to | ||
782 | // draw only the visible portion. | ||
783 | #define max(a, b) (a > b ? a : b) | ||
784 | const int px_offset = max(0, -top_left.x); | ||
785 | const int py_offset = max(0, -top_left.y); | ||
786 | |||
787 | // Rect can exceed screen bounds, so clip along Y and X as we draw. | ||
788 | for (int py = py_offset; | ||
789 | (py < rect_height) && (top_left.y + py < iso->screen_height); ++py) { | ||
790 | const int sy = top_left.y + py; | ||
791 | for (int px = px_offset; | ||
792 | (px < rect_width) && (top_left.x + px < iso->screen_width); ++px) { | ||
793 | const Pixel colour = rect_pixel(px, py); | ||
794 | if (colour.a > 0) { | ||
795 | const int sx = top_left.x + px; | ||
796 | const Pixel dst = screen_xy(iso, sx, sy); | ||
797 | const Pixel final = alpha_blend(colour, dst); | ||
798 | *screen_xy_mut(iso, sx, sy) = final; | ||
799 | } | ||
800 | } | ||
801 | } | ||
802 | } | ||
803 | |||
804 | /// Draw a tile. | ||
805 | /// | ||
806 | /// 'screen_origin' is the screen coordinates of the top diamond-corner of the | ||
807 | /// tile (the base tile for super tiles). | ||
808 | /// World (0, 0) -> (screen_width / 2, 0). | ||
809 | static void draw_tile(IsoGfx* iso, ivec2 screen_origin, Tile tile) { | ||
810 | assert(iso); | ||
811 | |||
812 | const TileData* tile_data = mempool_get_block(&iso->tiles, tile); | ||
813 | assert(tile_data); | ||
814 | const Pixel* pixels = tile_xy_const_ref(iso, tile_data, 0, 0); | ||
815 | |||
816 | // Move from the top diamond-corner to the top-left corner of the tile image. | ||
817 | // For regular tiles, tile height == base tile height, so the y offset is 0. | ||
818 | // For super tiles, move as high up as the height of the tile. | ||
819 | const ivec2 offset = { | ||
820 | -(iso->tile_width / 2), tile_data->height - iso->tile_height}; | ||
821 | const ivec2 top_left = ivec2_add(screen_origin, offset); | ||
822 | |||
823 | draw_rect(iso, top_left, tile_data->width, tile_data->height, pixels, 0); | ||
824 | } | ||
825 | |||
826 | static void draw_world(IsoGfx* iso) { | ||
827 | assert(iso); | ||
828 | |||
829 | const int W = iso->screen_width; | ||
830 | const int H = iso->screen_height; | ||
831 | |||
832 | memset(iso->screen, 0, W * H * sizeof(Pixel)); | ||
833 | |||
834 | const CoordSystem iso_space = make_iso_coord_system(iso); | ||
835 | |||
836 | // TODO: Culling. | ||
837 | // Ex: map the screen corners to tile space to cull. | ||
838 | // Ex: walk in screen space and fetch the tile. | ||
839 | // The tile-centric approach might be more cache-friendly since the | ||
840 | // screen-centric approach would juggle multiple tiles throughout the scan. | ||
841 | for (int wy = 0; wy < iso->world_height; ++wy) { | ||
842 | for (int wx = 0; wx < iso->world_width; ++wx) { | ||
843 | const Tile tile = world_xy(iso, wx, wy); | ||
844 | const ivec2 screen_origin = GetTileScreenOrigin(iso_space, wx, wy); | ||
845 | draw_tile(iso, screen_origin, tile); | ||
846 | } | ||
847 | } | ||
848 | } | ||
849 | |||
850 | static void draw_sprite( | ||
851 | IsoGfx* iso, ivec2 origin, const SpriteData* sprite, | ||
852 | const SpriteSheetData* sheet) { | ||
853 | assert(iso); | ||
854 | assert(sprite); | ||
855 | assert(sheet); | ||
856 | assert(sprite->animation >= 0); | ||
857 | assert(sprite->animation < sheet->num_rows); | ||
858 | assert(sprite->frame >= 0); | ||
859 | |||
860 | const SpriteSheetRow* row = get_sprite_sheet_row(sheet, sprite->animation); | ||
861 | const uint8_t* frame = get_sprite_sheet_sprite(sheet, row, sprite->frame); | ||
862 | draw_rect( | ||
863 | iso, origin, sheet->sprite_width, sheet->sprite_height, | ||
864 | sheet->palette.colours, frame); | ||
865 | } | ||
866 | |||
867 | static void draw_sprites(IsoGfx* iso) { | ||
868 | assert(iso); | ||
869 | |||
870 | const CoordSystem iso_space = make_iso_coord_system(iso); | ||
871 | |||
872 | mempool_foreach(&iso->sprites, sprite, { | ||
873 | const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet); | ||
874 | assert(sheet); | ||
875 | |||
876 | const ivec2 screen_origin = | ||
877 | GetTileScreenOrigin(iso_space, sprite->position.x, sprite->position.y); | ||
878 | draw_sprite(iso, screen_origin, sprite, sheet); | ||
879 | }); | ||
880 | } | ||
881 | |||
882 | void isogfx_render(IsoGfx* iso) { | ||
883 | assert(iso); | ||
884 | draw_world(iso); | ||
885 | draw_sprites(iso); | ||
886 | } | ||
887 | |||
888 | void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { | ||
889 | assert(iso); | ||
890 | assert(x >= 0); | ||
891 | assert(y >= 0); | ||
892 | assert(x < iso->world_width); | ||
893 | assert(y < iso->world_height); | ||
894 | |||
895 | const CoordSystem iso_space = make_iso_coord_system(iso); | ||
896 | const ivec2 screen_origin = GetTileScreenOrigin(iso_space, x, y); | ||
897 | draw_tile(iso, screen_origin, tile); | ||
898 | } | ||
899 | |||
900 | bool isogfx_resize(IsoGfx* iso, int screen_width, int screen_height) { | ||
901 | assert(iso); | ||
902 | assert(iso->screen); | ||
903 | |||
904 | const int current_size = iso->screen_width * iso->screen_height; | ||
905 | const int new_size = screen_width * screen_height; | ||
906 | |||
907 | if (new_size > current_size) { | ||
908 | Pixel* new_screen = calloc(new_size, sizeof(Pixel)); | ||
909 | if (new_screen) { | ||
910 | free(iso->screen); | ||
911 | iso->screen = new_screen; | ||
912 | } else { | ||
913 | return false; | ||
914 | } | ||
915 | } | ||
916 | iso->screen_width = screen_width; | ||
917 | iso->screen_height = screen_height; | ||
918 | return true; | ||
919 | } | ||
920 | |||
921 | void isogfx_get_screen_size(const IsoGfx* iso, int* width, int* height) { | ||
922 | assert(iso); | ||
923 | assert(width); | ||
924 | assert(height); | ||
925 | *width = iso->screen_width; | ||
926 | *height = iso->screen_height; | ||
927 | } | ||
928 | |||
929 | const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) { | ||
930 | assert(iso); | ||
931 | return iso->screen; | ||
932 | } | ||
933 | |||
934 | void isogfx_pick_tile( | ||
935 | const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) { | ||
936 | assert(iso); | ||
937 | assert(xiso); | ||
938 | assert(yiso); | ||
939 | |||
940 | const vec2 xy_iso = cart2iso( | ||
941 | (vec2){.x = xcart, .y = ycart}, iso->tile_width, iso->tile_height, | ||
942 | iso->screen_width); | ||
943 | |||
944 | if ((0 <= xy_iso.x) && (xy_iso.x < iso->world_width) && (0 <= xy_iso.y) && | ||
945 | (xy_iso.y < iso->world_height)) { | ||
946 | *xiso = (int)xy_iso.x; | ||
947 | *yiso = (int)xy_iso.y; | ||
948 | } else { | ||
949 | *xiso = -1; | ||
950 | *yiso = -1; | ||
951 | } | ||
952 | } | ||
diff --git a/gfx-iso/tools/mkasset.py b/gfx-iso/tools/mkasset.py deleted file mode 100644 index 3ca8a1d..0000000 --- a/gfx-iso/tools/mkasset.py +++ /dev/null | |||
@@ -1,324 +0,0 @@ | |||
1 | # Converts assets to binary formats (.ts, .tm, .ss) for the engine. | ||
2 | # | ||
3 | # Input file formats: | ||
4 | # - Tiled tile set (.tsx) | ||
5 | # - Tiled tile map (.tmx) | ||
6 | # - Sprite sheets (.jpg, .png, etc), 1 row per animation. | ||
7 | # | ||
8 | # Output file formats: | ||
9 | # - Binary tile set file (.ts) | ||
10 | # - Binary tile map file (.tm) | ||
11 | # - Binary sprite sheet file (.ss) | ||
12 | # | ||
13 | import argparse | ||
14 | import ctypes | ||
15 | import os | ||
16 | from PIL import Image | ||
17 | import sys | ||
18 | from xml.etree import ElementTree | ||
19 | |||
20 | # Maximum length of path strings in .TS and .TM files. | ||
21 | # Must match the engine's value. | ||
22 | MAX_PATH_LENGTH = 128 | ||
23 | |||
24 | |||
25 | def drop_extension(filepath): | ||
26 | return filepath[:filepath.rfind('.')] | ||
27 | |||
28 | |||
29 | def to_char_array(string, length): | ||
30 | """Convert a string to a fixed-length ASCII char array. | ||
31 | |||
32 | The length of str must be at most length-1 so that the resulting string can | ||
33 | be null-terminated. | ||
34 | """ | ||
35 | assert (len(string) < length) | ||
36 | chars = string.encode("ascii") | ||
37 | nulls = ("\0" * (length - len(string))).encode("ascii") | ||
38 | return chars + nulls | ||
39 | |||
40 | |||
41 | def convert_tsx(input_filepath, output_filepath): | ||
42 | """Converts a Tiled .tsx tileset file to a .TS tile set file.""" | ||
43 | xml = ElementTree.parse(input_filepath) | ||
44 | root = xml.getroot() | ||
45 | |||
46 | tile_count = int(root.attrib["tilecount"]) | ||
47 | max_tile_width = int(root.attrib["tilewidth"]) | ||
48 | max_tile_height = int(root.attrib["tileheight"]) | ||
49 | |||
50 | print(f"Tile count: {tile_count}") | ||
51 | print(f"Max width: {max_tile_width}") | ||
52 | print(f"Max height: {max_tile_height}") | ||
53 | |||
54 | with open(output_filepath, 'bw') as output: | ||
55 | output.write(ctypes.c_uint16(tile_count)) | ||
56 | output.write(ctypes.c_uint16(max_tile_width)) | ||
57 | output.write(ctypes.c_uint16(max_tile_height)) | ||
58 | |||
59 | num_tile = 0 | ||
60 | for tile in root: | ||
61 | # Skip the "grid" and other non-tile elements. | ||
62 | if not tile.tag == "tile": | ||
63 | continue | ||
64 | |||
65 | # Assuming tiles are numbered 0..N. | ||
66 | tile_id = int(tile.attrib["id"]) | ||
67 | assert (tile_id == num_tile) | ||
68 | num_tile += 1 | ||
69 | |||
70 | image = tile[0] | ||
71 | tile_width = int(image.attrib["width"]) | ||
72 | tile_height = int(image.attrib["height"]) | ||
73 | tile_path = image.attrib["source"] | ||
74 | |||
75 | output.write(ctypes.c_uint16(tile_width)) | ||
76 | output.write(ctypes.c_uint16(tile_height)) | ||
77 | |||
78 | with Image.open(tile_path) as im: | ||
79 | bytes = im.convert('RGBA').tobytes() | ||
80 | output.write(bytes) | ||
81 | |||
82 | |||
83 | def convert_tmx(input_filepath, output_filepath): | ||
84 | """Converts a Tiled .tmx file to a .TM tile map file.""" | ||
85 | xml = ElementTree.parse(input_filepath) | ||
86 | root = xml.getroot() | ||
87 | |||
88 | map_width = int(root.attrib["width"]) | ||
89 | map_height = int(root.attrib["height"]) | ||
90 | base_tile_width = int(root.attrib["tilewidth"]) | ||
91 | base_tile_height = int(root.attrib["tileheight"]) | ||
92 | num_layers = 1 | ||
93 | |||
94 | print(f"Map width: {map_width}") | ||
95 | print(f"Map height: {map_height}") | ||
96 | print(f"Tile width: {base_tile_width}") | ||
97 | print(f"Tile height: {base_tile_height}") | ||
98 | |||
99 | with open(output_filepath, 'bw') as output: | ||
100 | output.write(ctypes.c_uint16(map_width)) | ||
101 | output.write(ctypes.c_uint16(map_height)) | ||
102 | output.write(ctypes.c_uint16(base_tile_width)) | ||
103 | output.write(ctypes.c_uint16(base_tile_height)) | ||
104 | output.write(ctypes.c_uint16(num_layers)) | ||
105 | |||
106 | tileset_path = None | ||
107 | |||
108 | for child in root: | ||
109 | if child.tag == "tileset": | ||
110 | tileset = child | ||
111 | tileset_path = tileset.attrib["source"] | ||
112 | |||
113 | print(f"Tile set: {tileset_path}") | ||
114 | |||
115 | tileset_path = tileset_path.replace("tsx", "ts") | ||
116 | elif child.tag == "layer": | ||
117 | layer = child | ||
118 | layer_id = int(layer.attrib["id"]) | ||
119 | layer_width = int(layer.attrib["width"]) | ||
120 | layer_height = int(layer.attrib["height"]) | ||
121 | |||
122 | print(f"Layer: {layer_id}") | ||
123 | print(f"Width: {layer_width}") | ||
124 | print(f"Height: {layer_height}") | ||
125 | |||
126 | assert (tileset_path) | ||
127 | output.write(to_char_array(tileset_path, MAX_PATH_LENGTH)) | ||
128 | |||
129 | # Assume the layer's dimensions matches the map's. | ||
130 | assert (layer_width == map_width) | ||
131 | assert (layer_height == map_height) | ||
132 | |||
133 | data = layer[0] | ||
134 | # Handle other encodings later. | ||
135 | assert (data.attrib["encoding"] == "csv") | ||
136 | |||
137 | csv = data.text.strip() | ||
138 | rows = csv.split('\n') | ||
139 | for row in rows: | ||
140 | tile_ids = [x.strip() for x in row.split(',') if x] | ||
141 | for tile_id in tile_ids: | ||
142 | output.write(ctypes.c_uint16(int(tile_id))) | ||
143 | |||
144 | |||
145 | def get_num_cols(image, sprite_width): | ||
146 | """Return the number of non-empty columns in the image. | ||
147 | |||
148 | Assumes no gaps in the columns. | ||
149 | """ | ||
150 | assert (image.width % sprite_width == 0) | ||
151 | num_cols = image.width // sprite_width | ||
152 | |||
153 | # Start the search from right to left. | ||
154 | for col in reversed(range(1, num_cols)): | ||
155 | left = (col - 1) * sprite_width | ||
156 | right = col * sprite_width | ||
157 | rect = image.crop((left, 0, right, image.height)) | ||
158 | min_max = rect.getextrema() | ||
159 | for (channel_min, channel_max) in min_max: | ||
160 | if channel_min != 0 or channel_max != 0: | ||
161 | # 'col' is the rightmost non-empty column. | ||
162 | # Assuming no gaps, col+1 is the number of non-empty columns. | ||
163 | return col + 1 | ||
164 | |||
165 | return 0 | ||
166 | |||
167 | |||
168 | def get_sprite_sheet_rows(im, sprite_width, sprite_height): | ||
169 | """Gets the individual rows of a sprite sheet. | ||
170 | |||
171 | The input sprite sheet can have any number of rows. | ||
172 | |||
173 | Returns a list of lists [[sprite]], one inner list for the columns in each | ||
174 | row. | ||
175 | """ | ||
176 | # Sprite sheet's width and height must be integer multiples of the | ||
177 | # sprite's width and height. | ||
178 | assert (im.width % sprite_width == 0) | ||
179 | assert (im.height % sprite_height == 0) | ||
180 | |||
181 | num_rows = im.height // sprite_height | ||
182 | |||
183 | rows = [] | ||
184 | for row in range(num_rows): | ||
185 | # Get the number of columns. | ||
186 | upper = row * sprite_height | ||
187 | lower = (row + 1) * sprite_height | ||
188 | whole_row = im.crop((0, upper, im.width, lower)) | ||
189 | num_cols = get_num_cols(whole_row, sprite_width) | ||
190 | assert (num_cols > 0) | ||
191 | |||
192 | # Crop the row into N columns. | ||
193 | cols = [] | ||
194 | for i in range(num_cols): | ||
195 | left = i * sprite_width | ||
196 | right = (i + 1) * sprite_width | ||
197 | sprite = im.crop((left, upper, right, lower)) | ||
198 | cols.append(sprite) | ||
199 | |||
200 | assert (len(cols) == num_cols) | ||
201 | rows.append(cols) | ||
202 | |||
203 | return rows | ||
204 | |||
205 | |||
206 | def make_image_from_rows(rows, sprite_width, sprite_height): | ||
207 | """Concatenate the rows into a single RGBA image.""" | ||
208 | im_width = sprite_width * max(len(row) for row in rows) | ||
209 | im_height = len(rows) * sprite_height | ||
210 | im = Image.new('RGBA', (im_width, im_height)) | ||
211 | y = 0 | ||
212 | for row in rows: | ||
213 | x = 0 | ||
214 | for sprite in row: | ||
215 | im.paste(sprite.convert('RGBA'), (x, y)) | ||
216 | x += sprite_width | ||
217 | y += sprite_height | ||
218 | return im | ||
219 | |||
220 | |||
221 | def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height, | ||
222 | output_filepath): | ||
223 | """Converts a set of sprite sheet images into a binary sprite sheet file | ||
224 | (.ss). | ||
225 | |||
226 | The input sprite sheets can have any number of rows, one row per animation. | ||
227 | All rows from all sprite sheets are concatenated in the output file. | ||
228 | |||
229 | The sprite's width and height is assumed constant throughout the input | ||
230 | sprite sheets. | ||
231 | """ | ||
232 | rows = [] | ||
233 | for input_filepath in input_file_paths: | ||
234 | with Image.open(input_filepath) as sprite_sheet: | ||
235 | rows.extend( | ||
236 | get_sprite_sheet_rows(sprite_sheet, sprite_width, | ||
237 | sprite_height)) | ||
238 | |||
239 | im = make_image_from_rows(rows, sprite_width, sprite_height) | ||
240 | im = im.convert(mode="P", palette=Image.ADAPTIVE, colors=256) | ||
241 | |||
242 | # The sprite data in 'rows' is no longer needed. | ||
243 | # Keep just the number of columns per row. | ||
244 | rows = [len(row) for row in rows] | ||
245 | |||
246 | with open(output_filepath, 'bw') as output: | ||
247 | output.write(ctypes.c_uint16(sprite_width)) | ||
248 | output.write(ctypes.c_uint16(sprite_height)) | ||
249 | output.write(ctypes.c_uint16(len(rows))) | ||
250 | |||
251 | # Write palette. | ||
252 | # getpalette() returns 256 colors, but the palette might use less than | ||
253 | # that. getcolors() returns the number of unique colors. | ||
254 | # getpalette() also returns a flattened list, which is why we must *4. | ||
255 | num_colours = len(im.getcolors()) | ||
256 | colours = im.getpalette(rawmode="RGBA")[:4 * num_colours] | ||
257 | palette = [] | ||
258 | for i in range(0, 4 * num_colours, 4): | ||
259 | palette.append((colours[i], colours[i + 1], colours[i + 2], | ||
260 | colours[i + 3])) | ||
261 | |||
262 | output.write(ctypes.c_uint16(len(palette))) | ||
263 | output.write(bytearray(colours)) | ||
264 | |||
265 | print(f"Sprite width: {sprite_width}") | ||
266 | print(f"Sprite height: {sprite_height}") | ||
267 | print(f"Rows: {len(rows)}") | ||
268 | print(f"Colours: {len(palette)}") | ||
269 | |||
270 | # print("Palette") | ||
271 | # for i, colour in enumerate(palette): | ||
272 | # print(f"{i}: {colour}") | ||
273 | |||
274 | for row, num_columns in enumerate(rows): | ||
275 | output.write(ctypes.c_uint16(num_columns)) | ||
276 | upper = row * sprite_height | ||
277 | lower = (row + 1) * sprite_height | ||
278 | for col in range(num_columns): | ||
279 | left = col * sprite_width | ||
280 | right = (col + 1) * sprite_width | ||
281 | sprite = im.crop((left, upper, right, lower)) | ||
282 | sprite_bytes = sprite.tobytes() | ||
283 | |||
284 | assert (len(sprite_bytes) == sprite_width * sprite_height) | ||
285 | output.write(sprite_bytes) | ||
286 | |||
287 | # if (row == 0) and (col == 0): | ||
288 | # print(f"Sprite: ({len(sprite_bytes)})") | ||
289 | # print(list(sprite_bytes)) | ||
290 | # sprite.save("out.png") | ||
291 | |||
292 | |||
293 | def main(): | ||
294 | parser = argparse.ArgumentParser() | ||
295 | parser.add_argument("input", | ||
296 | nargs="+", | ||
297 | help="Input file (.tsx, .tmx) or path regex (sprite sheets)") | ||
298 | parser.add_argument("--width", type=int, help="Sprite width in pixels") | ||
299 | parser.add_argument("--height", type=int, help="Sprite height in pixels") | ||
300 | parser.add_argument("--out", help="Output file (sprite sheets)") | ||
301 | args = parser.parse_args() | ||
302 | |||
303 | if ".tsx" in args.input: | ||
304 | output_filepath_no_ext = drop_extension(args.input) | ||
305 | output_filepath = output_filepath_no_ext + ".ts" | ||
306 | convert_tsx(args.input, output_filepath) | ||
307 | elif ".tmx" in args.input: | ||
308 | output_filepath_no_ext = drop_extension(args.input) | ||
309 | output_filepath = output_filepath_no_ext + ".tm" | ||
310 | convert_tmx(args.input, output_filepath) | ||
311 | else: | ||
312 | # Sprite sheets. | ||
313 | if not args.width or not args.height: | ||
314 | print("Sprite width and height must be given") | ||
315 | return 1 | ||
316 | output_filepath = args.out if args.out else "out.ss" | ||
317 | convert_sprite_sheet(args.input, args.width, args.height, | ||
318 | output_filepath) | ||
319 | |||
320 | return 0 | ||
321 | |||
322 | |||
323 | if __name__ == '__main__': | ||
324 | sys.exit(main()) | ||