diff options
Diffstat (limited to 'src/gfx2d.c')
-rw-r--r-- | src/gfx2d.c | 826 |
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 | |||
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 IsoCoordSystem { | ||
36 | ivec2 o; // Origin. | ||
37 | ivec2 x; | ||
38 | ivec2 y; | ||
39 | } IsoCoordSystem; | ||
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 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 | |||
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 vec2 vec2_add(vec2 a, vec2 b) { | ||
83 | return (vec2){.x = a.x + b.x, .y = a.y + b.y}; | ||
84 | } | ||
85 | |||
86 | static inline vec2 ivec2_to_vec2(ivec2 a) { return (vec2){a.x, a.y}; } | ||
87 | |||
88 | /// Map map coordinates to screen coordinates, both Cartesian. | ||
89 | static 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. | ||
105 | static 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. | ||
123 | static 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. | ||
143 | static 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 | |||
151 | static 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 | |||
161 | static inline Pixel screen_xy(Screen* screen, int x, int y) { | ||
162 | return *screen_xy_const_ref(screen, x, y); | ||
163 | } | ||
164 | |||
165 | static 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 | |||
173 | Gfx2d* 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 | |||
202 | cleanup: | ||
203 | gfx2d_del(&gfx); | ||
204 | return nullptr; | ||
205 | } | ||
206 | |||
207 | void 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 | |||
219 | void 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 | |||
228 | void 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 | |||
278 | bool 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 | |||
331 | cleanup: | ||
332 | if (!success) { | ||
333 | gfx2d_clear(gfx); | ||
334 | } | ||
335 | return success; | ||
336 | } | ||
337 | |||
338 | int gfx2d_world_width(const Gfx2d* gfx) { | ||
339 | assert(gfx); | ||
340 | return gfx->map->world_width; | ||
341 | } | ||
342 | |||
343 | int gfx2d_world_height(const Gfx2d* gfx) { | ||
344 | assert(gfx); | ||
345 | return gfx->map->world_height; | ||
346 | } | ||
347 | |||
348 | static 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 | |||
373 | Tile 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 | |||
421 | void 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 | |||
430 | void 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 | |||
439 | SpriteSheet 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 | |||
463 | cleanup: | ||
464 | if (!success) { | ||
465 | if (ss_sheet) { | ||
466 | memstack_set_watermark(&gfx->stack, watermark); | ||
467 | } | ||
468 | } | ||
469 | return spriteSheet; | ||
470 | } | ||
471 | |||
472 | Sprite 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 | |||
489 | void 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 | |||
496 | void gfx2d_set_sprite_animation(Gfx2d* gfx, Sprite hSprite, int animation) { | ||
497 | assert(gfx); | ||
498 | SpriteInstance* sprite = (SpriteInstance*)hSprite; | ||
499 | sprite->animation = animation; | ||
500 | } | ||
501 | |||
502 | void 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 | |||
530 | static 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. | ||
557 | static 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. | ||
590 | static 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. | ||
610 | static 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 | |||
639 | static 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 | |||
655 | static 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 | |||
675 | static 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. | ||
697 | static 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. | ||
721 | static 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 | |||
745 | static 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 | |||
766 | void gfx2d_set_camera(Gfx2d* gfx, int x, int y) { | ||
767 | assert(gfx); | ||
768 | gfx->camera = (ivec2){x, y}; | ||
769 | } | ||
770 | |||
771 | void gfx2d_render(Gfx2d* gfx) { | ||
772 | assert(gfx); | ||
773 | draw_map(gfx); | ||
774 | draw_sprites(gfx); | ||
775 | } | ||
776 | |||
777 | void 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 | |||
792 | void 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 | |||
800 | const Pixel* gfx2d_get_screen_buffer(const Gfx2d* gfx) { | ||
801 | assert(gfx); | ||
802 | return gfx->screen.pixels; | ||
803 | } | ||
804 | |||
805 | void 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 | } | ||