/* img_mip.c - hl1 and q1 image mips Copyright (C) 2007 Uncle Mike This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. */ #include "imagelib.h" #include "xash3d_mathlib.h" #include "wadfile.h" #include "studio.h" #include "sprite.h" #include "qfont.h" #include "swaplib.h" le_struct_begin( charinfo_swap ) le_struct_field( charinfo, startoffset ) le_struct_field( charinfo, charwidth ) le_struct_end(); le_struct_begin( qfont_swap ) le_struct_field( qfont_t, width ) le_struct_field( qfont_t, height ) le_struct_field( qfont_t, rowcount ) le_struct_field( qfont_t, rowheight ) le_struct_array_child( qfont_t, fontinfo, charinfo_swap, NUM_GLYPHS ) le_struct_end(); le_struct_begin( lmp_swap ) le_struct_field( lmp_t, width ) le_struct_field( lmp_t, height ) le_struct_end(); le_struct_begin( mip_swap ) le_struct_field( mip_t, width ) le_struct_field( mip_t, height ) le_struct_array( mip_t, offsets, 4 ) le_struct_end(); le_struct_begin( dwadinfo_swap ) le_struct_field( dwadinfo_t, ident ) le_struct_field( dwadinfo_t, numlumps ) le_struct_field( dwadinfo_t, infotableofs ) le_struct_end(); le_struct_begin( dlumpinfo_swap ) le_struct_field( dlumpinfo_t, filepos ) le_struct_field( dlumpinfo_t, disksize ) le_struct_field( dlumpinfo_t, size ) le_struct_end(); /* ============ Image_LoadPAL ============ */ qboolean Image_LoadPAL( const char *name, const byte *buffer, fs_offset_t filesize ) { int rendermode = LUMP_NORMAL; byte pal[768]; if( filesize > sizeof( pal )) { Con_DPrintf( S_ERROR "%s: (%s) have invalid size (%li should be less or equal than %zu)\n", __func__, name, (long)filesize, sizeof( pal )); return false; } else if( filesize < sizeof( pal ) && buffer != NULL ) { // palette might be truncated, fill it with zeros memset( pal, 0, sizeof( pal )); memcpy( pal, buffer, filesize ); buffer = pal; } if( name[0] == '#' ) { // using palette name as rendermode if( Q_stristr( name, "normal" )) rendermode = LUMP_NORMAL; else if( Q_stristr( name, "masked" )) rendermode = LUMP_MASKED; else if( Q_stristr( name, "gradient" )) rendermode = LUMP_GRADIENT; else if( Q_stristr( name, "texgamma" )) rendermode = LUMP_TEXGAMMA; else if( Q_stristr( name, "valve" )) { rendermode = LUMP_HALFLIFE; buffer = NULL; // force to get HL palette } else if( Q_stristr( name, "id" )) { rendermode = LUMP_QUAKE1; buffer = NULL; // force to get Q1 palette } } // NOTE: image.d_currentpal not cleared with Image_Reset() // and stay valid any time before new call of Image_SetPalette Image_GetPaletteLMP( buffer, rendermode ); Image_CopyPalette32bit(); image.rgba = NULL; // only palette, not real image image.size = 1024; // expanded palette image.width = image.height = 0; image.depth = 1; return true; } /* ============ Image_LoadFNT ============ */ qboolean Image_LoadFNT( const char *name, const byte *buffer, fs_offset_t filesize ) { qfont_t font; if( image.hint == IL_HINT_Q1 ) return false; // Quake1 doesn't have qfonts if( filesize < sizeof( font )) return false; memcpy( &font, buffer, sizeof( font )); le_struct_swap( qfont_swap, &font ); // last sixty four bytes - what the hell ???? size_t size = sizeof( qfont_t ) - 4 + ( font.height * font.width * QCHAR_WIDTH ) + sizeof( short ) + 768 + 64; if( size != filesize ) { // oldstyle font: "conchars" or "creditsfont" image.width = 256; // hardcoded image.height = font.height; } else { // Half-Life 1.1.0.0 font style (qfont_t) image.width = font.width * QCHAR_WIDTH; image.height = font.height; } if( !Image_LumpValidSize( name )) return false; const byte *fin = buffer + sizeof( font ) - 4; const byte *pal = fin + (image.width * image.height); int numcolors = pal[0] | (pal[1] << 8); pal += sizeof( short ); if( numcolors == 768 || numcolors == 256 ) { // g-cont. make sure that is didn't hit anything Image_GetPaletteLMP( pal, LUMP_MASKED ); image.flags |= IMAGE_HAS_ALPHA; // fonts always have transparency } else { return false; } image.type = PF_INDEXED_32; // 32-bit palette image.depth = 1; return Image_AddIndexedImageToPack( fin, image.width, image.height ); } /* ====================== Image_SetMDLPointer Transfer buffer pointer before Image_LoadMDL ====================== */ static void *g_mdltexdata; void Image_SetMDLPointer( byte *p ) { g_mdltexdata = p; } /* ============ Image_LoadMDL ============ */ qboolean Image_LoadMDL( const char *name, const byte *buffer, fs_offset_t filesize ) { mstudiotexture_t *pin = (mstudiotexture_t *)buffer; int flags = pin->flags; image.width = pin->width; image.height = pin->height; size_t pixels = image.width * image.height; byte *fin = (byte *)g_mdltexdata; ASSERT(fin); g_mdltexdata = NULL; if( !Image_ValidSize( name )) return false; if( image.hint == IL_HINT_HL ) { if( filesize < ( sizeof( *pin ) + pixels + 768 )) return false; if( FBitSet( flags, STUDIO_NF_MASKED )) { byte *pal = fin + pixels; Image_GetPaletteLMP( pal, LUMP_MASKED ); image.flags |= IMAGE_HAS_ALPHA|IMAGE_ONEBIT_ALPHA; } else Image_GetPaletteLMP( fin + pixels, LUMP_TEXGAMMA ); } else { return false; // unknown or unsupported mode rejected } image.type = PF_INDEXED_32; // 32-bit palete image.depth = 1; return Image_AddIndexedImageToPack( fin, image.width, image.height ); } /* ============ Image_LoadSPR ============ */ qboolean Image_LoadSPR( const char *name, const byte *buffer, fs_offset_t filesize ) { dspriteframe_t pin; // identical for q1\hl sprites qboolean truecolor = false; if( image.hint == IL_HINT_HL ) { if( !image.d_currentpal ) return false; } else if( image.hint == IL_HINT_Q1 ) { Image_GetPaletteQ1(); } else { // unknown mode rejected return false; } memcpy( &pin, buffer, sizeof( dspriteframe_t )); image.width = pin.width; image.height = pin.height; if( filesize < (fs_offset_t)image.width * image.height ) return false; if( filesize == (fs_offset_t)image.width * image.height * 4 ) truecolor = true; // sorry, can't validate palette rendermode if( !Image_LumpValidSize( name )) return false; image.type = (truecolor) ? PF_RGBA_32 : PF_INDEXED_32; // 32-bit palete image.depth = 1; // detect alpha-channel by palette type switch( image.d_rendermode ) { case LUMP_MASKED: SetBits( image.flags, IMAGE_ONEBIT_ALPHA ); // intentionally fallthrough case LUMP_GRADIENT: SetBits( image.flags, IMAGE_HAS_ALPHA ); break; case LUMP_QUAKE1: SetBits( image.flags, IMAGE_HAS_ALPHA ); break; } byte *fin = (byte *)(buffer + sizeof(dspriteframe_t)); if( truecolor ) { // spr32 support image.size = image.width * image.height * 4; image.rgba = Mem_Malloc( host.imagepool, image.size ); memcpy( image.rgba, fin, image.size ); SetBits( image.flags, IMAGE_HAS_COLOR ); // Color. True Color! return true; } return Image_AddIndexedImageToPack( fin, image.width, image.height ); } /* ============ Image_LoadLMP ============ */ qboolean Image_LoadLMP( const char *name, const byte *buffer, fs_offset_t filesize ) { lmp_t lmp; byte *fin, *pal; int rendermode; if( filesize < sizeof( lmp )) return false; // valve software trick (particle palette) if( Q_stristr( name, "palette.lmp" )) return Image_LoadPAL( name, buffer, filesize ); // id software trick (image without header) if( Q_stristr( name, "conchars" ) && filesize == 16384 ) { image.width = image.height = 128; rendermode = LUMP_QUAKE1; filesize += sizeof( lmp ); fin = (byte *)buffer; // need to remap transparent color from first to last entry for( int i = 0; i < 16384; i++ ) if( !fin[i] ) fin[i] = 0xFF; } else { fin = (byte *)buffer; memcpy( &lmp, fin, sizeof( lmp )); le_struct_swap( lmp_swap, &lmp ); image.width = lmp.width; image.height = lmp.height; rendermode = LUMP_NORMAL; fin += sizeof( lmp ); } int pixels = image.width * image.height; if( filesize < sizeof( lmp ) + pixels ) return false; if( !Image_ValidSize( name )) return false; if( image.hint != IL_HINT_Q1 && filesize > (int)sizeof(lmp) + pixels ) { int numcolors; // HACKHACK: console background image shouldn't be transparent if( !Q_stristr( name, "conback" )) { for( int i = 0; i < pixels; i++ ) { if( fin[i] == 255 ) { image.flags |= IMAGE_HAS_ALPHA; rendermode = LUMP_MASKED; break; } } } pal = fin + pixels; numcolors = pal[0] | (pal[1] << 8); if( numcolors != 256 ) pal = NULL; // corrupted lump ? else pal += sizeof( short ); } else if( image.hint != IL_HINT_HL ) { image.flags |= IMAGE_HAS_ALPHA; rendermode = LUMP_QUAKE1; pal = NULL; } else { // unknown mode rejected return false; } Image_GetPaletteLMP( pal, rendermode ); image.type = PF_INDEXED_32; // 32-bit palete image.depth = 1; return Image_AddIndexedImageToPack( fin, image.width, image.height ); } static int Image_FindBestBlack( const uint *pal ) { int min_color = 32; int best_black = -1; for( int i = 0; i < 256; i++ ) { int color = pal[i] & 0xFFFFFF; if( color < min_color ) { min_color = color; best_black = i; } } return best_black; } /* ============= Image_LoadMIP ============= */ qboolean Image_LoadMIP( const char *name, const byte *buffer, fs_offset_t filesize ) { mip_t mip; qboolean hl_texture; byte *fin, *pal; int ofs[4], rendermode; int numcolors; uint reflectivity[3] = { 0, 0, 0 }; if( filesize < sizeof( mip )) return false; memcpy( &mip, buffer, sizeof( mip )); if( !Image_CheckFlag( IL_HOST_ENDIAN )) le_struct_swap( mip_swap, &mip ); image.width = mip.width; image.height = mip.height; if( !Image_ValidSize( name )) return false; memcpy( ofs, mip.offsets, sizeof( ofs )); int pixels = image.width * image.height; if( image.hint != IL_HINT_Q1 && filesize >= (int)sizeof(mip) + ((pixels * 85)>>6) + sizeof(short) + 768) { // half-life 1.0.0.1 mip version with palette fin = (byte *)buffer + mip.offsets[0]; pal = (byte *)buffer + mip.offsets[0] + (((image.width * image.height) * 85)>>6); numcolors = pal[0] | (pal[1] << 8); if( numcolors != 256 ) pal = NULL; // corrupted mip ? else pal += sizeof( short ); // skip colorsize hl_texture = true; // setup rendermode if( Q_strrchr( name, '{' )) { // NOTE: decals with 'blue base' can be interpret as colored decals if( !Image_CheckFlag( IL_LOAD_DECAL ) || ( pal && pal[765] == 0 && pal[766] == 0 && pal[767] == 255 )) { SetBits( image.flags, IMAGE_ONEBIT_ALPHA ); rendermode = LUMP_MASKED; } else { // classic gradient decals SetBits( image.flags, IMAGE_COLORINDEX ); rendermode = LUMP_GRADIENT; } SetBits( image.flags, IMAGE_HAS_ALPHA ); } else { // NOTE: we can have luma-pixels if quake1 texture // converted into the hl texture but palette leave unchanged // this is a good reason for using fullbright pixels const int pal_type = Image_ComparePalette( pal ); if( pal_type == PAL_QUAKE1 ) { // check for luma pixels (but ignore liquid textures because they have no lightmap) if( mip.name[0] != '*' && mip.name[0] != '!' ) { for( int i = 0; i < image.width * image.height; i++ ) { if( fin[i] > 224 ) { SetBits( image.flags, IMAGE_HAS_LUMA ); image.black_pixel = 0; break; } } } SetBits( image.flags, IMAGE_QUAKEPAL ); // if texture was converted from quake to half-life with no palette changes // then applying texgamma might make it too dark or even outright broken rendermode = LUMP_NORMAL; } else { // half-life mips need texgamma applied rendermode = LUMP_TEXGAMMA; // three checks here: check if we can load luma, check the texture name and, finally, // validate that palette isn't NULL if( Image_CheckFlag( IL_ALLOW_WAD3_LUMA ) && ( mip.name[0] == '~' || ( mip.name[0] == '+' && isdigit((byte)mip.name[1] ) && mip.name[2] == '~' )) && pal != NULL ) { SetBits( image.flags, IMAGE_HAS_LUMA ); } } } Image_GetPaletteLMP( pal, rendermode ); image.d_currentpal[255] &= 0xFFFFFF; if( rendermode == LUMP_TEXGAMMA && FBitSet( image.flags, IMAGE_HAS_LUMA )) { // thanks to Unkle Mike for an idea of looking best black pixel image.black_pixel = Image_FindBestBlack( image.d_currentpal ); // failed to find good pixel, refuse to load luma if( image.black_pixel == -1 ) ClearBits( image.flags, IMAGE_HAS_LUMA ); } } else if( image.hint != IL_HINT_HL && filesize >= (int)sizeof(mip) + ((pixels * 85)>>6)) { // quake1 1.01 mip version without palette fin = (byte *)buffer + mip.offsets[0]; pal = NULL; // clear palette rendermode = LUMP_NORMAL; hl_texture = false; // check for luma and alpha pixels // don't apply luma to water surfaces because they have no lightmap if( !image.custom_palette && mip.name[0] != '*' && mip.name[0] != '!' ) { for( int i = 0; i < image.width * image.height; i++ ) { if( fin[i] > 224 && fin[i] != 255 ) { SetBits( image.flags, IMAGE_HAS_LUMA ); image.black_pixel = 0; break; } } } // Arcane Dimensions has the transparent textures if( Q_strrchr( name, '{' )) { for( int i = 0; i < image.width * image.height; i++ ) { if( fin[i] == 255 ) { // don't set ONEBIT_ALPHA flag for some reasons image.flags |= IMAGE_HAS_ALPHA; break; } } } SetBits( image.flags, IMAGE_QUAKEPAL ); Image_GetPaletteQ1(); } else { return false; // unknown or unsupported mode rejected } // check for quake-sky texture if( !Q_strncmp( mip.name, "sky", 3 ) && image.width == ( image.height * 2 )) { // g-cont: we need to run additional checks for palette type and colors ? image.flags |= IMAGE_QUAKESKY; } // check for half-life water texture if( pal != NULL ) { if( hl_texture && ( mip.name[0] == '!' || !Q_strnicmp( mip.name, "water", 5 ))) { // grab the fog color image.fogParams[0] = pal[3*3+0]; image.fogParams[1] = pal[3*3+1]; image.fogParams[2] = pal[3*3+2]; // grab the fog density image.fogParams[3] = pal[4*3+0]; } else if( hl_texture && ( rendermode == LUMP_GRADIENT )) { // grab the decal color image.fogParams[0] = pal[255*3+0]; image.fogParams[1] = pal[255*3+1]; image.fogParams[2] = pal[255*3+2]; // calc the decal reflectivity image.fogParams[3] = VectorAvg( image.fogParams ); } else { // calc texture reflectivity for( int i = 0; i < 256; i++ ) { reflectivity[0] += pal[i*3+0]; reflectivity[1] += pal[i*3+1]; reflectivity[2] += pal[i*3+2]; } VectorDivide( reflectivity, 256, image.fogParams ); } } image.type = PF_INDEXED_32; // 32-bit palete image.depth = 1; return Image_AddIndexedImageToPack( fin, image.width, image.height ); } /* ============ Image_LoadWAD ============ */ qboolean Image_LoadWAD( const char *name, const byte *buffer, fs_offset_t filesize ) { dwadinfo_t whdr; if( !buffer || filesize < sizeof( dwadinfo_t )) return false; memcpy( &whdr, buffer, sizeof( whdr )); le_struct_swap( dwadinfo_swap, &whdr ); if( whdr.numlumps <= 0 || whdr.infotableofs <= 0 || whdr.infotableofs >= (int)filesize ) return false; for( int i = 0; i < whdr.numlumps; ++i ) { unsigned char grad_palette[256 * 3]; dlumpinfo_t lump; mip_t mip; qboolean alpha_mode = false; unsigned char frontR = 0, frontG = 0, frontB = 0; memcpy( &lump, buffer + whdr.infotableofs + i * sizeof( dlumpinfo_t ), sizeof( lump )); le_struct_swap( dlumpinfo_swap, &lump ); if( lump.type != TYP_MIPTEX && lump.type != TYP_PALETTE ) continue; // get lump data and validate const unsigned char *mipdata = (const unsigned char *)buffer + lump.filepos; int mip_size = lump.disksize; if( lump.filepos < 0 || lump.filepos + mip_size > (int)filesize ) continue; memcpy( &mip, mipdata, sizeof( mip )); le_struct_swap( mip_swap, &mip ); uint32_t width = mip.width; uint32_t height = mip.height; if( width <= 0 || height <= 0 || width > 256 || height > 256 ) continue; uint32_t offset0 = mip.offsets[0]; if( offset0 == 0 || offset0 + width * height > (uint32_t)mip_size ) continue; const unsigned char *pixels = mipdata + offset0; uint32_t m0size = width * height; uint32_t m1size = m0size / 4; uint32_t m2size = m0size / 16; uint32_t m3size = m0size / 64; const unsigned char *palette = mipdata + 0x28 + m0size + m1size + m2size + m3size + 2; const unsigned char *use_palette = palette; // handle gradient palette if( lump.type == TYP_PALETTE ) { // gradient palette const unsigned char *frontColorPtr = palette + 255 * 3; frontR = frontColorPtr[0]; frontG = frontColorPtr[1]; frontB = frontColorPtr[2]; for( int j = 0; j < 256; ++j ) { float t = j / 255.0f; grad_palette[j * 3 + 0] = (unsigned char)( frontR * t ); grad_palette[j * 3 + 1] = (unsigned char)( frontG * t ); grad_palette[j * 3 + 2] = (unsigned char)( frontB * t ); } use_palette = grad_palette; alpha_mode = true; // gradient } // prepare image structure Image_Reset(); image.width = width; image.height = height; image.type = PF_RGBA_32; image.flags = IMAGE_HAS_COLOR | IMAGE_HAS_ALPHA; image.size = width * height * 4; image.rgba = Mem_Malloc( host.imagepool, image.size ); image.palette = NULL; // convert indexed pixels to RGBA for( int j = 0; j < (int)( width * height ); ++j ) { byte idx = pixels[j]; image.rgba[j * 4 + 0] = use_palette[idx * 3 + 0]; image.rgba[j * 4 + 1] = use_palette[idx * 3 + 1]; image.rgba[j * 4 + 2] = use_palette[idx * 3 + 2]; if( alpha_mode ) image.rgba[j * 4 + 3] = idx; // soft transparency else image.rgba[j * 4 + 3] = ( idx == 255 ) ? 0 : 255; // classic } return true; } return false; } /* ============ Image_SaveWAD ============ */ qboolean Image_SaveWAD( const char *name, rgbdata_t *pix ) { if( !pix || !pix->buffer ) return false; const int lump_type = ( pix->flags & IMAGE_GRADIENT_DECAL ) ? TYP_PALETTE : TYP_MIPTEX; int m0size = pix->width * pix->height; int m1size = m0size / 4; int m2size = m0size / 16; int m3size = m0size / 64; byte *mip1_data = Mem_Malloc( host.imagepool, m1size ); byte *mip2_data = Mem_Malloc( host.imagepool, m2size ); byte *mip3_data = Mem_Malloc( host.imagepool, m3size ); if( !mip1_data || !mip2_data || !mip3_data ) // Mem_Malloc never returns NULL, but keep this check anyway { Mem_Free( mip1_data ); Mem_Free( mip2_data ); Mem_Free( mip3_data ); return false; } Image_GenerateMipmaps( pix->buffer, pix->width, pix->height, mip1_data, mip2_data, mip3_data ); file_t *f = FS_Open( name, "wb", false ); if( !f ) { Mem_Free( mip1_data ); Mem_Free( mip2_data ); Mem_Free( mip3_data ); return false; } { dwadinfo_t header = { .ident = IDWAD3HEADER, .numlumps = 1, }; le_struct_swap( dwadinfo_swap, &header ); FS_Write( f, &header, sizeof( header )); } mip_t miptex = { .name = "{LOGO", .width = pix->width, .height = pix->height, .offsets[0] = sizeof( mip_t ), .offsets[1] = sizeof( mip_t ) + m0size, .offsets[2] = sizeof( mip_t ) + m0size + m1size, .offsets[3] = sizeof( mip_t ) + m0size + m1size + m2size, }; le_struct_swap( mip_swap, &miptex ); FS_Write( f, &miptex, sizeof( mip_t )); le_struct_swap( mip_swap, &miptex ); FS_Write( f, pix->buffer, m0size ); FS_Write( f, mip1_data, m1size ); FS_Write( f, mip2_data, m2size ); FS_Write( f, mip3_data, m3size ); { short palette_size = LittleShort( 256 ); FS_Write( f, &palette_size, sizeof( short )); } const byte *palette = pix->palette ? pix->palette : (const byte *)image.palette; if( lump_type == TYP_PALETTE ) { const byte *frontColorPtr = palette + 255 * 3; byte grad_palette[256 * 3]; for( int i = 0; i < 256; ++i ) { float t = i / 255.0f; grad_palette[i * 3 + 0] = (byte)( frontColorPtr[0] * t ); grad_palette[i * 3 + 1] = (byte)( frontColorPtr[1] * t ); grad_palette[i * 3 + 2] = (byte)( frontColorPtr[2] * t ); } FS_Write( f, grad_palette, 256 * 3 ); } else { FS_Write( f, palette, 256 * 3 ); } // padding up to a multiple of 4 fs_offset_t pad = (( FS_Tell( f ) + 3 ) & ~3 ) - FS_Tell( f ); for( int i = 0; i < pad; ++i ) { byte x = 0; FS_Write( f, &x, 1 ); } fs_offset_t infotableofs = FS_Tell( f ); { dlumpinfo_t lump = { .filepos = sizeof( dwadinfo_t ), .disksize = (int)( miptex.offsets[3] + m3size + sizeof( short ) + 256 * 3 ), .size = (int)( miptex.offsets[3] + m3size + sizeof( short ) + 256 * 3 ), .type = (char)lump_type, .attribs = 0, .name = "tempdecal", }; le_struct_swap( dlumpinfo_swap, &lump ); FS_Write( f, &lump, sizeof( lump )); } FS_Seek( f, offsetof( dwadinfo_t, infotableofs ), SEEK_SET ); int infotableofs32 = LittleLong((int)infotableofs ); FS_Write( f, &infotableofs32, sizeof( int )); FS_Close( f ); Mem_Free( mip1_data ); Mem_Free( mip2_data ); Mem_Free( mip3_data ); return true; }