/* =========================================================================== 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/Cinematic.h" #include "renderer/Image.h" 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 1: case 2: case 3: case 4: return 32; case GL_RGBA4: return 16; case GL_RGB5_A1: return 16; 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); } } } } //======================================================================= 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}, }; /* ================== 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 ); } } // 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_EDGE ); qglTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE ); // Disabled for OES2 //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; } } //Code from raspberrypi q3 static int isopaque(GLint width, GLint height, const GLvoid *pixels) { unsigned char const *cpixels = (unsigned char const *)pixels; int i; for (i = 0; i < width * height; i++) { if (cpixels[i*4+3] != 0xff) return 0; } return 1; } void rgba4444_convert_tex_image( char* cachefname, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels) { unsigned char const *cpixels = (unsigned char const *)pixels; unsigned short *rgba4444data = (unsigned short *)malloc(2*width*height+1); ((unsigned char*)rgba4444data)[0]=1; rgba4444data=(unsigned short *)((unsigned char*)rgba4444data+1); int i; for (i = 0; i < width * height; i++) { unsigned char r,g,b,a; r = cpixels[4*i]>>4; g = cpixels[4*i+1]>>4; b = cpixels[4*i+2]>>4; a = cpixels[4*i+3]>>4; rgba4444data[i] = r << 12 | g << 8 | b << 4 | a; } qglTexImage2D(target, level, format, width, height,border,format,GL_UNSIGNED_SHORT_4_4_4_4,rgba4444data); rgba4444data=(unsigned short *)((unsigned char*)rgba4444data-1); if (cachefname!=0) { fileSystem->WriteFile(cachefname, rgba4444data, width*height*2+1); } free(rgba4444data); } //#define USE_RG_ETC1 #ifdef USE_RG_ETC1 #include "etc_rg_etc1.h" #else #include "etc1_android.h" #endif unsigned int etc1_data_size(unsigned int width, unsigned int height) { return (((width + 3) & ~3) * ((height + 3) & ~3)) >> 1; } void etc1_compress_tex_image( char* cachefname, GLenum target, GLint level, GLenum internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels) { unsigned char const *cpixels = (unsigned char const *)pixels; unsigned char *etc1data; unsigned int size=etc1_data_size(width,height); etc1data = (unsigned char *)malloc(size+1); etc1data[0]=0; etc1data++; #ifdef USE_RG_ETC1 rg_etc1::etc1_encode_image(cpixels, width, height, 4, width*4, etc1data); #else etc1_encode_image(cpixels, width, height, 4, width*4, etc1data); #endif qglCompressedTexImage2D( target, level, GL_ETC1_RGB8_OES, width, height, 0, size, etc1data); etc1data--; if (cachefname!=0) { fileSystem->WriteFile(cachefname, etc1data, size+1); } free(etc1data); } int etcavail(char* cachefname) { return (r_useETC1Cache.GetBool())&&(r_useETC1.GetBool())&&(cachefname!=0)&&(fileSystem->ReadFile(cachefname,0,0)!=-1); } int uploadetc(char* cachefname,GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type) { char* tmp; int failed=0; if( !etcavail( cachefname ) ) return 1; int sz = fileSystem->ReadFile(cachefname,(void**)&tmp,0); if( sz == -1 ) return 1; if (tmp[0]==0) { if (sz==etc1_data_size(width,height)+1) { qglCompressedTexImage2D(target,level,GL_ETC1_RGB8_OES,width,height,0,etc1_data_size(width,height),tmp + 1); } else { failed=1; } } else { if (sz==width*height*2+1) { qglTexImage2D(target,level,format,width,height,border,format,GL_UNSIGNED_SHORT_4_4_4_4,tmp + 1); } else { failed=1; } } tmp--; fileSystem->FreeFile(tmp); return failed; } void myglTexImage2D(char* cachefname,GLenum target, GLint level, GLint internalformat, GLsizei width, GLsizei height, GLint border, GLenum format, GLenum type, const GLvoid *pixels) { static int opaque = 0; // LOGI("myglTexImage2D, name = %s", cachefname ); if (r_useETC1.GetBool() && format == GL_RGBA && type == GL_UNSIGNED_BYTE) { if (level == 0) opaque = isopaque(width, height, pixels); if (!r_useETC1Cache.GetBool()) cachefname=0; if( uploadetc(cachefname, target, level, internalformat, width, height, border, format, type ) != 0 ) { if (opaque) etc1_compress_tex_image(cachefname,target, level, format, width, height, border, format, type, pixels); else rgba4444_convert_tex_image(cachefname,target, level, format, width, height, border, format, type, pixels); } else { LOGI("Loaded cached image from %s", cachefname); } } else { qglTexImage2D(target,level,internalformat,width,height,border,format,type,pixels); } } //end /* ================ 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 = GL_RGBA; // 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 ( depth == TD_BUMP ) { 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(); // LOGI("LOADING IMAGE %d (%s)",texnum,imgName.c_str()); // qglTexImage2D( GL_TEXTURE_2D, 0, internalFormat, scaled_width, scaled_height, 0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer ); char filename[MAX_IMAGE_NAME]; char*fptr=&filename[0]; ImageProgramStringToCompressedFileName(imgName, filename); char *ext = strrchr(filename, '.'); if (ext) { strcpy(ext, ".etc"); } else fptr=0; myglTexImage2D(fptr,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 //qglTexImage2D( GL_TEXTURE_2D, miplevel, internalFormat, scaled_width, scaled_height, // 0, GL_RGBA, GL_UNSIGNED_BYTE, scaledBuffer ); char filename[MAX_IMAGE_NAME]; char*fptr=&filename[0]; ImageProgramStringToCompressedFileName(imgName, filename); char *ext = strrchr(filename, '.'); if (ext) { strcpy(ext, ".e"); ext[2]='0'+miplevel/10; ext[3]='0'+miplevel%10; } else fptr=0; myglTexImage2D(fptr,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(); } /* ==================== 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; } width = height = size; // generate the texture number qglGenTextures( 1, &texnum ); // select proper internal format before we resample internalFormat = GL_RGBA; // 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, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); qglTexParameteri(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); // set the minimize / maximize filtering switch( filter ) { case TF_DEFAULT: qglTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, globalImages->textureMinFilter ); qglTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, globalImages->textureMaxFilter ); break; case TF_LINEAR: qglTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_LINEAR ); qglTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MAG_FILTER, GL_LINEAR ); break; case TF_NEAREST: qglTexParameterf(GL_TEXTURE_CUBE_MAP, GL_TEXTURE_MIN_FILTER, GL_NEAREST ); qglTexParameterf(GL_TEXTURE_CUBE_MAP, 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+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+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; } /* =============== 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 fromBind ) { int width, height; byte *pic; if(fromBind) { LOGI("ERROR!! CAN NOT LOAD IMAGE FROM BIND"); globalImages->AddAllocList( this ); return; } if( cinematic ) { cinData_t cin; cin = cinematic->ImageForTime( cinmaticNextTime ); if( texnum == TEXTURE_NOT_LOADED ) qglGenTextures( 1, &texnum ); if ( cin.image ) { UploadScratch( cin.image, cin.imageWidth, cin.imageHeight ); } else { //globalImages->blackImage->Bind(); } return; } // this is the ONLY place generatorFunction will ever be called if ( generatorFunction ) { generatorFunction( this ); 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 ); 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 R_LoadImageProgram( imgName, &pic, &width, &height, ×tamp, &depth ); if ( pic == NULL ) { common->Warning( "Couldn't load image: %s", imgName.c_str() ); MakeDefault(); return; } // 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; R_StaticFree( pic ); } } //========================================================================================================= /* =============== PurgeImage =============== */ void idImage::PurgeImage() { if ( texnum != TEXTURE_NOT_LOADED ) { // LOGI("DELETING IMAGE %d",texnum); qglDeleteTextures( 1, &texnum ); // this should be the ONLY place it is ever called! texnum = TEXTURE_NOT_LOADED; } } /* ============== Bind Automatically enables 2D mapping, cube mapping, or 3D texturing if needed ============== */ bool idImage::Bind() { // load the image if necessary (FIXME: not SMP safe!) if ( texnum == TEXTURE_NOT_LOADED ) { // load the image on demand here, which isn't our normal game operating mode ActuallyLoadImage( true ); // Load a black image to reduce flicker globalImages->blackImage->Bind(); return false; } // bump our statistic counters frameUsed = backEnd.frameCount; bindCount++; // bind the texture if ( type == TT_2D ) { qglBindTexture( GL_TEXTURE_2D, texnum ); } else if ( type == TT_CUBIC ) { qglBindTexture( GL_TEXTURE_CUBE_MAP, texnum ); } return true; } /* ============== 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() { // load the image if necessary (FIXME: not SMP safe!) if ( texnum == TEXTURE_NOT_LOADED ) { // load the image on demand here, which isn't our normal game operating mode ActuallyLoadImage( true ); } // bump our statistic counters frameUsed = backEnd.frameCount; bindCount++; // bind the texture if ( type == TT_2D ) { qglBindTexture( GL_TEXTURE_2D, texnum ); } else if ( type == TT_CUBIC ) { qglBindTexture( GL_TEXTURE_CUBE_MAP, 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 ); // Don't do this, otherwise the Grabber gun graphics from ROE do not work properly //GetDownsize( imageWidth, imageHeight ); //GetDownsize( potWidth, potHeight ); //Disabled for OES2 //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_RGB, 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 * 3 ); memset( junk, 0, potWidth * potHeight * 3 ); //!@# qglTexImage2D( GL_TEXTURE_2D, 0, GL_RGB, potWidth, potHeight, 0, GL_RGB, 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+i, 0, GL_RGBA, 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+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, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); qglTexParameteri(GL_TEXTURE_CUBE_MAP, 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_RGBA, 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; } bool idImage::isLoaded() { return (!purgePending && (texnum != TEXTURE_NOT_LOADED)); } /* ================== 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_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 ( generatorFunction ) { common->Printf( "F" ); } else { common->Printf( " " ); } switch ( type ) { case TT_2D: common->Printf( " " ); 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 1: case 2: case 3: case 4: common->Printf( "RGBA " ); break; case GL_RGBA4: common->Printf( "RGBA4 " ); break; case GL_RGB5_A1: common->Printf( "RGB5_A1 " ); 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() ); }