Files
xash3d-fwgs/engine/common/imagelib/img_wad.c
2026-04-10 15:00:02 +05:00

814 lines
20 KiB
C

/*
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"
/*
============
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;
const byte *pal, *fin;
size_t size;
int numcolors;
if( image.hint == IL_HINT_Q1 )
return false; // Quake1 doesn't have qfonts
if( filesize < sizeof( font ))
return false;
memcpy( &font, buffer, sizeof( font ));
// last sixty four bytes - what the hell ????
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;
fin = buffer + sizeof( font ) - 4;
pal = fin + (image.width * image.height);
numcolors = *(short *)pal, 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 )
{
byte *fin;
size_t pixels;
mstudiotexture_t *pin;
int flags;
pin = (mstudiotexture_t *)buffer;
flags = pin->flags;
image.width = pin->width;
image.height = pin->height;
pixels = image.width * image.height;
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;
byte *fin;
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 < image.width * image.height )
return false;
if( filesize == ( 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;
}
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;
int i, pixels;
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( i = 0; i < 16384; i++ ) if( !fin[i] ) fin[i] = 0xFF;
}
else
{
fin = (byte *)buffer;
memcpy( &lmp, fin, sizeof( lmp ));
image.width = lmp.width;
image.height = lmp.height;
rendermode = LUMP_NORMAL;
fin += sizeof( lmp );
}
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( i = 0; i < pixels; i++ )
{
if( fin[i] == 255 )
{
image.flags |= IMAGE_HAS_ALPHA;
rendermode = LUMP_MASKED;
break;
}
}
}
pal = fin + pixels;
numcolors = *(short *)pal;
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 i, pixels, numcolors;
uint reflectivity[3] = { 0, 0, 0 };
if( filesize < sizeof( mip ))
return false;
memcpy( &mip, buffer, sizeof( mip ));
image.width = mip.width;
image.height = mip.height;
if( !Image_ValidSize( name ))
return false;
memcpy( ofs, mip.offsets, sizeof( ofs ));
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 = *(short *)pal;
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( 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( 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( 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( 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( 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 )
{
const dwadinfo_t *header;
const dlumpinfo_t *lumps;
const unsigned char *mipdata;
int i, j;
if( !buffer || filesize < sizeof( dwadinfo_t ))
return false;
header = (const dwadinfo_t *)buffer;
if( header->numlumps <= 0 || header->infotableofs <= 0 || header->infotableofs >= (int)filesize )
return false;
lumps = (const dlumpinfo_t *)((const unsigned char *)buffer + header->infotableofs );
for( i = 0; i < header->numlumps; ++i )
{
const unsigned char *pixels, *palette, *use_palette;
unsigned char grad_palette[256 * 3];
int mip_size;
mip_t mip;
uint32_t width, height, offset0;
uint32_t m0size, m1size, m2size, m3size;
qboolean alpha_mode = false;
unsigned char frontR = 0, frontG = 0, frontB = 0;
float t;
byte idx;
if( lumps[i].type != TYP_MIPTEX && lumps[i].type != TYP_PALETTE )
continue;
// get lump data and validate
mipdata = (const unsigned char *)buffer + lumps[i].filepos;
mip_size = lumps[i].disksize;
if( lumps[i].filepos < 0 || lumps[i].filepos + mip_size > (int)filesize )
continue;
memcpy( &mip, mipdata, sizeof( mip ));
width = mip.width;
height = mip.height;
if( width <= 0 || height <= 0 || width > 256 || height > 256 )
continue;
offset0 = mip.offsets[0];
if( offset0 == 0 || offset0 + width * height > (uint32_t)mip_size )
continue;
pixels = mipdata + offset0;
m0size = width * height;
m1size = m0size / 4;
m2size = m0size / 16;
m3size = m0size / 64;
palette = mipdata + 0x28 + m0size + m1size + m2size + m3size + 2;
use_palette = palette;
// handle gradient palette
if( lumps[i].type == TYP_PALETTE )
{
// gradient palette
const unsigned char *frontColorPtr = palette + 255 * 3;
frontR = frontColorPtr[0];
frontG = frontColorPtr[1];
frontB = frontColorPtr[2];
for( j = 0; j < 256; ++j )
{
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( j = 0; j < (int)( width * height ); ++j )
{
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 )
{
int m0size, m1size, m2size, m3size;
byte *mip1_data = NULL, *mip2_data = NULL, *mip3_data = NULL;
const byte *palette;
byte grad_palette[256 * 3];
file_t *f;
dwadinfo_t header;
mip_t miptex;
long infotableofs;
dlumpinfo_t lump;
fs_offset_t pad;
int i;
qboolean result = false;
int lump_type = ( pix->flags & IMAGE_GRADIENT_DECAL ) ? TYP_PALETTE : TYP_MIPTEX;
short palette_size = 256;
int infotableofs32 = 0;
if( !pix || !pix->buffer )
return false;
palette = pix->palette ? pix->palette : (const byte *)image.palette;
m0size = pix->width * pix->height;
m1size = m0size / 4;
m2size = m0size / 16;
m3size = m0size / 64;
mip1_data = (byte *)Mem_Malloc( host.imagepool, m1size );
mip2_data = (byte *)Mem_Malloc( host.imagepool, m2size );
mip3_data = (byte *)Mem_Malloc( host.imagepool, m3size );
if( !mip1_data || !mip2_data || !mip3_data )
goto cleanup;
Image_GenerateMipmaps( pix->buffer, pix->width, pix->height, mip1_data, mip2_data, mip3_data );
memset( &miptex, 0, sizeof( mip_t ));
Q_strncpy( miptex.name, "{LOGO", sizeof( miptex.name ));
miptex.width = pix->width;
miptex.height = pix->height;
miptex.offsets[0] = sizeof( mip_t );
miptex.offsets[1] = miptex.offsets[0] + m0size;
miptex.offsets[2] = miptex.offsets[1] + m1size;
miptex.offsets[3] = miptex.offsets[2] + m2size;
f = FS_Open( name, "wb", false );
if( !f )
goto cleanup;
memset( &header, 0, sizeof( header ));
header.ident = IDWAD3HEADER;
header.numlumps = 1;
FS_Write( f, &header, sizeof( header ));
FS_Write( f, &miptex, sizeof( mip_t ));
FS_Write( f, pix->buffer, m0size );
FS_Write( f, mip1_data, m1size );
FS_Write( f, mip2_data, m2size );
FS_Write( f, mip3_data, m3size );
FS_Write( f, &palette_size, sizeof( short ));
if( lump_type == TYP_PALETTE )
{
const byte *frontColorPtr = palette + 255 * 3;
for( 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
pad = (( FS_Tell( f ) + 3 ) & ~3 ) - FS_Tell( f );
for( i = 0; i < pad; ++i )
FS_Write( f, (const void *)&(char){0}, 1 );
infotableofs = FS_Tell( f );
memset( &lump, 0, sizeof( lump ));
lump.filepos = sizeof( dwadinfo_t );
lump.disksize = (int)( miptex.offsets[3] + m3size + sizeof( short ) + 256 * 3 );
lump.size = lump.disksize;
lump.type = (char)lump_type;
lump.attribs = 0;
Q_strncpy( lump.name, "tempdecal", sizeof( lump.name ));
FS_Write( f, &lump, sizeof( lump ));
FS_Seek( f, offsetof( dwadinfo_t, infotableofs ), SEEK_SET );
infotableofs32 = (int)infotableofs;
FS_Write( f, &infotableofs32, sizeof( int ));
FS_Close( f );
result = true;
cleanup:
if( mip1_data )
Mem_Free( mip1_data );
if( mip2_data )
Mem_Free( mip2_data );
if( mip3_data )
Mem_Free( mip3_data );
return result;
}