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