/* =========================================================================== Doom 3 GPL Source Code Copyright (C) 1999-2011 id Software LLC, a ZeniMax Media company. This file is part of the Doom 3 GPL Source Code ("Doom 3 Source Code"). Doom 3 Source Code 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. Doom 3 Source Code 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. You should have received a copy of the GNU General Public License along with Doom 3 Source Code. If not, see . In addition, the Doom 3 Source Code is also subject to certain additional terms. You should have received a copy of these additional terms immediately following the terms and conditions of the GNU General Public License which accompanied the Doom 3 Source Code. If not, please request a copy in writing from id Software at the address below. If you have questions concerning this license or the applicable additional terms, you may contact in writing id Software LLC, c/o ZeniMax Media Inc., Suite 120, Rockville, Maryland 20850 USA. =========================================================================== */ #include "sys/platform.h" #include "idlib/hashing/MD4.h" #include "renderer/tr_local.h" #include "renderer/Image.h" /* PROBLEM: compressed textures may break the zero clamp rule! */ static bool FormatIsDXT( int internalFormat ) { if ( internalFormat < GL_COMPRESSED_RGB_S3TC_DXT1_EXT || internalFormat > GL_COMPRESSED_RGBA_S3TC_DXT5_EXT ) { return false; } return true; } int MakePowerOfTwo( int num ) { int pot; for (pot = 1 ; pot < num ; pot<<=1) { } return pot; } /* ================ BitsForInternalFormat Used for determining memory utilization ================ */ int idImage::BitsForInternalFormat( int internalFormat ) const { switch ( internalFormat ) { case GL_INTENSITY8: case 1: return 8; case 2: case GL_LUMINANCE8_ALPHA8: return 16; case 3: return 32; // on some future hardware, this may actually be 24, but be conservative case 4: return 32; case GL_LUMINANCE8: return 8; case GL_ALPHA8: return 8; case GL_RGBA8: return 32; case GL_RGB8: return 32; // on some future hardware, this may actually be 24, but be conservative case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: return 4; case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: return 4; case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: return 8; case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: return 8; case GL_RGBA4: return 16; case GL_RGB5: return 16; case GL_COLOR_INDEX8_EXT: return 8; case GL_COLOR_INDEX: return 8; case GL_COMPRESSED_RGB_ARB: return 4; // not sure case GL_COMPRESSED_RGBA_ARB: return 8; // not sure default: common->Error( "R_BitsForInternalFormat: BAD FORMAT:%i", internalFormat ); } return 0; } /* ================== UploadCompressedNormalMap Create a 256 color palette to be used by compressed normal maps ================== */ void idImage::UploadCompressedNormalMap( int width, int height, const byte *rgba, int mipLevel ) { byte *normals; const byte *in; byte *out; int i, j; int x, y, z; int row; // OpenGL's pixel packing rule row = width < 4 ? 4 : width; normals = (byte *)_alloca( row * height ); if ( !normals ) { common->Error( "R_UploadCompressedNormalMap: _alloca failed" ); } in = rgba; out = normals; for ( i = 0 ; i < height ; i++, out += row, in += width * 4 ) { for ( j = 0 ; j < width ; j++ ) { x = in[ j * 4 + 0 ]; y = in[ j * 4 + 1 ]; z = in[ j * 4 + 2 ]; int c; if ( x == 128 && y == 128 && z == 128 ) { // the "nullnormal" color c = 255; } else { c = ( globalImages->originalToCompressed[x] << 4 ) | globalImages->originalToCompressed[y]; if ( c == 255 ) { c = 254; // don't use the nullnormal color } } out[j] = c; } } if ( mipLevel == 0 ) { // Optionally write out the paletized normal map to a .tga if ( globalImages->image_writeNormalTGAPalletized.GetBool() ) { char filename[MAX_IMAGE_NAME]; ImageProgramStringToCompressedFileName( imgName, filename ); char *ext = strrchr(filename, '.'); if ( ext ) { strcpy(ext, "_pal.tga"); R_WritePalTGA( filename, normals, globalImages->compressedPalette, width, height); } } } if ( glConfig.sharedTexturePaletteAvailable ) { qglTexImage2D( GL_TEXTURE_2D, mipLevel, GL_COLOR_INDEX8_EXT, width, height, 0, GL_COLOR_INDEX, GL_UNSIGNED_BYTE, normals ); } } //======================================================================= static byte mipBlendColors[16][4] = { {0,0,0,0}, {255,0,0,128}, {0,255,0,128}, {0,0,255,128}, {255,0,0,128}, {0,255,0,128}, {0,0,255,128}, {255,0,0,128}, {0,255,0,128}, {0,0,255,128}, {255,0,0,128}, {0,255,0,128}, {0,0,255,128}, {255,0,0,128}, {0,255,0,128}, {0,0,255,128}, }; /* =============== SelectInternalFormat This may need to scan six cube map images =============== */ GLenum idImage::SelectInternalFormat( const byte **dataPtrs, int numDataPtrs, int width, int height, textureDepth_t minimumDepth ) const { int i, c; const byte *scan; int rgbOr, rgbAnd, aOr, aAnd; int rgbDiffer, rgbaDiffer; // determine if the rgb channels are all the same // and if either all rgb or all alpha are 255 c = width*height; rgbDiffer = 0; rgbaDiffer = 0; rgbOr = 0; rgbAnd = -1; aOr = 0; aAnd = -1; for ( int side = 0 ; side < numDataPtrs ; side++ ) { scan = dataPtrs[side]; for ( i = 0; i < c; i++, scan += 4 ) { int cor, cand; aOr |= scan[3]; aAnd &= scan[3]; cor = scan[0] | scan[1] | scan[2]; cand = scan[0] & scan[1] & scan[2]; // if rgb are all the same, the or and and will match rgbDiffer |= ( cor ^ cand ); rgbOr |= cor; rgbAnd &= cand; cor |= scan[3]; cand &= scan[3]; rgbaDiffer |= ( cor ^ cand ); } } // we assume that all 0 implies that the alpha channel isn't needed, // because some tools will spit out 32 bit images with a 0 alpha instead // of 255 alpha, but if the alpha actually is referenced, there will be // different behavior in the compressed vs uncompressed states. bool needAlpha; if ( aAnd == 255 || aOr == 0 ) { needAlpha = false; } else { needAlpha = true; } // catch normal maps first if ( minimumDepth == TD_BUMP ) { if ( globalImages->image_useCompression.GetBool() && globalImages->image_useNormalCompression.GetInteger() == 1 && glConfig.sharedTexturePaletteAvailable ) { // image_useNormalCompression should only be set to 1 on nv_10 and nv_20 paths return GL_COLOR_INDEX8_EXT; } else if ( globalImages->image_useCompression.GetBool() && globalImages->image_useNormalCompression.GetInteger() && glConfig.textureCompressionAvailable ) { // image_useNormalCompression == 2 uses rxgb format which produces really good quality for medium settings return GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; } else { // we always need the alpha channel for bump maps for swizzling return GL_RGBA8; } } // allow a complete override of image compression with a cvar if ( !globalImages->image_useCompression.GetBool() ) { minimumDepth = TD_HIGH_QUALITY; } if ( minimumDepth == TD_SPECULAR ) { // we are assuming that any alpha channel is unintentional if ( glConfig.textureCompressionAvailable ) { return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; } else { return GL_RGB5; } } if ( minimumDepth == TD_DIFFUSE ) { // we might intentionally have an alpha channel for alpha tested textures if ( glConfig.textureCompressionAvailable ) { if ( !needAlpha ) { return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; } else { return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; } } else if ( ( aAnd == 255 || aOr == 0 ) ) { return GL_RGB5; } else { return GL_RGBA4; } } // there will probably be some drivers that don't // correctly handle the intensity/alpha/luminance/luminance+alpha // formats, so provide a fallback that only uses the rgb/rgba formats if ( !globalImages->image_useAllFormats.GetBool() ) { // pretend rgb is varying and inconsistant, which // prevents any of the more compact forms rgbDiffer = 1; rgbaDiffer = 1; rgbAnd = 0; } // cases without alpha if ( !needAlpha ) { if ( minimumDepth == TD_HIGH_QUALITY ) { return GL_RGB8; // four bytes } if ( glConfig.textureCompressionAvailable ) { return GL_COMPRESSED_RGB_S3TC_DXT1_EXT; // half byte } return GL_RGB5; // two bytes } // cases with alpha if ( !rgbaDiffer ) { if ( minimumDepth != TD_HIGH_QUALITY && glConfig.textureCompressionAvailable ) { return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; // one byte } return GL_INTENSITY8; // single byte for all channels } #if 0 // we don't support alpha textures any more, because there // is a discrepancy in the definition of TEX_ENV_COMBINE that // causes them to be treated as 0 0 0 A, instead of 1 1 1 A as // normal texture modulation treats them if ( rgbAnd == 255 ) { return GL_ALPHA8; // single byte, only alpha } #endif if ( minimumDepth == TD_HIGH_QUALITY ) { return GL_RGBA8; // four bytes } if ( glConfig.textureCompressionAvailable ) { return GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; // one byte } if ( !rgbDiffer ) { return GL_LUMINANCE8_ALPHA8; // two bytes, max quality } return GL_RGBA4; // two bytes } /* ================== SetImageFilterAndRepeat ================== */ void idImage::SetImageFilterAndRepeat() const { // set the minimize / maximize filtering switch( filter ) { case TF_DEFAULT: qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, globalImages->textureMinFilter ); qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, globalImages->textureMaxFilter ); break; case TF_LINEAR: qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); break; case TF_NEAREST: qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); break; default: common->FatalError( "R_CreateImage: bad texture filter" ); } if ( glConfig.anisotropicAvailable ) { // only do aniso filtering on mip mapped images if ( filter == TF_DEFAULT ) { qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, globalImages->textureAnisotropy ); } else { qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAX_ANISOTROPY_EXT, 1 ); } } if ( glConfig.textureLODBiasAvailable ) { qglTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_LOD_BIAS_EXT, globalImages->textureLODBias ); } // set the wrap/clamp modes switch( repeat ) { case TR_REPEAT: qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT ); break; case TR_CLAMP_TO_BORDER: qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER ); break; case TR_CLAMP_TO_ZERO: case TR_CLAMP_TO_ZERO_ALPHA: case TR_CLAMP: qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); break; default: common->FatalError( "R_CreateImage: bad texture repeat" ); } } /* ================ idImage::Downsize helper function that takes the current width/height and might make them smaller ================ */ void idImage::GetDownsize( int &scaled_width, int &scaled_height ) const { int size = 0; // perform optional picmip operation to save texture memory if ( depth == TD_SPECULAR && globalImages->image_downSizeSpecular.GetInteger() ) { size = globalImages->image_downSizeSpecularLimit.GetInteger(); if ( size == 0 ) { size = 64; } } else if ( depth == TD_BUMP && globalImages->image_downSizeBump.GetInteger() ) { size = globalImages->image_downSizeBumpLimit.GetInteger(); if ( size == 0 ) { size = 64; } } else if ( ( allowDownSize || globalImages->image_forceDownSize.GetBool() ) && globalImages->image_downSize.GetInteger() ) { size = globalImages->image_downSizeLimit.GetInteger(); if ( size == 0 ) { size = 256; } } if ( size > 0 ) { while ( scaled_width > size || scaled_height > size ) { if ( scaled_width > 1 ) { scaled_width >>= 1; } if ( scaled_height > 1 ) { scaled_height >>= 1; } } } // clamp to minimum size if ( scaled_width < 1 ) { scaled_width = 1; } if ( scaled_height < 1 ) { scaled_height = 1; } // clamp size to the hardware specific upper limit // scale both axis down equally so we don't have to // deal with a half mip resampling // This causes a 512*256 texture to sample down to // 256*128 on a voodoo3, even though it could be 256*256 while ( scaled_width > glConfig.maxTextureSize || scaled_height > glConfig.maxTextureSize ) { scaled_width >>= 1; scaled_height >>= 1; } } /* ================ GenerateImage The alpha channel bytes should be 255 if you don't want the channel. We need a material characteristic to ask for specific texture modes. Designed limitations of flexibility: No support for texture borders. No support for texture border color. No support for texture environment colors or GL_BLEND or GL_DECAL texture environments, because the automatic optimization to single or dual component textures makes those modes potentially undefined. No non-power-of-two images. No palettized textures. There is no way to specify separate wrap/clamp values for S and T There is no way to specify explicit mip map levels ================ */ void idImage::GenerateImage( const byte *pic, int width, int height, textureFilter_t filterParm, bool allowDownSizeParm, textureRepeat_t repeatParm, textureDepth_t depthParm ) { bool preserveBorder; byte *scaledBuffer; int scaled_width, scaled_height; byte *shrunk; PurgeImage(); filter = filterParm; allowDownSize = allowDownSizeParm; repeat = repeatParm; depth = depthParm; // if we don't have a rendering context, just return after we // have filled in the parms. We must have the values set, or // an image match from a shader before OpenGL starts would miss // the generated texture if ( !glConfig.isInitialized ) { return; } // don't let mip mapping smear the texture into the clamped border if ( repeat == TR_CLAMP_TO_ZERO ) { preserveBorder = true; } else { preserveBorder = false; } // make sure it is a power of 2 scaled_width = MakePowerOfTwo( width ); scaled_height = MakePowerOfTwo( height ); if ( scaled_width != width || scaled_height != height ) { common->Error( "R_CreateImage: not a power of 2 image" ); } // Optionally modify our width/height based on options/hardware GetDownsize( scaled_width, scaled_height ); scaledBuffer = NULL; // generate the texture number qglGenTextures( 1, &texnum ); // select proper internal format before we resample internalFormat = SelectInternalFormat( &pic, 1, width, height, depth ); // copy or resample data as appropriate for first MIP level if ( ( scaled_width == width ) && ( scaled_height == height ) ) { // we must copy even if unchanged, because the border zeroing // would otherwise modify const data scaledBuffer = (byte *)R_StaticAlloc( sizeof( unsigned ) * scaled_width * scaled_height ); memcpy (scaledBuffer, pic, width*height*4); } else { // resample down as needed (FIXME: this doesn't seem like it resamples anymore!) // scaledBuffer = R_ResampleTexture( pic, width, height, width >>= 1, height >>= 1 ); scaledBuffer = R_MipMap( pic, width, height, preserveBorder ); width >>= 1; height >>= 1; if ( width < 1 ) { width = 1; } if ( height < 1 ) { height = 1; } while ( width > scaled_width || height > scaled_height ) { shrunk = R_MipMap( scaledBuffer, width, height, preserveBorder ); R_StaticFree( scaledBuffer ); scaledBuffer = shrunk; width >>= 1; height >>= 1; if ( width < 1 ) { width = 1; } if ( height < 1 ) { height = 1; } } // one might have shrunk down below the target size scaled_width = width; scaled_height = height; } uploadHeight = scaled_height; uploadWidth = scaled_width; type = TT_2D; // zero the border if desired, allowing clamped projection textures // even after picmip resampling or careless artists. if ( repeat == TR_CLAMP_TO_ZERO ) { byte rgba[4]; rgba[0] = rgba[1] = rgba[2] = 0; rgba[3] = 255; R_SetBorderTexels( (byte *)scaledBuffer, width, height, rgba ); } if ( repeat == TR_CLAMP_TO_ZERO_ALPHA ) { byte rgba[4]; rgba[0] = rgba[1] = rgba[2] = 255; rgba[3] = 0; R_SetBorderTexels( (byte *)scaledBuffer, width, height, rgba ); } if ( generatorFunction == NULL && ( (depth == TD_BUMP && globalImages->image_writeNormalTGA.GetBool()) || (depth != TD_BUMP && globalImages->image_writeTGA.GetBool()) ) ) { // Optionally write out the texture to a .tga char filename[MAX_IMAGE_NAME]; ImageProgramStringToCompressedFileName( imgName, filename ); char *ext = strrchr(filename, '.'); if ( ext ) { strcpy( ext, ".tga" ); // swap the red/alpha for the write /* if ( depth == TD_BUMP ) { for ( int i = 0; i < scaled_width * scaled_height * 4; i += 4 ) { scaledBuffer[ i ] = scaledBuffer[ i + 3 ]; scaledBuffer[ i + 3 ] = 0; } } */ R_WriteTGA( filename, scaledBuffer, scaled_width, scaled_height, false ); // put it back /* if ( depth == TD_BUMP ) { for ( int i = 0; i < scaled_width * scaled_height * 4; i += 4 ) { scaledBuffer[ i + 3 ] = scaledBuffer[ i ]; scaledBuffer[ i ] = 0; } } */ } } // swap the red and alpha for rxgb support // do this even on tga normal maps so we only have to use // one fragment program // if the image is precompressed ( either in palletized mode or true rxgb mode ) // then it is loaded above and the swap never happens here if ( depth == TD_BUMP && globalImages->image_useNormalCompression.GetInteger() != 1 ) { for ( int i = 0; i < scaled_width * scaled_height * 4; i += 4 ) { scaledBuffer[ i + 3 ] = scaledBuffer[ i ]; scaledBuffer[ i ] = 0; } } // upload the main image level Bind(); if ( internalFormat == GL_COLOR_INDEX8_EXT ) { /* if ( depth == TD_BUMP ) { for ( int i = 0; i < scaled_width * scaled_height * 4; i += 4 ) { scaledBuffer[ i ] = scaledBuffer[ i + 3 ]; scaledBuffer[ i + 3 ] = 0; } } */ UploadCompressedNormalMap( scaled_width, scaled_height, scaledBuffer, 0 ); } else { qglTexImage2D( GL_TEXTURE_2D, 0, internalFormat, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer ); } // create and upload the mip map levels, which we do in all cases, even if we don't think they are needed int miplevel; miplevel = 0; while ( scaled_width > 1 || scaled_height > 1 ) { // preserve the border after mip map unless repeating shrunk = R_MipMap( scaledBuffer, scaled_width, scaled_height, preserveBorder ); R_StaticFree( scaledBuffer ); scaledBuffer = shrunk; scaled_width >>= 1; scaled_height >>= 1; if ( scaled_width < 1 ) { scaled_width = 1; } if ( scaled_height < 1 ) { scaled_height = 1; } miplevel++; // this is a visualization tool that shades each mip map // level with a different color so you can see the // rasterizer's texture level selection algorithm // Changing the color doesn't help with lumminance/alpha/intensity formats... if ( depth == TD_DIFFUSE && globalImages->image_colorMipLevels.GetBool() ) { R_BlendOverTexture( (byte *)scaledBuffer, scaled_width * scaled_height, mipBlendColors[miplevel] ); } // upload the mip map if ( internalFormat == GL_COLOR_INDEX8_EXT ) { UploadCompressedNormalMap( scaled_width, scaled_height, scaledBuffer, miplevel ); } else { qglTexImage2D( GL_TEXTURE_2D, miplevel, internalFormat, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer ); } } if ( scaledBuffer != 0 ) { R_StaticFree( scaledBuffer ); } SetImageFilterAndRepeat(); // see if we messed anything up GL_CheckErrors(); } /* ================== Generate3DImage ================== */ void idImage::Generate3DImage( const byte *pic, int width, int height, int picDepth, textureFilter_t filterParm, bool allowDownSizeParm, textureRepeat_t repeatParm, textureDepth_t minDepthParm ) { int scaled_width, scaled_height, scaled_depth; PurgeImage(); filter = filterParm; allowDownSize = allowDownSizeParm; repeat = repeatParm; depth = minDepthParm; // if we don't have a rendering context, just return after we // have filled in the parms. We must have the values set, or // an image match from a shader before OpenGL starts would miss // the generated texture if ( !glConfig.isInitialized ) { return; } // make sure it is a power of 2 scaled_width = MakePowerOfTwo( width ); scaled_height = MakePowerOfTwo( height ); scaled_depth = MakePowerOfTwo( picDepth ); if ( scaled_width != width || scaled_height != height || scaled_depth != picDepth ) { common->Error( "R_Create3DImage: not a power of 2 image" ); } // FIXME: allow picmip here // generate the texture number qglGenTextures( 1, &texnum ); // select proper internal format before we resample // this function doesn't need to know it is 3D, so just make it very "tall" internalFormat = SelectInternalFormat( &pic, 1, width, height * picDepth, minDepthParm ); uploadHeight = scaled_height; uploadWidth = scaled_width; uploadDepth = scaled_depth; type = TT_3D; // upload the main image level Bind(); qglTexImage3D(GL_TEXTURE_3D, 0, internalFormat, scaled_width, scaled_height, scaled_depth, 0, GL_RGBA, GL_UNSIGNED_BYTE, pic ); // create and upload the mip map levels int miplevel; byte *scaledBuffer, *shrunk; scaledBuffer = (byte *)R_StaticAlloc( scaled_width * scaled_height * scaled_depth * 4 ); memcpy( scaledBuffer, pic, scaled_width * scaled_height * scaled_depth * 4 ); miplevel = 0; while ( scaled_width > 1 || scaled_height > 1 || scaled_depth > 1 ) { // preserve the border after mip map unless repeating shrunk = R_MipMap3D( scaledBuffer, scaled_width, scaled_height, scaled_depth, (bool)(repeat != TR_REPEAT) ); R_StaticFree( scaledBuffer ); scaledBuffer = shrunk; scaled_width >>= 1; scaled_height >>= 1; scaled_depth >>= 1; if ( scaled_width < 1 ) { scaled_width = 1; } if ( scaled_height < 1 ) { scaled_height = 1; } if ( scaled_depth < 1 ) { scaled_depth = 1; } miplevel++; // upload the mip map qglTexImage3D(GL_TEXTURE_3D, miplevel, internalFormat, scaled_width, scaled_height, scaled_depth, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer ); } R_StaticFree( scaledBuffer ); // set the minimize / maximize filtering switch( filter ) { case TF_DEFAULT: qglTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, globalImages->textureMinFilter ); qglTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, globalImages->textureMaxFilter ); break; case TF_LINEAR: qglTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); break; case TF_NEAREST: qglTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); qglTexParameterf(GL_TEXTURE_3D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); break; default: common->FatalError( "R_CreateImage: bad texture filter" ); } // set the wrap/clamp modes switch( repeat ) { case TR_REPEAT: qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_REPEAT ); qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_REPEAT ); qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_REPEAT ); break; case TR_CLAMP_TO_BORDER: qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_BORDER ); qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_BORDER ); break; case TR_CLAMP_TO_ZERO: case TR_CLAMP_TO_ZERO_ALPHA: case TR_CLAMP: qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); qglTexParameterf( GL_TEXTURE_3D, GL_TEXTURE_WRAP_R, GL_CLAMP_TO_EDGE ); break; default: common->FatalError( "R_CreateImage: bad texture repeat" ); } // see if we messed anything up GL_CheckErrors(); } /* ==================== GenerateCubeImage Non-square cube sides are not allowed ==================== */ void idImage::GenerateCubeImage( const byte *pic[6], int size, textureFilter_t filterParm, bool allowDownSizeParm, textureDepth_t depthParm ) { int scaled_width, scaled_height; int width, height; int i; PurgeImage(); filter = filterParm; allowDownSize = allowDownSizeParm; depth = depthParm; type = TT_CUBIC; // if we don't have a rendering context, just return after we // have filled in the parms. We must have the values set, or // an image match from a shader before OpenGL starts would miss // the generated texture if ( !glConfig.isInitialized ) { return; } if ( ! glConfig.cubeMapAvailable ) { return; } width = height = size; // generate the texture number qglGenTextures( 1, &texnum ); // select proper internal format before we resample internalFormat = SelectInternalFormat( pic, 6, width, height, depth ); // don't bother with downsample for now scaled_width = width; scaled_height = height; uploadHeight = scaled_height; uploadWidth = scaled_width; Bind(); // no other clamp mode makes sense qglTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); qglTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // set the minimize / maximize filtering switch( filter ) { case TF_DEFAULT: qglTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MIN_FILTER, globalImages->textureMinFilter ); qglTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MAG_FILTER, globalImages->textureMaxFilter ); break; case TF_LINEAR: qglTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); break; case TF_NEAREST: qglTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); qglTexParameterf(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); break; default: common->FatalError( "R_CreateImage: bad texture filter" ); } // upload the base level // FIXME: support GL_COLOR_INDEX8_EXT? for ( i = 0 ; i < 6 ; i++ ) { qglTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, 0, internalFormat, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pic[i] ); } // create and upload the mip map levels int miplevel; byte *shrunk[6]; for ( i = 0 ; i < 6 ; i++ ) { shrunk[i] = R_MipMap( pic[i], scaled_width, scaled_height, false ); } miplevel = 1; while ( scaled_width > 1 ) { for ( i = 0 ; i < 6 ; i++ ) { byte *shrunken; qglTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, miplevel, internalFormat, scaled_width / 2, scaled_height / 2, 0, GL_RGBA, GL_UNSIGNED_BYTE, shrunk[i] ); if ( scaled_width > 2 ) { shrunken = R_MipMap( shrunk[i], scaled_width/2, scaled_height/2, false ); } else { shrunken = NULL; } R_StaticFree( shrunk[i] ); shrunk[i] = shrunken; } scaled_width >>= 1; scaled_height >>= 1; miplevel++; } // see if we messed anything up GL_CheckErrors(); } /* ================ ImageProgramStringToFileCompressedFileName ================ */ void idImage::ImageProgramStringToCompressedFileName( const char *imageProg, char *fileName ) const { const char *s; char *f; strcpy( fileName, "dds/" ); f = fileName + strlen( fileName ); int depth = 0; // convert all illegal characters to underscores // this could conceivably produce a duplicated mapping, but we aren't going to worry about it for ( s = imageProg ; *s ; s++ ) { if ( *s == '/' || *s == '\\' || *s == '(') { if ( depth < 4 ) { *f = '/'; depth ++; } else { *f = ' '; } f++; } else if ( *s == '<' || *s == '>' || *s == ':' || *s == '|' || *s == '"' || *s == '.' ) { *f = '_'; f++; } else if ( *s == ' ' && *(f-1) == '/' ) { // ignore a space right after a slash } else if ( *s == ')' || *s == ',' ) { // always ignore these } else { *f = *s; f++; } } *f++ = 0; strcat( fileName, ".dds" ); } /* ================== NumLevelsForImageSize ================== */ int idImage::NumLevelsForImageSize( int width, int height ) const { int numLevels = 1; while ( width > 1 || height > 1 ) { numLevels++; width >>= 1; height >>= 1; } return numLevels; } /* ================ WritePrecompressedImage When we are happy with our source data, we can write out precompressed versions of everything to speed future load times. ================ */ void idImage::WritePrecompressedImage() { // Always write the precompressed image if we're making a build if ( !com_makingBuild.GetBool() ) { if ( !globalImages->image_writePrecompressedTextures.GetBool() || !globalImages->image_usePrecompressedTextures.GetBool() ) { return; } } if ( !glConfig.isInitialized ) { return; } char filename[MAX_IMAGE_NAME]; ImageProgramStringToCompressedFileName( imgName, filename ); int numLevels = NumLevelsForImageSize( uploadWidth, uploadHeight ); if ( numLevels > MAX_TEXTURE_LEVELS ) { common->Warning( "R_WritePrecompressedImage: level > MAX_TEXTURE_LEVELS for image %s", filename ); return; } // glGetTexImage only supports a small subset of all the available internal formats // We have to use BGRA because DDS is a windows based format int altInternalFormat = 0; int bitSize = 0; switch ( internalFormat ) { case GL_COLOR_INDEX8_EXT: case GL_COLOR_INDEX: // this will not work with dds viewers but we need it in this format to save disk // load speed ( i.e. size ) altInternalFormat = GL_COLOR_INDEX; bitSize = 24; break; case 1: case GL_INTENSITY8: case GL_LUMINANCE8: case 3: case GL_RGB8: altInternalFormat = GL_BGR_EXT; bitSize = 24; break; case GL_LUMINANCE8_ALPHA8: case 4: case GL_RGBA8: altInternalFormat = GL_BGRA_EXT; bitSize = 32; break; case GL_ALPHA8: altInternalFormat = GL_ALPHA; bitSize = 8; break; default: if ( FormatIsDXT( internalFormat ) ) { altInternalFormat = internalFormat; } else { common->Warning("Unknown or unsupported format for %s", filename); return; } } if ( globalImages->image_useOffLineCompression.GetBool() && FormatIsDXT( altInternalFormat ) ) { idStr outFile = fileSystem->RelativePathToOSPath( filename, "fs_basepath" ); idStr inFile = outFile; inFile.StripFileExtension(); inFile.SetFileExtension( "tga" ); idStr format; if ( depth == TD_BUMP ) { format = "RXGB +red 0.0 +green 0.5 +blue 0.5"; } else { switch ( altInternalFormat ) { case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: format = "DXT1"; break; case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: format = "DXT1 -alpha_threshold"; break; case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: format = "DXT3"; break; case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: format = "DXT5"; break; } } globalImages->AddDDSCommand( va( "z:/d3xp/compressonator/thecompressonator -convert \"%s\" \"%s\" %s -mipmaps\n", inFile.c_str(), outFile.c_str(), format.c_str() ) ); return; } ddsFileHeader_t header; memset( &header, 0, sizeof(header) ); header.dwSize = sizeof(header); header.dwFlags = DDSF_CAPS | DDSF_PIXELFORMAT | DDSF_WIDTH | DDSF_HEIGHT; header.dwHeight = uploadHeight; header.dwWidth = uploadWidth; if ( FormatIsDXT( altInternalFormat ) ) { // size (in bytes) of the compressed base image header.dwFlags |= DDSF_LINEARSIZE; header.dwPitchOrLinearSize = ( ( uploadWidth + 3 ) / 4 ) * ( ( uploadHeight + 3 ) / 4 )* (altInternalFormat <= GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 8 : 16); } else { // 4 Byte aligned line width (from nv_dds) header.dwFlags |= DDSF_PITCH; header.dwPitchOrLinearSize = ( ( uploadWidth * bitSize + 31 ) & -32 ) >> 3; } header.dwCaps1 = DDSF_TEXTURE; if ( numLevels > 1 ) { header.dwMipMapCount = numLevels; header.dwFlags |= DDSF_MIPMAPCOUNT; header.dwCaps1 |= DDSF_MIPMAP | DDSF_COMPLEX; } header.ddspf.dwSize = sizeof(header.ddspf); if ( FormatIsDXT( altInternalFormat ) ) { header.ddspf.dwFlags = DDSF_FOURCC; switch ( altInternalFormat ) { case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','1'); break; case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: header.ddspf.dwFlags |= DDSF_ALPHAPIXELS; header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','1'); break; case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','3'); break; case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: header.ddspf.dwFourCC = DDS_MAKEFOURCC('D','X','T','5'); break; } } else { header.ddspf.dwFlags = ( internalFormat == GL_COLOR_INDEX8_EXT ) ? DDSF_RGB | DDSF_ID_INDEXCOLOR : DDSF_RGB; header.ddspf.dwRGBBitCount = bitSize; switch ( altInternalFormat ) { case GL_BGRA_EXT: case GL_LUMINANCE_ALPHA: header.ddspf.dwFlags |= DDSF_ALPHAPIXELS; header.ddspf.dwABitMask = 0xFF000000; // Fall through case GL_BGR_EXT: case GL_LUMINANCE: case GL_COLOR_INDEX: header.ddspf.dwRBitMask = 0x00FF0000; header.ddspf.dwGBitMask = 0x0000FF00; header.ddspf.dwBBitMask = 0x000000FF; break; case GL_ALPHA: header.ddspf.dwFlags = DDSF_ALPHAPIXELS; header.ddspf.dwABitMask = 0xFF000000; break; default: common->Warning( "Unknown or unsupported format for %s", filename ); return; } } idFile *f = fileSystem->OpenFileWrite( filename ); if ( f == NULL ) { common->Warning( "Could not open %s trying to write precompressed image", filename ); return; } common->Printf( "Writing precompressed image: %s\n", filename ); f->Write( "DDS ", 4 ); f->Write( &header, sizeof(header) ); // bind to the image so we can read back the contents Bind(); qglPixelStorei( GL_PACK_ALIGNMENT, 1 ); // otherwise small rows get padded to 32 bits int uw = uploadWidth; int uh = uploadHeight; // Will be allocated first time through the loop byte *data = NULL; for ( int level = 0 ; level < numLevels ; level++ ) { int size = 0; if ( FormatIsDXT( altInternalFormat ) ) { size = ( ( uw + 3 ) / 4 ) * ( ( uh + 3 ) / 4 ) * (altInternalFormat <= GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 8 : 16); } else { size = uw * uh * (bitSize / 8); } if (data == NULL) { data = (byte *)R_StaticAlloc( size ); } if ( FormatIsDXT( altInternalFormat ) ) { qglGetCompressedTexImageARB( GL_TEXTURE_2D, level, data ); } else { qglGetTexImage( GL_TEXTURE_2D, level, altInternalFormat, GL_UNSIGNED_BYTE, data ); } f->Write( data, size ); uw /= 2; uh /= 2; if (uw < 1) { uw = 1; } if (uh < 1) { uh = 1; } } if (data != NULL) { R_StaticFree( data ); } fileSystem->CloseFile( f ); } /* ================ ShouldImageBePartialCached Returns true if there is a precompressed image, and it is large enough to be worth caching ================ */ bool idImage::ShouldImageBePartialCached() { if ( !glConfig.textureCompressionAvailable ) { return false; } if ( !globalImages->image_useCache.GetBool() ) { return false; } // the allowDownSize flag does double-duty as don't-partial-load if ( !allowDownSize ) { return false; } if ( globalImages->image_cacheMinK.GetInteger() <= 0 ) { return false; } // if we are doing a copyFiles, make sure the original images are referenced if ( fileSystem->PerformingCopyFiles() ) { return false; } char filename[MAX_IMAGE_NAME]; ImageProgramStringToCompressedFileName( imgName, filename ); // get the file timestamp fileSystem->ReadFile( filename, NULL, ×tamp ); if ( timestamp == FILE_NOT_FOUND_TIMESTAMP ) { return false; } // open it and get the file size idFile *f; f = fileSystem->OpenFileRead( filename ); if ( !f ) { return false; } int len = f->Length(); fileSystem->CloseFile( f ); if ( len <= globalImages->image_cacheMinK.GetInteger() * 1024 ) { return false; } // we do want to do a partial load return true; } /* ================ CheckPrecompressedImage If fullLoad is false, only the small mip levels of the image will be loaded ================ */ bool idImage::CheckPrecompressedImage( bool fullLoad ) { if ( !glConfig.isInitialized || !glConfig.textureCompressionAvailable ) { return false; } #if 1 // ( _D3XP had disabled ) - Allow grabbing of DDS's from original Doom pak files // if we are doing a copyFiles, make sure the original images are referenced if ( fileSystem->PerformingCopyFiles() ) { return false; } #endif if ( depth == TD_BUMP && globalImages->image_useNormalCompression.GetInteger() != 2 ) { return false; } // god i love last minute hacks :-) if ( com_machineSpec.GetInteger() >= 1 && imgName.Icmpn( "lights/", 7 ) == 0 ) { return false; } char filename[MAX_IMAGE_NAME]; ImageProgramStringToCompressedFileName( imgName, filename ); // get the file timestamp ID_TIME_T precompTimestamp; fileSystem->ReadFile( filename, NULL, &precompTimestamp ); if ( precompTimestamp == FILE_NOT_FOUND_TIMESTAMP ) { return false; } if ( !generatorFunction && timestamp != FILE_NOT_FOUND_TIMESTAMP ) { if ( precompTimestamp < timestamp ) { // The image has changed after being precompressed return false; } } timestamp = precompTimestamp; // open it and just read the header idFile *f; f = fileSystem->OpenFileRead( filename ); if ( !f ) { return false; } int len = f->Length(); if ( len < sizeof( ddsFileHeader_t ) ) { fileSystem->CloseFile( f ); return false; } #if 0 // DG: no idea what this was exactly meant to achieve, but it's definitely a bad idea: // we might try to load the lower mipmap levels of the image, but we'd still have // to load the whole .dds file first. // What's even weirder: idImage::ShouldImageBePartiallyCached() returns false // if the file size is LESS THAN image_cacheMinK * 1024... if ( !fullLoad && len > globalImages->image_cacheMinK.GetInteger() * 1024 ) { len = globalImages->image_cacheMinK.GetInteger() * 1024; } #endif byte *data = (byte *)R_StaticAlloc( len ); f->Read( data, len ); fileSystem->CloseFile( f ); unsigned int magic = LittleInt( *(unsigned int *)data ); ddsFileHeader_t *_header = (ddsFileHeader_t *)(data + 4); int ddspf_dwFlags = LittleInt( _header->ddspf.dwFlags ); if ( magic != DDS_MAKEFOURCC('D', 'D', 'S', ' ')) { common->Printf( "CheckPrecompressedImage( %s ): magic != 'DDS '\n", imgName.c_str() ); R_StaticFree( data ); return false; } // if we don't support color index textures, we must load the full image // should we just expand the 256 color image to 32 bit for upload? if ( ddspf_dwFlags & DDSF_ID_INDEXCOLOR && !glConfig.sharedTexturePaletteAvailable ) { R_StaticFree( data ); return false; } // upload all the levels UploadPrecompressedImage( data, len ); R_StaticFree( data ); return true; } /* =================== UploadPrecompressedImage This can be called by the front end during nromal loading, or by the backend after a background read of the file has completed =================== */ void idImage::UploadPrecompressedImage( byte *data, int len ) { ddsFileHeader_t *header = (ddsFileHeader_t *)(data + 4); // ( not byte swapping dwReserved1 dwReserved2 ) header->dwSize = LittleInt( header->dwSize ); header->dwFlags = LittleInt( header->dwFlags ); header->dwHeight = LittleInt( header->dwHeight ); header->dwWidth = LittleInt( header->dwWidth ); header->dwPitchOrLinearSize = LittleInt( header->dwPitchOrLinearSize ); header->dwDepth = LittleInt( header->dwDepth ); header->dwMipMapCount = LittleInt( header->dwMipMapCount ); header->dwCaps1 = LittleInt( header->dwCaps1 ); header->dwCaps2 = LittleInt( header->dwCaps2 ); header->ddspf.dwSize = LittleInt( header->ddspf.dwSize ); header->ddspf.dwFlags = LittleInt( header->ddspf.dwFlags ); header->ddspf.dwFourCC = LittleInt( header->ddspf.dwFourCC ); header->ddspf.dwRGBBitCount = LittleInt( header->ddspf.dwRGBBitCount ); header->ddspf.dwRBitMask = LittleInt( header->ddspf.dwRBitMask ); header->ddspf.dwGBitMask = LittleInt( header->ddspf.dwGBitMask ); header->ddspf.dwBBitMask = LittleInt( header->ddspf.dwBBitMask ); header->ddspf.dwABitMask = LittleInt( header->ddspf.dwABitMask ); // generate the texture number qglGenTextures( 1, &texnum ); int externalFormat = 0; precompressedFile = true; uploadWidth = header->dwWidth; uploadHeight = header->dwHeight; if ( header->ddspf.dwFlags & DDSF_FOURCC ) { switch ( header->ddspf.dwFourCC ) { case DDS_MAKEFOURCC( 'D', 'X', 'T', '1' ): if ( header->ddspf.dwFlags & DDSF_ALPHAPIXELS ) { internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT1_EXT; } else { internalFormat = GL_COMPRESSED_RGB_S3TC_DXT1_EXT; } break; case DDS_MAKEFOURCC( 'D', 'X', 'T', '3' ): internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT3_EXT; break; case DDS_MAKEFOURCC( 'D', 'X', 'T', '5' ): internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; case DDS_MAKEFOURCC( 'R', 'X', 'G', 'B' ): internalFormat = GL_COMPRESSED_RGBA_S3TC_DXT5_EXT; break; default: common->Warning( "Invalid compressed internal format\n" ); return; } } else if ( ( header->ddspf.dwFlags & DDSF_RGBA ) && header->ddspf.dwRGBBitCount == 32 ) { externalFormat = GL_BGRA_EXT; internalFormat = GL_RGBA8; } else if ( ( header->ddspf.dwFlags & DDSF_RGB ) && header->ddspf.dwRGBBitCount == 32 ) { externalFormat = GL_BGRA_EXT; internalFormat = GL_RGBA8; } else if ( ( header->ddspf.dwFlags & DDSF_RGB ) && header->ddspf.dwRGBBitCount == 24 ) { if ( header->ddspf.dwFlags & DDSF_ID_INDEXCOLOR ) { externalFormat = GL_COLOR_INDEX; internalFormat = GL_COLOR_INDEX8_EXT; } else { externalFormat = GL_BGR_EXT; internalFormat = GL_RGB8; } } else if ( header->ddspf.dwRGBBitCount == 8 ) { externalFormat = GL_ALPHA; internalFormat = GL_ALPHA8; } else { common->Warning( "Invalid uncompressed internal format\n" ); return; } type = TT_2D; // FIXME: we may want to support pre-compressed cube maps in the future Bind(); int numMipmaps = 1; if ( header->dwFlags & DDSF_MIPMAPCOUNT ) { numMipmaps = header->dwMipMapCount; } int uw = uploadWidth; int uh = uploadHeight; // We may skip some mip maps if we are downsizing int skipMip = 0; GetDownsize( uploadWidth, uploadHeight ); byte *imagedata = data + sizeof(ddsFileHeader_t) + 4; for ( int i = 0 ; i < numMipmaps; i++ ) { int size = 0; if ( FormatIsDXT( internalFormat ) ) { size = ( ( uw + 3 ) / 4 ) * ( ( uh + 3 ) / 4 ) * (internalFormat <= GL_COMPRESSED_RGBA_S3TC_DXT1_EXT ? 8 : 16); } else { size = uw * uh * (header->ddspf.dwRGBBitCount / 8); } if ( uw > uploadWidth || uh > uploadHeight ) { skipMip++; } else { if ( FormatIsDXT( internalFormat ) ) { qglCompressedTexImage2DARB( GL_TEXTURE_2D, i - skipMip, internalFormat, uw, uh, 0, size, imagedata ); } else { qglTexImage2D( GL_TEXTURE_2D, i - skipMip, internalFormat, uw, uh, 0, externalFormat, GL_UNSIGNED_BYTE, imagedata ); } } imagedata += size; uw /= 2; uh /= 2; if (uw < 1) { uw = 1; } if (uh < 1) { uh = 1; } } SetImageFilterAndRepeat(); } /* =============== ActuallyLoadImage Absolutely every image goes through this path On exit, the idImage will have a valid OpenGL texture number that can be bound =============== */ void idImage::ActuallyLoadImage( bool checkForPrecompressed, bool fromBackEnd ) { int width, height; byte *pic; // this is the ONLY place generatorFunction will ever be called if ( generatorFunction ) { generatorFunction( this ); return; } // if we are a partial image, we are only going to load from a compressed file if ( isPartialImage ) { if ( CheckPrecompressedImage( false ) ) { return; } // this is an error -- the partial image failed to load MakeDefault(); return; } // // load the image from disk // if ( cubeFiles != CF_2D ) { byte *pics[6]; // we don't check for pre-compressed cube images currently R_LoadCubeImages( imgName, cubeFiles, pics, &width, ×tamp ); if ( pics[0] == NULL ) { common->Warning( "Couldn't load cube image: %s", imgName.c_str() ); MakeDefault(); return; } GenerateCubeImage( (const byte **)pics, width, filter, allowDownSize, depth ); precompressedFile = false; for ( int i = 0 ; i < 6 ; i++ ) { if ( pics[i] ) { R_StaticFree( pics[i] ); } } } else { // see if we have a pre-generated image file that is // already image processed and compressed if ( checkForPrecompressed && globalImages->image_usePrecompressedTextures.GetBool() ) { if ( CheckPrecompressedImage( true ) ) { // we got the precompressed image return; } // fall through to load the normal image } R_LoadImageProgram( imgName, &pic, &width, &height, ×tamp, &depth ); if ( pic == NULL ) { common->Warning( "Couldn't load image: %s", imgName.c_str() ); MakeDefault(); return; } /* // swap the red and alpha for rxgb support // do this even on tga normal maps so we only have to use // one fragment program // if the image is precompressed ( either in palletized mode or true rxgb mode ) // then it is loaded above and the swap never happens here if ( depth == TD_BUMP && globalImages->image_useNormalCompression.GetInteger() != 1 ) { for ( int i = 0; i < width * height * 4; i += 4 ) { pic[ i + 3 ] = pic[ i ]; pic[ i ] = 0; } } */ // build a hash for checking duplicate image files // NOTE: takes about 10% of image load times (SD) // may not be strictly necessary, but some code uses it, so let's leave it in imageHash = MD4_BlockChecksum( pic, width * height * 4 ); GenerateImage( pic, width, height, filter, allowDownSize, repeat, depth ); timestamp = timestamp; precompressedFile = false; R_StaticFree( pic ); // write out the precompressed version of this file if needed WritePrecompressedImage(); } } //========================================================================================================= /* =============== PurgeImage =============== */ void idImage::PurgeImage() { if ( texnum != TEXTURE_NOT_LOADED ) { qglDeleteTextures( 1, &texnum ); // this should be the ONLY place it is ever called! texnum = TEXTURE_NOT_LOADED; } // clear all the current binding caches, so the next bind will do a real one for ( int i = 0 ; i < MAX_MULTITEXTURE_UNITS ; i++ ) { backEnd.glState.tmu[i].current2DMap = -1; backEnd.glState.tmu[i].current3DMap = -1; backEnd.glState.tmu[i].currentCubeMap = -1; } } /* ============== Bind Automatically enables 2D mapping, cube mapping, or 3D texturing if needed ============== */ void idImage::Bind() { // if this is an image that we are caching, move it to the front of the LRU chain if ( partialImage ) { if ( cacheUsageNext ) { // unlink from old position cacheUsageNext->cacheUsagePrev = cacheUsagePrev; cacheUsagePrev->cacheUsageNext = cacheUsageNext; } // link in at the head of the list cacheUsageNext = globalImages->cacheLRU.cacheUsageNext; cacheUsagePrev = &globalImages->cacheLRU; cacheUsageNext->cacheUsagePrev = this; cacheUsagePrev->cacheUsageNext = this; } // load the image if necessary (FIXME: not SMP safe!) if ( texnum == TEXTURE_NOT_LOADED ) { if ( partialImage ) { // if we have a partial image, go ahead and use that this->partialImage->Bind(); // start a background load of the full thing if it isn't already in the queue if ( !backgroundLoadInProgress ) { StartBackgroundImageLoad(); } return; } // load the image on demand here, which isn't our normal game operating mode ActuallyLoadImage( true, true ); // check for precompressed, load is from back end } // bump our statistic counters frameUsed = backEnd.frameCount; bindCount++; tmu_t *tmu = &backEnd.glState.tmu[backEnd.glState.currenttmu]; // enable or disable apropriate texture modes if ( tmu->textureType != type && ( backEnd.glState.currenttmu < glConfig.maxTextureUnits ) ) { if ( tmu->textureType == TT_CUBIC ) { qglDisable( GL_TEXTURE_CUBE_MAP_EXT ); } else if ( tmu->textureType == TT_3D ) { qglDisable( GL_TEXTURE_3D ); } else if ( tmu->textureType == TT_2D ) { qglDisable( GL_TEXTURE_2D ); } if ( type == TT_CUBIC ) { qglEnable( GL_TEXTURE_CUBE_MAP_EXT ); } else if ( type == TT_3D ) { qglEnable( GL_TEXTURE_3D ); } else if ( type == TT_2D ) { qglEnable( GL_TEXTURE_2D ); } tmu->textureType = type; } // bind the texture if ( type == TT_2D ) { if ( tmu->current2DMap != texnum ) { tmu->current2DMap = texnum; qglBindTexture( GL_TEXTURE_2D, texnum ); } } else if ( type == TT_CUBIC ) { if ( tmu->currentCubeMap != texnum ) { tmu->currentCubeMap = texnum; qglBindTexture( GL_TEXTURE_CUBE_MAP_EXT, texnum ); } } else if ( type == TT_3D ) { if ( tmu->current3DMap != texnum ) { tmu->current3DMap = texnum; qglBindTexture( GL_TEXTURE_3D, texnum ); } } if ( com_purgeAll.GetBool() ) { GLclampf priority = 1.0f; qglPrioritizeTextures( 1, &texnum, &priority ); } } /* ============== BindFragment Fragment programs explicitly say which type of map they want, so we don't need to do any enable / disable changes ============== */ void idImage::BindFragment() { // if this is an image that we are caching, move it to the front of the LRU chain if ( partialImage ) { if ( cacheUsageNext ) { // unlink from old position cacheUsageNext->cacheUsagePrev = cacheUsagePrev; cacheUsagePrev->cacheUsageNext = cacheUsageNext; } // link in at the head of the list cacheUsageNext = globalImages->cacheLRU.cacheUsageNext; cacheUsagePrev = &globalImages->cacheLRU; cacheUsageNext->cacheUsagePrev = this; cacheUsagePrev->cacheUsageNext = this; } // load the image if necessary (FIXME: not SMP safe!) if ( texnum == TEXTURE_NOT_LOADED ) { if ( partialImage ) { // if we have a partial image, go ahead and use that this->partialImage->BindFragment(); // start a background load of the full thing if it isn't already in the queue if ( !backgroundLoadInProgress ) { StartBackgroundImageLoad(); } return; } // load the image on demand here, which isn't our normal game operating mode ActuallyLoadImage( true, true ); // check for precompressed, load is from back end } // bump our statistic counters frameUsed = backEnd.frameCount; bindCount++; // bind the texture if ( type == TT_2D ) { qglBindTexture( GL_TEXTURE_2D, texnum ); } else if ( type == TT_RECT ) { qglBindTexture( GL_TEXTURE_RECTANGLE_NV, texnum ); } else if ( type == TT_CUBIC ) { qglBindTexture( GL_TEXTURE_CUBE_MAP_EXT, texnum ); } else if ( type == TT_3D ) { qglBindTexture( GL_TEXTURE_3D, texnum ); } } /* ==================== CopyFramebuffer ==================== */ void idImage::CopyFramebuffer( int x, int y, int imageWidth, int imageHeight, bool useOversizedBuffer ) { Bind(); if ( cvarSystem->GetCVarBool( "g_lowresFullscreenFX" ) ) { imageWidth = 512; imageHeight = 512; } // if the size isn't a power of 2, the image must be increased in size int potWidth, potHeight; potWidth = MakePowerOfTwo( imageWidth ); potHeight = MakePowerOfTwo( imageHeight ); GetDownsize( imageWidth, imageHeight ); GetDownsize( potWidth, potHeight ); qglReadBuffer( GL_BACK ); // only resize if the current dimensions can't hold it at all, // otherwise subview renderings could thrash this if ( ( useOversizedBuffer && ( uploadWidth < potWidth || uploadHeight < potHeight ) ) || ( !useOversizedBuffer && ( uploadWidth != potWidth || uploadHeight != potHeight ) ) ) { uploadWidth = potWidth; uploadHeight = potHeight; if ( potWidth == imageWidth && potHeight == imageHeight ) { qglCopyTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, x, y, imageWidth, imageHeight, 0 ); } else { byte *junk; // we need to create a dummy image with power of two dimensions, // then do a qglCopyTexSubImage2D of the data we want // this might be a 16+ meg allocation, which could fail on _alloca junk = (byte *)Mem_Alloc( potWidth * potHeight * 4 ); memset( junk, 0, potWidth * potHeight * 4 ); //!@# #if 0 // Disabling because it's unnecessary and introduces a green strip on edge of _currentRender for ( int i = 0 ; i < potWidth * potHeight * 4 ; i+=4 ) { junk[i+1] = 255; } #endif qglTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, potWidth, potHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, junk ); Mem_Free( junk ); qglCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight ); } } else { // otherwise, just subimage upload it so that drivers can tell we are going to be changing // it and don't try and do a texture compression or some other silliness qglCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight ); } // if the image isn't a full power of two, duplicate an extra row and/or column to fix bilerps if ( imageWidth != potWidth ) { qglCopyTexSubImage2D( GL_TEXTURE_2D, 0, imageWidth, 0, x+imageWidth-1, y, 1, imageHeight ); } if ( imageHeight != potHeight ) { qglCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, imageHeight, x, y+imageHeight-1, imageWidth, 1 ); } qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); backEnd.c_copyFrameBuffer++; } /* ==================== CopyDepthbuffer This should just be part of copyFramebuffer once we have a proper image type field ==================== */ void idImage::CopyDepthbuffer( int x, int y, int imageWidth, int imageHeight ) { Bind(); // if the size isn't a power of 2, the image must be increased in size int potWidth, potHeight; potWidth = MakePowerOfTwo( imageWidth ); potHeight = MakePowerOfTwo( imageHeight ); if ( uploadWidth != potWidth || uploadHeight != potHeight ) { uploadWidth = potWidth; uploadHeight = potHeight; if ( potWidth == imageWidth && potHeight == imageHeight ) { qglCopyTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, x, y, imageWidth, imageHeight, 0 ); } else { // we need to create a dummy image with power of two dimensions, // then do a qglCopyTexSubImage2D of the data we want qglTexImage2D( GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, potWidth, potHeight, 0, GL_DEPTH_COMPONENT, GL_UNSIGNED_BYTE, NULL ); qglCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight ); } } else { // otherwise, just subimage upload it so that drivers can tell we are going to be changing // it and don't try and do a texture compression or some other silliness qglCopyTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, x, y, imageWidth, imageHeight ); } // qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); // qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); } /* ============= RB_UploadScratchImage if rows = cols * 6, assume it is a cube map animation ============= */ void idImage::UploadScratch( const byte *data, int cols, int rows ) { int i; // if rows = cols * 6, assume it is a cube map animation if ( rows == cols * 6 ) { if ( type != TT_CUBIC ) { type = TT_CUBIC; uploadWidth = -1; // for a non-sub upload } Bind(); rows /= 6; // if the scratchImage isn't in the format we want, specify it as a new texture if ( cols != uploadWidth || rows != uploadHeight ) { uploadWidth = cols; uploadHeight = rows; // upload the base level for ( i = 0 ; i < 6 ; i++ ) { qglTexImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data + cols*rows*4*i ); } } else { // otherwise, just subimage upload it so that drivers can tell we are going to be changing // it and don't try and do a texture compression for ( i = 0 ; i < 6 ; i++ ) { qglTexSubImage2D( GL_TEXTURE_CUBE_MAP_POSITIVE_X_EXT+i, 0, 0, 0, cols, rows, GL_RGBA, GL_UNSIGNED_BYTE, data + cols*rows*4*i ); } } qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); // no other clamp mode makes sense qglTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); qglTexParameteri(GL_TEXTURE_CUBE_MAP_EXT, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); } else { // otherwise, it is a 2D image if ( type != TT_2D ) { type = TT_2D; uploadWidth = -1; // for a non-sub upload } Bind(); // if the scratchImage isn't in the format we want, specify it as a new texture if ( cols != uploadWidth || rows != uploadHeight ) { uploadWidth = cols; uploadHeight = rows; qglTexImage2D( GL_TEXTURE_2D, 0, GL_RGB8, cols, rows, 0, GL_RGBA, GL_UNSIGNED_BYTE, data ); } else { // otherwise, just subimage upload it so that drivers can tell we are going to be changing // it and don't try and do a texture compression qglTexSubImage2D( GL_TEXTURE_2D, 0, 0, 0, cols, rows, GL_RGBA, GL_UNSIGNED_BYTE, data ); } qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); // these probably should be clamp, but we have a lot of issues with editor // geometry coming out with texcoords slightly off one side, resulting in // a smear across the entire polygon #if 1 qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT ); #else qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); #endif } } void idImage::SetClassification( int tag ) { classification = tag; } /* ================== StorageSize ================== */ int idImage::StorageSize() const { int baseSize; if ( texnum == TEXTURE_NOT_LOADED ) { return 0; } switch ( type ) { default: case TT_2D: baseSize = uploadWidth*uploadHeight; break; case TT_3D: baseSize = uploadWidth*uploadHeight*uploadDepth; break; case TT_CUBIC: baseSize = 6 * uploadWidth*uploadHeight; break; } baseSize *= BitsForInternalFormat( internalFormat ); baseSize /= 8; // account for mip mapping baseSize = baseSize * 4 / 3; return baseSize; } /* ================== Print ================== */ void idImage::Print() const { if ( precompressedFile ) { common->Printf( "P" ); } else if ( generatorFunction ) { common->Printf( "F" ); } else { common->Printf( " " ); } switch ( type ) { case TT_2D: common->Printf( " " ); break; case TT_3D: common->Printf( "3" ); break; case TT_CUBIC: common->Printf( "C" ); break; case TT_RECT: common->Printf( "R" ); break; default: common->Printf( "", type ); break; } common->Printf( "%4i %4i ", uploadWidth, uploadHeight ); switch( filter ) { case TF_DEFAULT: common->Printf( "dflt " ); break; case TF_LINEAR: common->Printf( "linr " ); break; case TF_NEAREST: common->Printf( "nrst " ); break; default: common->Printf( "", filter ); break; } switch ( internalFormat ) { case GL_INTENSITY8: case 1: common->Printf( "I " ); break; case 2: case GL_LUMINANCE8_ALPHA8: common->Printf( "LA " ); break; case 3: common->Printf( "RGB " ); break; case 4: common->Printf( "RGBA " ); break; case GL_LUMINANCE8: common->Printf( "L " ); break; case GL_ALPHA8: common->Printf( "A " ); break; case GL_RGBA8: common->Printf( "RGBA8 " ); break; case GL_RGB8: common->Printf( "RGB8 " ); break; case GL_COMPRESSED_RGB_S3TC_DXT1_EXT: common->Printf( "DXT1 " ); break; case GL_COMPRESSED_RGBA_S3TC_DXT1_EXT: common->Printf( "DXT1A " ); break; case GL_COMPRESSED_RGBA_S3TC_DXT3_EXT: common->Printf( "DXT3 " ); break; case GL_COMPRESSED_RGBA_S3TC_DXT5_EXT: common->Printf( "DXT5 " ); break; case GL_RGBA4: common->Printf( "RGBA4 " ); break; case GL_RGB5: common->Printf( "RGB5 " ); break; case GL_COLOR_INDEX8_EXT: common->Printf( "CI8 " ); break; case GL_COLOR_INDEX: common->Printf( "CI " ); break; case GL_COMPRESSED_RGB_ARB: common->Printf( "RGBC " ); break; case GL_COMPRESSED_RGBA_ARB: common->Printf( "RGBAC " ); break; case 0: common->Printf( " " ); break; default: common->Printf( "", internalFormat ); break; } switch ( repeat ) { case TR_REPEAT: common->Printf( "rept " ); break; case TR_CLAMP_TO_ZERO: common->Printf( "zero " ); break; case TR_CLAMP_TO_ZERO_ALPHA: common->Printf( "azro " ); break; case TR_CLAMP: common->Printf( "clmp " ); break; default: common->Printf( "", repeat ); break; } common->Printf( "%4ik ", StorageSize() / 1024 ); common->Printf( " %s\n", imgName.c_str() ); }