diff options
author | 3gg <3gg@shellblade.net> | 2023-06-24 18:46:33 -0700 |
---|---|---|
committer | 3gg <3gg@shellblade.net> | 2023-06-24 18:46:33 -0700 |
commit | 0831d5bce79008bfa6404f8e8116ae8290442fde (patch) | |
tree | e488c719e16b34b60126837a90a44d1c3dd552ee /gfx-iso/src | |
parent | cf886f4fa406ddd48f30c00ad3c77f9dc134af3a (diff) |
Isometric Renderer initial commit.
Diffstat (limited to 'gfx-iso/src')
-rw-r--r-- | gfx-iso/src/isogfx.c | 361 |
1 files changed, 361 insertions, 0 deletions
diff --git a/gfx-iso/src/isogfx.c b/gfx-iso/src/isogfx.c new file mode 100644 index 0000000..27981f9 --- /dev/null +++ b/gfx-iso/src/isogfx.c | |||
@@ -0,0 +1,361 @@ | |||
1 | #include <isogfx/isogfx.h> | ||
2 | |||
3 | #include <mempool.h> | ||
4 | |||
5 | #include <assert.h> | ||
6 | #include <stdbool.h> | ||
7 | #include <stdint.h> | ||
8 | #include <stdlib.h> | ||
9 | #include <string.h> | ||
10 | |||
11 | /// Maximum number of tiles unless the user chooses a non-zero value. | ||
12 | #define DEFAULT_MAX_NUM_TILES 1024 | ||
13 | |||
14 | typedef struct TileData { | ||
15 | Pixel pixels[1]; // Dynamically allocated. | ||
16 | } TileData; | ||
17 | |||
18 | DEF_MEMPOOL_DYN(TilePool, TileData) | ||
19 | |||
20 | typedef struct IsoGfx { | ||
21 | Tile* world; | ||
22 | Pixel* screen; | ||
23 | uint8_t* tile_mask; | ||
24 | TilePool tiles; | ||
25 | int screen_width; | ||
26 | int screen_height; | ||
27 | int tile_width; | ||
28 | int tile_height; | ||
29 | int world_width; | ||
30 | int world_height; | ||
31 | int max_num_tiles; | ||
32 | } IsoGfx; | ||
33 | |||
34 | typedef struct ivec2 { | ||
35 | int x, y; | ||
36 | } ivec2; | ||
37 | |||
38 | typedef struct vec2 { | ||
39 | double x, y; | ||
40 | } vec2; | ||
41 | |||
42 | static inline ivec2 ivec2_add(ivec2 a, ivec2 b) { | ||
43 | return (ivec2){.x = a.x + b.x, .y = a.y + b.y}; | ||
44 | } | ||
45 | |||
46 | static inline ivec2 ivec2_scale(ivec2 a, int s) { | ||
47 | return (ivec2){.x = a.x * s, .y = a.y * s}; | ||
48 | } | ||
49 | |||
50 | static inline ivec2 iso2cart(ivec2 iso, int s, int t, int w) { | ||
51 | return (ivec2){ | ||
52 | .x = (iso.x - iso.y) * (s / 2) + (w / 2), .y = (iso.x + iso.y) * (t / 2)}; | ||
53 | } | ||
54 | |||
55 | static inline vec2 cart2iso(vec2 cart, int s, int t, int w) { | ||
56 | const double one_over_s = 1. / (double)s; | ||
57 | const double one_over_t = 1. / (double)t; | ||
58 | const double x = cart.x - (double)(w / 2); | ||
59 | |||
60 | return (vec2){ | ||
61 | .x = (int)(one_over_s * x + one_over_t * cart.y), | ||
62 | .y = (int)(-one_over_s * x + one_over_t * cart.y)}; | ||
63 | } | ||
64 | |||
65 | Pixel* tile_xy_mut(const IsoGfx* iso, TileData* tile, int x, int y) { | ||
66 | assert(iso); | ||
67 | assert(tile); | ||
68 | assert(tile->pixels); | ||
69 | assert(x >= 0); | ||
70 | assert(y >= 0); | ||
71 | assert(x < iso->tile_width); | ||
72 | assert(y < iso->tile_height); | ||
73 | return &tile->pixels[y * iso->tile_width + x]; | ||
74 | } | ||
75 | |||
76 | Pixel tile_xy(const IsoGfx* iso, const TileData* tile, int x, int y) { | ||
77 | assert(iso); | ||
78 | assert(tile); | ||
79 | assert(tile->pixels); | ||
80 | assert(x >= 0); | ||
81 | assert(y >= 0); | ||
82 | assert(x < iso->tile_width); | ||
83 | assert(y < iso->tile_height); | ||
84 | return tile->pixels[y * iso->tile_width + x]; | ||
85 | } | ||
86 | |||
87 | static inline Tile world_xy(IsoGfx* iso, int x, int y) { | ||
88 | assert(iso); | ||
89 | assert(x >= 0); | ||
90 | assert(y >= 0); | ||
91 | assert(x < iso->world_width); | ||
92 | assert(y < iso->world_height); | ||
93 | return iso->world[y * iso->world_width + x]; | ||
94 | } | ||
95 | |||
96 | static inline Tile* world_xy_mut(IsoGfx* iso, int x, int y) { | ||
97 | assert(iso); | ||
98 | assert(x >= 0); | ||
99 | assert(y >= 0); | ||
100 | assert(x < iso->world_width); | ||
101 | assert(y < iso->world_height); | ||
102 | return &iso->world[y * iso->world_width + x]; | ||
103 | } | ||
104 | |||
105 | static inline Pixel screen_xy(IsoGfx* iso, int x, int y) { | ||
106 | assert(iso); | ||
107 | assert(x >= 0); | ||
108 | assert(y >= 0); | ||
109 | assert(x < iso->screen_width); | ||
110 | assert(y < iso->screen_height); | ||
111 | return iso->screen[y * iso->screen_width + x]; | ||
112 | } | ||
113 | |||
114 | static inline Pixel* screen_xy_mut(IsoGfx* iso, int x, int y) { | ||
115 | assert(iso); | ||
116 | assert(x >= 0); | ||
117 | assert(y >= 0); | ||
118 | assert(x < iso->screen_width); | ||
119 | assert(y < iso->screen_height); | ||
120 | return &iso->screen[y * iso->screen_width + x]; | ||
121 | } | ||
122 | |||
123 | static void draw_tile(IsoGfx* iso, ivec2 so, Tile tile) { | ||
124 | assert(iso); | ||
125 | |||
126 | const TileData* data = mempool_get_block(&iso->tiles, tile); | ||
127 | assert(data); | ||
128 | |||
129 | for (int py = 0; py < iso->tile_height; ++py) { | ||
130 | for (int px = 0; px < iso->tile_width; ++px) { | ||
131 | const Pixel colour = tile_xy(iso, data, px, py); | ||
132 | const int sx = so.x + px; | ||
133 | const int sy = so.y + py; | ||
134 | if ((sx >= 0) && (sy >= 0) && (sx < iso->screen_width) && | ||
135 | (sy < iso->screen_height)) { | ||
136 | const uint8_t mask = iso->tile_mask[py * iso->tile_width + px]; | ||
137 | if (mask == 1) { | ||
138 | *screen_xy_mut(iso, sx, sy) = colour; | ||
139 | } | ||
140 | } | ||
141 | } | ||
142 | } | ||
143 | } | ||
144 | |||
145 | static void draw(IsoGfx* iso) { | ||
146 | assert(iso); | ||
147 | |||
148 | const int W = iso->screen_width; | ||
149 | const int H = iso->screen_height; | ||
150 | |||
151 | memset(iso->screen, 0, W * H * sizeof(Pixel)); | ||
152 | |||
153 | const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; | ||
154 | const ivec2 x = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
155 | const ivec2 y = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
156 | |||
157 | // TODO: Since the world will generally be larger than the screen, it | ||
158 | // would be best to walk in screen space and fetch the tile. | ||
159 | // The tile-centric approach might be more cache-friendly, however, since the | ||
160 | // screen-centric approach would juggle multiple tiles throughout the scan. | ||
161 | for (int ty = 0; ty < iso->world_height; ++ty) { | ||
162 | for (int tx = 0; tx < iso->world_width; ++tx) { | ||
163 | const Tile tile = world_xy(iso, tx, ty); | ||
164 | const ivec2 so = | ||
165 | ivec2_add(o, ivec2_add(ivec2_scale(x, tx), ivec2_scale(y, ty))); | ||
166 | draw_tile(iso, so, tile); | ||
167 | } | ||
168 | } | ||
169 | } | ||
170 | |||
171 | /// Creates a tile mask procedurally. | ||
172 | static void make_tile_mask(IsoGfx* iso) { | ||
173 | assert(iso); | ||
174 | assert(iso->tile_mask); | ||
175 | |||
176 | for (int y = 0; y < iso->tile_height / 2; ++y) { | ||
177 | const int mask_start = iso->tile_width / 2 - 2 * y - 1; | ||
178 | const int mask_end = iso->tile_width / 2 + 2 * y + 1; | ||
179 | for (int x = 0; x < iso->tile_width; ++x) { | ||
180 | const bool masked = (mask_start <= x) && (x <= mask_end); | ||
181 | const uint8_t val = masked ? 1 : 0; | ||
182 | |||
183 | // Top half. | ||
184 | iso->tile_mask[y * iso->tile_width + x] = val; | ||
185 | |||
186 | // Bottom half reflects the top half. | ||
187 | const int y_reflected = iso->tile_height - y - 1; | ||
188 | iso->tile_mask[y_reflected * iso->tile_width + x] = val; | ||
189 | } | ||
190 | } | ||
191 | } | ||
192 | |||
193 | /// Creates a tile with a constant colour. | ||
194 | static void make_tile_from_colour( | ||
195 | const IsoGfx* iso, Pixel colour, TileData* tile) { | ||
196 | assert(iso); | ||
197 | assert(tile); | ||
198 | |||
199 | for (int y = 0; y < iso->tile_height; ++y) { | ||
200 | for (int x = 0; x < iso->tile_width; ++x) { | ||
201 | *tile_xy_mut(iso, tile, x, y) = colour; | ||
202 | } | ||
203 | } | ||
204 | } | ||
205 | |||
206 | IsoGfx* isogfx_new(const IsoGfxDesc* desc) { | ||
207 | assert(desc->screen_width > 0); | ||
208 | assert(desc->screen_height > 0); | ||
209 | assert(desc->tile_width > 0); | ||
210 | assert(desc->tile_height > 0); | ||
211 | // Part of our implementation assumes even widths and heights for greater | ||
212 | // precision. | ||
213 | assert((desc->screen_width & 1) == 0); | ||
214 | assert((desc->screen_height & 1) == 0); | ||
215 | assert((desc->tile_width & 1) == 0); | ||
216 | assert((desc->tile_height & 1) == 0); | ||
217 | |||
218 | IsoGfx* iso = calloc(1, sizeof(IsoGfx)); | ||
219 | if (!iso) { | ||
220 | return 0; | ||
221 | } | ||
222 | |||
223 | iso->screen_width = desc->screen_width; | ||
224 | iso->screen_height = desc->screen_height; | ||
225 | iso->tile_width = desc->tile_width; | ||
226 | iso->tile_height = desc->tile_height; | ||
227 | iso->world_width = desc->world_width; | ||
228 | iso->world_height = desc->world_height; | ||
229 | iso->max_num_tiles = | ||
230 | desc->max_num_tiles > 0 ? desc->max_num_tiles : DEFAULT_MAX_NUM_TILES; | ||
231 | |||
232 | const int world_size = desc->world_width * desc->world_height; | ||
233 | const int screen_size = desc->screen_width * desc->screen_height; | ||
234 | const int tile_size = desc->tile_width * desc->tile_height; | ||
235 | |||
236 | const int tile_size_bytes = tile_size * (int)sizeof(Pixel); | ||
237 | |||
238 | if (!(iso->world = calloc(world_size, sizeof(Tile)))) { | ||
239 | goto cleanup; | ||
240 | } | ||
241 | if (!(iso->screen = calloc(screen_size, sizeof(Pixel)))) { | ||
242 | goto cleanup; | ||
243 | } | ||
244 | if (!(iso->tile_mask = calloc(tile_size, sizeof(uint8_t)))) { | ||
245 | goto cleanup; | ||
246 | } | ||
247 | if (!mempool_make_dyn(&iso->tiles, iso->max_num_tiles, tile_size_bytes)) { | ||
248 | goto cleanup; | ||
249 | } | ||
250 | |||
251 | make_tile_mask(iso); | ||
252 | |||
253 | return iso; | ||
254 | |||
255 | cleanup: | ||
256 | isogfx_del(&iso); | ||
257 | return 0; | ||
258 | } | ||
259 | |||
260 | void isogfx_del(IsoGfx** pIso) { | ||
261 | assert(pIso); | ||
262 | IsoGfx* iso = *pIso; | ||
263 | if (iso) { | ||
264 | if (iso->world) { | ||
265 | free(iso->world); | ||
266 | } | ||
267 | if (iso->screen) { | ||
268 | free(iso->screen); | ||
269 | } | ||
270 | if (iso->tile_mask) { | ||
271 | free(iso->tile_mask); | ||
272 | } | ||
273 | mempool_del(&iso->tiles); | ||
274 | free(iso); | ||
275 | } | ||
276 | } | ||
277 | |||
278 | Tile isogfx_make_tile(IsoGfx* iso, const TileDesc* desc) { | ||
279 | assert(iso); | ||
280 | assert(desc); | ||
281 | |||
282 | TileData* tile = mempool_alloc(&iso->tiles); | ||
283 | assert(tile); // TODO: Make this a hard assert. | ||
284 | |||
285 | switch (desc->type) { | ||
286 | case TileFromColour: | ||
287 | make_tile_from_colour(iso, desc->colour, tile); | ||
288 | break; | ||
289 | case TileFromFile: | ||
290 | assert(false); // TODO | ||
291 | break; | ||
292 | case TileFromMemory: | ||
293 | assert(false); // TODO | ||
294 | break; | ||
295 | } | ||
296 | |||
297 | return (Tile)mempool_get_block_index(&iso->tiles, tile); | ||
298 | } | ||
299 | |||
300 | void isogfx_set_tile(IsoGfx* iso, int x, int y, Tile tile) { | ||
301 | assert(iso); | ||
302 | *world_xy_mut(iso, x, y) = tile; | ||
303 | } | ||
304 | |||
305 | void isogfx_pick_tile( | ||
306 | const IsoGfx* iso, double xcart, double ycart, int* xiso, int* yiso) { | ||
307 | assert(iso); | ||
308 | assert(xiso); | ||
309 | assert(yiso); | ||
310 | |||
311 | const vec2 xy_iso = cart2iso( | ||
312 | (vec2){.x = xcart, .y = ycart}, iso->tile_width, iso->tile_height, | ||
313 | iso->screen_width); | ||
314 | |||
315 | const int x = (int)xy_iso.x; | ||
316 | const int y = (int)xy_iso.y; | ||
317 | |||
318 | if ((0 <= x) && (x < iso->world_width) && (0 <= y) && | ||
319 | (y < iso->world_height)) { | ||
320 | *xiso = x; | ||
321 | *yiso = y; | ||
322 | } else { | ||
323 | *xiso = -1; | ||
324 | } | ||
325 | } | ||
326 | |||
327 | void isogfx_render(IsoGfx* iso) { | ||
328 | assert(iso); | ||
329 | draw(iso); | ||
330 | } | ||
331 | |||
332 | void isogfx_draw_tile(IsoGfx* iso, int x, int y, Tile tile) { | ||
333 | assert(iso); | ||
334 | assert(x >= 0); | ||
335 | assert(y >= 0); | ||
336 | assert(x < iso->world_width); | ||
337 | assert(y < iso->world_height); | ||
338 | |||
339 | const ivec2 o = {(iso->screen_width / 2) - (iso->tile_width / 2), 0}; | ||
340 | const ivec2 vx = {.x = iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
341 | const ivec2 vy = {.x = -iso->tile_width / 2, .y = iso->tile_height / 2}; | ||
342 | const ivec2 so = | ||
343 | ivec2_add(o, ivec2_add(ivec2_scale(vx, x), ivec2_scale(vy, y))); | ||
344 | |||
345 | draw_tile(iso, so, tile); | ||
346 | } | ||
347 | |||
348 | const Pixel* isogfx_get_screen_buffer(const IsoGfx* iso) { | ||
349 | assert(iso); | ||
350 | return iso->screen; | ||
351 | } | ||
352 | |||
353 | int isogfx_world_width(const IsoGfx* iso) { | ||
354 | assert(iso); | ||
355 | return iso->world_width; | ||
356 | } | ||
357 | |||
358 | int isogfx_world_height(const IsoGfx* iso) { | ||
359 | assert(iso); | ||
360 | return iso->world_height; | ||
361 | } | ||