diff options
Diffstat (limited to 'sprite2gba/src/main.c')
-rw-r--r-- | sprite2gba/src/main.c | 396 |
1 files changed, 396 insertions, 0 deletions
diff --git a/sprite2gba/src/main.c b/sprite2gba/src/main.c new file mode 100644 index 0000000..f91ecd0 --- /dev/null +++ b/sprite2gba/src/main.c | |||
@@ -0,0 +1,396 @@ | |||
1 | #include <getopt.h> | ||
2 | |||
3 | #include "shorthand.h" | ||
4 | |||
5 | #define STB_IMAGE_IMPLEMENTATION | ||
6 | #include "stb_image.h" | ||
7 | |||
8 | #define TILE_SIZE 8 | ||
9 | #define DEFAULT_PALETTE_SIZE 16 | ||
10 | #define DEFAULT_BG_COLOR 0x0000 | ||
11 | |||
12 | typedef u16 Color; | ||
13 | |||
14 | static inline Color | ||
15 | rgb15(u32 red, u32 green, u32 blue ) { | ||
16 | return (blue << 10) | (green << 5) | red; | ||
17 | } | ||
18 | |||
19 | static inline | ||
20 | int quantize5(int color) { | ||
21 | return color * 31 / 255; | ||
22 | } | ||
23 | |||
24 | // TODO: Assuming 4bpp tiles. The tile is an index to a palette. | ||
25 | // TODO: Write more documentation about this. | ||
26 | typedef u32 Tile[TILE_SIZE]; | ||
27 | |||
28 | typedef struct Tiles { | ||
29 | Tile *data; | ||
30 | size_t size; | ||
31 | size_t capacity; | ||
32 | } Tiles; | ||
33 | |||
34 | typedef struct Palette { | ||
35 | Color *data; | ||
36 | size_t size; | ||
37 | size_t capacity; | ||
38 | } Palette; | ||
39 | |||
40 | typedef struct Image { | ||
41 | unsigned char *data; | ||
42 | int width; | ||
43 | int height; | ||
44 | int n_channels; | ||
45 | } Image; | ||
46 | |||
47 | u8 | ||
48 | pack_1bb(u32 hex) { | ||
49 | u32 low_nibble = hex & 0xFFFF; | ||
50 | u32 high_nibble = (hex >> 16) & 0xFFFF; | ||
51 | u32 low = (low_nibble & 0x1); | ||
52 | low += 2 * ((low_nibble >> 4) & 0x1); | ||
53 | low += 4 * ((low_nibble >> 8) & 0x1); | ||
54 | low += 8 * ((low_nibble >> 12) & 0x1); | ||
55 | u32 high = (high_nibble & 0x1); | ||
56 | high += 2 * ((high_nibble >> 4) & 0x1); | ||
57 | high += 4 * ((high_nibble >> 8) & 0x1); | ||
58 | high += 8 * ((high_nibble >> 12) & 0x1); | ||
59 | return (high << 4) | low; | ||
60 | } | ||
61 | |||
62 | void | ||
63 | export_c_file(Tiles *tiles, Palette *palette, FILE *file, bool pack_1bpp) { | ||
64 | // Output tiles. | ||
65 | if (pack_1bpp) { | ||
66 | fprintf(file, "u32 packed_tiles[] = {\n "); | ||
67 | size_t counter = 0; | ||
68 | for (size_t i = 0; i < tiles->capacity; ++i) { | ||
69 | for (size_t j = 0; j < TILE_SIZE; j += 4, counter++) { | ||
70 | if (counter == 4) { | ||
71 | counter = 0; | ||
72 | fprintf(file, "\n "); | ||
73 | } | ||
74 | u32 tile_a = tiles->data[i][j]; | ||
75 | u32 tile_b = tiles->data[i][j + 1]; | ||
76 | u32 tile_c = tiles->data[i][j + 2]; | ||
77 | u32 tile_d = tiles->data[i][j + 3]; | ||
78 | u32 packed = pack_1bb(tile_d); | ||
79 | packed |= pack_1bb(tile_c) << 8; | ||
80 | packed |= pack_1bb(tile_b) << 16; | ||
81 | packed |= pack_1bb(tile_a) << 24; | ||
82 | fprintf(file, "0x%08x,", packed); | ||
83 | if (counter < 3) { | ||
84 | fprintf(file, " "); | ||
85 | } | ||
86 | } | ||
87 | } | ||
88 | fprintf(file, "\n};\n"); | ||
89 | fprintf(file, "\n"); | ||
90 | } else { | ||
91 | fprintf(file, "u32 tiles[%lu][%u] = {\n", tiles->capacity, TILE_SIZE); | ||
92 | for (size_t i = 0; i < tiles->capacity; ++i) { | ||
93 | fprintf(file, " {"); | ||
94 | for (size_t j = 0; j < TILE_SIZE; ++j) { | ||
95 | fprintf(file, "0x%08x", tiles->data[i][j]); | ||
96 | if (j != (TILE_SIZE - 1)) { | ||
97 | fprintf(file, ", "); | ||
98 | } | ||
99 | } | ||
100 | fprintf(file, "},\n"); | ||
101 | } | ||
102 | fprintf(file, "};\n"); | ||
103 | fprintf(file, "\n"); | ||
104 | } | ||
105 | |||
106 | // Output palette. | ||
107 | fprintf(file, "u16 palette[%lu] = {\n", palette->capacity); | ||
108 | fprintf(file, " "); | ||
109 | size_t counter = 0; | ||
110 | for (size_t i = 0; i < palette->capacity; ++i, ++counter) { | ||
111 | if (counter == 4) { | ||
112 | counter = 0; | ||
113 | fprintf(file, "\n "); | ||
114 | } | ||
115 | fprintf(file, "0x%04x,", palette->data[i]); | ||
116 | if (counter < 3) { | ||
117 | fprintf(file, " "); | ||
118 | } | ||
119 | } | ||
120 | fprintf(file, "\n"); | ||
121 | fprintf(file, "};\n"); | ||
122 | } | ||
123 | |||
124 | void | ||
125 | extract_tile(Tiles *tiles, Palette *palette, Image *img, | ||
126 | size_t offset_x, size_t offset_y) { | ||
127 | for (size_t j = 0; j < TILE_SIZE; ++j) { | ||
128 | u32 col_index = 0x00000000; | ||
129 | for (size_t i = 0; i < TILE_SIZE; ++i) { | ||
130 | size_t pal_index = palette->size; | ||
131 | // Find the memory index for this pixel. | ||
132 | int idx = (i + offset_x) + (j + offset_y) * img->width; | ||
133 | idx *= img->n_channels; | ||
134 | |||
135 | int red = img->data[idx]; | ||
136 | int green = img->data[idx + 1]; | ||
137 | int blue = img->data[idx + 2]; | ||
138 | |||
139 | // Quantize to 5 bits per channel. | ||
140 | red = quantize5(red); | ||
141 | green = quantize5(green); | ||
142 | blue = quantize5(blue); | ||
143 | |||
144 | Color clr = rgb15(red, green, blue); | ||
145 | bool found = false; | ||
146 | // Brute force search, but since the palette size will likely be | ||
147 | // small, shouldn't be a big problem. | ||
148 | if (palette->size < palette->capacity) { | ||
149 | // If the palette is not full try to find if the color is | ||
150 | // already stored. | ||
151 | for (size_t p = 0; p < palette->capacity; ++p) { | ||
152 | if (clr == palette->data[p]) { | ||
153 | pal_index = p; | ||
154 | found = true; | ||
155 | break; | ||
156 | } | ||
157 | } | ||
158 | } else { | ||
159 | // If the palette is full, find the closest perceived color | ||
160 | // instead. | ||
161 | size_t best_index = 0; | ||
162 | double best_distance = 1e99; | ||
163 | for (size_t p = 0; p < palette->capacity; ++p) { | ||
164 | Color pal_clr = palette->data[p]; | ||
165 | int pal_red = pal_clr & 0x1F; | ||
166 | int pal_green = (pal_clr >> 5) & 0x1F; | ||
167 | int pal_blue = (pal_clr >> 10) & 0x1F; | ||
168 | double r = (pal_red - red); | ||
169 | double g = (pal_green - green); | ||
170 | double b = (pal_blue - blue); | ||
171 | double distance = r * r * 0.3 + g * g * 0.59 + b * b * 0.11; | ||
172 | if (distance < best_distance) { | ||
173 | best_distance = distance; | ||
174 | best_index = p; | ||
175 | } | ||
176 | } | ||
177 | pal_index = best_index; | ||
178 | found = true; | ||
179 | } | ||
180 | if (!found) { | ||
181 | palette->data[palette->size++] = clr; | ||
182 | } | ||
183 | col_index |= pal_index << i * 4; // TODO: FlipH? | ||
184 | } | ||
185 | tiles->data[tiles->size][j] = col_index; | ||
186 | } | ||
187 | tiles->size++; | ||
188 | } | ||
189 | |||
190 | void | ||
191 | print_usage(void) { | ||
192 | printf("Usage: %s [options] <filename>\n", BIN_NAME); | ||
193 | printf("\n"); | ||
194 | printf("\t-b <RRGGBB>\tSelects the background color used for transparency.\n"); | ||
195 | printf("\t-o <out_file.c>\tPath to the output file. If blank, stdout will be used.\n"); | ||
196 | printf("\t-x <0>\tStart x position (in pixels).\n"); | ||
197 | printf("\t-y <0>\tStart y position (in pixels).\n"); | ||
198 | printf("\t-W <8>\tSelect the sprite width in pixels. Must be a multiple of the tile size.\n"); | ||
199 | printf("\t-H <8>\tSelect the sprite height in pixels. Must be a multiple of the tile size.\n"); | ||
200 | printf("\t-1\tBit pack the output to 1bbp (4x size reduction).\n"); | ||
201 | printf("\t-w <0>\tWidth of region to extract from (x0,y0). Defaults to image width.\n"); | ||
202 | printf("\t-h <0>\tHeight of region to extract from (x0,y0). Defaults to image height.\n"); | ||
203 | printf("\n"); | ||
204 | } | ||
205 | |||
206 | // TODO: Support multiple files at once? | ||
207 | int | ||
208 | main(int argc, char *argv[]) { | ||
209 | // Initialize default parameters. | ||
210 | u16 background_color = DEFAULT_BG_COLOR; | ||
211 | u16 palette_size = DEFAULT_PALETTE_SIZE; | ||
212 | size_t sprite_width = TILE_SIZE; | ||
213 | size_t sprite_height = TILE_SIZE; | ||
214 | char *out_file_path = NULL; | ||
215 | bool pack_1bpp = false; | ||
216 | size_t start_x = 0; | ||
217 | size_t start_y = 0; | ||
218 | size_t width = 0; | ||
219 | size_t height = 0; | ||
220 | |||
221 | int option; | ||
222 | while ((option = getopt(argc, argv, "b:o:W:H:1x:y:w:h:")) != -1) { | ||
223 | switch (option) { | ||
224 | case 'b': { | ||
225 | // Background color. | ||
226 | char *bg = optarg; | ||
227 | if (strlen(bg) != 6) { | ||
228 | fprintf(stderr, "%s: Invalid background color.\n", BIN_NAME); | ||
229 | print_usage(); | ||
230 | return EXIT_FAILURE; | ||
231 | } | ||
232 | unsigned int red = 0; | ||
233 | unsigned int green = 0; | ||
234 | unsigned int blue = 0; | ||
235 | |||
236 | // NOTE: Undefined behaviour, here we go! Also, no error checks, | ||
237 | // so if an invalid number is given it will be parsed as 0. | ||
238 | char r_str[3] = {bg[0], bg[1], '\0'}; | ||
239 | char g_str[3] = {bg[2], bg[3], '\0'}; | ||
240 | char b_str[3] = {bg[4], bg[5], '\0'}; | ||
241 | sscanf(r_str, "%x", &red); | ||
242 | sscanf(g_str, "%x", &green); | ||
243 | sscanf(b_str, "%x", &blue); | ||
244 | |||
245 | // Quantize and transform to rgb15. | ||
246 | red = quantize5(red); | ||
247 | green = quantize5(green); | ||
248 | blue = quantize5(blue); | ||
249 | background_color = rgb15(red, green, blue); | ||
250 | } break; | ||
251 | case 'W': { | ||
252 | // Sprite width. | ||
253 | char *arg = optarg; | ||
254 | sprite_width = 0; | ||
255 | sprite_width = atoi(arg); | ||
256 | |||
257 | // Check if a valid number is given. | ||
258 | if (sprite_width == 0 || sprite_width % TILE_SIZE != 0) { | ||
259 | fprintf(stderr, "%s: Invalid sprite width.\n", BIN_NAME); | ||
260 | print_usage(); | ||
261 | return EXIT_FAILURE; | ||
262 | } | ||
263 | } break; | ||
264 | case 'H': { | ||
265 | // Sprite height. | ||
266 | char *arg = optarg; | ||
267 | sprite_height = 0; | ||
268 | sprite_height = atoi(arg); | ||
269 | |||
270 | // Check if a valid number is given. | ||
271 | if (sprite_height == 0 || sprite_height % TILE_SIZE != 0) { | ||
272 | fprintf(stderr, "%s: Invalid sprite height.\n", BIN_NAME); | ||
273 | print_usage(); | ||
274 | return EXIT_FAILURE; | ||
275 | } | ||
276 | } break; | ||
277 | case 'o': { | ||
278 | // Output file. | ||
279 | out_file_path = optarg; | ||
280 | } break; | ||
281 | case '1': { | ||
282 | // Pack output to 1bpp. | ||
283 | pack_1bpp = true; | ||
284 | } break; | ||
285 | case 'x': { | ||
286 | // Start x position in pixels. | ||
287 | start_x = atoi(optarg); | ||
288 | } break; | ||
289 | case 'y': { | ||
290 | // Start y position in pixels. | ||
291 | start_y = atoi(optarg); | ||
292 | } break; | ||
293 | case 'w': { | ||
294 | // Width of region to extract from (x0,y0). | ||
295 | width = atoi(optarg); | ||
296 | } break; | ||
297 | case 'h': { | ||
298 | // Height of region to extract from (x0,y0). | ||
299 | height = atoi(optarg); | ||
300 | } break; | ||
301 | default: { | ||
302 | print_usage(); | ||
303 | return EXIT_FAILURE; | ||
304 | } break; | ||
305 | } | ||
306 | } | ||
307 | |||
308 | // Get the path to the file to be exported. | ||
309 | if (optind != argc - 1) { | ||
310 | fprintf(stderr, "%s: No filename given.\n", BIN_NAME); | ||
311 | print_usage(); | ||
312 | return EXIT_FAILURE; | ||
313 | } | ||
314 | char *file_name = argv[optind]; | ||
315 | |||
316 | // Fill the palette with the background color if one was given. | ||
317 | Palette palette = { | ||
318 | .data = malloc(palette_size * sizeof(Color)), | ||
319 | .size = 1, | ||
320 | .capacity = palette_size, | ||
321 | }; | ||
322 | for (size_t i = 0; i < palette_size; ++i) { | ||
323 | palette.data[i] = background_color; | ||
324 | } | ||
325 | |||
326 | Image img = {0}; | ||
327 | |||
328 | // Open the given file. | ||
329 | FILE *input_file = fopen(file_name, "rb"); | ||
330 | if (input_file == NULL) { | ||
331 | fprintf(stderr, "%s: can't open input file: %s\n", BIN_NAME, file_name); | ||
332 | return EXIT_FAILURE; | ||
333 | } | ||
334 | img.data = stbi_load_from_file(input_file, &img.width, &img.height, &img.n_channels, 0); | ||
335 | fclose(input_file); | ||
336 | |||
337 | // TODO: Implement support for different file inputs. | ||
338 | if (img.n_channels != 3) { | ||
339 | fprintf(stderr, "File format not supported. Only 3 channel files for now.\n"); | ||
340 | return EXIT_FAILURE; | ||
341 | } | ||
342 | |||
343 | if (width == 0) { | ||
344 | width = img.width; | ||
345 | } | ||
346 | if (height == 0) { | ||
347 | height = img.height; | ||
348 | } | ||
349 | size_t n_tiles_x = width / TILE_SIZE; | ||
350 | size_t n_tiles_y = height / TILE_SIZE; | ||
351 | size_t n_tiles = n_tiles_x * n_tiles_y; | ||
352 | |||
353 | // Allocate memory for the tiles in this file, with zero-initialization. | ||
354 | Tiles tiles = { | ||
355 | .data = calloc(n_tiles, sizeof(Tile)), | ||
356 | .size = 0, | ||
357 | .capacity = n_tiles, | ||
358 | }; | ||
359 | |||
360 | int offset_x = start_x; | ||
361 | int offset_y = start_y; | ||
362 | size_t n_sprites_x = width / sprite_width; | ||
363 | size_t n_sprites_y = height / sprite_height; | ||
364 | size_t n_sprite_tiles_x = sprite_width / TILE_SIZE; | ||
365 | size_t n_sprite_tiles_y = sprite_height / TILE_SIZE; | ||
366 | for (size_t p = 0; p < n_sprites_y; ++p) { | ||
367 | for (size_t k = 0; k < n_sprites_x; ++k) { | ||
368 | for (size_t j = 0; j < n_sprite_tiles_y; ++j) { | ||
369 | int offset_x = start_x + k * sprite_width; | ||
370 | int offset_y = start_y + p * sprite_height + j * TILE_SIZE; | ||
371 | for (size_t i = 0; i < n_sprite_tiles_x; ++i) { | ||
372 | extract_tile(&tiles, &palette, &img, offset_x, offset_y); | ||
373 | offset_x += TILE_SIZE; | ||
374 | } | ||
375 | } | ||
376 | } | ||
377 | } | ||
378 | |||
379 | if (out_file_path == NULL) { | ||
380 | export_c_file(&tiles, &palette, stdout, pack_1bpp); | ||
381 | } else { | ||
382 | FILE *file = fopen(out_file_path, "w"); | ||
383 | if (!file) { | ||
384 | fprintf(stderr, "%s: can't open output file: %s\n", BIN_NAME, out_file_path); | ||
385 | return EXIT_FAILURE; | ||
386 | } | ||
387 | export_c_file(&tiles, &palette, file, pack_1bpp); | ||
388 | fclose(file); | ||
389 | } | ||
390 | |||
391 | // Cleanup resources and exit. | ||
392 | free(tiles.data); | ||
393 | free(palette.data); | ||
394 | stbi_image_free(img.data); | ||
395 | return EXIT_SUCCESS; | ||
396 | } | ||