Update README.md
[sm64pc.git] / tools / skyconv.c
blob778295db56c8128211196f8692166437612f18fc
1 /* skybox generator */
3 #define _GNU_SOURCE
4 #include <assert.h>
5 #include <string.h>
6 #include <stdint.h>
7 #include <stdlib.h>
8 #include <limits.h>
9 #include <stdio.h>
10 #include <stdbool.h>
11 #include <math.h>
13 #include "n64graphics.h"
14 #include "utils.h"
16 typedef struct {
17 rgba *px;
18 bool useless;
19 unsigned int pos;
20 } TextureTile;
22 typedef enum {
23 InvalidType = -1,
24 Skybox,
25 Cake,
26 CakeEU,
27 ImageType_MAX
28 } ImageType;
30 typedef enum {
31 InvalidMode = -1,
32 Combine,
33 Split
34 } OperationMode;
36 typedef struct {
37 int imageWidth, imageHeight;
38 int tileWidth, tileHeight;
39 int numCols, numRows;
40 bool wrapX;
41 bool optimizePositions;
42 } ImageProps;
44 static const ImageProps IMAGE_PROPERTIES[ImageType_MAX][2] = {
45 [Skybox] = {
46 {248, 248, 31, 31, 8, 8, true, true},
47 {256, 256, 32, 32, 8, 8, true, true},
49 [Cake] = {
50 {316, 228, 79, 19, 4, 12, false, false},
51 {320, 240, 80, 20, 4, 12, false, false},
53 [CakeEU] = {
54 {320, 224, 64, 32, 5, 7, false, false},
55 {320, 224, 64, 32, 5, 7, false, false},
59 typedef struct {
60 int cols, rows;
61 } TableDimension;
63 static const TableDimension TABLE_DIMENSIONS[ImageType_MAX] = {
64 [Skybox] = {8, 10},
65 [Cake] = {4, 12},
66 [CakeEU] = {5, 7},
69 TextureTile *tiles;
70 ImageType type = InvalidType;
71 OperationMode mode = InvalidMode;
72 char *programName;
73 char *input, *output;
74 char *writeDir;
75 char skyboxName[256];
76 bool expanded = false;
77 bool writeTiles;
78 bool storeNamesOnly = false;
80 static void allocate_tiles() {
81 const ImageProps props = IMAGE_PROPERTIES[type][true];
82 int tileWidth = props.tileWidth;
83 int tileHeight = props.tileHeight;
84 int numRows = props.numRows;
85 int numCols = props.numCols;
87 int tileSize = tileWidth * tileHeight * sizeof(rgba);
88 int totalSize = numRows * numCols * tileSize;
89 tiles = calloc(1, numRows * numCols * sizeof(TextureTile));
90 rgba *tileData = calloc(1, totalSize);
91 for (int row = 0; row < numRows; ++row) {
92 for (int col = 0; col < numCols; ++col) {
93 tiles[row * numCols + col].px = (tileData + (row * numCols + col) * (tileWidth * tileHeight));
98 static void free_tiles() {
99 free(tiles->px);
100 free(tiles);
103 static void split_tile(int col, int row, rgba *image, bool expanded) {
104 const ImageProps props = IMAGE_PROPERTIES[type][expanded];
105 int tileWidth = props.tileWidth;
106 int tileHeight = props.tileHeight;
107 int imageWidth = props.imageWidth;
108 int numCols = props.numCols;
110 int expandedWidth = IMAGE_PROPERTIES[type][true].tileWidth;
112 for (int y = 0; y < tileHeight; y++) {
113 for (int x = 0; x < tileWidth; x++) {
114 int ny = row * tileHeight + y;
115 int nx = col * tileWidth + x;
116 tiles[row * numCols + col].px[y * expandedWidth + x] = image[(ny * imageWidth + nx)];
121 static void expand_tiles(ImageType imageType) {
122 const ImageProps props = IMAGE_PROPERTIES[imageType][true];
123 int numRows = props.numRows;
124 int numCols = props.numCols;
125 int tileWidth = props.tileWidth;
126 int tileHeight = props.tileHeight;
128 // If the image type wraps,
129 // Copy each tile's left edge to the previous tile's right edge
130 // Each tile's height is still tileHeight - 1
131 if (props.wrapX) {
132 for (int row = 0; row < numRows; ++row) {
133 for (int col = 0; col < numCols; ++col) {
134 int nextCol = (col + 1) % numCols;
135 for (int y = 0; y < (tileHeight - 1); ++y) {
136 tiles[row * numCols + col].px[(tileWidth - 1) + y * tileWidth] = tiles[row * numCols + nextCol].px[y * tileWidth];
140 } else {
141 // Don't wrap, copy the second to last column instead.
142 for (int row = 0; row < numRows; ++row) {
143 for (int col = 0; col < numCols - 1; ++col) {
144 int nextCol = (col + 1) % numCols;
145 for (int y = 0; y < (tileHeight - 1); ++y) {
146 tiles[row * numCols + col].px[(tileWidth - 1) + y * tileWidth] = tiles[row * numCols + nextCol].px[y * tileWidth];
149 for (int y = 0; y < (tileHeight - 1); ++y) {
150 tiles[row * numCols + (numCols - 1)].px[(tileWidth - 1) + y * tileWidth] = tiles[row * numCols + (numCols - 1)].px[(tileWidth - 2) + y * tileWidth];
156 // Copy each tile's top edge to the previous tile's bottom edge, EXCEPT for the bottom row, which
157 // just duplicates its second-to-last row
158 for (int row = 0; row < numRows; ++row) {
159 if (row < numRows - 1) {
160 for (int col = 0; col < numCols; ++col) {
161 int nextRow = (row + 1) % numRows;
162 for (int x = 0; x < tileWidth; ++x) {
163 tiles[row * numCols + col].px[x + (tileHeight - 1) * tileWidth] = tiles[nextRow * numCols + col].px[x];
167 // For the last row of tiles, duplicate each one's second to last row
168 else {
169 for (int col = 0; col < numCols; ++col) {
170 for (int x = 0; x < tileWidth; ++x) {
171 tiles[row * numCols + col].px[x + (tileHeight - 1) * tileWidth] = tiles[row * numCols + col].px[x + (tileHeight - 2) * tileWidth];
178 static void init_tiles(rgba *image, bool expanded) {
179 const ImageProps props = IMAGE_PROPERTIES[type][expanded];
181 for (int row = 0; row < props.numRows; row++) {
182 for (int col = 0; col < props.numCols; col++) {
183 split_tile(col, row, image, expanded);
187 // Expand the tiles to their full size
188 if (!expanded) {
189 expand_tiles(type);
193 static void assign_tile_positions() {
194 const ImageProps props = IMAGE_PROPERTIES[type][true];
195 const size_t TILE_SIZE = props.tileWidth * props.tileHeight * sizeof(rgba);
197 unsigned int newPos = 0;
198 for (int i = 0; i < props.numRows * props.numCols; i++) {
199 if (props.optimizePositions) {
200 for (int j = 0; j < i; j++) {
201 if (!tiles[j].useless && memcmp(tiles[j].px, tiles[i].px, TILE_SIZE) == 0) {
202 tiles[i].useless = 1;
203 tiles[i].pos = j;
204 break;
209 if (!tiles[i].useless) {
210 tiles[i].pos = newPos;
211 newPos++;
216 /* write pngs to disc */
217 void write_tiles() {
218 const ImageProps props = IMAGE_PROPERTIES[type][true];
219 char buffer[PATH_MAX];
221 if (realpath(writeDir, buffer) == NULL) {
222 fprintf(stderr, "err: Could not find find img dir %s", writeDir);
223 exit(EXIT_FAILURE);
226 strcat(buffer, "/");
228 switch(type) {
229 case Skybox:
230 strcat(buffer, skyboxName);
231 break;
232 case Cake:
233 strcat(buffer, "cake");
234 break;
235 case CakeEU:
236 strcat(buffer, "cake_eu");
237 break;
238 default:
239 exit(EXIT_FAILURE);
240 break;
243 int dirLength = strlen(buffer);
244 char *filename = buffer + dirLength;
245 for (int i = 0; i < props.numRows * props.numCols; i++) {
246 if (!tiles[i].useless) {
247 *filename = 0;
248 snprintf(filename, PATH_MAX, ".%d.rgba16.png", tiles[i].pos);
249 rgba2png(buffer, tiles[i].px, props.tileWidth, props.tileHeight);
254 static unsigned int get_index(TextureTile *t, unsigned int i) {
255 if (t[i].useless) {
256 i = t[i].pos;
258 return t[i].pos;
261 static void print_raw_data(FILE *cFile, TextureTile *tile) {
262 ImageProps props = IMAGE_PROPERTIES[type][true];
263 uint8_t *raw = malloc(props.tileWidth * props.tileHeight * 2);
264 int size = rgba2raw(raw, tile->px, props.tileWidth, props.tileHeight, 16);
265 for (int i = 0; i < size; ++i) {
266 fprintf(cFile, "0x%hhX,", raw[i]);
268 free(raw);
271 static void write_skybox_c() { /* write c data to disc */
272 const ImageProps props = IMAGE_PROPERTIES[type][true];
274 char fBuffer[PATH_MAX] = "";
275 FILE *cFile;
277 if (realpath(output, fBuffer) == NULL) {
278 fprintf(stderr, "err: Could not find find src dir %s", output);
279 exit(EXIT_FAILURE);
282 sprintf(fBuffer, "%s/%s_skybox.c", output, skyboxName);
283 cFile = fopen(fBuffer, "w"); /* reset file */
285 /* setup C file */
287 if (cFile == NULL) {
288 fprintf(stderr, "err: Could not open %s\n", fBuffer);
291 fprintf(cFile, "#include \"sm64.h\"\n\n#include \"make_const_nonconst.h\"\n\n");
293 for (int i = 0; i < props.numRows * props.numCols; i++) {
294 if (!tiles[i].useless) {
295 if (storeNamesOnly) {
296 fprintf(
297 cFile,
298 "ALIGNED8 static const u8 %s_skybox_texture_%05X[] = "
299 "\"textures/skybox_tiles/%s.%d.rgba16\";\n\n",
300 skyboxName, tiles[i].pos, skyboxName, tiles[i].pos
302 } else {
303 fprintf(cFile, "ALIGNED8 static const u8 %s_skybox_texture_%05X[] = {\n", skyboxName, tiles[i].pos);
304 print_raw_data(cFile, &tiles[i]);
305 fputs("};\n\n", cFile);
310 fprintf(cFile, "const u8 *const %s_skybox_ptrlist[] = {\n", skyboxName);
312 for (int row = 0; row < 8; row++) {
313 for (int col = 0; col < 10; col++) {
314 fprintf(cFile, "%s_skybox_texture_%05X,\n", skyboxName, get_index(tiles, row * 8 + (col % 8)));
318 fputs("};\n\n", cFile);
319 fclose(cFile);
322 static void write_cake_c() {
323 char buffer[PATH_MAX] = "";
324 if (realpath(output, buffer) == NULL) {
325 fprintf(stderr, "err: Could not find find src dir %s", output);
326 exit(EXIT_FAILURE);
329 if (type == CakeEU) {
330 strcat(buffer, "/cake_eu.inc.c");
332 else {
333 strcat(buffer, "/cake.inc.c");
336 FILE *cFile = fopen(buffer, "w");
338 const char *euSuffx = "";
339 if (type == CakeEU) {
340 euSuffx = "eu_";
343 int numTiles = TABLE_DIMENSIONS[type].cols * TABLE_DIMENSIONS[type].rows;
344 for (int i = 0; i < numTiles; ++i) {
345 if (storeNamesOnly) {
346 fprintf(
347 cFile,
348 "ALIGNED8 static const u8 cake_end_texture_%s%d[] = "
349 "\"textures/skybox_tiles/cake%s.%d.rgba16\";\n\n",
350 euSuffx, i, *euSuffx ? "_eu" : "", tiles[i].pos
352 } else {
353 fprintf(cFile, "ALIGNED8 static const u8 cake_end_texture_%s%d[] = {\n", euSuffx, i);
354 print_raw_data(cFile, &tiles[i]);
355 fputs("};\n\n", cFile);
358 fclose(cFile);
361 // input: the skybox tiles + the table = up to 64 32x32 images (rgba16) + 80 pointers (u32)
362 // some pointers point to duplicate entries
363 void combine_skybox(const char *input, const char *output) {
364 enum { W = 10, H = 8, W2 = 8 };
366 FILE *file = fopen(input, "rb");
367 if (!file) goto fail;
368 if (fseek(file, 0, SEEK_END)) goto fail;
370 ssize_t fileSize = ftell(file);
371 if (fileSize < 8*10*4) goto fail;
372 rewind(file);
374 size_t tableIndex = fileSize - 8*10*4;
375 if (tableIndex % (32*32*2) != 0) goto fail;
377 // there are at most 64 tiles before the table
378 rgba *tiles[8*8];
379 size_t tileIndex = 0;
380 for (size_t pos = 0; pos < tableIndex; pos += 32*32*2) {
381 uint8_t buf[32*32*2];
382 if (fread(buf, sizeof(buf), 1, file) != 1) goto fail;
383 tiles[tileIndex] = raw2rgba(buf, 32, 32, 16);
384 tileIndex++;
387 uint32_t table[W*H];
388 if (fread(table, sizeof(table), 1, file) != 1) goto fail;
390 #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
391 reverse_endian((unsigned char *) table, W*H*4);
392 #endif
394 uint32_t base = table[0];
395 for (int i = 0; i < W*H; i++) {
396 table[i] -= base;
399 // Convert the 256x256 skybox to an editable 248x248 image by skipping the duplicated rows and columns
400 // every 32nd column is a repeat of the 33rd, and
401 // every 32nd row is a repeat of the 33rd, EXCEPT for the last row, but that only matters when
402 // expanding the tiles
403 rgba combined[31*H * 31*W2];
404 for (int i = 0; i < H; i++) {
405 for (int j = 0; j < W2; j++) {
406 int index = table[i*W+j] / 0x800;
407 for (int y = 0; y < 31; y++) {
408 for (int x = 0; x < 31; x++) {
409 combined[(i*31 + y) * (31*W2) + (j*31 + x)] = tiles[index][y*32 + x];
414 if (!rgba2png(output, combined, 31*W2, 31*H)) {
415 fprintf(stderr, "Failed to write skybox image.\n");
416 exit(1);
418 return;
419 fail:
420 fprintf(stderr, "Failed to read skybox binary.\n");
421 exit(1);
424 void combine_cakeimg(const char *input, const char *output, bool eu) {
425 int W, H, SMALLH, SMALLW;
426 if (eu) {
427 W = 5;
428 H = 7;
429 SMALLH = 32;
430 SMALLW = 64;
431 } else {
432 W = 4;
433 H = 12;
434 SMALLH = 20;
435 SMALLW = 80;
438 FILE *file = fopen(input, "rb");
439 if (!file) goto fail;
441 rgba *combined;
442 if (!eu) {
443 combined = malloc((SMALLH-1)*H * (SMALLW-1)*W * sizeof(rgba));
444 for (int i = 0; i < H; i++) {
445 for (int j = 0; j < W; j++) {
446 //Read the full tile
447 uint8_t buf[SMALLH * SMALLW * 2];
448 if (fread(buf, sizeof(buf), 1, file) != 1) goto fail;
449 rgba *tile = raw2rgba(buf, SMALLH, SMALLW, 16);
451 //Only write the unique parts of each tile
452 for (int y = 0; y < SMALLH - 1; y++) {
453 for (int x = 0; x < SMALLW - 1; x++) {
454 combined[(i*(SMALLH-1) + y) * (SMALLW-1)*W + (j*(SMALLW-1) + x)] = tile[y*(SMALLW) + x];
459 if (!rgba2png(output, combined, (SMALLW-1)*W, (SMALLH-1)*H)) {
460 fprintf(stderr, "Failed to write cake image.\n");
461 exit(1);
464 else {
465 combined = malloc(SMALLH*H * SMALLW*W * sizeof(rgba));
466 for (int i = 0; i < H; i++) {
467 for (int j = 0; j < W; j++) {
468 uint8_t buf[SMALLH * SMALLW * 2];
469 if (fread(buf, sizeof(buf), 1, file) != 1) goto fail;
470 rgba *tile = raw2rgba(buf, SMALLH, SMALLW, 16);
471 for (int y = 0; y < SMALLH; y++) {
472 for (int x = 0; x < SMALLW; x++) {
473 combined[(i*SMALLH + y) * SMALLW*W + (j*SMALLW + x)] = tile[y*SMALLW + x];
478 if (!rgba2png(output, combined, SMALLW*W, SMALLH*H)) {
479 fprintf(stderr, "Failed to write cake image.\n");
480 exit(1);
483 return;
484 fail:
485 fprintf(stderr, "Failed to read cake binary.\n");
486 exit(1);
489 // Modified from n64split
490 static void usage() {
491 fprintf(stderr,
492 "Usage: %s --type sky|cake|cake_eu {--combine INPUT OUTPUT | --split INPUT OUTPUT}\n"
493 "\n"
494 "Optional arguments:\n"
495 " --write-tiles OUTDIR Also create the individual tiles' PNG files\n"
496 " --store-names Store texture file names instead of actual data\n", programName);
499 // Modified from n64split
500 static int parse_arguments(int argc, char *argv[]) {
501 for (int i = 1; i < argc; ++i) {
502 if (strcmp(argv[i], "--combine") == 0) {
503 if (++i >= argc || mode != InvalidMode) {
504 goto invalid;
507 mode = Combine;
508 input = argv[i];
509 if (++i >= argc) {
510 goto invalid;
513 output = argv[i];
516 if (strcmp(argv[i], "--split") == 0) {
517 if (++i >= argc || mode != InvalidMode) {
518 goto invalid;
521 mode = Split;
522 input = argv[i];
523 if (++i >= argc) {
524 goto invalid;
527 output = argv[i];
530 if (strcmp(argv[i], "--type") == 0) {
531 if (++i >= argc || type != InvalidType) {
532 goto invalid;
535 if (strcmp(argv[i], "sky") == 0) {
536 type = Skybox;
537 } else if(strcmp(argv[i], "cake-eu") == 0) {
538 type = CakeEU;
539 } else if(strcmp(argv[i], "cake") == 0) {
540 type = Cake;
544 if (strcmp(argv[i], "--write-tiles") == 0) {
545 if (++i >= argc || argv[i][0] == '-') {
546 goto invalid;
549 writeTiles = true;
550 writeDir = argv[i];
553 if (strcmp(argv[i], "--store-names") == 0) {
554 storeNamesOnly = true;
558 return 1;
559 invalid:
560 usage();
561 return 0;
564 bool imageMatchesDimensions(int width, int height) {
565 bool matchesDimensions = false;
566 for (int expand = false; expand <= true; ++expand) {
567 if (width == IMAGE_PROPERTIES[type][expand].imageWidth &&
568 height == IMAGE_PROPERTIES[type][expand].imageHeight) {
569 matchesDimensions = true;
570 expanded = expand;
571 break;
574 if (!matchesDimensions) {
575 if (type != CakeEU) {
576 fprintf(stderr, "err: That type of image must be either %d x %d or %d x %d. Yours is %d x %d.\n",
577 IMAGE_PROPERTIES[type][false].imageWidth, IMAGE_PROPERTIES[type][false].imageHeight,
578 IMAGE_PROPERTIES[type][true].imageWidth, IMAGE_PROPERTIES[type][true].imageHeight,
579 width, height);
581 else {
582 fprintf(stderr, "err: That type of image must be %d x %d. Yours is %d x %d.\n",
583 IMAGE_PROPERTIES[type][true].imageWidth, IMAGE_PROPERTIES[type][true].imageHeight,
584 width, height);
587 return false;
590 if (type == CakeEU) {
591 expanded = true;
594 return true;
597 int main(int argc, char *argv[]) {
598 if (parse_arguments(argc, argv) == false) {
599 return EXIT_FAILURE;
602 if (type == Skybox && mode == Split) {
603 // Extract the skybox's name (ie: bbh, bidw) from the input png
604 char *base = basename(input);
605 strcpy(skyboxName, base);
606 char *extension = strrchr(skyboxName, '.');
607 if (extension) *extension = '\0';
610 switch (mode) {
611 case Combine:
612 switch (type) {
613 case Skybox:
614 combine_skybox(input, output);
615 break;
616 case Cake:
617 combine_cakeimg(input, output, 0);
618 break;
619 case CakeEU:
620 combine_cakeimg(input, output, 1);
621 break;
622 default:
623 usage();
624 return EXIT_FAILURE;
625 break;
627 break;
629 case Split: {
630 int width, height;
631 rgba *image = png2rgba(input, &width, &height);
632 if (image == NULL) {
633 fprintf(stderr, "err: Could not load image %s\n", argv[1]);
634 return EXIT_FAILURE;
637 if (!imageMatchesDimensions(width, height)) {
638 return EXIT_FAILURE;
641 allocate_tiles();
643 init_tiles(image, expanded);
644 switch (type) {
645 case Skybox:
646 assign_tile_positions();
647 write_skybox_c();
648 break;
649 case Cake:
650 case CakeEU:
651 assign_tile_positions();
652 write_cake_c();
653 break;
654 default:
655 fprintf(stderr, "err: Unknown image type.\n");
656 return EXIT_FAILURE;
657 break;
660 if (writeTiles) {
661 write_tiles();
663 free_tiles();
664 free(image);
665 } break;
666 default:
667 usage();
668 return EXIT_FAILURE;
669 break;
673 return EXIT_SUCCESS;