summaryrefslogtreecommitdiff
path: root/gfx-iso
diff options
context:
space:
mode:
author3gg <3gg@shellblade.net>2023-07-19 08:35:00 -0700
committer3gg <3gg@shellblade.net>2023-07-19 08:35:00 -0700
commit48cef82988d6209987ae27fe29b72d7d5e402b3c (patch)
treefe5df57729a61839322ae8c1226d134e317b049f /gfx-iso
parent2c668763a1d6e645dcfaa713b924de26260542d0 (diff)
Add sprites.
Diffstat (limited to 'gfx-iso')
-rw-r--r--gfx-iso/app/isogfx-demo.c15
-rw-r--r--gfx-iso/app/main.c2
-rw-r--r--gfx-iso/asset/mkasset.py128
-rw-r--r--gfx-iso/include/isogfx/isogfx.h35
-rw-r--r--gfx-iso/src/isogfx.c362
5 files changed, 491 insertions, 51 deletions
diff --git a/gfx-iso/app/isogfx-demo.c b/gfx-iso/app/isogfx-demo.c
index d463d1c..9889275 100644
--- a/gfx-iso/app/isogfx-demo.c
+++ b/gfx-iso/app/isogfx-demo.c
@@ -9,8 +9,10 @@
9#include <stdlib.h> 9#include <stdlib.h>
10 10
11typedef struct State { 11typedef struct State {
12 int xpick; 12 int xpick;
13 int ypick; 13 int ypick;
14 SpriteSheet stag_sheet;
15 Sprite stag;
14} State; 16} State;
15 17
16static void shutdown(IsoGfx* iso, void* app_state) { 18static void shutdown(IsoGfx* iso, void* app_state) {
@@ -54,6 +56,15 @@ bool make_demo_app(IsoGfx* iso, IsoGfxApp* app) {
54 goto cleanup; 56 goto cleanup;
55 } 57 }
56 58
59 if (!isogfx_load_sprite_sheet(
60 iso, "/home/jeanne/assets/tilesets/scrabling/critters/stag/stag.ss",
61 &state->stag_sheet)) {
62 goto cleanup;
63 }
64
65 state->stag = isogfx_make_sprite(iso, state->stag_sheet);
66 isogfx_set_sprite_position(iso, state->stag, 5, 4);
67
57 app->pixel_scale = 2; 68 app->pixel_scale = 2;
58 app->state = state; 69 app->state = state;
59 app->shutdown = shutdown; 70 app->shutdown = shutdown;
diff --git a/gfx-iso/app/main.c b/gfx-iso/app/main.c
index 5b441d3..ff8a266 100644
--- a/gfx-iso/app/main.c
+++ b/gfx-iso/app/main.c
@@ -138,6 +138,8 @@ static void update(void* app_state, double t, double dt) {
138 assert(app_state); 138 assert(app_state);
139 State* state = (State*)(app_state); 139 State* state = (State*)(app_state);
140 140
141 isogfx_update(state->iso, t);
142
141 assert(state->app.update); 143 assert(state->app.update);
142 (*state->app.update)(state->iso, state->app.state, t, dt); 144 (*state->app.update)(state->iso, state->app.state, t, dt);
143} 145}
diff --git a/gfx-iso/asset/mkasset.py b/gfx-iso/asset/mkasset.py
index 15f7912..b4e335f 100644
--- a/gfx-iso/asset/mkasset.py
+++ b/gfx-iso/asset/mkasset.py
@@ -1,15 +1,24 @@
1# Converts tile sets and tile maps to binary formats (.TS, .TM) for the engine. 1# Converts assets to binary formats (.ts, .tm, .ss) for the engine.
2# 2#
3# Currently handles Tiled's .tsx and .tmx file formats. 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)
4# 12#
5# The output is a binary tile set file (.TS) or a binary tile map file (.TM).
6import argparse 13import argparse
7import ctypes 14import ctypes
15import os
8from PIL import Image 16from PIL import Image
9import sys 17import sys
10from xml.etree import ElementTree 18from xml.etree import ElementTree
11 19
12# Maximum length of path strings in .TS and .TM files. 20# Maximum length of path strings in .TS and .TM files.
21# Must match the engine's value.
13MAX_PATH_LENGTH = 128 22MAX_PATH_LENGTH = 128
14 23
15 24
@@ -133,20 +142,127 @@ def convert_tmx(input_filepath, output_filepath):
133 output.write(ctypes.c_uint16(int(tile_id))) 142 output.write(ctypes.c_uint16(int(tile_id)))
134 143
135 144
145def 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
168def get_sprite_sheet_rows(input_filepath, 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 bytes]], one inner list for the columns in
174 each row.
175 """
176 with Image.open(input_filepath) as im:
177 # Sprite sheet's width and height must be integer multiples of the
178 # sprite's width and height.
179 assert (im.width % sprite_width == 0)
180 assert (im.height % sprite_height == 0)
181
182 num_rows = im.height // sprite_height
183
184 rows = []
185 for row in range(num_rows):
186 # Get the number of columns.
187 upper = row * sprite_height
188 lower = (row + 1) * sprite_height
189 whole_row = im.crop((0, upper, im.width, lower))
190 num_cols = get_num_cols(whole_row, sprite_width)
191 assert (num_cols > 0)
192
193 # Crop the row into N columns.
194 cols = []
195 for i in range(num_cols):
196 left = i * sprite_width
197 right = (i + 1) * sprite_width
198 sprite = im.crop((left, upper, right, lower))
199 cols.append(sprite)
200
201 sprite_bytes = [sprite.convert('RGBA').tobytes() for sprite in cols]
202 assert (len(sprite_bytes) == num_cols)
203 rows.append(sprite_bytes)
204
205 return rows
206
207
208def convert_sprite_sheet(input_file_paths, sprite_width, sprite_height,
209 output_filepath):
210 """Converts a set of sprite sheet images into a binary sprite sheet file
211 (.ss).
212
213 The input sprite sheets can have any number of rows, one row per animation.
214 All rows from all sprite sheets are concatenated in the output file.
215
216 The sprite's width and height is assumed constant throughout the input
217 sprite sheets.
218 """
219 rows = []
220
221 for sprite_sheet in input_file_paths:
222 rows.extend(
223 get_sprite_sheet_rows(sprite_sheet, sprite_width, sprite_height))
224
225 with open(output_filepath, 'bw') as output:
226 output.write(ctypes.c_uint16(sprite_width))
227 output.write(ctypes.c_uint16(sprite_height))
228 output.write(ctypes.c_uint16(len(rows)))
229
230 print(f"Sprite width: {sprite_width}")
231 print(f"Sprite height: {sprite_height}")
232 print(f"Rows: {len(rows)}")
233
234 for sprites in rows:
235 output.write(ctypes.c_uint16(len(sprites)))
236 for sprite_bytes in sprites:
237 output.write(sprite_bytes)
238
239
136def main(): 240def main():
137 parser = argparse.ArgumentParser() 241 parser = argparse.ArgumentParser()
138 parser.add_argument("input", help="Input file (.tsx, .tmx)") 242 parser.add_argument("input",
243 nargs="+",
244 help="Input file (.tsx, .tmx) or path regex (sprite sheets)")
245 parser.add_argument("--width", type=int, help="Sprite width in pixels")
246 parser.add_argument("--height", type=int, help="Sprite height in pixels")
247 parser.add_argument("--out", help="Output file (sprite sheets)")
139 args = parser.parse_args() 248 args = parser.parse_args()
140 249
141 output_filepath_no_ext = drop_extension(args.input)
142 if ".tsx" in args.input: 250 if ".tsx" in args.input:
251 output_filepath_no_ext = drop_extension(args.input)
143 output_filepath = output_filepath_no_ext + ".ts" 252 output_filepath = output_filepath_no_ext + ".ts"
144 convert_tsx(args.input, output_filepath) 253 convert_tsx(args.input, output_filepath)
145 elif ".tmx" in args.input: 254 elif ".tmx" in args.input:
255 output_filepath_no_ext = drop_extension(args.input)
146 output_filepath = output_filepath_no_ext + ".tm" 256 output_filepath = output_filepath_no_ext + ".tm"
147 convert_tmx(args.input, output_filepath) 257 convert_tmx(args.input, output_filepath)
148 else: 258 else:
149 print(f"Unhandled file format: {args.input}") 259 # Sprite sheets.
260 if not args.width or not args.height:
261 print("Sprite width and height must be given")
262 return 1
263 output_filepath = args.out if args.out else "out.ss"
264 convert_sprite_sheet(args.input, args.width, args.height,
265 output_filepath)
150 266
151 return 0 267 return 0
152 268
diff --git a/gfx-iso/include/isogfx/isogfx.h b/gfx-iso/include/isogfx/isogfx.h
index 6b7ce8e..e96606c 100644
--- a/gfx-iso/include/isogfx/isogfx.h
+++ b/gfx-iso/include/isogfx/isogfx.h
@@ -8,6 +8,12 @@
8 8
9typedef struct IsoGfx IsoGfx; 9typedef struct IsoGfx IsoGfx;
10 10
11/// Sprite sheet handle.
12typedef uint16_t SpriteSheet;
13
14/// Sprite handle.
15typedef uint16_t Sprite;
16
11/// Tile handle. 17/// Tile handle.
12typedef uint16_t Tile; 18typedef uint16_t Tile;
13 19
@@ -48,8 +54,10 @@ typedef struct WorldDesc {
48} WorldDesc; 54} WorldDesc;
49 55
50typedef struct IsoGfxDesc { 56typedef struct IsoGfxDesc {
51 int screen_width; /// Screen width in pixels. 57 int screen_width; /// Screen width in pixels.
52 int screen_height; /// Screen height 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.
53} IsoGfxDesc; 61} IsoGfxDesc;
54 62
55/// Create a new isometric graphics engine. 63/// Create a new isometric graphics engine.
@@ -79,6 +87,29 @@ void isogfx_set_tile(IsoGfx*, int x, int y, Tile);
79/// Set the tiles in positions in the range (x0,y0) - (x1,y1). 87/// Set the tiles in positions in the range (x0,y0) - (x1,y1).
80void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile); 88void isogfx_set_tiles(IsoGfx*, int x0, int y0, int x1, int y1, Tile);
81 89
90/// Load a sprite sheet (.SS) file.
91bool isogfx_load_sprite_sheet(IsoGfx*, const char* filepath, SpriteSheet*);
92
93/// Create an animated sprite.
94Sprite isogfx_make_sprite(IsoGfx*, SpriteSheet);
95
96/// Destroy the sprite.
97void isogfx_del_sprite(IsoGfx*, Sprite);
98
99/// Destroy all the sprites.
100void isogfx_del_sprites(IsoGfx*);
101
102/// Set the sprite's position.
103void isogfx_set_sprite_position(IsoGfx*, Sprite, int x, int y);
104
105/// Set the sprite's current animation.
106void isogfx_set_sprite_animation(IsoGfx*, Sprite, int animation);
107
108/// Update the renderer.
109///
110/// Currently this updates the sprite animations.
111void isogfx_update(IsoGfx*, double t);
112
82/// Render the world. 113/// Render the world.
83void isogfx_render(IsoGfx*); 114void isogfx_render(IsoGfx*);
84 115
diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c
index 3ed0fde..9ba1bec 100644
--- a/gfx-iso/src/isogfx.c
+++ b/gfx-iso/src/isogfx.c
@@ -13,9 +13,29 @@
13#include <stdlib.h> 13#include <stdlib.h>
14#include <string.h> 14#include <string.h>
15 15
16/// Maximum number of tiles unless the user chooses a non-zero value. 16/// Maximum number of tiles unless the user specifies a value.
17#define DEFAULT_MAX_NUM_TILES 1024 17#define DEFAULT_MAX_NUM_TILES 1024
18 18
19/// Maximum number of sprites unless the user specifies a value.
20#define DEFAULT_MAX_NUM_SPRITES 128
21
22/// Size of sprite sheet pool in bytes unless the user specifies a value.
23#define DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES (8 * 1024 * 1024)
24
25/// Default animation speed.
26#define ANIMATION_FPS 10
27
28/// Time between animation updates.
29#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS)
30
31typedef struct ivec2 {
32 int x, y;
33} ivec2;
34
35typedef struct vec2 {
36 double x, y;
37} vec2;
38
19// ----------------------------------------------------------------------------- 39// -----------------------------------------------------------------------------
20// Tile set (TS) and tile map (TM) file formats. 40// Tile set (TS) and tile map (TM) file formats.
21// ----------------------------------------------------------------------------- 41// -----------------------------------------------------------------------------
@@ -70,6 +90,39 @@ static inline const Ts_Tile* ts_tileset_get_next_tile(
70} 90}
71 91
72// ----------------------------------------------------------------------------- 92// -----------------------------------------------------------------------------
93// Sprite sheet file format.
94// -----------------------------------------------------------------------------
95
96/// A row of sprites in a sprite sheet.
97///
98/// Each row in a sprite sheet can have a different number of columns.
99///
100/// The pixels of the row follow a "sprite-major" order. It contains the
101/// 'sprite_width * sprite_height' pixels for the first column/sprite, then the
102/// second column/sprite, etc.
103typedef struct Ss_Row {
104 uint16_t num_cols; /// Number of columns in this row.
105 Pixel pixels[1]; /// Count: num_cols * sprite_width * sprite_height.
106} Ss_Row;
107
108/// Sprite sheet top-level data definition.
109///
110/// Sprite width and height are assumed constant throughout the sprite sheet.
111typedef struct Ss_SpriteSheet {
112 uint16_t sprite_width; /// Sprite width in pixels.
113 uint16_t sprite_height; /// Sprite height in pixels.
114 uint16_t num_rows;
115 Ss_Row rows[1]; /// Count: num_rows.
116} Ss_SpriteSheet;
117
118const Ss_Row* get_sprite_sheet_row(const Ss_SpriteSheet* sheet, int row) {
119 assert(sheet);
120 assert(row >= 0);
121 assert(row < sheet->num_rows);
122 return &sheet->rows[row];
123}
124
125// -----------------------------------------------------------------------------
73// Renderer state. 126// Renderer state.
74// ----------------------------------------------------------------------------- 127// -----------------------------------------------------------------------------
75 128
@@ -79,34 +132,45 @@ typedef struct TileData {
79 uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool. 132 uint16_t pixels_handle; // Handle to the tile's pixels in the pixel pool.
80} TileData; 133} TileData;
81 134
135// File format is already convenient for working in memory.
136typedef Ss_Row SpriteSheetRow;
137typedef Ss_SpriteSheet SpriteSheetData;
138
139typedef struct SpriteData {
140 SpriteSheet sheet; // Handle to the sprite's sheet.
141 ivec2 position;
142 int animation; // Current animation.
143 int frame; // Current frame of animation.
144} SpriteData;
145
82DEF_MEMPOOL_DYN(TilePool, TileData) 146DEF_MEMPOOL_DYN(TilePool, TileData)
83DEF_MEM_DYN(PixelPool, Pixel) 147DEF_MEM_DYN(PixelPool, Pixel)
84 148
149DEF_MEMPOOL_DYN(SpritePool, SpriteData)
150DEF_MEM_DYN(SpriteSheetPool, SpriteSheetData)
151
85typedef struct IsoGfx { 152typedef struct IsoGfx {
86 int screen_width; 153 int screen_width;
87 int screen_height; 154 int screen_height;
88 int tile_width; 155 int tile_width;
89 int tile_height; 156 int tile_height;
90 int world_width; 157 int world_width;
91 int world_height; 158 int world_height;
92 Tile* world; 159 int max_num_sprites;
93 Pixel* screen; 160 int sprite_sheet_pool_size_bytes;
94 TilePool tiles; 161 double last_animation_time;
95 PixelPool pixels; 162 Tile* world;
163 Pixel* screen;
164 TilePool tiles;
165 PixelPool pixels;
166 SpritePool sprites;
167 SpriteSheetPool sheets;
96} IsoGfx; 168} IsoGfx;
97 169
98// ----------------------------------------------------------------------------- 170// -----------------------------------------------------------------------------
99// Math and world / tile / screen access. 171// Math and world / tile / screen access.
100// ----------------------------------------------------------------------------- 172// -----------------------------------------------------------------------------
101 173
102typedef struct ivec2 {
103 int x, y;
104} ivec2;
105
106typedef struct vec2 {
107 double x, y;
108} vec2;
109
110static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { 174static inline ivec2 ivec2_add(ivec2 a, ivec2 b) {
111 return (ivec2){.x = a.x + b.x, .y = a.y + b.y}; 175 return (ivec2){.x = a.x + b.x, .y = a.y + b.y};
112} 176}
@@ -220,8 +284,15 @@ IsoGfx* isogfx_new(const IsoGfxDesc* desc) {
220 iso->screen_width = desc->screen_width; 284 iso->screen_width = desc->screen_width;
221 iso->screen_height = desc->screen_height; 285 iso->screen_height = desc->screen_height;
222 286
223 const int screen_size = desc->screen_width * desc->screen_height; 287 iso->last_animation_time = 0.0;
288
289 iso->max_num_sprites = desc->max_num_sprites == 0 ? DEFAULT_MAX_NUM_SPRITES
290 : desc->max_num_sprites;
291 iso->sprite_sheet_pool_size_bytes = desc->sprite_sheet_pool_size_bytes == 0
292 ? DEFAULT_SPRITE_SHEET_POOL_SIZE_BYTES
293 : desc->sprite_sheet_pool_size_bytes;
224 294
295 const int screen_size = desc->screen_width * desc->screen_height;
225 if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) { 296 if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) {
226 goto cleanup; 297 goto cleanup;
227 } 298 }
@@ -233,7 +304,7 @@ cleanup:
233 return 0; 304 return 0;
234} 305}
235 306
236/// Destroy the world and its tile set. 307/// Destroy the world, its tile set, and the underlying pools.
237static void destroy_world(IsoGfx* iso) { 308static void destroy_world(IsoGfx* iso) {
238 assert(iso); 309 assert(iso);
239 if (iso->world) { 310 if (iso->world) {
@@ -244,11 +315,19 @@ static void destroy_world(IsoGfx* iso) {
244 mem_del(&iso->pixels); 315 mem_del(&iso->pixels);
245} 316}
246 317
318/// Destroy all loaded sprites and the underlying pools.
319static void destroy_sprites(IsoGfx* iso) {
320 assert(iso);
321 mempool_del(&iso->sprites);
322 mem_del(&iso->sheets);
323}
324
247void isogfx_del(IsoGfx** pIso) { 325void isogfx_del(IsoGfx** pIso) {
248 assert(pIso); 326 assert(pIso);
249 IsoGfx* iso = *pIso; 327 IsoGfx* iso = *pIso;
250 if (iso) { 328 if (iso) {
251 destroy_world(iso); 329 destroy_world(iso);
330 destroy_sprites(iso);
252 if (iso->screen) { 331 if (iso->screen) {
253 free(iso->screen); 332 free(iso->screen);
254 iso->screen = 0; 333 iso->screen = 0;
@@ -341,7 +420,7 @@ bool isogfx_load_world(IsoGfx* iso, const char* filepath) {
341 // Tile set path is relative to the tile map file. Make it relative to the 420 // Tile set path is relative to the tile map file. Make it relative to the
342 // current working directory before loading. 421 // current working directory before loading.
343 char ts_path_cwd[PATH_MAX] = {0}; 422 char ts_path_cwd[PATH_MAX] = {0};
344 if (!make_relative_path(MAX_PATH_LENGTH, filepath, ts_path, ts_path_cwd)) { 423 if (!make_relative_path(filepath, ts_path, ts_path_cwd, PATH_MAX)) {
345 goto cleanup; 424 goto cleanup;
346 } 425 }
347 426
@@ -498,36 +577,199 @@ void isogfx_set_tiles(IsoGfx* iso, int x0, int y0, int x1, int y1, Tile tile) {
498 } 577 }
499} 578}
500 579
580bool isogfx_load_sprite_sheet(
581 IsoGfx* iso, const char* filepath, SpriteSheet* p_sheet) {
582 assert(iso);
583 assert(filepath);
584 assert(p_sheet);
585
586 bool success = false;
587
588 // Lazy initialization of sprite pools.
589 if (mempool_capacity(&iso->sprites) == 0) {
590 if (!mempool_make_dyn(
591 &iso->sprites, iso->max_num_sprites, sizeof(SpriteData))) {
592 return false;
593 }
594 }
595 if (mem_capacity(&iso->sheets) == 0) {
596 // Using a block size of 1 byte for sprite sheet data.
597 if (!mem_make_dyn(&iso->sheets, iso->sprite_sheet_pool_size_bytes, 1)) {
598 return false;
599 }
600 }
601
602 // Load sprite sheet file.
603 printf("Load sprite sheet: %s\n", filepath);
604 FILE* file = fopen(filepath, "rb");
605 if (file == NULL) {
606 goto cleanup;
607 }
608 const size_t sheet_size = get_file_size(file);
609 SpriteSheetData* ss_sheet = mem_alloc(&iso->sheets, sheet_size);
610 if (!ss_sheet) {
611 goto cleanup;
612 }
613 if (fread(ss_sheet, sheet_size, 1, file) != 1) {
614 goto cleanup;
615 }
616
617 *p_sheet = mem_get_chunk_handle(&iso->sheets, ss_sheet);
618 success = true;
619
620cleanup:
621 // Pools remain initialized since client may attempt to load other sprites.
622 if (file != NULL) {
623 fclose(file);
624 }
625 if (!success) {
626 if (ss_sheet) {
627 mem_free(&iso->sheets, &ss_sheet);
628 }
629 }
630 return success;
631}
632
633Sprite isogfx_make_sprite(IsoGfx* iso, SpriteSheet sheet) {
634 assert(iso);
635
636 SpriteData* sprite = mempool_alloc(&iso->sprites);
637 assert(sprite);
638
639 sprite->sheet = sheet;
640
641 return mempool_get_block_index(&iso->sprites, sprite);
642}
643
644#define with_sprite(SPRITE, BODY) \
645 { \
646 SpriteData* data = mempool_get_block(&iso->sprites, sprite); \
647 assert(data); \
648 BODY; \
649 }
650
651void isogfx_set_sprite_position(IsoGfx* iso, Sprite sprite, int x, int y) {
652 assert(iso);
653 with_sprite(sprite, {
654 data->position.x = x;
655 data->position.y = y;
656 });
657}
658
659void isogfx_set_sprite_animation(IsoGfx* iso, Sprite sprite, int animation) {
660 assert(iso);
661 with_sprite(sprite, { data->animation = animation; });
662}
663
664void isogfx_update(IsoGfx* iso, double t) {
665 assert(iso);
666
667 // If this is the first time update() is called after initialization, just
668 // record the starting animation time.
669 if (iso->last_animation_time == 0.0) {
670 iso->last_animation_time = t;
671 return;
672 }
673
674 if ((t - iso->last_animation_time) >= ANIMATION_UPDATE_DELTA) {
675 // TODO: Consider linking animated sprites in a list so that we only walk
676 // over those here and not also the static sprites.
677 mempool_foreach(&iso->sprites, sprite, {
678 const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet);
679 assert(sheet); // TODO: Make this a hard assert inside the mem/pool.
680 const SpriteSheetRow* row =
681 get_sprite_sheet_row(sheet, sprite->animation);
682 sprite->frame = (sprite->frame + 1) % row->num_cols;
683 });
684
685 iso->last_animation_time = t;
686 }
687}
688
501// ----------------------------------------------------------------------------- 689// -----------------------------------------------------------------------------
502// Rendering and picking. 690// Rendering and picking.
503// ----------------------------------------------------------------------------- 691// -----------------------------------------------------------------------------
504 692
505static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) { 693typedef struct CoordSystem {
694 ivec2 o; /// Origin.
695 ivec2 x;
696 ivec2 y;
697} CoordSystem;
698
699/// Create the basis for the isometric coordinate system with origin and vectors
700/// expressed in the Cartesian system.
701static CoordSystem make_iso_coord_system(const IsoGfx* iso) {
506 assert(iso); 702 assert(iso);
703 // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0};
704 const ivec2 o = {
705 (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height};
706 const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
707 const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
708 return (CoordSystem){o, x, y};
709}
507 710
508 const TileData* tile_data = mempool_get_block(&iso->tiles, tile); 711static Pixel alpha_blend(Pixel src, Pixel dst) {
509 assert(tile_data); 712 if ((src.a == 255) || (dst.a == 0)) {
713 return src;
714 }
715 const uint16_t one_minus_alpha = 255 - src.a;
716#define blend(s, d) \
717 (Channel)( \
718 (double)((uint16_t)s * (uint16_t)src.a + \
719 (uint16_t)d * one_minus_alpha) / \
720 255.0)
721 return (Pixel){
722 .r = blend(src.r, dst.r),
723 .g = blend(src.g, dst.g),
724 .b = blend(src.b, dst.b),
725 .a = src.a};
726}
727
728/// Draw a rectangle (tile or sprite).
729///
730/// The rectangle's bottom-left corner is mapped to the given origin. The
731/// rectangle then extends to the right and top of the origin.
732///
733/// The rectangle's pixels are assumed to be arranged in a linear, row-major
734/// fashion.
735static void draw_rect(
736 IsoGfx* iso, ivec2 origin, int rect_width, int rect_height,
737 const Pixel* pixels) {
738 assert(iso);
510 739
511 // Tile can exceed screen bounds, so we must clip it. 740 // Rect can exceed screen bounds, so we must clip it.
512#define max(a, b) (a > b ? a : b) 741#define max(a, b) (a > b ? a : b)
513 const int py_offset = max(0, (int)tile_data->height - origin.y); 742 const int py_offset = max(0, rect_height - origin.y);
514 origin.y = max(0, origin.y - (int)tile_data->height); 743 origin.y = max(0, origin.y - rect_height);
515 744
516 // Clip along Y and X as we draw. 745 // Clip along Y and X as we draw.
517 for (int py = py_offset; 746 for (int py = py_offset;
518 (py < tile_data->height) && (origin.y + py < iso->screen_height); ++py) { 747 (py < rect_height) && (origin.y + py < iso->screen_height); ++py) {
519 const int sy = origin.y + py - py_offset; 748 const int sy = origin.y + py - py_offset;
520 for (int px = 0; 749 for (int px = 0; (px < rect_width) && (origin.x + px < iso->screen_width);
521 (px < tile_data->width) && (origin.x + px < iso->screen_width); ++px) { 750 ++px) {
522 const Pixel colour = tile_xy(iso, tile_data, px, py); 751 const Pixel colour = pixels[py * rect_width + px];
523 if (colour.a > 0) { 752 if (colour.a > 0) {
524 const int sx = origin.x + px; 753 const int sx = origin.x + px;
525 *screen_xy_mut(iso, sx, sy) = colour; 754 const Pixel dst = screen_xy(iso, sx, sy);
755 const Pixel final = alpha_blend(colour, dst);
756 *screen_xy_mut(iso, sx, sy) = final;
526 } 757 }
527 } 758 }
528 } 759 }
529} 760}
530 761
762static void draw_tile(IsoGfx* iso, ivec2 origin, Tile tile) {
763 assert(iso);
764
765 const TileData* tile_data = mempool_get_block(&iso->tiles, tile);
766 assert(tile_data);
767
768 const Pixel* pixels = tile_xy_const_ref(iso, tile_data, 0, 0);
769
770 draw_rect(iso, origin, tile_data->width, tile_data->height, pixels);
771}
772
531static void draw(IsoGfx* iso) { 773static void draw(IsoGfx* iso) {
532 assert(iso); 774 assert(iso);
533 775
@@ -536,11 +778,7 @@ static void draw(IsoGfx* iso) {
536 778
537 memset(iso->screen, 0, W * H * sizeof(Pixel)); 779 memset(iso->screen, 0, W * H * sizeof(Pixel));
538 780
539 // const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; 781 const CoordSystem iso_space = make_iso_coord_system(iso);
540 const ivec2 o = {
541 (iso->screen_width / 2) - (iso->tile_width / 2), iso->tile_height};
542 const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2};
543 const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2};
544 782
545 // TODO: Culling. 783 // TODO: Culling.
546 // Ex: map the screen corners to tile space to cull. 784 // Ex: map the screen corners to tile space to cull.
@@ -550,16 +788,58 @@ static void draw(IsoGfx* iso) {
550 for (int ty = 0; ty < iso->world_height; ++ty) { 788 for (int ty = 0; ty < iso->world_height; ++ty) {
551 for (int tx = 0; tx < iso->world_width; ++tx) { 789 for (int tx = 0; tx < iso->world_width; ++tx) {
552 const Tile tile = world_xy(iso, tx, ty); 790 const Tile tile = world_xy(iso, tx, ty);
553 const ivec2 so = 791 const ivec2 so = ivec2_add(
554 ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty))); 792 iso_space.o,
793 ivec2_add(
794 ivec2_scale(iso_space.x, tx), ivec2_scale(iso_space.y, ty)));
555 draw_tile(iso, so, tile); 795 draw_tile(iso, so, tile);
556 } 796 }
557 } 797 }
558} 798}
559 799
800static void draw_sprite(
801 IsoGfx* iso, ivec2 origin, const SpriteData* sprite,
802 const SpriteSheetData* sheet) {
803 assert(iso);
804 assert(sprite);
805 assert(sheet);
806 assert(sprite->animation >= 0);
807 assert(sprite->animation < sheet->num_rows);
808 assert(sprite->frame >= 0);
809
810 const SpriteSheetRow* ss_row = &sheet->rows[sprite->animation];
811 assert(sprite->frame < ss_row->num_cols);
812
813 const int sprite_offset =
814 sprite->frame * sheet->sprite_width * sheet->sprite_height;
815
816 const Pixel* frame = &ss_row->pixels[sprite_offset];
817
818 draw_rect(iso, origin, sheet->sprite_width, sheet->sprite_height, frame);
819}
820
821static void draw_sprites(IsoGfx* iso) {
822 assert(iso);
823
824 const CoordSystem iso_space = make_iso_coord_system(iso);
825
826 mempool_foreach(&iso->sprites, sprite, {
827 const SpriteSheetData* sheet = mem_get_chunk(&iso->sheets, sprite->sheet);
828 assert(sheet);
829
830 const ivec2 so = ivec2_add(
831 iso_space.o, ivec2_add(
832 ivec2_scale(iso_space.x, sprite->position.x),
833 ivec2_scale(iso_space.y, sprite->position.y)));
834
835 draw_sprite(iso, so, sprite, sheet);
836 });
837}
838
560void isogfx_render(IsoGfx* iso) { 839void isogfx_render(IsoGfx* iso) {
561 assert(iso); 840 assert(iso);
562 draw(iso); 841 draw(iso);
842 draw_sprites(iso);
563} 843}
564 844
565void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { 845void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) {