You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

684 lines
21 KiB

/*==LICENSE==*
CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011 Cyan Worlds, Inc.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
You can contact Cyan Worlds, Inc. by email legal@cyan.com
or by snail mail at:
Cyan Worlds, Inc.
14617 N Newport Hwy
Mead, WA 99021
*==LICENSE==*/
#include "hsTypes.h"
#include "max.h"
#include <commdlg.h>
#include "bmmlib.h"
#include "../../Plasma/PubUtilLib/plGImage/plMipmap.h"
#include "hsExceptionStack.h"
#include "../../Plasma/PubUtilLib/plGImage/hsCodecManager.h"
#include "plBitmapCreator.h"
#include "../MaxMain/plPluginResManager.h"
#include "../MaxExport/plErrorMsg.h"
#include "../MaxPlasmaMtls/Layers/plStaticEnvLayer.h"
#include "../plGImage/plMipmap.h"
#include "../plGImage/plDynamicTextMap.h"
#include "../plGImage/plCubicEnvironmap.h"
#include "../pnKeyedObject/plKey.h"
#include "../pnKeyedObject/plUoid.h"
#include "../plResMgr/plRegistryHelpers.h"
#include "../plResMgr/plLocalization.h"
#include "../plAgeDescription/plAgeDescription.h"
//// plCommonBitmapLib ///////////////////////////////////////////////////////
// Derived class for our textures, since they all go in a common page
// (namely, "Textures")
#include "../MaxMain/plCommonObjLib.h"
class plCommonBitmapLib : public plCommonObjLib
{
public:
virtual hsBool IsInteresting( const plKey &objectKey )
{
if( objectKey->GetUoid().GetClassType() == plCubicEnvironmap::Index() ||
objectKey->GetUoid().GetClassType() == plMipmap::Index() )
{
return true;
}
return false;
}
};
static plCommonBitmapLib sCommonBitmapLib;
plBitmapCreator::plBitmapCreator()
{
fErrorMsg = nil;
}
plBitmapCreator::~plBitmapCreator()
{
}
plBitmapCreator &plBitmapCreator::Instance()
{
static plBitmapCreator fInstance;
return fInstance;
}
void plBitmapCreator::Init( hsBool save, plErrorMsg *msg )
{
fErrorMsg = msg;
}
void plBitmapCreator::DeInit( void )
{
CleanUpMaps();
}
void plBitmapCreator::CleanUpMaps( void )
{
sCommonBitmapLib.ClearObjectList();
}
void plBitmapCreator::DeleteExportedBitmap( const plKey &constKey )
{
plKey key = constKey;
sCommonBitmapLib.RemoveObjectAndKey( key );
}
//
// Create Bitmap
//
plMipmap *plBitmapCreator::ICreateBitmap(plBitmapData *bd)
{
hsGuardBegin("hsConverterUtils::CreateBitmap");
// Load the bitmap
BitmapInfo bi;
bi.SetName(bd->fileName);
#if 0 // This isn't really an issue since the textures are packed -Colin
const int kMaxFileNameLength = 30;
if (strlen(bi.Filename()) > kMaxFileNameLength)
{
// Allow to continue, But make it painful
char errStr[256];
sprintf(errStr, "File name longer than %d, won't burn to CD (%s)", kMaxFileNameLength, bi.Filename());//bitmapTex->GetName());
MessageBox(GetActiveWindow(), errStr, bd->fileName, MB_OK|MB_ICONEXCLAMATION);
}
#endif
hsBool notMipped = (bd->texFlags & plMipmap::kForceOneMipLevel) != 0;
float sigma = bd->sig;
// Load the bitmap
Bitmap *bm = TheManager->Load(&bi);
if (!bm)
{
// FIXME
/*
if (fErrorMsg->Set(!(fWarned & kWarnedNoMoreBitmapLoadErr),
"Error loading bitmap", pathName).CheckAskOrCancel())
{
fWarned |= kWarnedNoMoreBitmapLoadErr;
}
*/
return nil;
}
BitmapStorage *storage = bm->Storage();
BitmapInfo *bInfo = &storage->bi;
ICheckOutBitmap(bInfo, bm, bd->fileName);
//
// Create a plMipmap
//
plMipmap *hBitmap = TRACKED_NEW plMipmap;
if( (bm->Width() ^ (bm->Width() & -bm->Width()))
||(bm->Height() ^ (bm->Height() & -bm->Height())) )
{
IResampBitmap(bm, *hBitmap);
}
else if( ((bm->Width() >> 3) > bm->Height())||((bm->Height() >> 3) > bm->Width()) )
{
IResampBitmap(bm, *hBitmap);
}
else
{
ICopyBitmap(bm, *hBitmap);
}
bm->DeleteThis();
if( bd->invertAlpha )
IInvertAlpha(*hBitmap);
// Do it
plMipmap *hMipmap = nil;
if (sigma > 0.f)
{
hMipmap = TRACKED_NEW plMipmap(hBitmap, sigma, bd->createFlags, bd->detailDropoffStart,
bd->detailDropoffStop, bd->detailMax, bd->detailMin);
}
else
{
hMipmap = TRACKED_NEW plMipmap(hBitmap, -1.f, bd->createFlags, bd->detailDropoffStart,
bd->detailDropoffStop, bd->detailMax, bd->detailMin);
}
delete hBitmap;
/// Clamp the border if we're using clamping
if( bd->clampFlags != 0 )
{
hMipmap->EnsureKonstantBorder( ( bd->clampFlags & plBitmapData::kClampU ) ? true : false,
( bd->clampFlags & plBitmapData::kClampV ) ? true : false );
}
/// Cut this down to whatever size we were told to :)
if( bd->maxDimension != 0 )
hMipmap->ClipToMaxSize( bd->maxDimension );
if( notMipped )
{
// Done AFTER ClipToMaxSize() so we still get the export size specified
hMipmap->RemoveMipping();
}
hBitmap = hMipmap;
UInt32 flagsToSet = 0;
if( bd->texFlags & plMipmap::kNoMaxSize )
flagsToSet |= plMipmap::kNoMaxSize;
if( bd->texFlags & plMipmap::kHalfSize )
flagsToSet |= plMipmap::kHalfSize;
if( bd->texFlags & plMipmap::kDontThrowAwayImage )
flagsToSet |= plMipmap::kDontThrowAwayImage;
hBitmap->SetFlags( hBitmap->GetFlags() | flagsToSet );
if (bd->useJPEG)
hBitmap->fCompressionType = plMipmap::kJPEGCompression;
// FIXME
if (/*fSave &&*/ !(bd->texFlags & plMipmap::kForceNonCompressed) && !bd->useJPEG)
{
// Are we on? Check Plasma Util panel
// SwitchUtil *pu = (SwitchUtil *)CreateInstance(UTILITY_CLASS_ID, PlasmaUtilClassID);
// if (!pu || pu->TextureCompressionEnabled())
{
plMipmap *compressed = hsCodecManager::Instance().CreateCompressedMipmap(plMipmap::kDirectXCompression, hBitmap);
// hsDXTSoftwareCodec::Instance().CreateCompressedBitmap(hBitmap);
// hsDXTDirectXCodec::Instance().CreateCompressedBitmap(hBitmap);
if (compressed)
{
delete hBitmap;
hBitmap = compressed;
hBitmap->SetFlags( hBitmap->GetFlags() | flagsToSet );
}
}
}
return hBitmap;
hsGuardEnd;
}
//
// Verify that bitmap is the correct type/size
//
void plBitmapCreator::ICheckOutBitmap(BitmapInfo* bInfo, Bitmap* bm, const char *fileName)
{
hsGuardBegin("hsConverterUtils::ICheckOutBitmap");
// Check out bitmap
if (bm->Flags() & MAP_FLIPPED)
MessageBox(GetActiveWindow(), "Bitmap is flipped horizontally", fileName, MB_OK);
if (bm->Flags() & MAP_INVERTED)
MessageBox(GetActiveWindow(), "Bitmap is inverted vertically", fileName, MB_OK);
if (bInfo->Flags() & MAP_FLIPPED)
MessageBox(GetActiveWindow(), "BI:Bitmap is flipped horizontally", fileName, MB_OK);
if (bInfo->Flags() & MAP_INVERTED)
MessageBox(GetActiveWindow(), "BI:Bitmap is inverted vertically", fileName, MB_OK);
hsGuardEnd;
}
int plBitmapCreator::IResampBitmap(Bitmap *bm, plMipmap &hBitmap)
{
hsGuardBegin("hsConverterUtils::IResampBitmap");
BitmapStorage *storage = bm->Storage();
BitmapInfo *bInfo = &storage->bi;
int dbgW = bm->Width(), dbgH = bm->Height();
int it;
for( it = 1; it <= bm->Width(); it <<= 1 );
it >>= 1;
hBitmap.fWidth = it;
for( it = 1; it <= bm->Height(); it <<= 1 );
it >>= 1;
hBitmap.fHeight = it;
if( (hBitmap.fHeight >> 3) > hBitmap.fWidth )
hBitmap.fHeight = hBitmap.fWidth << 3;
else
if( (hBitmap.fWidth >> 3) > hBitmap.fHeight )
hBitmap.fWidth = hBitmap.fHeight << 3;
hBitmap.fPixelSize = 32;
hBitmap.fRowBytes = hBitmap.fWidth * hBitmap.fPixelSize >> 3;
hBitmap.fNumLevels = 1;
hBitmap.fImage = HSMemory::New(hBitmap.fRowBytes * hBitmap.fHeight);
#ifdef COLOR_BLACK_WHITE
hBitmap.fFlags |= plMipmap::kColorWhite | plMipmap::kColorBlack;
#endif // COLOR_BLACK_WHITE
hBitmap.fFlags &= ~( plMipmap::kAlphaBitFlag | plMipmap::kAlphaChannelFlag );
int y,x;
float scaleY, scaleX;
hsRGBAColor32 *dstColor;
dstColor = (hsRGBAColor32*)hBitmap.fImage;
scaleX = ((float)bm->Width())/(float)hBitmap.fWidth;
scaleY = ((float)bm->Height())/(float)hBitmap.fHeight;
for (y = 0; y < hBitmap.fHeight; y++)
{
for (x = 0; x < hBitmap.fWidth; x++)
{
BMM_Color_64 c64_00, c64_01, c64_10, c64_11;
int ix, iy;
float fracX, fracY;
float t;
t = x * scaleX;
ix = (int)t;
fracX = t-ix;
t = y * scaleY;
iy = (int)t;
fracY = t-iy;
int ret = storage->GetPixels(ix,iy,1,&c64_00);
// FIXME
// fErrorMsg->Set(ret == 0, "ResampBitmap", "Failure getting pixels %dX%d", x, y).Check();
ret = storage->GetPixels(ix+1,iy,1,&c64_10);
ret = storage->GetPixels(ix,iy+1,1,&c64_01);
ret = storage->GetPixels(ix+1,iy+1,1,&c64_11);
dstColor->r = (unsigned char)(255.0 / 65535.0
* ( c64_00.r * (1.f - fracX) * (1.f - fracY)
+ c64_10.r * ( fracX) * (1.f - fracY)
+ c64_01.r * (1.f - fracX) * ( fracY)
+ c64_11.r * ( fracX) * ( fracY) ));
dstColor->g = (unsigned char)(255.0 / 65535.0
* ( c64_00.g * (1.f - fracX) * (1.f - fracY)
+ c64_10.g * ( fracX) * (1.f - fracY)
+ c64_01.g * (1.f - fracX) * ( fracY)
+ c64_11.g * ( fracX) * ( fracY) ));
dstColor->b = (unsigned char)(255.0 / 65535.0
* ( c64_00.b * (1.f - fracX) * (1.f - fracY)
+ c64_10.b * ( fracX) * (1.f - fracY)
+ c64_01.b * (1.f - fracX) * ( fracY)
+ c64_11.b * ( fracX) * ( fracY) ));
dstColor->a = (unsigned char)(255.0 / 65535.0
* ( c64_00.a * (1.f - fracX) * (1.f - fracY)
+ c64_10.a * ( fracX) * (1.f - fracY)
+ c64_01.a * (1.f - fracX) * ( fracY)
+ c64_11.a * ( fracX) * ( fracY) ));
#ifdef COLOR_BLACK_WHITE
if( dstColor->r | dstColor->g | dstColor->b )
hBitmap.fFlags &= ~plMipmap::kColorBlack;
if( ~(dstColor->r & dstColor->g & dstColor->b) )
hBitmap.fFlags &= ~plMipmap::kColorWhite;
#endif // COLOR_BLACK_WHITE
if( dstColor->a < 255 )
{
hBitmap.fFlags |= plMipmap::kAlphaBitFlag;
if( dstColor->a > 0 )
hBitmap.fFlags |= plMipmap::kAlphaChannelFlag;
}
dstColor++;
}
}
if( hBitmap.fFlags & plMipmap::kAlphaChannelFlag )
hBitmap.fFlags &= ~plMipmap::kAlphaBitFlag;
return 0;
hsGuardEnd;
}
int plBitmapCreator::ICopyBitmap(Bitmap *bm, plMipmap &hBitmap)
{
hsGuardBegin("hsConverterUtils::ICopyBitmap");
BitmapStorage *storage = bm->Storage();
BitmapInfo *bInfo = &storage->bi;
hBitmap.fWidth = bm->Width();
hBitmap.fHeight = bm->Height();
hBitmap.fPixelSize = 32;
hBitmap.fRowBytes = bm->Width()*hBitmap.fPixelSize/8;
hBitmap.fNumLevels = 1;
hBitmap.fImage = HSMemory::New(hBitmap.fRowBytes * hBitmap.fHeight);
#ifdef COLOR_BLACK_WHITE
hBitmap.fFlags |= plMipmap::kColorWhite | plMipmap::kColorBlack;
#endif // COLOR_BLACK_WHITE
hBitmap.fFlags &= ~( plMipmap::kAlphaBitFlag | plMipmap::kAlphaChannelFlag );
int y,x;
hsRGBAColor32 *dstColor;
dstColor = (hsRGBAColor32*)hBitmap.fImage;
for (y = 0; y < hBitmap.fHeight; y++)
{
for (x = 0; x < hBitmap.fWidth; x++)
{
BMM_Color_64 c64;
int ret = storage->GetPixels(x,y,1,&c64);
// FIXME
// fErrorMsg->Set(ret == 0, "CopyBitmap", "Failure getting pixels %dX%d", x, y).Check();
// Convert from 16 bits to 8 bits
dstColor->r = (char)(255.0*c64.r/65535.0);
dstColor->g = (char)(255.0*c64.g/65535.0);
dstColor->b = (char)(255.0*c64.b/65535.0);
dstColor->a = (char)(255.0*c64.a/65535.0);
#ifdef COLOR_BLACK_WHITE
if( dstColor->r | dstColor->g | dstColor->b )
hBitmap.fFlags &= ~plMipmap::kColorBlack;
if( ~(dstColor->r & dstColor->g & dstColor->b) )
hBitmap.fFlags &= ~plMipmap::kColorWhite;
#endif // COLOR_BLACK_WHITE
if( dstColor->a < 255 )
{
hBitmap.fFlags |= plMipmap::kAlphaBitFlag;
if( dstColor->a > 0 )
hBitmap.fFlags |= plMipmap::kAlphaChannelFlag;
}
dstColor++;
}
}
if( hBitmap.fFlags & plMipmap::kAlphaChannelFlag )
hBitmap.fFlags &= ~plMipmap::kAlphaBitFlag;
return 0;
hsGuardEnd;
}
int plBitmapCreator::IInvertAlpha(plMipmap& hBitmap)
{
hsGuardBegin("hsConverterUtils::ICopyBitmap");
hsAssert(hBitmap.fPixelSize == 32, "Only RGBA32 implemented");
if( hBitmap.fPixelSize != 32 )
return -1;
hsRGBAColor32* dstColor = (hsRGBAColor32*)hBitmap.fImage;
int n = hBitmap.GetWidth() * hBitmap.GetHeight();
int i;
for( i = 0; i < n; i++ )
{
dstColor->a = 255 - dstColor->a;
dstColor++;
}
return 0;
hsGuardEnd;
}
plBitmap *plBitmapCreator::CreateTexture(plBitmapData *bd, const plLocation &loc, int clipID)
{
plBitmap* bm = ICreateTexture(bd, loc, clipID);
for (int i = 0; i < plLocalization::GetNumLocales(); i++)
{
char localName[MAX_PATH];
if (plLocalization::ExportGetLocalized(bd->fileName, i, localName))
{
const char* oldName = bd->fileName;
bd->fileName = localName;
ICreateTexture(bd, loc, clipID);
bd->fileName = oldName;
}
}
return bm;
}
//// ICreateTexture ////////////////////////////////////////////////////////////
// Plasma texture creator. Pass it a completed bitmapdata structure and it
// returns a registered texture pointer, or nil if something goes wrong.
//
// 9.14.2001 mcn - clipID added to uniquely identify mipmaps that have been
// rescaled differently (approximately represents how many powers of 2 it was
// scaled down from the original source).
//
// 3.29.2002 mcn - Moved to plBitmapCreator, where it really belongs, and
// added code to handle tracking/cleaning up all materials that are exported.
plBitmap *plBitmapCreator::ICreateTexture( plBitmapData *bd, const plLocation &loc, int clipID )
{
hsGuardBegin( "plBitmapCreator::CreateTexture" );
const plLocation &textureLoc = plPluginResManager::ResMgr()->GetCommonPage( loc, plAgeDescription::kTextures );
if( !bd )
{
fErrorMsg->Set( true, "Bitmap Error", "No bitmap data" ).Show();
fErrorMsg->Set();
return nil;
}
if( bd->fileName == nil || bd->fileName[ 0 ] == 0 )
{
fErrorMsg->Set( true, "Bitmap Error", "Material texture has null bitmap name." ).Show();
fErrorMsg->Set();
return nil;
}
// Get and mangle key name
char name[ 256 ], temp[ 256 ];
_splitpath(bd->fileName, NULL, NULL, temp, NULL);
// Somehow, sometimes, we get the same file in with different cases. So we need to force the
// case identical all the time, else the patching process for dat files will think they're
// "different" when they're really not
strlwr( temp );
/// Mangle name for detail textures, so we don't end up overwriting settings elsewhere
if( bd->createFlags & plMipmap::kCreateDetailMask )
{
// Mangle of the form: name@dropStart&dropStop&max&min
if( clipID != -1 )
sprintf( name, "%s*%x#%d@%s&%3.2f&%3.2f&%3.2f&%3.2f", temp, bd->texFlags, clipID,
bd->createFlags & plMipmap::kCreateDetailAlpha ? "al" : ( bd->createFlags & plMipmap::kCreateDetailAdd ? "ad" : "mu" ),
bd->detailDropoffStart, bd->detailDropoffStop, bd->detailMax, bd->detailMin );
else
sprintf( name, "%s*%x@%s&%3.2f&%3.2f&%3.2f&%3.2f", temp, bd->texFlags,
bd->createFlags & plMipmap::kCreateDetailAlpha ? "al" : ( bd->createFlags == plMipmap::kCreateDetailAdd ? "ad" : "mu" ),
bd->detailDropoffStart, bd->detailDropoffStop, bd->detailMax, bd->detailMin );
}
else if( clipID != -1 )
sprintf( name, "%s*%x#%d", temp, bd->texFlags, clipID );
else
sprintf( name, "%s*%x", temp, bd->texFlags );
if( bd->invertAlpha )
strcat( name, "_inva" );
strcat( name, ".hsm" );
// Has this texture been used before?
plKey key;
plBitmap *texture = plBitmap::ConvertNoRef( sCommonBitmapLib.FindObject( name, ( bd->isStaticCubicEnvMap ) ? plCubicEnvironmap::Index() : plMipmap::Index() ) );
//hsAssert( texture == nil || texture->GetKey()->GetUoid().GetLocation() == textureLoc, "Somehow our texture objectLib has a texture not in the right page? Should be harmless tho..." );
// Texture reuse optimization
if( texture )
{
WIN32_FILE_ATTRIBUTE_DATA fileAttrib;
GetFileAttributesEx(bd->fileName, GetFileExInfoStandard, &fileAttrib);
FILETIME &fileTime = fileAttrib.ftLastWriteTime;
// If this texture has been modified since the last export, delete the old version but reuse the key
if (!texture->IsSameModifiedTime(fileTime.dwLowDateTime, fileTime.dwHighDateTime))
{
DeleteExportedBitmap( texture->GetKey() );
texture = nil;
key = nil;
}
}
if( texture )
{
// If it's in the registry, great, use it.
if( bd->texFlags & plMipmap::kNoMaxSize )
texture->SetFlags( texture->GetFlags() | plMipmap::kNoMaxSize );
if( bd->texFlags & plMipmap::kHalfSize )
texture->SetFlags( texture->GetFlags() | plMipmap::kHalfSize );
}
else
{
// If it hasn't been used before, make a new texture
if( bd->isStaticCubicEnvMap )
{
plCubicEnvironmap *cubic = TRACKED_NEW plCubicEnvironmap;
plMipmap *face;
/// Build and set the faces
bd->fileName = bd->faceNames[ plStaticEnvLayer::kTopFace ];
face = ICreateBitmap( bd );
if( face == nil ) return nil;
cubic->CopyToFace( face, plCubicEnvironmap::kTopFace );
bd->fileName = bd->faceNames[ plStaticEnvLayer::kBottomFace ];
face = ICreateBitmap( bd );
if( face == nil ) return nil;
cubic->CopyToFace( face, plCubicEnvironmap::kBottomFace );
bd->fileName = bd->faceNames[ plStaticEnvLayer::kLeftFace ];
face = ICreateBitmap( bd );
if( face == nil ) return nil;
cubic->CopyToFace( face, plCubicEnvironmap::kLeftFace );
bd->fileName = bd->faceNames[ plStaticEnvLayer::kRightFace ];
face = ICreateBitmap( bd );
if( face == nil ) return nil;
cubic->CopyToFace( face, plCubicEnvironmap::kRightFace );
/// NOTE: For whatever reason, MAX decided that the front and back faces should be'
/// switched, literally. It's as if the cube for the cube map starts at the back face
/// and then wraps around, instead of starting at the front face. Since we do things
/// the RIGHT way (or rather, the front way :) on client-side, we need to flip the
/// two here. If you convert this to the real MAX UI, make sure the faces are still
/// flipped!!!!!!!!
bd->fileName = bd->faceNames[ plStaticEnvLayer::kBackFace ];
face = ICreateBitmap( bd );
if( face == nil ) return nil;
cubic->CopyToFace( face, plCubicEnvironmap::kFrontFace );
bd->fileName = bd->faceNames[ plStaticEnvLayer::kFrontFace ];
face = ICreateBitmap( bd );
if( face == nil ) return nil;
cubic->CopyToFace( face, plCubicEnvironmap::kBackFace );
key = hsgResMgr::ResMgr()->NewKey( name, cubic, textureLoc );
texture = (plBitmap *)cubic;
}
else
{
plMipmap *mipmap = ICreateBitmap(bd);
if (!mipmap)
return nil;
key = hsgResMgr::ResMgr()->NewKey( name, mipmap, textureLoc );
texture = (plBitmap *)mipmap;
}
// Texture reuse optimization
WIN32_FILE_ATTRIBUTE_DATA fileAttrib;
GetFileAttributesEx(bd->fileName, GetFileExInfoStandard, &fileAttrib);
FILETIME &fileTime = fileAttrib.ftLastWriteTime;
texture->SetModifiedTime(fileTime.dwLowDateTime, fileTime.dwHighDateTime);
// Add to our list of created textures and ref, since we have a hold of them
IAddBitmap( texture );
}
return texture;
hsGuardEnd;
}
//// IAddBitmap ///////////////////////////////////////////////////////////////
void plBitmapCreator::IAddBitmap( plBitmap *bitmap, hsBool dontRef )
{
sCommonBitmapLib.AddObject( bitmap );
}
//// CreateBlankMipmap ////////////////////////////////////////////////////////
// Simple mipmap creator, but importantly, it also adds the mipmap to the list
// of "converted" maps to clean up at the end of export.
plMipmap *plBitmapCreator::CreateBlankMipmap( UInt32 width, UInt32 height, unsigned config, UInt8 numLevels,
const char *keyName, const plLocation &keyLocation )
{
hsGuardBegin( "plBitmapCreator::CreateBlankMipmap" );
// Get our real location
const plLocation &textureLoc = plPluginResManager::ResMgr()->GetCommonPage( keyLocation, plAgeDescription::kTextures );
// Is it already created?
plKey key = hsgResMgr::ResMgr()->FindKey( plUoid( textureLoc, plMipmap::Index(), keyName ) );
if( key != nil )
return plMipmap::ConvertNoRef( key->GetObjectPtr() );
// Create
plMipmap *mip = TRACKED_NEW plMipmap( width, height, config, numLevels );
// Assign key
hsgResMgr::ResMgr()->NewKey( keyName, mip, textureLoc );
// Add to our list
IAddBitmap( mip );
return mip;
hsGuardEnd;
}