commit 967b43b549805957c8c20f0cfb591ec76c22dfcd Author: Marco Hladik Date: Wed Aug 25 00:20:01 2021 +0200 dump diff --git a/README b/README new file mode 100644 index 0000000..f875297 --- /dev/null +++ b/README @@ -0,0 +1,38 @@ +Quake Tools + +Each one of them is a single C file that should compile OOTB on any platform. +E.g. gcc -o pal2tga pal2tga.c + +Their usage is pretty consistent - you supply the input files. +PAL2TGA wants a .lmp palette file, TGA2PAL wants a .tga image file - and so on. + +They are all released under the MIT License. Binaries for Win32 are outdated but usable. + +TrueVision Targa seems to be one of the saner image formats. The GIMP handles those fine. +Most advanced editors do... + +PAL2TGA +Converts a Quake palette file into a 24bit TrueVision Targa file for direct editing. +pal2tga.c + +TGA2PAL +Imports a 16x16 24bit Targa (like one generated by PAL2TGA) and converts it into a Quake palette file. +tga2pal.c + +LMP2TGA +Convert Quake graphic lump files (.lmp) into a TrueVision Targa for editing. +Allows use of a custom palette.lmp file for non-standard modifications/games. +Note: colormap.lmp and palette.lmp are no typical graphic lumps - opening them +will most likely result in an error when validating the header. +lmp2tga.c + +TGA2LMP +Converts a 24bit Targa into a Quake graphic lump file. +Allows use of a custom palette.lmp file for non-standard modifications/games. +tga2lmp.c + +TGA2SPR +Converts 24bit Targas into a Quake sprite lump file. +Allows use of a custom palette.lmp file for non-standard modifications/games, like the others... +Arguably the most complex one, README instructions required! +tga2spr.c diff --git a/lmp2tga.c b/lmp2tga.c new file mode 100644 index 0000000..2647f4a --- /dev/null +++ b/lmp2tga.c @@ -0,0 +1,193 @@ +/* +LMP 2 TGA SOURCECODE + +The MIT License (MIT) + +Copyright (c) 2016-2019 Marco "eukara" Hladik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include +#include + +#define TGAHEADER 18 +#define QPALSIZE 768 + +typedef union +{ + int rgba:32; + struct { + unsigned char b:8; + unsigned char g:8; + unsigned char r:8; + unsigned char a:8; + } u; +} palette_t; + +/* Fallback palette from Quake, put into the Public Domain by John Carmack */ +palette_t pal_fallb[256] = { + 0x000000,0x0f0f0f,0x1f1f1f,0x2f2f2f,0x3f3f3f,0x4b4b4b,0x5b5b5b,0x6b6b6b, + 0x7b7b7b,0x8b8b8b,0x9b9b9b,0xababab,0xbbbbbb,0xcbcbcb,0xdbdbdb,0xebebeb, + 0x0f0b07,0x170f0b,0x1f170b,0x271b0f,0x2f2313,0x372b17,0x3f2f17,0x4b371b, + 0x533b1b,0x5b431f,0x634b1f,0x6b531f,0x73571f,0x7b5f23,0x836723,0x8f6f23, + 0x0b0b0f,0x13131b,0x1b1b27,0x272733,0x2f2f3f,0x37374b,0x3f3f57,0x474767, + 0x4f4f73,0x5b5b7f,0x63638b,0x6b6b97,0x7373a3,0x7b7baf,0x8383bb,0x8b8bcb, + 0x000000,0x070700,0x0b0b00,0x131300,0x1b1b00,0x232300,0x2b2b07,0x2f2f07, + 0x373707,0x3f3f07,0x474707,0x4b4b0b,0x53530b,0x5b5b0b,0x63630b,0x6b6b0f, + 0x070000,0x0f0000,0x170000,0x1f0000,0x270000,0x2f0000,0x370000,0x3f0000, + 0x470000,0x4f0000,0x570000,0x5f0000,0x670000,0x6f0000,0x770000,0x7f0000, + 0x131300,0x1b1b00,0x232300,0x2f2b00,0x372f00,0x433700,0x4b3b07,0x574307, + 0x5f4707,0x6b4b0b,0x77530f,0x835713,0x8b5b13,0x975f1b,0xa3631f,0xaf6723, + 0x231307,0x2f170b,0x3b1f0f,0x4b2313,0x572b17,0x632f1f,0x733723,0x7f3b2b, + 0x8f4333,0x9f4f33,0xaf632f,0xbf772f,0xcf8f2b,0xdfab27,0xefcb1f,0xfff31b, + 0x0b0700,0x1b1300,0x2b230f,0x372b13,0x47331b,0x533723,0x633f2b,0x6f4733, + 0x7f533f,0x8b5f47,0x9b6b53,0xa77b5f,0xb7876b,0xc3937b,0xd3a38b,0xe3b397, + 0xab8ba3,0x9f7f97,0x937387,0x8b677b,0x7f5b6f,0x775363,0x6b4b57,0x5f3f4b, + 0x573743,0x4b2f37,0x43272f,0x371f23,0x2b171b,0x231313,0x170b0b,0x0f0707, + 0xbb739f,0xaf6b8f,0xa35f83,0x975777,0x8b4f6b,0x7f4b5f,0x734353,0x6b3b4b, + 0x5f333f,0x532b37,0x47232b,0x3b1f23,0x2f171b,0x231313,0x170b0b,0x0f0707, + 0xdbc3bb,0xcbb3a7,0xbfa39b,0xaf978b,0xa3877b,0x977b6f,0x876f5f,0x7b6353, + 0x6b5747,0x5f4b3b,0x533f33,0x433327,0x372b1f,0x271f17,0x1b130f,0x0f0b07, + 0x6f837b,0x677b6f,0x5f7367,0x576b5f,0x4f6357,0x475b4f,0x3f5347,0x374b3f, + 0x2f4337,0x2b3b2f,0x233327,0x1f2b1f,0x172317,0x0f1b13,0x0b130b,0x070b07, + 0xfff31b,0xefdf17,0xdbcb13,0xcbb70f,0xbba70f,0xab970b,0x9b8307,0x8b7307, + 0x7b6307,0x6b5300,0x5b4700,0x4b3700,0x3b2b00,0x2b1f00,0x1b0f00,0x0b0700, + 0x0000ff,0x0b0bef,0x1313df,0x1b1bcf,0x2323bf,0x2b2baf,0x2f2f9f,0x2f2f8f, + 0x2f2f7f,0x2f2f6f,0x2f2f5f,0x2b2b4f,0x23233f,0x1b1b2f,0x13131f,0x0b0b0f, + 0x2b0000,0x3b0000,0x4b0700,0x5f0700,0x6f0f00,0x7f1707,0x931f07,0xa3270b, + 0xb7330f,0xc34b1b,0xcf632b,0xdb7f3b,0xe3974f,0xe7ab5f,0xefbf77,0xf7d38b, + 0xa77b3b,0xb79b37,0xc7c337,0xe7e357,0x7fbfff,0xabe7ff,0xd7ffff,0x670000, + 0x8b0000,0xb30000,0xd70000,0xff0000,0xfff393,0xfff7c7,0xffffff,0x9f5b53 +}; + +unsigned char cust_pal[QPALSIZE]; /* Custom 256 color palette */ + +void process_lmp2tga(char *filename) +{ + FILE *fLMP; + int lmp_header[2]; /* Resolution of loaded LUMP file */ + unsigned char *lmp_buff; /* Buffer of the LUMP */ + struct stat lmp_st; + FILE *fTGA; + unsigned char *tga_buff; /* 24bit BGA output buffer + TGA header */ + int col, row, done; /* used for the sorting loop */ + + fLMP = fopen(filename, "rb"); + + if (!fLMP) { + fprintf(stderr, "couldn't find %s\n", filename); + return; + } + + stat(filename, &lmp_st); + + if (lmp_st.st_size <= 8) { + fprintf(stderr, "couldn't read %s\n", filename); + return; + } + + fread(lmp_header, sizeof(int) * 2, 1, fLMP); + + /* Other types of lumps (e.g. palette.lmp) don't have a header, skip them */ + if (lmp_st.st_size != (lmp_header[0] * lmp_header[1] + 8)) { + fprintf(stderr, "incomplete lump %s, skipping\n", filename); + return; + } + + lmp_buff = malloc(lmp_header[0] * lmp_header[1]); + fseek(fLMP, sizeof(int) * 2, SEEK_SET); + fread(lmp_buff, 1, lmp_header[0] * lmp_header[1], fLMP); + fclose(fLMP); + + /* Allocate enough memory for the header + buffer of the TARGA */ + tga_buff = malloc((lmp_header[0] * lmp_header[1] * 3) + TGAHEADER); + + memset (tga_buff, 0, 18); + tga_buff[2] = 2; /* Uncompressed TARGA */ + tga_buff[12] = lmp_header[0] & 0xFF; /* Width */ + tga_buff[13] = lmp_header[0] >> 8; + tga_buff[14] = lmp_header[1] & 0xFF; /* Height */ + tga_buff[15] = lmp_header[1] >> 8; + tga_buff[16] = 24; /* Color depth */ + + /* TARGAs are flipped in a messy way, + * so we gotta do some sorting magic (vertical flip) */ + done = 0; /* readability */ + + for (row = lmp_header[1] - 1; row >= 0; row--) { + for (col = 0; col < lmp_header[0]; col++) { + tga_buff[18 + ((row * (lmp_header[0] * 3)) + (col * 3 + 0))] = + cust_pal[lmp_buff[done] * 3 + 2]; + + tga_buff[18 + ((row * (lmp_header[0] * 3)) + (col * 3 + 1))] = + cust_pal[lmp_buff[done] * 3 + 1]; + + tga_buff[18 + ((row * (lmp_header[0] * 3)) + (col * 3 + 2))] = + cust_pal[lmp_buff[done] * 3 + 0]; + done++; + } + } + + /* FIXME: We assume too much! + * Save the output to FILENAME.tga + * This is ugly when the input name has no .lmp extension. + * But who's going to try that. ...right? */ + filename[strlen(filename)-3] = 't'; + filename[strlen(filename)-2] = 'g'; + filename[strlen(filename)-1] = 'a'; + fprintf(stdout, "writing %s\n", filename); + fTGA = fopen(filename, "w+b"); + fwrite(tga_buff, 1, (lmp_header[0] * lmp_header[1] * 3) + TGAHEADER, fTGA); + fclose(fTGA); +} + +int main(int argc, char *argv[]) +{ + int c; + short p; + FILE *fPAL; + + if (argc <= 1) { + fprintf(stderr, "usage: lmp2tga [file ...]\n"); + return 1; + } + + fPAL = fopen("palette.lmp", "rb"); + + if (!fPAL) { + fprintf(stdout, "no palette.lmp found, using builtin palette.\n"); + for (p = 0; p < 256; p++) { + cust_pal[p * 3 + 0] = pal_fallb[p].u.r; + cust_pal[p * 3 + 1] = pal_fallb[p].u.g; + cust_pal[p * 3 + 2] = pal_fallb[p].u.b; + } + } else { + fprintf(stdout, "custom palette.lmp found\n"); + fread(cust_pal, 1, QPALSIZE, fPAL); + fclose(fPAL); + } + + for (c = 1; c < argc; c++) + process_lmp2tga(argv[c]); + + return 0; +} diff --git a/pal2tga.c b/pal2tga.c new file mode 100644 index 0000000..bfb6a0b --- /dev/null +++ b/pal2tga.c @@ -0,0 +1,106 @@ +/* +PAL 2 TGA SOURCECODE + +The MIT License (MIT) + +Copyright (c) 2016-2019 Marco "eukara" Hladik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include + +#define TGAHEADER 18 +#define QPALSIZE 768 +#define T2PVERSION "1.1" + +void process_pal2tga(char *filename) +{ + FILE *fLMP; + unsigned char pal_buff[QPALSIZE]; + FILE *fTGA; + unsigned char tga_buff[QPALSIZE + TGAHEADER]; + short pal_loop, tga_loop; + struct stat pal_st; + + fLMP = fopen(filename, "rb"); + + if (!fLMP) { + fprintf(stderr, "couldn't find %s\n", filename); + return; + } + + /* There are no other types of palette lumps. Sorry */ + stat(filename, &pal_st); + if (pal_st.st_size != QPALSIZE) { + fprintf(stderr, "invalid palette lump %s, skipping\n", filename); + return; + } + + fread(pal_buff, 1, QPALSIZE, fLMP); + fclose(fLMP); + + memset(tga_buff, 0, 18); + tga_buff[2] = 2; /* Uncompressed TARGA */ + tga_buff[12] = 16; /* Width */ + tga_buff[14] = 16; /* Height */ + tga_buff[16] = 24; /* Color depth */ + + /* TARGAs are flipped in an odd way, + * so we gotta do some sorting magic (vertical flip) */ + for (tga_loop = 15; tga_loop >= 0; tga_loop--) { + for (pal_loop = 0; pal_loop < 16; pal_loop++) { + tga_buff[18 + (tga_loop * 48) + (pal_loop * 3) + 0] = + pal_buff[((15 - tga_loop) * 48) + (pal_loop * 3) + 2]; + tga_buff[18 + (tga_loop * 48) + (pal_loop * 3) + 1] = + pal_buff[((15 - tga_loop) * 48) + (pal_loop * 3) + 1]; + tga_buff[18 + (tga_loop * 48) + (pal_loop * 3) + 2] = + pal_buff[((15 - tga_loop) * 48) + (pal_loop * 3) + 0]; + } + } + + /* FIXME: We assume too much! + * Save the output to FILENAME.tga + * This is ugly when the input name has no .lmp extension. + * But who's going to try that. ...right? */ + filename[strlen(filename)-3] = 't'; + filename[strlen(filename)-2] = 'g'; + filename[strlen(filename)-1] = 'a'; + fprintf(stdout, "writing %s\n", filename); + fTGA = fopen(filename, "w+b"); + fwrite(tga_buff, 1, QPALSIZE + TGAHEADER, fTGA); + fclose(fTGA); +} + +int main(int argc, char *argv[]) +{ + int c; + + if (argc <= 1) { + fprintf(stderr, "usage: pal2tga [file.lmp ...]\n"); + return 1; + } + + for (c = 1; c < argc; c++) + process_pal2tga(argv[c]); + + return 0; +} diff --git a/tga2lmp.c b/tga2lmp.c new file mode 100644 index 0000000..b4c2d8e --- /dev/null +++ b/tga2lmp.c @@ -0,0 +1,262 @@ +/* +TGA 2 LMP SOURCECODE + +The MIT License (MIT) + +Copyright (c) 2016-2019 Marco "eukara" Hladik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include +#include +#include + +#define TGAHEADER 18 +#define QPALSIZE 768 +#define VERSION "1.1" + +typedef unsigned char byte; + +typedef union { + int rgba:32; + struct { + byte b:8; + byte g:8; + byte r:8; + byte a:8; + } u; +} pixel_t; + +byte cust_pal[QPALSIZE]; /* custom 256 color palette */ + +/* fallback palette from Quake, put into the Public Domain by John Carmack */ +pixel_t pal_fallb[256] = { + 0x000000,0x0f0f0f,0x1f1f1f,0x2f2f2f,0x3f3f3f,0x4b4b4b,0x5b5b5b,0x6b6b6b, + 0x7b7b7b,0x8b8b8b,0x9b9b9b,0xababab,0xbbb,0xcbcbcb,0xdbdbdb,0xebebeb, + 0x0f0b07,0x170f0b,0x1f170b,0x271b0f,0x2f2313,0x372b17,0x3f2f17,0x4b371b, + 0x533b1b,0x5b431f,0x634b1f,0x6b531f,0x73571f,0x7b5f23,0x836723,0x8f6f23, + 0x0b0b0f,0x13131b,0x1b1b27,0x272733,0x2f2f3f,0x37374b,0x3f3f57,0x474767, + 0x4f4f73,0x5b5b7f,0x63638b,0x6b6b97,0x7373a3,0x7b7baf,0x8383b,0x8b8bcb, + 0x000000,0x070700,0x0b0b00,0x131300,0x1b1b00,0x232300,0x2b2b07,0x2f2f07, + 0x373707,0x3f3f07,0x474707,0x4b4b0b,0x53530b,0x5b5b0b,0x63630b,0x6b6b0f, + 0x070000,0x0f0000,0x170000,0x1f0000,0x270000,0x2f0000,0x370000,0x3f0000, + 0x470000,0x4f0000,0x570000,0x5f0000,0x670000,0x6f0000,0x770000,0x7f0000, + 0x131300,0x1b1b00,0x232300,0x2f2b00,0x372f00,0x433700,0x4b3b07,0x574307, + 0x5f4707,0x6b4b0b,0x77530f,0x835713,0x8b5b13,0x975f1b,0xa3631f,0xaf6723, + 0x231307,0x2f170b,0x3b1f0f,0x4b2313,0x572b17,0x632f1f,0x733723,0x7f3b2b, + 0x8f4333,0x9f4f33,0xaf632f,0xbf772f,0xcf8f2b,0xdfab27,0xefcb1f,0xfff31b, + 0x0b0700,0x1b1300,0x2b230f,0x372b13,0x47331b,0x533723,0x633f2b,0x6f4733, + 0x7f533f,0x8b5f47,0x9b6b53,0xa77b5f,0xb7876b,0xc3937b,0xd3a38b,0xe3b397, + 0xab8ba3,0x9f7f97,0x937387,0x8b677b,0x7f5b6f,0x775363,0x6b4b57,0x5f3f4b, + 0x573743,0x4b2f37,0x43272f,0x371f23,0x2b171b,0x231313,0x170b0b,0x0f0707, + 0xb739f,0xaf6b8f,0xa35f83,0x975777,0x8b4f6b,0x7f4b5f,0x734353,0x6b3b4b, + 0x5f333f,0x532b37,0x47232b,0x3b1f23,0x2f171b,0x231313,0x170b0b,0x0f0707, + 0xdbc3b,0xcb3a7,0xbfa39b,0xaf978b,0xa3877b,0x977b6f,0x876f5f,0x7b6353, + 0x6b5747,0x5f4b3b,0x533f33,0x433327,0x372b1f,0x271f17,0x1b130f,0x0f0b07, + 0x6f837b,0x677b6f,0x5f7367,0x576b5f,0x4f6357,0x475b4f,0x3f5347,0x374b3f, + 0x2f4337,0x2b3b2f,0x233327,0x1f2b1f,0x172317,0x0f1b13,0x0b130b,0x070b07, + 0xfff31b,0xefdf17,0xdbcb13,0xcb70f,0xba70f,0xab970b,0x9b8307,0x8b7307, + 0x7b6307,0x6b5300,0x5b4700,0x4b3700,0x3b2b00,0x2b1f00,0x1b0f00,0x0b0700, + 0x0000ff,0x0b0bef,0x1313df,0x1b1bcf,0x2323bf,0x2b2baf,0x2f2f9f,0x2f2f8f, + 0x2f2f7f,0x2f2f6f,0x2f2f5f,0x2b2b4f,0x23233f,0x1b1b2f,0x13131f,0x0b0b0f, + 0x2b0000,0x3b0000,0x4b0700,0x5f0700,0x6f0f00,0x7f1707,0x931f07,0xa3270b, + 0xb7330f,0xc34b1b,0xcf632b,0xdb7f3b,0xe3974f,0xe7ab5f,0xefbf77,0xf7d38b, + 0xa77b3b,0xb79b37,0xc7c337,0xe7e357,0x7fbfff,0xabe7ff,0xd7ffff,0x670000, + 0x8b0000,0xb30000,0xd70000,0xff0000,0xfff393,0xfff7c7,0xffffff,0x9f5b53 +}; + +byte c_last; /* last palette index we chose */ +pixel_t last_px; /* last RGB value we chose */ + +/* quickly translate 24 bit RGB value to our palette */ +byte pal24to8(byte r, byte g, byte b) +{ + pixel_t px; + byte c_red, c_green, c_blue, c_best, l; + int dist, best; + + /* compare the last with the current pixel color for speed */ + if ((last_px.u.r == r) && (last_px.u.g == g) && (last_px.u.b == b)) { + return c_last; + } + + px.u.r = last_px.u.r = r; + px.u.g = last_px.u.g = g; + px.u.b = last_px.u.b = b; + + best = 255 + 255 + 255; + c_last = c_best = c_red = c_green = c_blue = 255; + l = 0; + + while (1) { + if ((cust_pal[l * 3 + 0] == r) && (cust_pal[l * 3 + 0] == g) + && (cust_pal[l * 3 + 0] == b)) + { + last_px.u.r = cust_pal[l * 3 + 0]; + last_px.u.g = cust_pal[l * 3 + 1]; + last_px.u.b = cust_pal[l * 3 + 2]; + c_last = l; + return l; + } + + c_red = abs(cust_pal[l * 3 + 0] - px.u.r); + c_green = abs(cust_pal[l * 3 + 1] - px.u.g); + c_blue = abs(cust_pal[l * 3 + 2] - px.u.b); + dist = (c_red + c_green + c_blue); + + /* is it better than the last? */ + if (dist < best) { + best = dist; + c_best = l; + } + + if (l != 255) { + l++; + } else { + break; + } + } + + c_last = c_best; + return c_best; +} + +void process_tga2lmp(char *filename) +{ + FILE *fLMP; /* file Ident of the LUMP */ + byte *lmp_buff; /* buffer of the LUMP */ + FILE *fTGA; /* file Ident of the TARGA */ + byte tga_header[TGAHEADER]; /* TARGA Header (18 bytes usually) */ + byte *tga_buff; /* 24bit gR input buffer + TGA header */ + int col, row, done; /* used for the sorting loop */ + int img_w, img_h; /* dimensions */ + + /* load the TARGA */ + fTGA = fopen(filename, "rb"); + + /* check whether the file exists or not */ + if (!fTGA) { + fprintf(stderr, "couldn't find %s\n", filename); + return; + } + + /* put the TARGA header into the buffer for validation */ + fread(tga_header, 1, TGAHEADER, fTGA); + + /* only allow uncompressed, 24bit TARGAs */ + if (tga_header[2] != 2) { + fprintf(stderr, "%s should be an uncompressed, RGB image\n", filename); + return; + } + if (tga_header[16] != 24) { + fprintf(stderr, "%s is not 24 bit in depth\n", filename); + return; + } + + /* read the resolution into an int (TGA uses shorts for the dimensions) */ + img_w = (tga_header[12]) | (tga_header[13] << 8); + img_h = (tga_header[14]) | (tga_header[15] << 8); + tga_buff = malloc(img_w * img_h * 3); + + if (tga_buff == NULL) { + fprintf(stderr, "mem alloc failed at %d bytes\n", (img_w * img_h * 3)); + return; + } + + /* skip to after the TARGA HEADER... and then read the buffer */ + fseek(fTGA, TGAHEADER, SEEK_SET); + fread(tga_buff, 1, img_w * img_h * 3, fTGA); + fclose(fTGA); + + /* start generating the lump data */ + lmp_buff = malloc(img_w * img_h + 8); + + if (lmp_buff == NULL) { + fprintf(stderr, "mem alloc failed at %d bytes\n", (img_w * img_h + 8)); + return; + } + + /* split the integer dimensions into 4 bytes */ + lmp_buff[3] = (img_w >> 24) & 0xFF; + lmp_buff[2] = (img_w >> 16) & 0xFF; + lmp_buff[1] = (img_w >> 8) & 0xFF; + lmp_buff[0] = img_w & 0xFF; + lmp_buff[7] = (img_h >> 24) & 0xFF; + lmp_buff[6] = (img_h >> 16) & 0xFF; + lmp_buff[5] = (img_h >> 8) & 0xFF; + lmp_buff[4] = img_h & 0xFF; + + /* translate the rgb values into indexed entries and flip */ + done = 0; + for (row = img_h - 1; row >= 0; row--) { + for (col = 0; col < img_w; col++) { + lmp_buff[8 + done] = + pal24to8(tga_buff[((row * (img_w * 3)) + (col * 3 + 2))], + tga_buff[((row * (img_w * 3)) + (col * 3 + 1))], + tga_buff[((row * (img_w * 3)) + (col * 3 + 0))]); + done++; + } + } + + /* FIXME: We assume too much! + * Save the output to FILENAME.tga + * This is ugly when the input name has no .lmp extension. + * But who's going to try that. ...right? */ + filename[strlen(filename)-3] = 'l'; + filename[strlen(filename)-2] = 'm'; + filename[strlen(filename)-1] = 'p'; + fprintf(stdout, "writing %s\n", filename); + fLMP = fopen(filename, "w+b"); + fwrite(lmp_buff, 1, (img_w * img_h) + 8, fLMP); + fclose(fLMP); +} + +int main(int argc, char *argv[]) +{ + int c; + short p; + FILE *fPAL; + + if (argc <= 1) { + fprintf(stderr, "usage: tga2lmp [file.tga ...]\n"); + return 1; + } + + fPAL = fopen("palette.lmp", "rb"); + + if (!fPAL) { + fprintf(stdout, "no palette.lmp found, using builtin palette.\n"); + for (p = 0; p < 256; p++) { + cust_pal[p * 3 + 0] = pal_fallb[p].u.r; + cust_pal[p * 3 + 1] = pal_fallb[p].u.g; + cust_pal[p * 3 + 2] = pal_fallb[p].u.b; + } + } else { + fprintf(stdout, "custom palette.lmp found\n"); + fread(cust_pal, 1, QPALSIZE, fPAL); + fclose(fPAL); + } + + for (c = 1; c < argc; c++) + process_tga2lmp(argv[c]); + + return 0; +} diff --git a/tga2pal.c b/tga2pal.c new file mode 100644 index 0000000..362be23 --- /dev/null +++ b/tga2pal.c @@ -0,0 +1,109 @@ +/* +TGA 2 PAL SOURCECODE + +The MIT License (MIT) + +Copyright (c) 2016-2019 Marco "eukara" Hladik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include +#include + +#define TGAHEADER 18 +#define QPALSIZE 768 + +void process_tga2pal(char *filename) +{ + FILE *fTGA; + unsigned char tga_header[TGAHEADER]; + unsigned char tga_buff[QPALSIZE]; + FILE *fLMP; + unsigned char pal_buff[QPALSIZE]; + short loop_pal, loop_tga; + + fTGA = fopen(filename, "rb"); + if (!fTGA) { + fprintf(stderr, "couldn't find %s\n", filename); + return; + } + + /* Put the TARGA header into the buffer for validation */ + fread(tga_header, 1, TGAHEADER, fTGA); + + /* only allow uncompressed, 24bit TARGAs */ + if (tga_header[2] != 2) { + fprintf(stderr, "%s should be an uncompressed, RGB image\n", filename); + return; + } + if (tga_header[16] != 24) { + fprintf(stderr, "%s is not 24 bit in depth\n", filename); + return; + } + if (tga_header[12] != 16 || tga_header[14] != 16) { + fprintf(stderr, "%s is not a 16x16 image\n", filename); + return; + } + + /* Skip to after the TARGA HEADER... and then read the buffer */ + fseek(fTGA, 18, SEEK_SET); + fread(tga_buff, 1, QPALSIZE, fTGA); + fclose(fTGA); + + /* TARGAs are flipped in an odd way, + * so we gotta do the sorting dance */ + for(loop_tga = 15; loop_tga >= 0; loop_tga--) { + for(loop_pal = 0; loop_pal < 16; loop_pal++) { + pal_buff[((15 - loop_tga) * 48) + (loop_pal * 3) + 0] = + tga_buff[(loop_tga * 48) + (loop_pal * 3) + 2]; + pal_buff[((15 - loop_tga) * 48) + (loop_pal * 3) + 1] = + tga_buff[(loop_tga * 48) + (loop_pal * 3) + 1]; + pal_buff[((15 - loop_tga) * 48) + (loop_pal * 3) + 2] = + tga_buff[(loop_tga * 48) + (loop_pal * 3) + 0]; + } + } + + /* FIXME: We assume too much! + * Save the output to FILENAME.tga + * This is ugly when the input name has no .lmp extension. + * But who's going to try that. ...right? */ + filename[strlen(filename)-3] = 'l'; + filename[strlen(filename)-2] = 'm'; + filename[strlen(filename)-1] = 'p'; + fprintf(stdout, "writing %s\n", filename); + fLMP = fopen(filename, "w+b"); + fwrite(pal_buff, 1, QPALSIZE, fLMP); + fclose(fLMP); +} + +int main(int argc, char *argv[]) +{ + int c; + + if (argc <= 1) { + fprintf(stderr, "usage: tga2pal [file.tga ...]\n"); + return 1; + } + + for (c = 1; c < argc; c++) + process_tga2pal(argv[c]); + + return 0; +} diff --git a/tga2spr.c b/tga2spr.c new file mode 100644 index 0000000..1642b7f --- /dev/null +++ b/tga2spr.c @@ -0,0 +1,535 @@ +/* +TGA 2 SPR SOURCECODE + +The MIT License (MIT) + +Copyright (c) 2016-2019 Marco "eukara" Hladik + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +/* +This tool converts 24-bit, uncompressed Targa files into Quake sprites. +You just have to give it a few more infos. + +Syntax: +.\tga2spr sprite.qc + +In which "sprite.qc" is a plaintext file containing a number of commands. +Notice that you can also pop in a palette.lmp from whatever mod you're +targetting into the same directory. By default it uses Quake's palette. +It will not add any dithering. +If you want any dithering during palettization, use an external tool. +The GNU Image Manipulation Program provides that feature. +Just remember to export it as a 24-bit image again. + +================ +LIST OF COMMANDS +================ +output [STRING] + Specifies the output sprite. E.g. flame.spr +identifer [CHARS] + Specifies the magic number. In case you use some modded engine that allows + that. Default: IDSP +version [INT] + Specifies the sprite version. Default 1. +type [INT] + Specifies the sprite type. Used for orientation. Default is 0. + 0: parallel upright + 1: facing upright + 2: parallel + 3: oriented + 4: parallel oriented +radius [FLOAT] + Default is 1.0. Not sure if even used. +synctype [INT] + Default is 0. If not 0, animation starts at random offsets. +maxwidth [INT] + Used for the visible bounding box. Use the width value of the largest frame. +maxheight [INT] + Used for the visible bounding box. Use the height value of the largest + frame. +reserved [INT] + Unused in default Quake. Kurok uses this I believe. Default is 0. +frame [TGANAME] [OFFSET X] [OFFSET Y] + Single frame. Loads for [TGANAME] (e.g. flame1.tga) and specifies an offset + in INT form (X and Y) +anim [TGATITLE] [NUMFRAMES] [OFFSET X] [OFFSET Y] [...FPS*FRAME] + An entire animationgroup. TGATITLE is the Targa name without extension. + E.g. if you specify "flame" it will look for flame_1.tga, flame_2.tga and so + on. + You then specify the number of frames and offset of that group and a + frame-delay for each frame in the animation. Play with it. + Keep in mind that some engines ignore variable framerates in sprites. + +You don't have to use ALL available parameters. +For example you have a sprite that's 64x64 in size. +3 Targa images, One is static (wow.tga) and the two others are meant to be an +animation sequence (face_1.tga, face_2.tga). + +Then you'd have this: + +output test.spr +maxwidth 64 +maxheight 64 +frame wow 0 0 +anim face 2 0 0 8 5 + +For every frame inside the anim parameter, you should append a FPS number. +Otherwise a value of 1 is assumed for each frame. +In the above exmaple, face_1 will skip to the next at 8 fps, while the +other will do so at 5 fps. + +Despite the somewhat in-complete implementation of the SPR spec in most engines. +I hope this tool will be of use to somebody. + +Use the program as-is. If you notice any glaring problems feel free +to mail me (marco at icculus dot org) and we can talk about them. + +Due to the way it's written, it doesn't allocate much memory, it streams it from +the input right to the output. This is by design. This way you can export large +sprites (gigabytes worth) even on DOS. Use at your own risk. +*/ + +#include +#include +#include +#include + +#define TGAHEADER 18 +#define QPALSIZE 768 + +typedef struct { + char identifer[4]; + long version; + long sprtype; + float radius; + long max_width; + long max_height; + long total_frames; + long unused; + long synctype; +} spr_header_t; + +typedef unsigned char byte; + +typedef union { + int rgba:32; + struct { + byte b:8; + byte g:8; + byte r:8; + byte a:8; + } u; +} pixel_t; + +byte cust_pal[QPALSIZE]; /* custom 256 color palette */ + +/* fallback palette from Quake, put into the Public Domain by John Carmack */ +pixel_t pal_fallb[256] = { + 0x000000,0x0f0f0f,0x1f1f1f,0x2f2f2f,0x3f3f3f,0x4b4b4b,0x5b5b5b,0x6b6b6b, + 0x7b7b7b,0x8b8b8b,0x9b9b9b,0xababab,0xbbb,0xcbcbcb,0xdbdbdb,0xebebeb, + 0x0f0b07,0x170f0b,0x1f170b,0x271b0f,0x2f2313,0x372b17,0x3f2f17,0x4b371b, + 0x533b1b,0x5b431f,0x634b1f,0x6b531f,0x73571f,0x7b5f23,0x836723,0x8f6f23, + 0x0b0b0f,0x13131b,0x1b1b27,0x272733,0x2f2f3f,0x37374b,0x3f3f57,0x474767, + 0x4f4f73,0x5b5b7f,0x63638b,0x6b6b97,0x7373a3,0x7b7baf,0x8383b,0x8b8bcb, + 0x000000,0x070700,0x0b0b00,0x131300,0x1b1b00,0x232300,0x2b2b07,0x2f2f07, + 0x373707,0x3f3f07,0x474707,0x4b4b0b,0x53530b,0x5b5b0b,0x63630b,0x6b6b0f, + 0x070000,0x0f0000,0x170000,0x1f0000,0x270000,0x2f0000,0x370000,0x3f0000, + 0x470000,0x4f0000,0x570000,0x5f0000,0x670000,0x6f0000,0x770000,0x7f0000, + 0x131300,0x1b1b00,0x232300,0x2f2b00,0x372f00,0x433700,0x4b3b07,0x574307, + 0x5f4707,0x6b4b0b,0x77530f,0x835713,0x8b5b13,0x975f1b,0xa3631f,0xaf6723, + 0x231307,0x2f170b,0x3b1f0f,0x4b2313,0x572b17,0x632f1f,0x733723,0x7f3b2b, + 0x8f4333,0x9f4f33,0xaf632f,0xbf772f,0xcf8f2b,0xdfab27,0xefcb1f,0xfff31b, + 0x0b0700,0x1b1300,0x2b230f,0x372b13,0x47331b,0x533723,0x633f2b,0x6f4733, + 0x7f533f,0x8b5f47,0x9b6b53,0xa77b5f,0xb7876b,0xc3937b,0xd3a38b,0xe3b397, + 0xab8ba3,0x9f7f97,0x937387,0x8b677b,0x7f5b6f,0x775363,0x6b4b57,0x5f3f4b, + 0x573743,0x4b2f37,0x43272f,0x371f23,0x2b171b,0x231313,0x170b0b,0x0f0707, + 0xb739f,0xaf6b8f,0xa35f83,0x975777,0x8b4f6b,0x7f4b5f,0x734353,0x6b3b4b, + 0x5f333f,0x532b37,0x47232b,0x3b1f23,0x2f171b,0x231313,0x170b0b,0x0f0707, + 0xdbc3b,0xcb3a7,0xbfa39b,0xaf978b,0xa3877b,0x977b6f,0x876f5f,0x7b6353, + 0x6b5747,0x5f4b3b,0x533f33,0x433327,0x372b1f,0x271f17,0x1b130f,0x0f0b07, + 0x6f837b,0x677b6f,0x5f7367,0x576b5f,0x4f6357,0x475b4f,0x3f5347,0x374b3f, + 0x2f4337,0x2b3b2f,0x233327,0x1f2b1f,0x172317,0x0f1b13,0x0b130b,0x070b07, + 0xfff31b,0xefdf17,0xdbcb13,0xcb70f,0xba70f,0xab970b,0x9b8307,0x8b7307, + 0x7b6307,0x6b5300,0x5b4700,0x4b3700,0x3b2b00,0x2b1f00,0x1b0f00,0x0b0700, + 0x0000ff,0x0b0bef,0x1313df,0x1b1bcf,0x2323bf,0x2b2baf,0x2f2f9f,0x2f2f8f, + 0x2f2f7f,0x2f2f6f,0x2f2f5f,0x2b2b4f,0x23233f,0x1b1b2f,0x13131f,0x0b0b0f, + 0x2b0000,0x3b0000,0x4b0700,0x5f0700,0x6f0f00,0x7f1707,0x931f07,0xa3270b, + 0xb7330f,0xc34b1b,0xcf632b,0xdb7f3b,0xe3974f,0xe7ab5f,0xefbf77,0xf7d38b, + 0xa77b3b,0xb79b37,0xc7c337,0xe7e357,0x7fbfff,0xabe7ff,0xd7ffff,0x670000, + 0x8b0000,0xb30000,0xd70000,0xff0000,0xfff393,0xfff7c7,0xffffff,0x9f5b53 +}; + +byte c_last; /* last palette index we chose */ +pixel_t last_px; /* last RGB value we chose */ + +/* quickly translate 24 bit RGB value to our palette */ +byte pal24to8(byte r, byte g, byte b) +{ + pixel_t px; + byte c_red, c_green, c_blue, c_best, l; + int dist, best; + + /* compare the last with the current pixel color for speed */ + if ((last_px.u.r == r) && (last_px.u.g == g) && (last_px.u.b == b)) { + return c_last; + } + + px.u.r = last_px.u.r = r; + px.u.g = last_px.u.g = g; + px.u.b = last_px.u.b = b; + + best = 255 + 255 + 255; + c_last = c_best = c_red = c_green = c_blue = 255; + l = 0; + + while (1) { + if ((cust_pal[l * 3 + 0] == r) && (cust_pal[l * 3 + 0] == g) + && (cust_pal[l * 3 + 0] == b)) + { + last_px.u.r = cust_pal[l * 3 + 0]; + last_px.u.g = cust_pal[l * 3 + 1]; + last_px.u.b = cust_pal[l * 3 + 2]; + c_last = l; + return l; + } + + c_red = abs(cust_pal[l * 3 + 0] - px.u.r); + c_green = abs(cust_pal[l * 3 + 1] - px.u.g); + c_blue = abs(cust_pal[l * 3 + 2] - px.u.b); + dist = (c_red + c_green + c_blue); + + /* is it better than the last? */ + if (dist < best) { + best = dist; + c_best = l; + } + + if (l != 255) { + l++; + } else { + break; + } + } + + c_last = c_best; + return c_best; +} + +void parse_targa(FILE *fdesc, char *tga_filename, int ofs_x, int ofs_y) +{ + FILE *fTGA; /* File Ident of the TARGA */ + byte tga_header[TGAHEADER]; /* TARGA Header (18 bytes usually) */ + byte *tga_buff; /* 24bit BGR input buffer + TGA header */ + int col, row, done; /* used for the sorting loop */ + int img_w, img_h; /* Dimensions */ + byte bTemp; + + fTGA = fopen(tga_filename, "rb"); + + /* Check whether the file exists or not */ + if (!fTGA) { + fprintf(stderr, "couldn't find %s\n", tga_filename); + exit(0); + } + + /* Put the TARGA header into the buffer for validation */ + fread(tga_header, 1, TGAHEADER, fTGA); + + /* only allow uncompressed, 24bit TARGAs */ + if (tga_header[2] != 2) { + fprintf(stderr, "%s should be an uncompressed image\n", tga_filename); + return; + } + if (tga_header[16] != 24) { + fprintf(stderr, "%s is not 24 bit in depth\n", tga_filename); + return; + } + + /* Read the resolution into an int (TGA uses shorts for the dimensions) */ + img_w = (tga_header[12]) | (tga_header[13] << 8); + img_h = (tga_header[14]) | (tga_header[15] << 8); + + printf("\tframe image size: %d x %d\n", img_w, img_h); + tga_buff = malloc(img_w * img_h * 3); + + if (tga_buff == NULL) { + fprintf(stderr, "error: Memory allocation failed. Exiting.\n"); + exit(0); + } + + /* Skip to after the TARGA HEADER... and then read the buffer */ + fseek(fTGA, TGAHEADER, SEEK_SET); + fread(tga_buff, 1, img_w * img_h * 3, fTGA); + fclose(fTGA); + + /* Let's write the sprite */ + fwrite(&ofs_x, 1, 4, fdesc); + fwrite(&ofs_y, 1, 4, fdesc); + fwrite(&img_w, 1, 4, fdesc); + fwrite(&img_h, 1, 4, fdesc); + + /* Translate the BGR values into indexed entries and flip */ + done = 0; + for (row = img_h - 1; row >= 0; row--) { + for (col = 0; col < img_w; col++) { + bTemp = pal24to8(tga_buff[((row * (img_w * 3)) + (col * 3 + 2))], + tga_buff[((row * (img_w * 3)) + (col * 3 + 1))], + tga_buff[((row * (img_w * 3)) + (col * 3 + 0))]); + fwrite(&bTemp, 1, 1, fdesc); + done++; + } + } + free(tga_buff); +} + +void process_qc(char *filename) +{ + FILE *spr_file; + FILE *qc_file; + spr_header_t spr_header; + char qcline[255]; + char out_filename[32]; + char tga_filename[64]; + char *tok; + int ofs_x; + int ofs_y; + long single; + float fps; + int num_frames; + int i; + + /* Default values */ + spr_header.identifer[0] = 'I'; + spr_header.identifer[1] = 'D'; + spr_header.identifer[2] = 'S'; + spr_header.identifer[3] = 'P'; + spr_header.version = 1; + spr_header.sprtype = 0; + spr_header.radius = 1.0f; + spr_header.max_width = 0; + spr_header.max_height = 0; + spr_header.total_frames = 0; + spr_header.unused = 0; + spr_header.synctype = 0; + + if ((qc_file = fopen(filename, "r")) == NULL) { + fprintf(stderr, "cannot find %s\n", filename); + return; + } + + fprintf(stdout, "processing control file %s\n", filename); + + /* Read it the first time to cache the header, get the amount of frames */ + while (fgets(qcline, sizeof(qcline), qc_file) != NULL) { + const char* headercmd = strtok(qcline, " "); + + if (strcmp(headercmd , "output") == 0) { + strcpy(out_filename, strtok(NULL, " \n")); + } else if (strcmp(headercmd , "identifer") == 0) { + const char* cIdent = strtok(NULL, " \n"); + spr_header.identifer[0] = cIdent[0]; + spr_header.identifer[1] = cIdent[1]; + spr_header.identifer[2] = cIdent[2]; + spr_header.identifer[3] = cIdent[3]; + } else if (strcmp(headercmd , "version") == 0) { + spr_header.version = atoi(strtok(NULL, " \n")); + } else if (strcmp(headercmd , "type") == 0) { + spr_header.sprtype = atoi(strtok(NULL, " \n")); + } else if (strcmp(headercmd , "radius") == 0) { + spr_header.radius = (float)atof(strtok(NULL, " \n")); + } else if (strcmp(headercmd , "synctype") == 0) { + spr_header.synctype = atoi(strtok(NULL, " \n")); + } else if (strcmp(headercmd , "maxwidth") == 0) { + spr_header.max_width = atoi(strtok(NULL, " \n")); + } else if (strcmp(headercmd , "maxheight") == 0) { + spr_header.max_height = atoi(strtok(NULL, " \n")); + } else if (strcmp(headercmd , "reserved") == 0) { + spr_header.unused = atoi(strtok(NULL, " \n")); + } else if (strcmp(headercmd , "frame") == 0) { + spr_header.total_frames++; + } else if (strcmp(headercmd , "anim") == 0) { + spr_header.total_frames++; + } + } + + /* Go back to the start to start parsing the frames properly */ + fseek(qc_file, 0, SEEK_SET); + + /* Too lazy */ + if (out_filename != NULL) { + spr_file = fopen(out_filename, "w+b"); + } else { + spr_file = fopen("output.spr", "w+b"); + } + + /* Write the header first */ + fwrite(&spr_header.identifer, 1, 4, spr_file); + fwrite(&spr_header.version, 1, 4, spr_file); + fwrite(&spr_header.sprtype, 1, 4, spr_file); + fwrite(&spr_header.radius, 1, 4, spr_file); + fwrite(&spr_header.max_width, 1, 4, spr_file); + fwrite(&spr_header.max_height, 1, 4, spr_file); + fwrite(&spr_header.total_frames, 1, 4, spr_file); + fwrite(&spr_header.unused, 1, 4, spr_file); + fwrite(&spr_header.synctype, 1, 4, spr_file); + + printf("header info:\n"); + printf("\tidentifer: %s\n", spr_header.identifer); + printf("\tversion: %i\n", spr_header.version); + printf("\ttype: %i\n", spr_header.sprtype); + printf("\tradius: %f\n", spr_header.radius); + printf("\tmax Width: %i\n", spr_header.max_width); + printf("\tmax Height: %i\n", spr_header.max_height); + printf("\tframes: %i\n", spr_header.total_frames); + printf("\treserved: %i\n", spr_header.unused); + printf("\tsync type: %i\n\n", spr_header.synctype); + + /* Read the TGAs and write them directly to the file */ + while (fgets(qcline, sizeof(qcline), qc_file) != NULL) { + const char* framecmd = strtok(qcline, " "); + + /* Single frames */ + if (strcmp(framecmd , "frame") == 0) { + tok = strtok(NULL, " \n"); + if (tok == NULL) { + fprintf(stderr, "\terror: nspecified filename for frame!\n"); + continue; + } + sprintf(tga_filename, "%s.tga", tok); + + tok = strtok(NULL, " \n"); + if (tok == NULL) { + fprintf(stderr, "\terror: unspecified offset (x) for frame!\n"); + continue; + } + ofs_x = atoi(tok); + + tok = strtok(NULL, " \n"); + if (tok == NULL) { + fprintf(stderr, "\terror: unspecified offset (y) for frame!\n"); + continue; + } + ofs_y = atoi(tok); + + printf("saving frame %s (%i, %i)\n", tga_filename, ofs_x, ofs_y); + + /* Group */ + single = 0; + fwrite(&single, 1, 4, spr_file); + + /* Frame */ + parse_targa(spr_file, tga_filename, ofs_x, ofs_y); + printf("\n"); + } + + /* Animated groups */ + if (strcmp(framecmd , "anim") == 0) { + const char* basename = strtok(NULL, " \n"); + if (basename == NULL) { + fprintf(stderr, "\terror: unknown filename for anim!\n"); + continue; + } + + tok = strtok(NULL, " \n"); + if (tok == NULL) { + fprintf(stderr, "\terror: unknown number of frames in anim!\n"); + continue; + } + num_frames = atoi(tok); + + tok = strtok(NULL, " \n"); + if (tok == NULL) { + fprintf(stderr, "\terror: unknown offset (x) in anim!\n"); + continue; + } + ofs_x = atoi(tok); + + tok = strtok(NULL, " \n"); + if (tok == NULL) { + fprintf(stderr, "\terror: unknown offset (y) in anim!\n"); + continue; + } + ofs_y = atoi(tok); + + printf("saving group %s (%i, %i)\n", basename, ofs_x, ofs_y); + + /* Group */ + single = 0x1; + fwrite(&single, 1, 4, spr_file); + fwrite(&num_frames, 1, 4, spr_file); + + for (i = 0; i < num_frames; i++) { + tok = strtok(NULL, " \n"); + if (tok == NULL) { + fprintf(stderr, "\terror: no fps for frame %i in anim!\n", i); + fps = 1.0f; + } else { + fps = (float)atof(tok); + } + + fwrite(&fps, 1, 4, spr_file); + printf("\tframe %i animated at %fFPS\n", i+1, fps); + } + + for (i = 0; i < num_frames; i++) { + sprintf(tga_filename, "%s_%i.tga", basename, i); + parse_targa(spr_file, tga_filename, ofs_x, ofs_y); + } + + printf("\n"); + } + } + + fclose(spr_file); + fclose(qc_file); +} + +int main(int argc, char *argv[]) +{ + int c; + short p; + FILE *fPAL; + + if (argc <= 1) { + fprintf(stderr, "usage: tga2spr [file.qc ...]\n"); + return 1; + } + + fPAL = fopen("palette.lmp", "rb"); + + if (!fPAL) { + fprintf(stdout, "no palette.lmp found, using builtin palette.\n"); + for (p = 0; p < 256; p++) { + cust_pal[p * 3 + 0] = pal_fallb[p].u.r; + cust_pal[p * 3 + 1] = pal_fallb[p].u.g; + cust_pal[p * 3 + 2] = pal_fallb[p].u.b; + } + } else { + fprintf(stdout, "custom palette.lmp found\n"); + fread(cust_pal, 1, QPALSIZE, fPAL); + fclose(fPAL); + } + + for (c = 1; c < argc; c++) + process_qc(argv[c]); + + return 0; +} diff --git a/tga2spr.txt b/tga2spr.txt new file mode 100644 index 0000000..765f706 --- /dev/null +++ b/tga2spr.txt @@ -0,0 +1,83 @@ +This tool converts 24-bit, uncompressed Targa files into Quake sprites. +You just have to give it a few more infos. + +Syntax: +.\tga2spr sprite.qc + +In which "sprite.qc" is a plaintext file containing a number of commands. +Notice that you can also pop in a palette.lmp from whatever mod you're +targetting into the same directory. By default it uses Quake's palette. +It will not add any dithering. +If you want any dithering during palettization, use an external tool. +The GNU Image Manipulation Program provides that feature. +Just remember to export it as a 24-bit image again. + +================ +LIST OF COMMANDS +================ +output [STRING] + Specifies the output sprite. E.g. flame.spr +identifer [CHARS] + Specifies the magic number. In case you use some modded engine that allows + that. Default: IDSP +version [INT] + Specifies the sprite version. Default 1. +type [INT] + Specifies the sprite type. Used for orientation. Default is 0. + 0: parallel upright + 1: facing upright + 2: parallel + 3: oriented + 4: parallel oriented +radius [FLOAT] + Default is 1.0. Not sure if even used. +synctype [INT] + Default is 0. If not 0, animation starts at random offsets. +maxwidth [INT] + Used for the visible bounding box. Use the width value of the largest frame. +maxheight [INT] + Used for the visible bounding box. Use the height value of the largest + frame. +reserved [INT] + Unused in default Quake. Kurok uses this I believe. Default is 0. +frame [TGANAME] [OFFSET X] [OFFSET Y] + Single frame. Loads for [TGANAME] (e.g. flame1.tga) and specifies an offset + in INT form (X and Y) +anim [TGATITLE] [NUMFRAMES] [OFFSET X] [OFFSET Y] [...FPS*FRAME] + An entire animationgroup. TGATITLE is the Targa name without extension. + E.g. if you specify "flame" it will look for flame_1.tga, flame_2.tga and so + on. + You then specify the number of frames and offset of that group and a + frame-delay for each frame in the animation. Play with it. + Keep in mind that some engines ignore variable framerates in sprites. + +You don't have to use ALL available parameters. +For example you have a sprite that's 64x64 in size. +3 Targa images, One is static (wow.tga) and the two others are meant to be an +animation sequence (face_1.tga, face_2.tga). + +Then you'd have this: + +output test.spr +maxwidth 64 +maxheight 64 +frame wow 0 0 +anim face 2 0 0 8 5 + +For every frame inside the anim parameter, you should append a FPS number. +Otherwise a value of 1 is assumed for each frame. +In the above exmaple, face_1 will skip to the next at 8 fps, while the +other will do so at 5 fps. + +Despite the somewhat in-complete implementation of the SPR spec in most engines. +I hope this tool will be of use to somebody. + +Use the program as-is. If you notice any glaring problems feel free +to mail me (marco at icculus dot org) and we can talk about them. + +Due to the way it's written, it doesn't allocate much memory, it streams it from +the input right to the output. This is by design. This way you can export large +sprites (gigabytes worth) even on DOS. Use at your own risk. + +Copyright (c) 2016-2019 Marco "eukara" Hladik +Released under the MIT License.