summaryrefslogtreecommitdiff
path: root/src/gfx2d.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/gfx2d.c')
-rw-r--r--src/gfx2d.c826
1 files changed, 826 insertions, 0 deletions
diff --git a/src/gfx2d.c b/src/gfx2d.c
new file mode 100644
index 0000000..f609c98
--- /dev/null
+++ b/src/gfx2d.c
@@ -0,0 +1,826 @@
1#include <isogfx/gfx2d.h>
2
3#include <isogfx/asset.h>
4
5#include <filesystem.h>
6#include <memstack.h>
7#include <path.h>
8
9#include <assert.h>
10#include <stdint.h>
11#include <stdio.h>
12#include <string.h>
13
14/// Maximum path length.
15#define MAX_PATH 256
16
17/// Default animation speed.
18#define ANIMATION_FPS 10
19
20/// Time between animation updates.
21#define ANIMATION_UPDATE_DELTA (1.0 / ANIMATION_FPS)
22
23typedef struct ivec2 {
24 int x, y;
25} ivec2;
26
27typedef struct vec2 {
28 double x, y;
29} vec2;
30
31// -----------------------------------------------------------------------------
32// Renderer state.
33// -----------------------------------------------------------------------------
34
35typedef struct IsoCoordSystem {
36 ivec2 o; // Origin.
37 ivec2 x;
38 ivec2 y;
39} IsoCoordSystem;
40
41typedef struct Screen {
42 int width;
43 int height;
44 Pixel* pixels;
45} Screen;
46
47typedef struct SpriteInstance {
48 struct SpriteInstance* next;
49 const Ss_SpriteSheet* sheet;
50 ivec2 position;
51 int animation; // Current animation.
52 int frame; // Current frame of animation.
53} SpriteInstance;
54
55typedef struct Gfx2d {
56 Screen screen;
57 IsoCoordSystem iso_space;
58 ivec2 camera;
59 double last_animation_time;
60 Tile next_tile; // For procedurally-generated tiles.
61 Tm_Map* map;
62 Ts_TileSet* tileset;
63 SpriteInstance* head_sprite; // Head of sprites list.
64 memstack stack;
65 size_t watermark;
66} Gfx2d;
67
68// -----------------------------------------------------------------------------
69// Math and world / tile / screen access.
70// -----------------------------------------------------------------------------
71
72static inline ivec2 ivec2_add(ivec2 a, ivec2 b) {
73 return (ivec2){.x = a.x + b.x, .y = a.y + b.y};
74}
75
76static inline ivec2 ivec2_scale(ivec2 a, int s) {
77 return (ivec2){.x = a.x * s, .y = a.y * s};
78}
79
80static inline ivec2 ivec2_neg(ivec2 a) { return (ivec2){.x = -a.x, .y = -a.y}; }
81
82static inline vec2 vec2_add(vec2 a, vec2 b) {
83 return (vec2){.x = a.x + b.x, .y = a.y + b.y};
84}
85
86static inline vec2 ivec2_to_vec2(ivec2 a) { return (vec2){a.x, a.y}; }
87
88/// Map map coordinates to screen coordinates, both Cartesian.
89static ivec2 map2screen(
90 ivec2 camera, int tile_width, int tile_height, int map_x, int map_y) {
91 return ivec2_add(
92 ivec2_neg(camera),
93 (ivec2){.x = map_x * tile_width, .y = map_y * tile_height});
94}
95
96// Not actually used because we pre-compute the two axis vectors instead.
97// See make_iso_coord_system() and the other definition of iso2cart() below.
98// static inline ivec2 iso2cart(ivec2 iso, int s, int t, int w) {
99// return (ivec2){.x = (iso.x - iso.y) * (s / 2) + (w / 2),
100// .y = (iso.x + iso.y) * (t / 2)};
101// }
102
103/// Create the basis for the isometric coordinate system with origin and vectors
104/// expressed in the Cartesian system.
105static IsoCoordSystem make_iso_coord_system(
106 const Tm_Map* const map, const Screen* const screen) {
107 assert(map);
108 assert(screen);
109 const ivec2 o = {screen->width / 2, 0};
110 const ivec2 x = {
111 .x = map->base_tile_width / 2, .y = map->base_tile_height / 2};
112 const ivec2 y = {
113 .x = -map->base_tile_width / 2, .y = map->base_tile_height / 2};
114 return (IsoCoordSystem){o, x, y};
115}
116
117/// Map isometric coordinates to Cartesian coordinates.
118///
119/// For a tile, this gets the screen position of the top diamond-corner of the
120/// tile.
121///
122/// Takes the camera displacement into account.
123static ivec2 iso2cart(
124 const IsoCoordSystem iso_space, ivec2 camera, int iso_x, int iso_y) {
125 const ivec2 vx_offset = ivec2_scale(iso_space.x, iso_x);
126 const ivec2 vy_offset = ivec2_scale(iso_space.y, iso_y);
127 const ivec2 origin_world_space =
128 ivec2_add(iso_space.o, ivec2_add(vx_offset, vy_offset));
129 const ivec2 origin_view_space =
130 ivec2_add(origin_world_space, ivec2_neg(camera));
131 return origin_view_space;
132}
133
134// Method 1.
135// static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
136// const double x = cart.x - (double)(w / 2);
137// const double xiso = (x * t + cart.y * s) / (double)(s * t);
138// return (vec2){
139// .x = (int)(xiso), .y = (int)((2.0 / (double)t) * cart.y - xiso)};
140//}
141
142// Method 2.
143static inline vec2 cart2iso(vec2 cart, int s, int t, int w) {
144 const double one_over_s = 1. / (double)s;
145 const double one_over_t = 1. / (double)t;
146 const double x = cart.x - (double)(w / 2);
147 return (vec2){.x = (one_over_s * x + one_over_t * cart.y),
148 .y = (-one_over_s * x + one_over_t * cart.y)};
149}
150
151static inline const Pixel* screen_xy_const_ref(
152 const Screen* screen, int x, int y) {
153 assert(screen);
154 assert(x >= 0);
155 assert(y >= 0);
156 assert(x < screen->width);
157 assert(y < screen->height);
158 return &screen->pixels[y * screen->width + x];
159}
160
161static inline Pixel screen_xy(Screen* screen, int x, int y) {
162 return *screen_xy_const_ref(screen, x, y);
163}
164
165static inline Pixel* screen_xy_mut(Screen* screen, int x, int y) {
166 return (Pixel*)screen_xy_const_ref(screen, x, y);
167}
168
169// -----------------------------------------------------------------------------
170// Renderer, world and tile management.
171// -----------------------------------------------------------------------------
172
173Gfx2d* gfx2d_new(const Gfx2dDesc* desc) {
174 assert(desc->screen_width > 0);
175 assert(desc->screen_height > 0);
176 // Part of our implementation assumes even widths and heights for precision.
177 assert((desc->screen_width & 1) == 0);
178 assert((desc->screen_height & 1) == 0);
179
180 Gfx2d tmp = {0};
181 if (!memstack_make(&tmp.stack, desc->memory_size, desc->memory)) {
182 goto cleanup;
183 }
184 Gfx2d* gfx =
185 memstack_alloc_aligned(&tmp.stack, sizeof(Gfx2d), alignof(Gfx2d));
186 *gfx = tmp;
187
188 const size_t screen_size_bytes =
189 desc->screen_width * desc->screen_height * sizeof(Pixel);
190 Pixel* screen =
191 memstack_alloc_aligned(&gfx->stack, screen_size_bytes, alignof(Pixel));
192
193 gfx->screen = (Screen){.width = desc->screen_width,
194 .height = desc->screen_height,
195 .pixels = screen};
196
197 gfx->last_animation_time = 0.0;
198 gfx->watermark = memstack_watermark(&gfx->stack);
199
200 return gfx;
201
202cleanup:
203 gfx2d_del(&gfx);
204 return nullptr;
205}
206
207void gfx2d_clear(Gfx2d* gfx) {
208 assert(gfx);
209 gfx->last_animation_time = 0.0;
210 gfx->next_tile = 0;
211 gfx->map = nullptr;
212 gfx->tileset = nullptr;
213 gfx->head_sprite = nullptr;
214 // The base of the stack contains the Gfx2d and the screen buffer. Make sure
215 // we don't clear them.
216 memstack_set_watermark(&gfx->stack, gfx->watermark);
217}
218
219void gfx2d_del(Gfx2d** ppGfx) {
220 assert(ppGfx);
221 Gfx2d* gfx = *ppGfx;
222 if (gfx) {
223 memstack_del(&gfx->stack);
224 *ppGfx = nullptr;
225 }
226}
227
228void gfx2d_make_map(Gfx2d* gfx, const MapDesc* desc) {
229 assert(gfx);
230 assert(desc);
231 assert(desc->tile_width > 0);
232 assert(desc->tile_height > 0);
233 // Part of our implementation assumes even widths and heights for greater
234 // precision.
235 assert((desc->tile_width & 1) == 0);
236 assert((desc->tile_height & 1) == 0);
237 // World must be non-empty.
238 assert(desc->world_width > 0);
239 assert(desc->world_height > 0);
240 // Must have >0 tiles.
241 assert(desc->num_tiles > 0);
242
243 // Handle recreation by destroying the previous world and sprites.
244 gfx2d_clear(gfx);
245
246 const int world_size = desc->world_width * desc->world_height;
247 const size_t map_size_bytes = sizeof(Tm_Map) + (world_size * sizeof(Tile));
248
249 // This implies that all tiles are of the base tile dimensions.
250 // We could enhance the API to allow for supertiles as well. Take in max tile
251 // width and height and allocate enough space using those values.
252 const size_t tile_size = desc->tile_width * desc->tile_height;
253 const size_t tile_size_bytes = tile_size * sizeof(Pixel);
254 const size_t tile_data_size_bytes = desc->num_tiles * tile_size_bytes;
255 const size_t tileset_size_bytes = sizeof(Ts_TileSet) +
256 (desc->num_tiles * sizeof(Ts_Tile)) +
257 tile_data_size_bytes;
258
259 gfx->map = memstack_alloc_aligned(&gfx->stack, map_size_bytes, 4);
260 *gfx->map = (Tm_Map){
261 .world_width = desc->world_width,
262 .world_height = desc->world_height,
263 .base_tile_width = desc->tile_width,
264 .base_tile_height = desc->tile_height,
265 .num_layers = 1,
266 .flags =
267 (desc->orientation == MapOrthogonal) ? Tm_Orthogonal : Tm_Isometric,
268 };
269
270 gfx->tileset = memstack_alloc_aligned(&gfx->stack, tileset_size_bytes, 4);
271 *gfx->tileset = (Ts_TileSet){
272 .num_tiles = desc->num_tiles,
273 };
274
275 gfx->iso_space = make_iso_coord_system(gfx->map, &gfx->screen);
276}
277
278bool gfx2d_load_map(Gfx2d* gfx, const char* filepath) {
279 assert(gfx);
280 assert(filepath);
281
282 bool success = false;
283
284 // Handle recreation by destroying the previous world and sprites.
285 gfx2d_clear(gfx);
286
287 // Load the map.
288 printf("Load tile map: %s\n", filepath);
289 WITH_FILE(filepath, {
290 const size_t map_size = get_file_size_f(file);
291 gfx->map = memstack_alloc_aligned(&gfx->stack, map_size, 4);
292 success = read_file_f(file, gfx->map);
293 });
294 if (!success) {
295 goto cleanup;
296 }
297 Tm_Map* const map = gfx->map;
298
299 printf("Map orientation: %d\n", ((Tm_Flags*)&map->flags)->orientation);
300
301 // Load the tile set.
302 //
303 // Tile set path is relative to the tile map file. Make it relative to the
304 // current working directory before loading.
305 const char* ts_path = map->tileset_path;
306 char ts_path_cwd[MAX_PATH] = {0};
307 if (!path_make_relative(filepath, ts_path, ts_path_cwd, MAX_PATH)) {
308 goto cleanup;
309 }
310 printf("Load tile set: %s\n", ts_path_cwd);
311 WITH_FILE(ts_path_cwd, {
312 const size_t file_size = get_file_size_f(file);
313 gfx->tileset = memstack_alloc_aligned(&gfx->stack, file_size, 4);
314 success = read_file_f(file, gfx->tileset);
315 });
316 if (!success) {
317 // TODO: Log errors using the log library.
318 goto cleanup;
319 }
320 const Ts_TileSet* const tileset = gfx->tileset;
321 printf("Loaded tile set (%u tiles): %s\n", tileset->num_tiles, ts_path_cwd);
322
323 // TODO: These assertions on input data should be library runtime errors.
324 assert(ts_validate_tileset(tileset));
325 assert(tm_validate_map(map, tileset));
326
327 gfx->iso_space = make_iso_coord_system(gfx->map, &gfx->screen);
328
329 success = true;
330
331cleanup:
332 if (!success) {
333 gfx2d_clear(gfx);
334 }
335 return success;
336}
337
338int gfx2d_world_width(const Gfx2d* gfx) {
339 assert(gfx);
340 return gfx->map->world_width;
341}
342
343int gfx2d_world_height(const Gfx2d* gfx) {
344 assert(gfx);
345 return gfx->map->world_height;
346}
347
348static void make_tile_from_colour(
349 Pixel colour, const Ts_Tile* tile, Pixel* tile_pixels) {
350 assert(tile);
351 assert(tile_pixels);
352
353 const int width = tile->width;
354 const int height = tile->height;
355 const int r = width / height;
356 for (int y = 0; y < height / 2; ++y) {
357 const int mask_start = width / 2 - r * y - 1;
358 const int mask_end = width / 2 + r * y + 1;
359 for (int x = 0; x < width; ++x) {
360 const bool mask = (mask_start <= x) && (x <= mask_end);
361 const Pixel val = mask ? colour : (Pixel){.r = 0, .g = 0, .b = 0, .a = 0};
362
363 // Top half.
364 *ts_tile_xy_mut(tile_pixels, tile, x, y) = val;
365
366 // Bottom half reflects the top half.
367 const int y_reflected = height - y - 1;
368 *ts_tile_xy_mut(tile_pixels, tile, x, y_reflected) = val;
369 }
370 }
371}
372
373Tile gfx2d_make_tile(Gfx2d* gfx, const TileDesc* desc) {
374 assert(gfx);
375 assert(desc);
376 // Client must create a world first.
377 assert(gfx->map);
378 assert(gfx->tileset);
379 // Currently, procedural tiles must match the base tile size.
380 assert(desc->width == gfx->map->base_tile_width);
381 assert(desc->height == gfx->map->base_tile_height);
382 // Cannot exceed max tiles.
383 assert(gfx->next_tile < gfx->tileset->num_tiles);
384
385 const Tile tile = gfx->next_tile++;
386
387 const size_t tile_size_bytes = desc->width * desc->height * sizeof(Pixel);
388
389 switch (desc->type) {
390 case TileFromColour: {
391 assert(desc->width > 0);
392 assert(desc->height > 0);
393
394 Ts_Tile* const ts_tile = ts_tileset_get_tile_mut(gfx->tileset, tile);
395
396 *ts_tile = (Ts_Tile){
397 .width = gfx->map->base_tile_width,
398 .height = gfx->map->base_tile_height,
399 .pixels = tile * tile_size_bytes,
400 };
401
402 Pixel* const tile_pixels =
403 ts_tileset_get_tile_pixels_mut(gfx->tileset, tile);
404 make_tile_from_colour(desc->colour, ts_tile, tile_pixels);
405 break;
406 }
407 case TileFromFile:
408 assert(false); // TODO
409 break;
410 case TileFromMemory: {
411 assert(desc->width > 0);
412 assert(desc->height > 0);
413 assert(false); // TODO
414 break;
415 }
416 }
417
418 return tile;
419}
420
421void gfx2d_set_tile(Gfx2d* gfx, int x, int y, Tile tile) {
422 assert(gfx);
423
424 Tm_Layer* const layer = tm_map_get_layer_mut(gfx->map, 0);
425 Tile* map_tile = tm_layer_get_tile_mut(gfx->map, layer, x, y);
426
427 *map_tile = tile;
428}
429
430void gfx2d_set_tiles(Gfx2d* gfx, int x0, int y0, int x1, int y1, Tile tile) {
431 assert(gfx);
432 for (int y = y0; y < y1; ++y) {
433 for (int x = x0; x < x1; ++x) {
434 gfx2d_set_tile(gfx, x, y, tile);
435 }
436 }
437}
438
439SpriteSheet gfx2d_load_sprite_sheet(Gfx2d* gfx, const char* filepath) {
440 assert(gfx);
441 assert(filepath);
442
443 bool success = false;
444 SpriteSheet spriteSheet = 0;
445 const size_t watermark = memstack_watermark(&gfx->stack);
446
447 // Load sprite sheet file.
448 printf("Load sprite sheet: %s\n", filepath);
449 Ss_SpriteSheet* ss_sheet = nullptr;
450 WITH_FILE(filepath, {
451 const size_t file_size = get_file_size_f(file);
452 ss_sheet =
453 memstack_alloc_aligned(&gfx->stack, file_size, alignof(Ss_SpriteSheet));
454 success = read_file_f(file, ss_sheet);
455 });
456 if (!success) {
457 goto cleanup;
458 }
459 assert(ss_sheet);
460
461 spriteSheet = (SpriteSheet)ss_sheet;
462
463cleanup:
464 if (!success) {
465 if (ss_sheet) {
466 memstack_set_watermark(&gfx->stack, watermark);
467 }
468 }
469 return spriteSheet;
470}
471
472Sprite gfx2d_make_sprite(Gfx2d* gfx, SpriteSheet sheet) {
473 assert(gfx);
474 assert(sheet);
475
476 // TODO: Remove memstack_alloc() and replace it with a same-name macro that
477 // calls memstack_alloc_aligned() with sizeof/alignof. No real point in
478 // having unaligned allocations.
479 SpriteInstance* sprite = memstack_alloc_aligned(
480 &gfx->stack, sizeof(SpriteInstance), alignof(SpriteInstance));
481
482 sprite->sheet = (const Ss_SpriteSheet*)sheet;
483 sprite->next = gfx->head_sprite;
484 gfx->head_sprite = sprite;
485
486 return (Sprite)sprite;
487}
488
489void gfx2d_set_sprite_position(Gfx2d* gfx, Sprite hSprite, int x, int y) {
490 assert(gfx);
491 SpriteInstance* sprite = (SpriteInstance*)hSprite;
492 sprite->position.x = x;
493 sprite->position.y = y;
494}
495
496void gfx2d_set_sprite_animation(Gfx2d* gfx, Sprite hSprite, int animation) {
497 assert(gfx);
498 SpriteInstance* sprite = (SpriteInstance*)hSprite;
499 sprite->animation = animation;
500}
501
502void gfx2d_update(Gfx2d* gfx, double t) {
503 assert(gfx);
504
505 // If this is the first time update() is called after initialization, just
506 // record the starting animation time.
507 if (gfx->last_animation_time == 0.0) {
508 gfx->last_animation_time = t;
509 return;
510 }
511
512 if ((t - gfx->last_animation_time) >= ANIMATION_UPDATE_DELTA) {
513 // TODO: Consider linking animated sprites in a separate list so that we
514 // only walk over those here and not also the static sprites.
515 for (SpriteInstance* sprite = gfx->head_sprite; sprite;
516 sprite = sprite->next) {
517 const Ss_SpriteSheet* sheet = sprite->sheet;
518 const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation);
519 sprite->frame = (sprite->frame + 1) % row->num_cols;
520 }
521
522 gfx->last_animation_time = t;
523 }
524}
525
526// -----------------------------------------------------------------------------
527// Rendering and picking.
528// -----------------------------------------------------------------------------
529
530static Pixel alpha_blend(Pixel src, Pixel dst) {
531 if ((src.a == 255) || (dst.a == 0)) {
532 return src;
533 }
534 const uint16_t one_minus_alpha = 255 - src.a;
535#define blend(s, d) \
536 (Channel)( \
537 (double)((uint16_t)s * (uint16_t)src.a + \
538 (uint16_t)d * one_minus_alpha) / \
539 255.0)
540 return (Pixel){.r = blend(src.r, dst.r),
541 .g = blend(src.g, dst.g),
542 .b = blend(src.b, dst.b),
543 .a = src.a};
544}
545
546/// Draw a rectangle (tile or sprite).
547///
548/// The rectangle's top-left corner is mapped to the screen space position given
549/// by 'top_left'.
550///
551/// The rectangle's pixels are assumed to be arranged in a linear, row-major
552/// fashion.
553///
554/// If indices are given, then the image is assumed to be colour-paletted, where
555/// 'pixels' is the palette and 'indices' the pixel indices. Otherwise, the
556/// image is assumed to be in plain RGBA format.
557static void draw_rect(
558 Screen* screen, ivec2 top_left, int rect_width, int rect_height,
559 const Pixel* pixels, const uint8_t* indices) {
560 assert(screen);
561 assert(pixels);
562
563#define rect_pixel(X, Y) \
564 (indices ? pixels[indices[Y * rect_width + X]] : pixels[Y * rect_width + X])
565
566 // Rect origin can be outside screen bounds, so we must offset accordingly to
567 // draw only the visible portion.
568#define max(a, b) (a > b ? a : b)
569 const int px_offset = max(0, -top_left.x);
570 const int py_offset = max(0, -top_left.y);
571
572 // Rect can exceed screen bounds, so clip along Y and X as we draw.
573 for (int py = py_offset;
574 (py < rect_height) && (top_left.y + py < screen->height); ++py) {
575 const int sy = top_left.y + py;
576 for (int px = px_offset;
577 (px < rect_width) && (top_left.x + px < screen->width); ++px) {
578 const Pixel colour = rect_pixel(px, py);
579 if (colour.a > 0) {
580 const int sx = top_left.x + px;
581 const Pixel dst = screen_xy(screen, sx, sy);
582 const Pixel final = alpha_blend(colour, dst);
583 *screen_xy_mut(screen, sx, sy) = final;
584 }
585 }
586 }
587}
588
589/// Draw a tile in an orthogonal map.
590static void draw_tile_ortho(Gfx2d* gfx, Tile tile, int x, int y) {
591 assert(gfx);
592 assert(gfx->tileset);
593 assert(x >= 0);
594 assert(y >= 0);
595 assert(x < gfx->map->world_width);
596 assert(y < gfx->map->world_height);
597
598 const Ts_Tile* pTile = ts_tileset_get_tile(gfx->tileset, tile);
599 const Pixel* pixels = ts_tileset_get_tile_pixels(gfx->tileset, tile);
600
601 const ivec2 screen_origin = map2screen(
602 gfx->camera, gfx->map->base_tile_width, gfx->map->base_tile_height, x, y);
603
604 draw_rect(
605 &gfx->screen, screen_origin, pTile->width, pTile->height, pixels,
606 nullptr);
607}
608
609/// Draw a tile in an isometric map.
610static void draw_tile_iso(Gfx2d* gfx, Tile tile, int iso_x, int iso_y) {
611 assert(gfx);
612 assert(gfx->tileset);
613 assert(iso_x >= 0);
614 assert(iso_y >= 0);
615 assert(iso_x < gfx->map->world_width);
616 assert(iso_y < gfx->map->world_height);
617
618 const Ts_Tile* pTile = ts_tileset_get_tile(gfx->tileset, tile);
619 const Pixel* pixels = ts_tileset_get_tile_pixels(gfx->tileset, tile);
620
621 // Compute the screen coordinates of the top diamond-corner of the tile (the
622 // base tile for super tiles).
623 // World (0, 0) -> (screen_width / 2, 0).
624 const ivec2 screen_origin =
625 iso2cart(gfx->iso_space, gfx->camera, iso_x, iso_y);
626
627 // Move from the top diamond-corner to the top-left corner of the tile image.
628 // For regular tiles, tile height == base tile height, so the y offset is 0.
629 // For super tiles, move as high up as the height of the tile.
630 const ivec2 offset = {
631 -(gfx->map->base_tile_width / 2),
632 pTile->height - gfx->map->base_tile_height};
633 const ivec2 top_left = ivec2_add(screen_origin, offset);
634
635 draw_rect(
636 &gfx->screen, top_left, pTile->width, pTile->height, pixels, nullptr);
637}
638
639static void draw_map_ortho(Gfx2d* gfx) {
640 assert(gfx);
641 assert(gfx->map);
642
643 // TODO: Same TODOs as in draw_map_iso().
644
645 const Tm_Layer* layer = tm_map_get_layer(gfx->map, 0);
646
647 for (int wy = 0; wy < gfx->map->world_height; ++wy) {
648 for (int wx = 0; wx < gfx->map->world_width; ++wx) {
649 const Tile tile = tm_layer_get_tile(gfx->map, layer, wx, wy);
650 draw_tile_ortho(gfx, tile, wx, wy);
651 }
652 }
653}
654
655static void draw_map_iso(Gfx2d* gfx) {
656 assert(gfx);
657 assert(gfx->map);
658
659 // TODO: Support for multiple layers.
660 const Tm_Layer* layer = tm_map_get_layer(gfx->map, 0);
661
662 // TODO: Culling.
663 // Ex: map the screen corners to tile space to cull.
664 // Ex: walk in screen space and fetch the tile.
665 // The tile-centric approach might be more cache-friendly since the
666 // screen-centric approach would juggle multiple tiles throughout the scan.
667 for (int wy = 0; wy < gfx->map->world_height; ++wy) {
668 for (int wx = 0; wx < gfx->map->world_width; ++wx) {
669 const Tile tile = tm_layer_get_tile(gfx->map, layer, wx, wy);
670 draw_tile_iso(gfx, tile, wx, wy);
671 }
672 }
673}
674
675static void draw_map(Gfx2d* gfx) {
676 assert(gfx);
677 assert(gfx->map);
678 assert(gfx->screen.pixels);
679
680 const int W = gfx->screen.width;
681 const int H = gfx->screen.height;
682
683 memset(gfx->screen.pixels, 0, W * H * sizeof(Pixel));
684
685 const Tm_Flags* flags = (const Tm_Flags*)&gfx->map->flags;
686 switch (flags->orientation) {
687 case Tm_Orthogonal:
688 draw_map_ortho(gfx);
689 break;
690 case Tm_Isometric:
691 draw_map_iso(gfx);
692 break;
693 }
694}
695
696/// Draw a sprite in an orthogonal/Cartesian coordinate system.
697static void draw_sprite_ortho(
698 Gfx2d* gfx, const SpriteInstance* sprite, const Ss_SpriteSheet* sheet) {
699 assert(gfx);
700 assert(sprite);
701 assert(sheet);
702 assert(sprite->animation >= 0);
703 assert(sprite->animation < sheet->num_rows);
704 assert(sprite->frame >= 0);
705
706 // Apply an offset similarly to how we offset tiles. The sprite is offset by
707 // -base_tile_width/2 along the x-axis to align the sprite with the leftmost
708 // edge of the tile it is on.
709 const ivec2 screen_origin = map2screen(
710 gfx->camera, gfx->map->base_tile_width, gfx->map->base_tile_height,
711 sprite->position.x, sprite->position.y);
712
713 const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation);
714 const uint8_t* frame = ss_get_sprite_sheet_sprite(sheet, row, sprite->frame);
715 draw_rect(
716 &gfx->screen, screen_origin, sheet->sprite_width, sheet->sprite_height,
717 sheet->palette.colours, frame);
718}
719
720/// Draw a sprite in an isometric coordinate system.
721static void draw_sprite_iso(
722 Gfx2d* gfx, const SpriteInstance* sprite, const Ss_SpriteSheet* sheet) {
723 assert(gfx);
724 assert(sprite);
725 assert(sheet);
726 assert(sprite->animation >= 0);
727 assert(sprite->animation < sheet->num_rows);
728 assert(sprite->frame >= 0);
729
730 // Apply an offset similarly to how we offset tiles. The sprite is offset by
731 // -base_tile_width/2 along the x-axis to align the sprite with the leftmost
732 // edge of the tile it is on.
733 const ivec2 screen_origin = iso2cart(
734 gfx->iso_space, gfx->camera, sprite->position.x, sprite->position.y);
735 const ivec2 offset = {-(gfx->map->base_tile_width / 2), 0};
736 const ivec2 top_left = ivec2_add(screen_origin, offset);
737
738 const Ss_Row* row = ss_get_sprite_sheet_row(sheet, sprite->animation);
739 const uint8_t* frame = ss_get_sprite_sheet_sprite(sheet, row, sprite->frame);
740 draw_rect(
741 &gfx->screen, top_left, sheet->sprite_width, sheet->sprite_height,
742 sheet->palette.colours, frame);
743}
744
745static void draw_sprites(Gfx2d* gfx) {
746 assert(gfx);
747 assert(gfx->map);
748
749 const Tm_Flags* flags = (const Tm_Flags*)&gfx->map->flags;
750 switch (flags->orientation) {
751 case Tm_Orthogonal:
752 for (const SpriteInstance* sprite = gfx->head_sprite; sprite;
753 sprite = sprite->next) {
754 draw_sprite_ortho(gfx, sprite, sprite->sheet);
755 }
756 break;
757 case Tm_Isometric:
758 for (const SpriteInstance* sprite = gfx->head_sprite; sprite;
759 sprite = sprite->next) {
760 draw_sprite_iso(gfx, sprite, sprite->sheet);
761 }
762 break;
763 }
764}
765
766void gfx2d_set_camera(Gfx2d* gfx, int x, int y) {
767 assert(gfx);
768 gfx->camera = (ivec2){x, y};
769}
770
771void gfx2d_render(Gfx2d* gfx) {
772 assert(gfx);
773 draw_map(gfx);
774 draw_sprites(gfx);
775}
776
777void gfx2d_draw_tile(Gfx2d* gfx, int x, int y, Tile tile) {
778 assert(gfx);
779 assert(gfx->map);
780
781 const Tm_Flags* flags = (const Tm_Flags*)&gfx->map->flags;
782 switch (flags->orientation) {
783 case Tm_Orthogonal:
784 draw_tile_ortho(gfx, tile, x, y);
785 break;
786 case Tm_Isometric:
787 draw_tile_iso(gfx, tile, x, y);
788 break;
789 }
790}
791
792void gfx2d_get_screen_size(const Gfx2d* gfx, int* width, int* height) {
793 assert(gfx);
794 assert(width);
795 assert(height);
796 *width = gfx->screen.width;
797 *height = gfx->screen.height;
798}
799
800const Pixel* gfx2d_get_screen_buffer(const Gfx2d* gfx) {
801 assert(gfx);
802 return gfx->screen.pixels;
803}
804
805void gfx2d_pick_tile(
806 const Gfx2d* gfx, double xcart, double ycart, int* xiso, int* yiso) {
807 assert(gfx);
808 assert(xiso);
809 assert(yiso);
810
811 const vec2 camera = ivec2_to_vec2(gfx->camera);
812 const vec2 xy_cart = vec2_add(camera, (vec2){xcart, ycart});
813
814 const vec2 xy_iso = cart2iso(
815 xy_cart, gfx->map->base_tile_width, gfx->map->base_tile_height,
816 gfx->screen.width);
817
818 if ((0 <= xy_iso.x) && (xy_iso.x < gfx->map->world_width) &&
819 (0 <= xy_iso.y) && (xy_iso.y < gfx->map->world_height)) {
820 *xiso = (int)xy_iso.x;
821 *yiso = (int)xy_iso.y;
822 } else {
823 *xiso = -1;
824 *yiso = -1;
825 }
826}