//========= Copyright 1996-2005, Valve Corporation, All rights reserved. ============// // // Purpose: The VTF file format I/O class to help simplify access to VTF files // //=====================================================================================// #undef fopen #include "bitmap/imageformat.h" #include "cvtf.h" #include "utlbuffer.h" #include "tier0/dbg.h" #include "mathlib/vector.h" #include "mathlib/mathlib.h" #include "tier1/strtools.h" #include "tier0/mem.h" #include "s3tc_decode.h" #include "utlvector.h" // memdbgon must be the last include file in a .cpp file!!! #include "tier0/memdbgon.h" // byteswap data descriptions BEGIN_BYTESWAP_DATADESC( VTFFileBaseHeader_t ) DEFINE_ARRAY( fileTypeString, FIELD_CHARACTER, 4 ), DEFINE_ARRAY( version, FIELD_INTEGER, 2 ), DEFINE_FIELD( headerSize, FIELD_INTEGER ), END_DATADESC() BEGIN_BYTESWAP_DATADESC_( VTFFileHeaderV7_1_t, VTFFileBaseHeader_t ) DEFINE_FIELD( width, FIELD_SHORT ), DEFINE_FIELD( height, FIELD_SHORT ), DEFINE_FIELD( flags, FIELD_INTEGER ), DEFINE_FIELD( numFrames, FIELD_SHORT ), DEFINE_FIELD( startFrame, FIELD_SHORT ), DEFINE_FIELD( reflectivity, FIELD_VECTOR ), DEFINE_FIELD( bumpScale, FIELD_FLOAT ), DEFINE_FIELD( imageFormat, FIELD_INTEGER ), DEFINE_FIELD( numMipLevels, FIELD_CHARACTER ), DEFINE_FIELD( lowResImageFormat, FIELD_INTEGER ), DEFINE_FIELD( lowResImageWidth, FIELD_CHARACTER ), DEFINE_FIELD( lowResImageHeight, FIELD_CHARACTER ), END_DATADESC() BEGIN_BYTESWAP_DATADESC_( VTFFileHeaderV7_2_t, VTFFileHeaderV7_1_t ) DEFINE_FIELD( depth, FIELD_SHORT ), END_DATADESC() BEGIN_BYTESWAP_DATADESC_( VTFFileHeaderV7_3_t, VTFFileHeaderV7_2_t ) DEFINE_FIELD( numResources, FIELD_INTEGER ), END_DATADESC() BEGIN_BYTESWAP_DATADESC_( VTFFileHeader_t, VTFFileHeaderV7_2_t ) END_DATADESC() BEGIN_BYTESWAP_DATADESC_( VTFFileHeaderX360_t, VTFFileBaseHeader_t ) DEFINE_FIELD( flags, FIELD_INTEGER ), DEFINE_FIELD( width, FIELD_SHORT ), DEFINE_FIELD( height, FIELD_SHORT ), DEFINE_FIELD( depth, FIELD_SHORT ), DEFINE_FIELD( numFrames, FIELD_SHORT ), DEFINE_FIELD( preloadDataSize, FIELD_SHORT ), DEFINE_FIELD( mipSkipCount, FIELD_CHARACTER ), DEFINE_FIELD( numResources, FIELD_CHARACTER ), DEFINE_FIELD( reflectivity, FIELD_VECTOR ), DEFINE_FIELD( bumpScale, FIELD_FLOAT ), DEFINE_FIELD( imageFormat, FIELD_INTEGER ), DEFINE_ARRAY( lowResImageSample, FIELD_CHARACTER, 4 ), DEFINE_FIELD( compressedSize, FIELD_INTEGER ), END_DATADESC() BEGIN_BYTESWAP_DATADESC_( VTFFileHeaderPS3_t, VTFFileBaseHeader_t ) DEFINE_FIELD( flags, FIELD_INTEGER ), DEFINE_FIELD( width, FIELD_SHORT ), DEFINE_FIELD( height, FIELD_SHORT ), DEFINE_FIELD( depth, FIELD_SHORT ), DEFINE_FIELD( numFrames, FIELD_SHORT ), DEFINE_FIELD( preloadDataSize, FIELD_SHORT ), DEFINE_FIELD( mipSkipCount, FIELD_CHARACTER ), DEFINE_FIELD( numResources, FIELD_CHARACTER ), DEFINE_FIELD( reflectivity, FIELD_VECTOR ), DEFINE_FIELD( bumpScale, FIELD_FLOAT ), DEFINE_FIELD( imageFormat, FIELD_INTEGER ), DEFINE_ARRAY( lowResImageSample, FIELD_CHARACTER, 4 ), DEFINE_FIELD( compressedSize, FIELD_INTEGER ), END_DATADESC() #if defined( POSIX ) || defined( _X360 ) // stub functions const char* S3TC_GetBlock( const void *pCompressed, ImageFormat format, int nBlocksWide, // How many blocks wide is the image (pixels wide / 4). int xBlock, int yBlock ) { return NULL; } char* S3TC_GetBlock( void *pCompressed, ImageFormat format, int nBlocksWide, // How many blocks wide is the image (pixels wide / 4). int xBlock, int yBlock ) { return NULL; } S3PaletteIndex S3TC_GetPaletteIndex( unsigned char *pFaceData, ImageFormat format, int imageWidth, int x, int y ) { S3PaletteIndex nullPalette; memset(&nullPalette, 0x0, sizeof(nullPalette)); return nullPalette; } // Merge the two palettes and copy the colors void S3TC_MergeBlocks( char **blocks, S3RGBA **pOriginals, int nBlocks, int lPitch, // (in BYTES) ImageFormat format ) { } // Note: width, x, and y are in texels, not S3 blocks. void S3TC_SetPaletteIndex( unsigned char *pFaceData, ImageFormat format, int imageWidth, int x, int y, S3PaletteIndex paletteIndex ) { } #endif // This gives a vertex number to each of the 4 verts on each face. // We use this to match the verts and determine which edges need to be blended together. // The vert ordering is lower-left, top-left, top-right, bottom-right. int g_leftFaceVerts[4] = { 2, 6, 7, 3 }; int g_frontFaceVerts[4] = { 2, 3, 5, 4 }; int g_downFaceVerts[4] = { 4, 0, 6, 2 }; int g_rightFaceVerts[4] = { 5, 1, 0, 4 }; int g_backFaceVerts[4] = { 7, 6, 0, 1 }; int g_upFaceVerts[4] = { 3, 7, 1, 5 }; int *g_FaceVerts[6] = { g_rightFaceVerts, g_leftFaceVerts, g_backFaceVerts, g_frontFaceVerts, g_upFaceVerts, g_downFaceVerts }; // For skyboxes.. // These were constructed for the engine skybox, which looks like this // (assuming X goes forward, Y goes left, and Z goes up). // // 6 ------------- 5 // / / // / | / | // / | / | // 2 ------------- 1 | // | | // | | // | 7 ------|------ 4 // | / | / // | / | / // / / // 3 ------------- 0 // int g_skybox_rightFaceVerts[4] = { 7, 6, 5, 4 }; int g_skybox_leftFaceVerts[4] = { 0, 1, 2, 3 }; int g_skybox_backFaceVerts[4] = { 3, 2, 6, 7 }; int g_skybox_frontFaceVerts[4] = { 4, 5, 1, 0 }; int g_skybox_upFaceVerts[4] = { 6, 2, 1, 5 }; int g_skybox_downFaceVerts[4] = { 3, 7, 4, 0 }; int *g_skybox_FaceVerts[6] = { g_skybox_rightFaceVerts, g_skybox_leftFaceVerts, g_skybox_backFaceVerts, g_skybox_frontFaceVerts, g_skybox_upFaceVerts, g_skybox_downFaceVerts }; //----------------------------------------------------------------------------- // Class factory //----------------------------------------------------------------------------- IVTFTexture *CreateVTFTexture() { return new CVTFTexture; } void DestroyVTFTexture( IVTFTexture *pTexture ) { CVTFTexture *pTex = static_cast(pTexture); if ( pTex ) { delete pTex; } } //----------------------------------------------------------------------------- // Allows us to only load in the first little bit of the VTF file to get info //----------------------------------------------------------------------------- int VTFFileHeaderSize( int nMajorVersion, int nMinorVersion ) { if ( nMajorVersion == -1 ) { nMajorVersion = VTF_MAJOR_VERSION; } if ( nMinorVersion == -1 ) { nMinorVersion = VTF_MINOR_VERSION; } switch ( nMajorVersion ) { case VTF_MAJOR_VERSION: switch ( nMinorVersion ) { case 0: // fall through case 1: return sizeof( VTFFileHeaderV7_1_t ); case 2: return sizeof( VTFFileHeaderV7_2_t ); case 3: return sizeof( VTFFileHeaderV7_3_t ) + sizeof( ResourceEntryInfo ) * MAX_RSRC_DICTIONARY_ENTRIES; case 4: case VTF_MINOR_VERSION: int size1 = sizeof( VTFFileHeader_t ); int size2 = sizeof( ResourceEntryInfo ) * MAX_RSRC_DICTIONARY_ENTRIES; int result = size1 + size2; //printf("\n VTFFileHeaderSize (%i %i) is %i + %i -> %i",nMajorVersion,nMinorVersion, size1, size2, result ); return result; } break; case VTF_X360_MAJOR_VERSION: return sizeof( VTFFileHeaderX360_t ) + sizeof( ResourceEntryInfo ) * MAX_X360_RSRC_DICTIONARY_ENTRIES; case VTF_PS3_MAJOR_VERSION: return sizeof( VTFFileHeaderPS3_t ) + sizeof( ResourceEntryInfo ) * MAX_X360_RSRC_DICTIONARY_ENTRIES; } return 0; } //----------------------------------------------------------------------------- // Constructor, destructor //----------------------------------------------------------------------------- CVTFTexture::CVTFTexture() { m_nVersion[0] = 0; m_nVersion[1] = 0; m_nWidth = 0; m_nHeight = 0; m_nDepth = 1; m_Format = IMAGE_FORMAT_UNKNOWN; m_nMipCount = 0; m_nFaceCount = 0; m_nFrameCount = 0; // FIXME: Is the start frame needed? m_iStartFrame = 0; m_flAlphaThreshhold = -1.0f; m_flAlphaHiFreqThreshhold = -1.0f; m_flBumpScale = 1.0f; m_vecReflectivity.Init( 1.0, 1.0, 1.0f ); m_nFlags = 0; m_pImageData = NULL; m_nImageAllocSize = 0; // LowRes data m_LowResImageFormat = IMAGE_FORMAT_UNKNOWN; m_nLowResImageWidth = 0; m_nLowResImageHeight = 0; m_pLowResImageData = NULL; m_nLowResImageAllocSize = 0; #if defined( _X360 ) || defined ( _PS3 ) m_nMipSkipCount = 0; *(unsigned int *)m_LowResImageSample = 0; #endif Assert( m_arrResourcesInfo.Count() == 0 ); Assert( m_arrResourcesData.Count() == 0 ); Assert( m_arrResourcesData_ForReuse.Count() == 0 ); memset( &m_Options, 0, sizeof( m_Options ) ); m_Options.cbSize = sizeof( m_Options ); } CVTFTexture::~CVTFTexture() { Shutdown(); } #ifndef PLATFORM_X360 bool CVTFTexture::IsPreTiled() const { return false; } #endif //----------------------------------------------------------------------------- // Compute the mip count based on the size + flags //----------------------------------------------------------------------------- int CVTFTexture::ComputeMipCount() const { if ( IsX360() && ( m_nVersion[0] == VTF_X360_MAJOR_VERSION ) && ( m_nFlags & TEXTUREFLAGS_NOMIP ) ) { // 360 vtf format culled unused mips at conversion time return 1; } if ( IsPS3() && ( m_nVersion[0] == VTF_PS3_MAJOR_VERSION ) && ( m_nFlags & TEXTUREFLAGS_NOMIP ) ) { // PS3 vtf format culled unused mips at conversion time return 1; } // NOTE: No matter what, all mip levels should be created because // we have to worry about various fallbacks return ImageLoader::GetNumMipMapLevels( m_nWidth, m_nHeight, m_nDepth ); } //----------------------------------------------------------------------------- // Allocate data blocks with an eye toward re-using memory //----------------------------------------------------------------------------- static bool GenericAllocateReusableData( unsigned char **ppData, int *pNumAllocated, int numRequested ) { // If we're asking for memory and we have way more than we expect, free some. if ( *pNumAllocated < numRequested || ( numRequested > 0 && *pNumAllocated > 16 * numRequested ) ) { delete [] *ppData; *ppData = new unsigned char[ numRequested ]; if ( *ppData ) { *pNumAllocated = numRequested; return true; } *pNumAllocated = 0; return false; } return true; } bool CVTFTexture::AllocateImageData( int nMemorySize ) { return GenericAllocateReusableData( &m_pImageData, &m_nImageAllocSize, nMemorySize ); } bool CVTFTexture::ResourceMemorySection::AllocateData( int nMemorySize ) { if ( GenericAllocateReusableData( &m_pData, &m_nDataAllocSize, nMemorySize ) ) { m_nDataLength = nMemorySize; return true; } return false; } bool CVTFTexture::AllocateLowResImageData( int nMemorySize ) { return GenericAllocateReusableData( &m_pLowResImageData, &m_nLowResImageAllocSize, nMemorySize ); } inline bool IsMultipleOf4( int value ) { // NOTE: This catches powers of 2 less than 4 also return ( value <= 2 ) || ( (value & 0x3) == 0 ); } //----------------------------------------------------------------------------- // Initialization //----------------------------------------------------------------------------- bool CVTFTexture::Init( int nWidth, int nHeight, int nDepth, ImageFormat fmt, int iFlags, int iFrameCount, int nForceMipCount ) { if ( nDepth == 0 ) { nDepth = 1; } if (iFlags & TEXTUREFLAGS_ENVMAP) { if (nWidth != nHeight) { Warning( "Height and width must be equal for cubemaps!\n" ); return false; } if (nDepth != 1) { Warning( "Depth must be 1 for cubemaps!\n" ); return false; } } if ( ( fmt == IMAGE_FORMAT_DXT1 ) || ( fmt == IMAGE_FORMAT_DXT3 ) || ( fmt == IMAGE_FORMAT_DXT5 ) || ( fmt == IMAGE_FORMAT_DXT1_RUNTIME ) || ( fmt == IMAGE_FORMAT_DXT5_RUNTIME ) ) { if ( !IsMultipleOf4( nWidth ) || !IsMultipleOf4( nHeight ) || !IsMultipleOf4( nDepth ) ) { Warning( "Image dimensions must be multiple of 4!\n" ); return false; } } if ( fmt == IMAGE_FORMAT_DEFAULT ) { fmt = IMAGE_FORMAT_RGBA8888; } m_nWidth = nWidth; m_nHeight = nHeight; m_nDepth = nDepth; m_Format = fmt; m_nFlags = iFlags; // THIS CAUSED A BUG!!! We want all of the mip levels in the vtf file even with nomip in case we have lod. // NOTE: But we don't want more than 1 mip level for procedural textures if ( (iFlags & (TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_PROCEDURAL)) == (TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_PROCEDURAL) ) { nForceMipCount = 1; } if ( nForceMipCount == -1 ) { m_nMipCount = ComputeMipCount(); } else { m_nMipCount = nForceMipCount; } m_nFrameCount = iFrameCount; m_nFaceCount = (iFlags & TEXTUREFLAGS_ENVMAP) ? CUBEMAP_FACE_COUNT : 1; #if defined( _X360 ) || defined ( _PS3 ) m_nMipSkipCount = 0; #endif // Need to do this because Shutdown deallocates the low-res image m_nLowResImageWidth = m_nLowResImageHeight = 0; // Allocate me some bits! int iMemorySize = ComputeTotalSize(); if ( !AllocateImageData( iMemorySize ) ) return false; // As soon as we have image indicate so in the resources if ( iMemorySize ) FindOrCreateResourceEntryInfo( VTF_LEGACY_RSRC_IMAGE ); else RemoveResourceEntryInfo( VTF_LEGACY_RSRC_IMAGE ); return true; } //----------------------------------------------------------------------------- // Methods to initialize the low-res image //----------------------------------------------------------------------------- void CVTFTexture::InitLowResImage( int nWidth, int nHeight, ImageFormat fmt ) { m_nLowResImageWidth = nWidth; m_nLowResImageHeight = nHeight; m_LowResImageFormat = fmt; // Allocate low-res bits int iLowResImageSize = ImageLoader::GetMemRequired( m_nLowResImageWidth, m_nLowResImageHeight, 1, m_LowResImageFormat, false ); if ( !AllocateLowResImageData( iLowResImageSize ) ) return; // As soon as we have low-res image indicate so in the resources if ( iLowResImageSize ) FindOrCreateResourceEntryInfo( VTF_LEGACY_RSRC_LOW_RES_IMAGE ); else RemoveResourceEntryInfo( VTF_LEGACY_RSRC_LOW_RES_IMAGE ); } //----------------------------------------------------------------------------- // Methods to set other texture fields //----------------------------------------------------------------------------- void CVTFTexture::SetBumpScale( float flScale ) { m_flBumpScale = flScale; } void CVTFTexture::SetReflectivity( const Vector &vecReflectivity ) { VectorCopy( vecReflectivity, m_vecReflectivity ); } // Sets threshhold values for alphatest mipmapping void CVTFTexture::SetAlphaTestThreshholds( float flBase, float flHighFreq ) { m_flAlphaThreshhold = flBase; m_flAlphaHiFreqThreshhold = flHighFreq; } //----------------------------------------------------------------------------- // Release and reset the resources. //----------------------------------------------------------------------------- void CVTFTexture::ReleaseResources() { m_arrResourcesInfo.RemoveAll(); for ( ResourceMemorySection *pRms = m_arrResourcesData.Base(), *pRmsEnd = pRms + m_arrResourcesData.Count(); pRms < pRmsEnd; ++pRms ) { delete [] pRms->m_pData; } m_arrResourcesData.RemoveAll(); for ( ResourceMemorySection *pRms = m_arrResourcesData_ForReuse.Base(), *pRmsEnd = pRms + m_arrResourcesData_ForReuse.Count(); pRms < pRmsEnd; ++pRms ) { delete [] pRms->m_pData; } m_arrResourcesData_ForReuse.RemoveAll(); } //----------------------------------------------------------------------------- // Shutdown //----------------------------------------------------------------------------- void CVTFTexture::Shutdown() { #if defined( _GAMECONSOLE ) // must be first to ensure X360/PS3 aliased pointers are unhooked, otherwise memory corruption ReleaseImageMemory(); #endif delete[] m_pImageData; m_pImageData = NULL; m_nImageAllocSize = 0; delete[] m_pLowResImageData; m_pLowResImageData = NULL; m_nLowResImageAllocSize = 0; ReleaseResources(); } //----------------------------------------------------------------------------- // These are methods to help with optimization of file access //----------------------------------------------------------------------------- void CVTFTexture::LowResFileInfo( int *pStartLocation, int *pSizeInBytes ) const { // Once the header is read in, they indicate where to start reading // other data, and how many bytes to read.... if ( ResourceEntryInfo const *pLowResData = FindResourceEntryInfo( VTF_LEGACY_RSRC_LOW_RES_IMAGE ) ) { *pStartLocation = pLowResData->resData; *pSizeInBytes = ImageLoader::GetMemRequired( m_nLowResImageWidth, m_nLowResImageHeight, 1, m_LowResImageFormat, false ); } else { *pStartLocation = 0; *pSizeInBytes = 0; } } void CVTFTexture::ImageFileInfo( int nFrame, int nFace, int nMipLevel, int *pStartLocation, int *pSizeInBytes) const { int i; int iMipWidth; int iMipHeight; int iMipDepth; ResourceEntryInfo const *pImageDataInfo = FindResourceEntryInfo( VTF_LEGACY_RSRC_IMAGE ); if ( pImageDataInfo == NULL ) { // This should never happen for real, but can happen if someone intentionally fed us a bad VTF. Assert( pImageDataInfo ); ( *pStartLocation ) = 0; ( *pSizeInBytes ) = 0; return; } // The image data start offset int nOffset = pImageDataInfo->resData; // get to the right miplevel for( i = m_nMipCount - 1; i > nMipLevel; --i ) { ComputeMipLevelDimensions( i, &iMipWidth, &iMipHeight, &iMipDepth ); int iMipLevelSize = ImageLoader::GetMemRequired( iMipWidth, iMipHeight, iMipDepth, m_Format, false ); nOffset += iMipLevelSize * m_nFrameCount * m_nFaceCount; } // get to the right frame ComputeMipLevelDimensions( nMipLevel, &iMipWidth, &iMipHeight, &iMipDepth ); int nFaceSize = ImageLoader::GetMemRequired( iMipWidth, iMipHeight, iMipDepth, m_Format, false ); // For backwards compatibility, we don't read in the spheremap fallback on // older format .VTF files... int nFacesToRead = m_nFaceCount; if ( IsCubeMap() ) { if ((m_nVersion[0] == 7) && (m_nVersion[1] < 1)) { nFacesToRead = 6; if (nFace == CUBEMAP_FACE_SPHEREMAP) { --nFace; } } } int nFrameSize = nFacesToRead * nFaceSize; nOffset += nFrameSize * nFrame; // get to the right face nOffset += nFace * nFaceSize; *pStartLocation = nOffset; *pSizeInBytes = nFaceSize; } int CVTFTexture::FileSize( int nMipSkipCount ) const { ResourceEntryInfo const *pImageDataInfo = FindResourceEntryInfo( VTF_LEGACY_RSRC_IMAGE ); // Can be null when someone gives us an intentionally malformed VTF. if ( pImageDataInfo == NULL ) { // Still do the assert so we can catch this in debug--we don't expect this for well formed files. Assert( pImageDataInfo != NULL ); return 0; } int nOffset = pImageDataInfo->resData; int nFaceSize = ComputeFaceSize( nMipSkipCount ); int nImageSize = nFaceSize * m_nFaceCount * m_nFrameCount; return nOffset + nImageSize; } //----------------------------------------------------------------------------- // Unserialization of low-res data //----------------------------------------------------------------------------- bool CVTFTexture::LoadLowResData( CUtlBuffer &buf ) { // Allocate low-res bits InitLowResImage( m_nLowResImageWidth, m_nLowResImageHeight, m_LowResImageFormat ); int nLowResImageSize = ImageLoader::GetMemRequired( m_nLowResImageWidth, m_nLowResImageHeight, 1, m_LowResImageFormat, false ); buf.Get( m_pLowResImageData, nLowResImageSize ); bool bValid = buf.IsValid(); return bValid; } //----------------------------------------------------------------------------- // Unserialization of image data //----------------------------------------------------------------------------- bool CVTFTexture::LoadImageData( CUtlBuffer &buf, const VTFFileHeader_t &header, int nSkipMipLevels ) { // Fix up the mip count + size based on how many mip levels we skip... if (nSkipMipLevels > 0) { Assert( m_nMipCount > nSkipMipLevels ); if (header.numMipLevels < nSkipMipLevels) { // NOTE: This can only happen with older format .vtf files Warning("Warning! Encountered old format VTF file; please rebuild it!\n"); return false; } ComputeMipLevelDimensions( nSkipMipLevels, &m_nWidth, &m_nHeight, &m_nDepth ); m_nMipCount -= nSkipMipLevels; } // read the texture image (including mipmaps if they are there and needed.) int iImageSize = ComputeFaceSize(); iImageSize *= m_nFaceCount * m_nFrameCount; // For backwards compatibility, we don't read in the spheremap fallback on // older format .VTF files... if ( !AllocateImageData( iImageSize ) ) return false; // NOTE: The mip levels are stored ascending from smallest (1x1) to largest (NxN) // in order to allow for truncated reads of the minimal required data // NOTE: I checked in a bad version 4 where it stripped out the spheremap. // To make it all work, need to check for that bad case. bool bNoSkip = false; if ( IsCubeMap() && ( header.version[0] == 7 ) && ( header.version[1] == 4 ) ) { int nBytesRemaining = buf.TellMaxPut() - buf.TellGet(); int nFileSize = ComputeFaceSize( nSkipMipLevels ) * m_nFaceCount * m_nFrameCount; if ( nBytesRemaining == nFileSize ) { bNoSkip = true; } } int nGet = buf.TellGet(); retryCubemapLoad: for (int iMip = m_nMipCount; --iMip >= 0; ) { // NOTE: This is for older versions... if ( header.numMipLevels - nSkipMipLevels <= iMip ) continue; int iMipSize = ComputeMipSize( iMip ); for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for (int iFace = 0; iFace < m_nFaceCount; ++iFace) { // printf("\n tex %p mip %i frame %i face %i size %i buf offset %i", this, iMip, iFrame, iFace, iMipSize, buf.TellGet() ); unsigned char *pMipBits = ImageData( iFrame, iFace, iMip ); buf.Get( pMipBits, iMipSize ); } // Strip out the spheremap in older versions if ( IsCubeMap() && !bNoSkip && ( header.version[0] == 7 ) && ( header.version[1] >= 1 ) && ( header.version[1] < 5 ) ) { buf.SeekGet( CUtlBuffer::SEEK_CURRENT, iMipSize ); } } } bool bOk = buf.IsValid(); if ( !bOk && IsCubeMap() && ( header.version[0] == 7 ) && ( header.version[1] <= 4 ) ) { if ( !bNoSkip ) { bNoSkip = true; buf.SeekGet( CUtlBuffer::SEEK_HEAD, nGet ); goto retryCubemapLoad; } Warning( "** Encountered stale cubemap! Please rebuild the following vtf:\n" ); } return bOk; } void *CVTFTexture::SetResourceData( uint32 eType, void const *pData, size_t nNumBytes ) { Assert( ( eType & RSRCF_MASK ) == 0 ); eType &= ~RSRCF_MASK; // Very inefficient to set less than 4 bytes of data Assert( !nNumBytes || ( nNumBytes >= sizeof( uint32 ) ) ); if ( nNumBytes ) { ResourceEntryInfo *pInfo = FindOrCreateResourceEntryInfo( eType ); int idx = pInfo - m_arrResourcesInfo.Base(); ResourceMemorySection &rms = m_arrResourcesData[ idx ]; if ( nNumBytes == sizeof( pInfo->resData ) ) { // store 4 bytes directly pInfo->eType |= RSRCF_HAS_NO_DATA_CHUNK; if ( pData ) pInfo->resData = reinterpret_cast< const int * >( pData )[0]; return &pInfo->resData; } else { if ( !rms.AllocateData( nNumBytes ) ) { RemoveResourceEntryInfo( eType ); return NULL; } if ( pData ) memcpy( rms.m_pData, pData, nNumBytes ); return rms.m_pData; } } else { RemoveResourceEntryInfo( eType ); return NULL; } } void *CVTFTexture::GetResourceData( uint32 eType, size_t *pDataSize ) const { Assert( ( eType & RSRCF_MASK ) == 0 ); eType &= ~RSRCF_MASK; ResourceEntryInfo const *pInfo = FindResourceEntryInfo( eType ); if ( pInfo ) { if ( ( pInfo->eType & RSRCF_HAS_NO_DATA_CHUNK ) == 0 ) { int idx = pInfo - m_arrResourcesInfo.Base(); ResourceMemorySection const &rms = m_arrResourcesData[ idx ]; if ( pDataSize ) { *pDataSize = rms.m_nDataLength; } return rms.m_pData; } else { if ( pDataSize ) { *pDataSize = sizeof( pInfo->resData ); } return (void *)&pInfo->resData; } } else { if ( pDataSize ) *pDataSize = 0; } return NULL; } bool CVTFTexture::HasResourceEntry( uint32 eType ) const { return ( FindResourceEntryInfo( eType ) != NULL ); } unsigned int CVTFTexture::GetResourceTypes( unsigned int *arrTypesBuffer, int numTypesBufferElems ) const { for ( ResourceEntryInfo const *pInfo = m_arrResourcesInfo.Base(), *pInfoEnd = pInfo + m_arrResourcesInfo.Count(); numTypesBufferElems-- > 0 && pInfo < pInfoEnd; ) { *( arrTypesBuffer++ ) = ( ( pInfo++ )->eType & ~RSRCF_MASK ); } return m_arrResourcesInfo.Count(); } //----------------------------------------------------------------------------- // Serialization/Unserialization of resource data //----------------------------------------------------------------------------- bool CVTFTexture::ResourceMemorySection::LoadData( CUtlBuffer &buf, CByteswap &byteSwap ) { // Read the size int iDataSize = 0; buf.Get( &iDataSize, sizeof( iDataSize ) ); byteSwap.SwapBufferToTargetEndian( &iDataSize ); // Read the actual data if ( !AllocateData( iDataSize ) ) return false; buf.Get( m_pData, iDataSize ); // Test valid bool bValid = buf.IsValid(); return bValid; } bool CVTFTexture::ResourceMemorySection::WriteData( CUtlBuffer &buf ) const { Assert( m_nDataLength && m_pData ); int iBufSize = m_nDataLength; buf.Put( &iBufSize, sizeof( iBufSize ) ); buf.Put( m_pData, m_nDataLength ); return buf.IsValid(); } //----------------------------------------------------------------------------- // Checks if the file data needs to be swapped //----------------------------------------------------------------------------- bool CVTFTexture::SetupByteSwap( CUtlBuffer &buf ) { VTFFileBaseHeader_t *header = (VTFFileBaseHeader_t*)buf.PeekGet(); if ( header->version[0] == SwapLong( VTF_MAJOR_VERSION ) ) { m_Swap.ActivateByteSwapping( true ); return true; } return false; } //----------------------------------------------------------------------------- // Unserialization //----------------------------------------------------------------------------- static bool ReadHeaderFromBufferPastBaseHeader( CUtlBuffer &buf, VTFFileHeader_t &header ) { unsigned char *pBuf = (unsigned char*)(&header) + sizeof(VTFFileBaseHeader_t); if ( header.version[1] <= VTF_MINOR_VERSION && header.version[1] >= 4 ) { buf.Get( pBuf, sizeof(VTFFileHeader_t) - sizeof(VTFFileBaseHeader_t) ); } else if ( header.version[1] == 3 ) { buf.Get( pBuf, sizeof(VTFFileHeaderV7_3_t) - sizeof(VTFFileBaseHeader_t) ); } else if ( header.version[1] == 2 ) { buf.Get( pBuf, sizeof(VTFFileHeaderV7_2_t) - sizeof(VTFFileBaseHeader_t) ); #if defined( _X360 ) || defined (POSIX) // read 15 dummy bytes to be properly positioned with 7.2 PC data byte dummy[15]; buf.Get( dummy, 15 ); #endif } else if ( header.version[1] == 1 || header.version[1] == 0 ) { // previous version 7.0 or 7.1 buf.Get( pBuf, sizeof(VTFFileHeaderV7_1_t) - sizeof(VTFFileBaseHeader_t) ); #if defined( _X360 ) || defined (POSIX) // read a dummy byte to be properly positioned with 7.0/1 PC data byte dummy; buf.Get( &dummy, 1 ); #endif } else { Warning( "*** Encountered VTF file with an invalid minor version!\n" ); return false; } return buf.IsValid(); } bool CVTFTexture::ReadHeader( CUtlBuffer &buf, VTFFileHeader_t &header ) { if ( (IsX360() || IsPS3()) && SetupByteSwap( buf ) ) { VTFFileBaseHeader_t baseHeader; m_Swap.SwapFieldsToTargetEndian( &baseHeader, (VTFFileBaseHeader_t*)buf.PeekGet() ); // Swap the header inside the UtlBuffer if ( baseHeader.version[0] == VTF_MAJOR_VERSION ) { if ( baseHeader.version[1] == 0 || baseHeader.version[1] == 1 ) { // version 7.0 or 7.1 m_Swap.SwapFieldsToTargetEndian( (VTFFileHeaderV7_1_t*)buf.PeekGet() ); } else if ( baseHeader.version[1] == 2 ) { // version 7.2 m_Swap.SwapFieldsToTargetEndian( (VTFFileHeaderV7_2_t*)buf.PeekGet() ); } else if ( baseHeader.version[1] == 3 ) { m_Swap.SwapFieldsToTargetEndian( (VTFFileHeaderV7_3_t*)buf.PeekGet() ); } else if ( baseHeader.version[1] >= 4 && baseHeader.version[1] <= VTF_MINOR_VERSION ) { m_Swap.SwapFieldsToTargetEndian( (VTFFileHeader_t*)buf.PeekGet() ); } } } memset( &header, 0, sizeof(VTFFileHeader_t) ); buf.Get( &header, sizeof(VTFFileBaseHeader_t) ); if ( !buf.IsValid() ) { Warning( "*** Error unserializing VTF file... is the file empty?\n" ); return false; } // Validity check if ( Q_strncmp( header.fileTypeString, "VTF", 4 ) ) { Warning( "*** Tried to load a non-VTF file as a VTF file!\n" ); return false; } if ( header.version[0] != VTF_MAJOR_VERSION ) { Warning( "*** Encountered VTF file with an invalid version!\n" ); return false; } if ( !ReadHeaderFromBufferPastBaseHeader( buf, header ) ) { Warning( "*** Encountered VTF file with an invalid full header!\n" ); return false; } // version fixups switch ( header.version[1] ) { case 0: case 1: header.depth = 1; // fall-through case 2: header.numResources = 0; // fall-through case 3: header.flags &= VERSIONED_VTF_FLAGS_MASK_7_3; // fall-through case 4: case VTF_MINOR_VERSION: break; } return true; } //----------------------------------------------------------------------------- // Unserialization //----------------------------------------------------------------------------- bool CVTFTexture::Unserialize( CUtlBuffer &buf, bool bHeaderOnly, int nSkipMipLevels ) { // When unserializing, we can skip a certain number of mip levels, // and we also can just load everything but the image data VTFFileHeader_t header; if ( !ReadHeader( buf, header ) ) return false; if ( (header.flags & TEXTUREFLAGS_ENVMAP) && (header.width != header.height) ) { Warning( "*** Encountered VTF non-square cubemap!\n" ); return false; } if ( (header.flags & TEXTUREFLAGS_ENVMAP) && (header.depth != 1) ) { Warning("*** Encountered VTF volume texture cubemap!\n"); return false; } if ( header.width <= 0 || header.height <= 0 || header.depth <= 0 ) { Warning( "*** Encountered VTF invalid texture size!\n" ); return false; } m_nWidth = header.width; m_nHeight = header.height; m_nDepth = header.depth; m_Format = header.imageFormat; m_nFlags = header.flags; m_nFrameCount = header.numFrames; m_nFaceCount = (m_nFlags & TEXTUREFLAGS_ENVMAP) ? CUBEMAP_FACE_COUNT : 1; // NOTE: We're going to store space for all mip levels, even if we don't // have data on disk for them. This is for backward compatibility m_nMipCount = ComputeMipCount(); m_vecReflectivity = header.reflectivity; m_flBumpScale = header.bumpScale; // FIXME: Why is this needed? m_iStartFrame = header.startFrame; // This is to make sure old-format .vtf files are read properly m_nVersion[0] = header.version[0]; m_nVersion[1] = header.version[1]; if ( header.lowResImageWidth == 0 || header.lowResImageHeight == 0 ) { m_nLowResImageWidth = 0; m_nLowResImageHeight = 0; } else { m_nLowResImageWidth = header.lowResImageWidth; m_nLowResImageHeight = header.lowResImageHeight; } m_LowResImageFormat = header.lowResImageFormat; // Keep the allocated memory chunks of data if ( int( header.numResources ) < m_arrResourcesData.Count() ) { m_arrResourcesData_ForReuse.EnsureCapacity( m_arrResourcesData_ForReuse.Count() + m_arrResourcesData.Count() - header.numResources ); for ( ResourceMemorySection const *pRms = &m_arrResourcesData[ header.numResources ], *pRmsEnd = m_arrResourcesData.Base() + m_arrResourcesData.Count(); pRms < pRmsEnd; ++ pRms ) { if ( pRms->m_pData ) { int idxReuse = m_arrResourcesData_ForReuse.AddToTail( *pRms ); m_arrResourcesData_ForReuse[ idxReuse ].m_nDataLength = 0; // Data for reuse shouldn't have length set } } } m_arrResourcesData.SetCount( header.numResources ); // Read the dictionary of resources info if ( header.numResources > 0 ) { m_arrResourcesInfo.RemoveAll(); m_arrResourcesInfo.SetCount( header.numResources ); buf.Get( m_arrResourcesInfo.Base(), m_arrResourcesInfo.Count() * sizeof( ResourceEntryInfo ) ); if ( !buf.IsValid() ) return false; if ( IsX360() || IsPS3() ) { // Byte-swap the dictionary data offsets for ( int k = 0; k < m_arrResourcesInfo.Count(); ++ k ) { ResourceEntryInfo &rei = m_arrResourcesInfo[k]; if ( ( rei.eType & RSRCF_HAS_NO_DATA_CHUNK ) == 0 ) { m_Swap.SwapBufferToTargetEndian( &rei.resData ); } } } } else { // Older version (7.0 - 7.2): // - low-res image data first (optional) // - then image data m_arrResourcesInfo.RemoveAll(); // Low-res image data int nLowResImageSize = ImageLoader::GetMemRequired( m_nLowResImageWidth, m_nLowResImageHeight, 1, m_LowResImageFormat, false ); if ( nLowResImageSize ) { ResourceEntryInfo &rei = *FindOrCreateResourceEntryInfo( VTF_LEGACY_RSRC_LOW_RES_IMAGE ); rei.resData = buf.TellGet(); } // Image data ResourceEntryInfo &rei = *FindOrCreateResourceEntryInfo( VTF_LEGACY_RSRC_IMAGE ); rei.resData = buf.TellGet() + nLowResImageSize; } // Caller wants the header component only, avoids reading large image data sets if ( bHeaderOnly ) return true; // Load the low res image if ( ResourceEntryInfo const *pLowResDataInfo = FindResourceEntryInfo( VTF_LEGACY_RSRC_LOW_RES_IMAGE ) ) { buf.SeekGet( CUtlBuffer::SEEK_HEAD, pLowResDataInfo->resData ); if ( !LoadLowResData( buf ) ) return false; } // Load any new resources if ( !LoadNewResources( buf ) ) { return false; } // Load the image data if ( ResourceEntryInfo const *pImageDataInfo = FindResourceEntryInfo( VTF_LEGACY_RSRC_IMAGE ) ) { buf.SeekGet( CUtlBuffer::SEEK_HEAD, pImageDataInfo->resData ); if ( !LoadImageData( buf, header, nSkipMipLevels ) ) return false; } else { // No image data return false; } return true; } bool CVTFTexture::LoadNewResources( CUtlBuffer &buf ) { // Load the new resources for ( int idxRsrc = 0; idxRsrc < m_arrResourcesInfo.Count(); ++idxRsrc ) { ResourceEntryInfo &rei = m_arrResourcesInfo[ idxRsrc ]; ResourceMemorySection &rms = m_arrResourcesData[ idxRsrc ]; if ( ( rei.eType & RSRCF_HAS_NO_DATA_CHUNK ) == 0 ) { switch( rei.eType ) { case VTF_LEGACY_RSRC_LOW_RES_IMAGE: case VTF_LEGACY_RSRC_IMAGE: // these legacy resources are loaded differently continue; default: buf.SeekGet( CUtlBuffer::SEEK_HEAD, rei.resData ); if ( !rms.LoadData( buf, m_Swap ) ) return false; } } } return true; } ResourceEntryInfo const *CVTFTexture::FindResourceEntryInfo( uint32 eType ) const { Assert( ( eType & RSRCF_MASK ) == 0 ); ResourceEntryInfo const *pRange[2]; pRange[0] = m_arrResourcesInfo.Base(); pRange[1] = pRange[0] + m_arrResourcesInfo.Count(); if ( IsPC() ) { // Quick-search in a sorted array ResourceEntryInfo const *pMid; find_routine: if ( pRange[0] != pRange[1] ) { pMid = pRange[0] + ( pRange[1] - pRange[0] ) / 2; if ( int diff = int( pMid->eType & ~RSRCF_MASK ) - int( eType ) ) { int off = !( diff > 0 ); pRange[ !off ] = pMid + off; goto find_routine; } else return pMid; } else return NULL; } else { // 360 eschews a sorted format due to endian issues // use a linear search for compatibility with reading pc formats for ( ; pRange[0] < pRange[1]; ++pRange[0] ) { if ( ( pRange[0]->eType & ~RSRCF_MASK ) == eType ) return pRange[0]; } } return NULL; } ResourceEntryInfo * CVTFTexture::FindResourceEntryInfo( uint32 eType ) { return const_cast< ResourceEntryInfo * >( ( ( CVTFTexture const * ) this )->FindResourceEntryInfo( eType ) ); } ResourceEntryInfo * CVTFTexture::FindOrCreateResourceEntryInfo( uint32 eType ) { Assert( ( eType & RSRCF_MASK ) == 0 ); int k = 0; for ( ; k < m_arrResourcesInfo.Count(); ++ k ) { uint32 rsrcType = ( m_arrResourcesInfo[ k ].eType & ~RSRCF_MASK ); if ( rsrcType == eType ) { // found return &m_arrResourcesInfo[ k ]; } // sort for PC only, 360 uses linear sort for compatibility with PC endian if ( IsPC() ) { if ( rsrcType > eType ) break; } } ResourceEntryInfo rei; memset( &rei, 0, sizeof( rei ) ); rei.eType = eType; // Inserting before "k" if ( m_arrResourcesData_ForReuse.Count() ) { m_arrResourcesData.InsertBefore( k, m_arrResourcesData_ForReuse[ m_arrResourcesData_ForReuse.Count() - 1 ] ); m_arrResourcesData_ForReuse.FastRemove( m_arrResourcesData_ForReuse.Count() - 1 ); } else { m_arrResourcesData.InsertBefore( k ); } m_arrResourcesInfo.InsertBefore( k, rei ); return &m_arrResourcesInfo[k]; } bool CVTFTexture::RemoveResourceEntryInfo( uint32 eType ) { Assert( ( eType & RSRCF_MASK ) == 0 ); for ( int k = 0; k < m_arrResourcesInfo.Count(); ++ k ) { if ( ( m_arrResourcesInfo[ k ].eType & ~RSRCF_MASK ) == eType ) { m_arrResourcesInfo.Remove( k ); if ( m_arrResourcesData[k].m_pData ) { int idxReuse = m_arrResourcesData_ForReuse.AddToTail( m_arrResourcesData[k] ); m_arrResourcesData_ForReuse[ idxReuse ].m_nDataLength = 0; // Data for reuse shouldn't have length set } m_arrResourcesData.Remove( k ); return true; } } return false; } //----------------------------------------------------------------------------- // Serialization of image data //----------------------------------------------------------------------------- bool CVTFTexture::WriteImageData( CUtlBuffer &buf ) { // NOTE: We load the bits this way because we store the bits in memory // differently that the way they are stored on disk; we store on disk // differently so we can only load up // NOTE: The smallest mip levels are stored first!! for (int iMip = m_nMipCount; --iMip >= 0; ) { int iMipSize = ComputeMipSize( iMip ); for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for (int iFace = 0; iFace < m_nFaceCount; ++iFace) { unsigned char *pMipBits = ImageData( iFrame, iFace, iMip ); buf.Put( pMipBits, iMipSize ); } } } return buf.IsValid(); } // Inserts padding to have a multiple of "iAlignment" bytes in the buffer // Returns number of pad bytes written static int PadBuffer( CUtlBuffer &buf, int iAlignment ) { unsigned int uiCurrentBytes = buf.TellPut(); int iPadBytes = AlignValue( uiCurrentBytes, iAlignment ) - uiCurrentBytes; // Fill data for ( int i=0; iresData = buf.TellPut(); Assert( m_pLowResImageData ); int iLowResImageSize = ImageLoader::GetMemRequired( m_nLowResImageWidth, m_nLowResImageHeight, 1, m_LowResImageFormat, false ); buf.Put( m_pLowResImageData, iLowResImageSize ); if ( !buf.IsValid() ) return false; } // Serialize the new resources for ( int iRsrc = 0; iRsrc < m_arrResourcesInfo.Count(); ++ iRsrc ) { ResourceEntryInfo &rei = m_arrResourcesInfo[ iRsrc ]; switch ( rei.eType ) { case VTF_LEGACY_RSRC_LOW_RES_IMAGE: case VTF_LEGACY_RSRC_IMAGE: // written differently continue; default: { if ( rei.eType & RSRCF_HAS_NO_DATA_CHUNK ) continue; rei.resData = buf.TellPut(); ResourceMemorySection &rms = m_arrResourcesData[ iRsrc ]; if ( !rms.WriteData( buf ) ) return false; } } } // Write image data last if ( ResourceEntryInfo *pRei = FindResourceEntryInfo( VTF_LEGACY_RSRC_IMAGE ) ) { pRei->resData = buf.TellPut(); WriteImageData( buf ); } else return false; // Now fixup the resources dictionary int iTotalBytesPut = buf.TellPut(); buf.SeekPut( CUtlBuffer::SEEK_HEAD, iSeekOffsetResInfoFixup ); buf.Put( m_arrResourcesInfo.Base(), m_arrResourcesInfo.Count() * sizeof( ResourceEntryInfo ) ); buf.SeekPut( CUtlBuffer::SEEK_HEAD, iTotalBytesPut ); // Return if the buffer is valid return buf.IsValid(); } //----------------------------------------------------------------------------- // Attributes... //----------------------------------------------------------------------------- int CVTFTexture::Width() const { return m_nWidth; } int CVTFTexture::Height() const { return m_nHeight; } int CVTFTexture::Depth() const { return m_nDepth; } int CVTFTexture::MipCount() const { return m_nMipCount; } ImageFormat CVTFTexture::Format() const { return m_Format; } int CVTFTexture::FaceCount() const { return m_nFaceCount; } int CVTFTexture::FrameCount() const { return m_nFrameCount; } int CVTFTexture::Flags() const { return m_nFlags; } bool CVTFTexture::IsCubeMap() const { return (m_nFlags & TEXTUREFLAGS_ENVMAP) != 0; } bool CVTFTexture::IsNormalMap() const { return (m_nFlags & TEXTUREFLAGS_NORMAL) != 0; } bool CVTFTexture::IsVolumeTexture() const { return (m_nDepth > 1); } float CVTFTexture::BumpScale() const { return m_flBumpScale; } const Vector &CVTFTexture::Reflectivity() const { return m_vecReflectivity; } unsigned char *CVTFTexture::ImageData() { return m_pImageData; } int CVTFTexture::LowResWidth() const { return m_nLowResImageWidth; } int CVTFTexture::LowResHeight() const { return m_nLowResImageHeight; } ImageFormat CVTFTexture::LowResFormat() const { return m_LowResImageFormat; } unsigned char *CVTFTexture::LowResImageData() { return m_pLowResImageData; } int CVTFTexture::RowSizeInBytes( int nMipLevel ) const { int nWidth = (m_nWidth >> nMipLevel); if (nWidth < 1) { nWidth = 1; } return ImageLoader::SizeInBytes( m_Format ) * nWidth; } //----------------------------------------------------------------------------- // returns the size of one face of a particular mip level //----------------------------------------------------------------------------- int CVTFTexture::FaceSizeInBytes( int nMipLevel ) const { int nWidth = (m_nWidth >> nMipLevel); if (nWidth < 1) { nWidth = 1; } int nHeight = (m_nHeight >> nMipLevel); if (nHeight < 1) { nHeight = 1; } return ImageLoader::SizeInBytes( m_Format, nWidth, nHeight ); } //----------------------------------------------------------------------------- // Returns a pointer to the data associated with a particular frame, face, and mip level //----------------------------------------------------------------------------- unsigned char *CVTFTexture::ImageData( int iFrame, int iFace, int iMipLevel ) { Assert( m_pImageData ); int iOffset = GetImageOffset( iFrame, iFace, iMipLevel, m_Format ); return &m_pImageData[iOffset]; } //----------------------------------------------------------------------------- // Returns a pointer to the data associated with a particular frame, face, mip level, and offset //----------------------------------------------------------------------------- unsigned char *CVTFTexture::ImageData( int iFrame, int iFace, int iMipLevel, int x, int y, int z ) { #ifdef _DEBUG int nWidth, nHeight, nDepth; ComputeMipLevelDimensions( iMipLevel, &nWidth, &nHeight, &nDepth ); Assert( (x >= 0) && (x <= nWidth) && (y >= 0) && (y <= nHeight) && (z >= 0) && (z <= nDepth) ); #endif int nFaceBytes = FaceSizeInBytes( iMipLevel ); int nRowBytes = RowSizeInBytes( iMipLevel ); int nTexelBytes = ImageLoader::SizeInBytes( m_Format ); unsigned char *pMipBits = ImageData( iFrame, iFace, iMipLevel ); pMipBits += z * nFaceBytes + y * nRowBytes + x * nTexelBytes; return pMipBits; } //----------------------------------------------------------------------------- // Computes the size (in bytes) of a single mipmap of a single face of a single frame //----------------------------------------------------------------------------- inline int CVTFTexture::ComputeMipSize( int iMipLevel, ImageFormat fmt ) const { Assert( iMipLevel < m_nMipCount ); int w, h, d; ComputeMipLevelDimensions( iMipLevel, &w, &h, &d ); return ImageLoader::GetMemRequired( w, h, d, fmt, false ); } int CVTFTexture::ComputeMipSize( int iMipLevel ) const { // Version for the public interface; don't want to expose the fmt parameter return ComputeMipSize( iMipLevel, m_Format ); } //----------------------------------------------------------------------------- // Computes the size of a single face of a single frame // All mip levels starting at the specified mip level are included //----------------------------------------------------------------------------- inline int CVTFTexture::ComputeFaceSize( int iStartingMipLevel, ImageFormat fmt ) const { int iSize = 0; int w = m_nWidth; int h = m_nHeight; int d = m_nDepth; for( int i = 0; i < m_nMipCount; ++i ) { if (i >= iStartingMipLevel) { iSize += ImageLoader::GetMemRequired( w, h, d, fmt, false ); } w >>= 1; h >>= 1; d >>= 1; if ( w < 1 ) { w = 1; } if ( h < 1 ) { h = 1; } if ( d < 1 ) { d = 1; } } return iSize; } int CVTFTexture::ComputeFaceSize( int iStartingMipLevel ) const { // Version for the public interface; don't want to expose the fmt parameter return ComputeFaceSize( iStartingMipLevel, m_Format ); } //----------------------------------------------------------------------------- // Computes the total size of all faces, all frames //----------------------------------------------------------------------------- inline int CVTFTexture::ComputeTotalSize( ImageFormat fmt ) const { // Compute the number of bytes required to store a single face/frame int iMemRequired = ComputeFaceSize( 0, fmt ); // Now compute the total image size return m_nFaceCount * m_nFrameCount * iMemRequired; } int CVTFTexture::ComputeTotalSize( ) const { // Version for the public interface; don't want to expose the fmt parameter return ComputeTotalSize( m_Format ); } //----------------------------------------------------------------------------- // Computes the location of a particular frame, face, and mip level //----------------------------------------------------------------------------- int CVTFTexture::GetImageOffset( int iFrame, int iFace, int iMipLevel, ImageFormat fmt ) const { Assert( iFrame < m_nFrameCount ); Assert( iFace < m_nFaceCount ); Assert( iMipLevel < m_nMipCount ); int i; int iOffset = 0; if ( ( IsX360() && ( m_nVersion[0] == VTF_X360_MAJOR_VERSION ) ) || ( IsPS3() && ( m_nVersion[0] == VTF_PS3_MAJOR_VERSION ) ) ) { // 360 data is stored same as disk, 1x1 up to NxN // get to the right miplevel int iMipWidth, iMipHeight, iMipDepth; for ( i = m_nMipCount - 1; i > iMipLevel; --i ) { ComputeMipLevelDimensions( i, &iMipWidth, &iMipHeight, &iMipDepth ); int iMipLevelSize = ImageLoader::GetMemRequired( iMipWidth, iMipHeight, iMipDepth, fmt, false ); iOffset += m_nFrameCount * m_nFaceCount * iMipLevelSize; } // get to the right frame ComputeMipLevelDimensions( iMipLevel, &iMipWidth, &iMipHeight, &iMipDepth ); int nFaceSize = ImageLoader::GetMemRequired( iMipWidth, iMipHeight, iMipDepth, fmt, false ); iOffset += iFrame * m_nFaceCount * nFaceSize; // get to the right face iOffset += iFace * nFaceSize; return iOffset; } // get to the right frame int iFaceSize = ComputeFaceSize( 0, fmt ); iOffset = iFrame * m_nFaceCount * iFaceSize; // Get to the right face iOffset += iFace * iFaceSize; // Get to the right mip level for (i = 0; i < iMipLevel; ++i) { iOffset += ComputeMipSize( i, fmt ); } return iOffset; } //----------------------------------------------------------------------------- // Computes the dimensions of a particular mip level //----------------------------------------------------------------------------- void CVTFTexture::ComputeMipLevelDimensions( int iMipLevel, int *pMipWidth, int *pMipHeight, int *pMipDepth ) const { Assert( iMipLevel < m_nMipCount ); *pMipWidth = m_nWidth >> iMipLevel; *pMipHeight = m_nHeight >> iMipLevel; *pMipDepth = m_nDepth >> iMipLevel; if ( *pMipWidth < 1 ) { *pMipWidth = 1; } if ( *pMipHeight < 1 ) { *pMipHeight = 1; } if ( *pMipDepth < 1 ) { *pMipDepth = 1; } } //----------------------------------------------------------------------------- // Computes the size of a subrect at a particular mip level //----------------------------------------------------------------------------- void CVTFTexture::ComputeMipLevelSubRect( Rect_t *pSrcRect, int nMipLevel, Rect_t *pSubRect ) const { Assert( pSrcRect->x >= 0 && pSrcRect->y >= 0 && (pSrcRect->x + pSrcRect->width <= m_nWidth) && (pSrcRect->y + pSrcRect->height <= m_nHeight) ); if (nMipLevel == 0) { *pSubRect = *pSrcRect; return; } float flInvShrink = 1.0f / (float)(1 << nMipLevel); pSubRect->x = ( int )( pSrcRect->x * flInvShrink ); pSubRect->y = ( int )( pSrcRect->y * flInvShrink ); pSubRect->width = (int)ceil( (pSrcRect->x + pSrcRect->width) * flInvShrink ) - pSubRect->x; pSubRect->height = (int)ceil( (pSrcRect->y + pSrcRect->height) * flInvShrink ) - pSubRect->y; } //----------------------------------------------------------------------------- // Converts the texture's image format. Use IMAGE_FORMAT_DEFAULT // if you want to be able to use various tool functions below //----------------------------------------------------------------------------- void CVTFTexture::ConvertImageFormat( ImageFormat fmt, bool bNormalToDUDV, bool bNormalToDXT5GA ) { if ( !m_pImageData ) { return; } if ( fmt == IMAGE_FORMAT_DEFAULT ) { fmt = IMAGE_FORMAT_RGBA8888; } if ( bNormalToDUDV && !( fmt == IMAGE_FORMAT_UV88 || fmt == IMAGE_FORMAT_UVWQ8888 || fmt == IMAGE_FORMAT_UVLX8888 ) ) { Assert( 0 ); return; } if ( m_Format == fmt ) { return; } if ( ( IsX360() && ( m_nVersion[0] == VTF_X360_MAJOR_VERSION ) ) || ( IsPS3() && ( m_nVersion[0] == VTF_PS3_MAJOR_VERSION ) ) ) { // 360 textures should be baked in final format Assert( 0 ); return; } // FIXME: Should this be re-written to not do an allocation? int iConvertedSize = ComputeTotalSize( fmt ); unsigned char *pConvertedImage = (unsigned char*)MemAllocScratch(iConvertedSize); for (int iMip = 0; iMip < m_nMipCount; ++iMip) { int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMip, &nMipWidth, &nMipHeight, &nMipDepth ); int nSrcFaceStride = ImageLoader::GetMemRequired( nMipWidth, nMipHeight, 1, m_Format, false ); int nDstFaceStride = ImageLoader::GetMemRequired( nMipWidth, nMipHeight, 1, fmt, false ); for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for (int iFace = 0; iFace < m_nFaceCount; ++iFace) { unsigned char *pSrcData = ImageData( iFrame, iFace, iMip ); unsigned char *pDstData = pConvertedImage + GetImageOffset( iFrame, iFace, iMip, fmt ); for ( int z = 0; z < nMipDepth; ++z, pSrcData += nSrcFaceStride, pDstData += nDstFaceStride ) { if( bNormalToDUDV ) { if( fmt == IMAGE_FORMAT_UV88 ) { ImageLoader::ConvertNormalMapRGBA8888ToDUDVMapUV88( pSrcData, nMipWidth, nMipHeight, pDstData ); } else if( fmt == IMAGE_FORMAT_UVWQ8888 ) { ImageLoader::ConvertNormalMapRGBA8888ToDUDVMapUVWQ8888( pSrcData, nMipWidth, nMipHeight, pDstData ); } else if ( fmt == IMAGE_FORMAT_UVLX8888 ) { ImageLoader::ConvertNormalMapRGBA8888ToDUDVMapUVLX8888( pSrcData, nMipWidth, nMipHeight, pDstData ); } else { Assert( 0 ); return; } } else if( bNormalToDXT5GA ) { ImageLoader::ConvertNormalMapARGB8888ToDXT5GA( pSrcData, pDstData, nMipWidth, nMipHeight ); } else { ImageLoader::ConvertImageFormat( pSrcData, m_Format, pDstData, fmt, nMipWidth, nMipHeight ); } } } } } if ( !AllocateImageData(iConvertedSize) ) return; Assert(iConvertedSize<=m_nImageAllocSize); memcpy( m_pImageData, pConvertedImage, iConvertedSize ); m_Format = fmt; if ( !ImageLoader::IsCompressed( fmt ) ) { int nAlphaBits = ImageLoader::ImageFormatInfo( fmt ).m_nNumAlphaBits; if ( nAlphaBits > 1 ) { m_nFlags |= TEXTUREFLAGS_EIGHTBITALPHA; m_nFlags &= ~TEXTUREFLAGS_ONEBITALPHA; } if ( nAlphaBits <= 1 ) { m_nFlags &= ~TEXTUREFLAGS_EIGHTBITALPHA; if ( nAlphaBits == 0 ) { m_nFlags &= ~TEXTUREFLAGS_ONEBITALPHA; } } } else { // Only DXT5 has alpha bits if ( ( fmt == IMAGE_FORMAT_DXT1 ) || ( fmt == IMAGE_FORMAT_ATI2N ) || ( fmt == IMAGE_FORMAT_ATI1N ) ) { m_nFlags &= ~(TEXTUREFLAGS_ONEBITALPHA|TEXTUREFLAGS_EIGHTBITALPHA); } } MemFreeScratch(); } //----------------------------------------------------------------------------- // Enums + structures related to conversion from cube to spheremap //----------------------------------------------------------------------------- struct SphereCalc_t { Vector dir; float m_flRadius; float m_flOORadius; float m_flRadiusSq; LookDir_t m_LookDir; Vector m_vecLookDir; unsigned char m_pColor[4]; unsigned char **m_ppCubeFaces; int m_iSize; }; //----------------------------------------------------------------------------- // // Methods associated with computing a spheremap from a cubemap // //----------------------------------------------------------------------------- static void CalcInit( SphereCalc_t *pCalc, int iSize, unsigned char **ppCubeFaces, LookDir_t lookDir = LOOK_DOWN_Z ) { // NOTE: Width + height should be the same pCalc->m_flRadius = iSize * 0.5f; pCalc->m_flRadiusSq = pCalc->m_flRadius * pCalc->m_flRadius; pCalc->m_flOORadius = 1.0f / pCalc->m_flRadius; pCalc->m_LookDir = lookDir; pCalc->m_ppCubeFaces = ppCubeFaces; pCalc->m_iSize = iSize; switch( lookDir) { case LOOK_DOWN_X: pCalc->m_vecLookDir.Init( 1, 0, 0 ); break; case LOOK_DOWN_NEGX: pCalc->m_vecLookDir.Init( -1, 0, 0 ); break; case LOOK_DOWN_Y: pCalc->m_vecLookDir.Init( 0, 1, 0 ); break; case LOOK_DOWN_NEGY: pCalc->m_vecLookDir.Init( 0, -1, 0 ); break; case LOOK_DOWN_Z: pCalc->m_vecLookDir.Init( 0, 0, 1 ); break; case LOOK_DOWN_NEGZ: pCalc->m_vecLookDir.Init( 0, 0, -1 ); break; } } static void TransformNormal( SphereCalc_t *pCalc, Vector& normal ) { Vector vecTemp = normal; switch( pCalc->m_LookDir) { // Look down +x case LOOK_DOWN_X: normal[0] = vecTemp[2]; normal[2] = -vecTemp[0]; break; // Look down -x case LOOK_DOWN_NEGX: normal[0] = -vecTemp[2]; normal[2] = vecTemp[0]; break; // Look down +y case LOOK_DOWN_Y: normal[0] = -vecTemp[0]; normal[1] = vecTemp[2]; normal[2] = vecTemp[1]; break; // Look down -y case LOOK_DOWN_NEGY: normal[0] = vecTemp[0]; normal[1] = -vecTemp[2]; normal[2] = vecTemp[1]; break; // Look down +z case LOOK_DOWN_Z: return; // Look down -z case LOOK_DOWN_NEGZ: normal[0] = -vecTemp[0]; normal[2] = -vecTemp[2]; break; } } //----------------------------------------------------------------------------- // Given a iFace normal, determine which cube iFace to sample //----------------------------------------------------------------------------- static int CalcFaceIndex( const Vector& normal ) { float absx, absy, absz; absx = normal[0] >= 0 ? normal[0] : -normal[0]; absy = normal[1] >= 0 ? normal[1] : -normal[1]; absz = normal[2] >= 0 ? normal[2] : -normal[2]; if ( absx > absy ) { if ( absx > absz ) { // left/right if ( normal[0] >= 0 ) return CUBEMAP_FACE_RIGHT; return CUBEMAP_FACE_LEFT; } } else { if ( absy > absz ) { // front / back if ( normal[1] >= 0 ) return CUBEMAP_FACE_BACK; return CUBEMAP_FACE_FRONT; } } // top / bottom if ( normal[2] >= 0 ) return CUBEMAP_FACE_UP; return CUBEMAP_FACE_DOWN; } static void CalcColor( SphereCalc_t *pCalc, int iFace, const Vector &normal, unsigned char *color ) { float x, y, w; int size = pCalc->m_iSize; float hw = 0.5 * size; if ( (iFace == CUBEMAP_FACE_LEFT) || (iFace == CUBEMAP_FACE_RIGHT) ) { w = hw / normal[0]; x = -normal[2]; y = -normal[1]; if ( iFace == CUBEMAP_FACE_LEFT ) y = -y; } else if ( (iFace == CUBEMAP_FACE_FRONT) || (iFace == CUBEMAP_FACE_BACK) ) { w = hw / normal[1]; x = normal[0]; y = normal[2]; if ( iFace == CUBEMAP_FACE_FRONT ) x = -x; } else { w = hw / normal[2]; x = -normal[0]; y = -normal[1]; if ( iFace == CUBEMAP_FACE_UP ) x = -x; } x = (x * w) + hw - 0.5; y = (y * w) + hw - 0.5; int u = (int)(x+0.5); int v = (int)(y+0.5); if ( u < 0 ) u = 0; else if ( u > (size-1) ) u = (size-1); if ( v < 0 ) v = 0; else if ( v > (size-1) ) v = (size-1); int offset = (v * size + u) * 4; unsigned char *pPix = pCalc->m_ppCubeFaces[iFace] + offset; color[0] = pPix[0]; color[1] = pPix[1]; color[2] = pPix[2]; color[3] = pPix[3]; } //----------------------------------------------------------------------------- // Computes the spheremap color at a particular (x,y) texcoord //----------------------------------------------------------------------------- static void CalcSphereColor( SphereCalc_t *pCalc, float x, float y ) { Vector normal; float flRadiusSq = x*x + y*y; if (flRadiusSq > pCalc->m_flRadiusSq) { // Force a glancing reflection normal.Init( 0, 1, 0 ); } else { // Compute the z distance based on x*x + y*y + z*z = r*r float z = sqrt( pCalc->m_flRadiusSq - flRadiusSq ); // Here's the untransformed surface normal normal.Init( x, y, z ); normal *= pCalc->m_flOORadius; } // Transform the normal based on the actual view direction TransformNormal( pCalc, normal ); // Compute the reflection vector (full spheremap solution) // R = 2 * (N dot L)N - L Vector vecReflect; float nDotL = DotProduct( normal, pCalc->m_vecLookDir ); VectorMA( pCalc->m_vecLookDir, -2.0f * nDotL, normal, vecReflect ); vecReflect *= -1.0f; int iFace = CalcFaceIndex( vecReflect ); CalcColor( pCalc, iFace, vecReflect, pCalc->m_pColor ); } //----------------------------------------------------------------------------- // Computes the spheremap color at a particular (x,y) texcoord //----------------------------------------------------------------------------- static void CalcHemisphereColor( SphereCalc_t *pCalc, float x, float y ) { Vector normal; float flRadiusSq = x*x + y*y; if (flRadiusSq > pCalc->m_flRadiusSq) { normal.Init( x, y, 0.0f ); VectorNormalize( normal ); normal *= pCalc->m_flRadiusSq; flRadiusSq = pCalc->m_flRadiusSq; } // Compute the z distance based on x*x + y*y + z*z = r*r float z = sqrt( pCalc->m_flRadiusSq - flRadiusSq ); // Here's the untransformed surface normal normal.Init( x, y, z ); normal *= pCalc->m_flOORadius; // Transform the normal based on the actual view direction TransformNormal( pCalc, normal ); // printf( "x: %f y: %f normal: %f %f %f\n", x, y, normal.x, normal.y, normal.z ); /* // Compute the reflection vector (full spheremap solution) // R = 2 * (N dot L)N - L Vector vecReflect; float nDotL = DotProduct( normal, pCalc->m_vecLookDir ); VectorMA( pCalc->m_vecLookDir, -2.0f * nDotL, normal, vecReflect ); vecReflect *= -1.0f; */ int iFace = CalcFaceIndex( normal ); CalcColor( pCalc, iFace, normal, pCalc->m_pColor ); #if 0 pCalc->m_pColor[0] = normal[0] * 127 + 127; pCalc->m_pColor[1] = normal[1] * 127 + 127; pCalc->m_pColor[2] = normal[2] * 127 + 127; #endif } //----------------------------------------------------------------------------- // Makes a single frame of spheremap //----------------------------------------------------------------------------- void CVTFTexture::ComputeSpheremapFrame( unsigned char **ppCubeFaces, unsigned char *pSpheremap, LookDir_t lookDir ) { SphereCalc_t sphere; CalcInit( &sphere, m_nWidth, ppCubeFaces, lookDir ); int offset = 0; for ( int y = 0; y < m_nHeight; y++ ) { for ( int x = 0; x < m_nWidth; x++ ) { int r = 0, g = 0, b = 0, a = 0; float u = (float)x - m_nWidth * 0.5f; float v = m_nHeight * 0.5f - (float)y; CalcSphereColor( &sphere, u, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; CalcSphereColor( &sphere, u + 0.25, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; v += 0.25; CalcSphereColor( &sphere, u + 0.25, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; CalcSphereColor( &sphere, u, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; pSpheremap[ offset + 0 ] = r >> 2; pSpheremap[ offset + 1 ] = g >> 2; pSpheremap[ offset + 2 ] = b >> 2; pSpheremap[ offset + 3 ] = a >> 2; offset += 4; } } } void CVTFTexture::ComputeHemispheremapFrame( unsigned char **ppCubeFaces, unsigned char *pSpheremap, LookDir_t lookDir ) { SphereCalc_t sphere; CalcInit( &sphere, m_nWidth, ppCubeFaces, lookDir ); int offset = 0; for ( int y = 0; y < m_nHeight; y++ ) { for ( int x = 0; x < m_nWidth; x++ ) { int r = 0, g = 0, b = 0, a = 0; float u = (float)x - m_nWidth * 0.5f; float v = m_nHeight * 0.5f - (float)y; CalcHemisphereColor( &sphere, u, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; CalcHemisphereColor( &sphere, u + 0.25, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; v += 0.25; CalcHemisphereColor( &sphere, u + 0.25, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; CalcHemisphereColor( &sphere, u, v ); r += sphere.m_pColor[0]; g += sphere.m_pColor[1]; b += sphere.m_pColor[2]; a += sphere.m_pColor[3]; pSpheremap[ offset + 0 ] = r >> 2; pSpheremap[ offset + 1 ] = g >> 2; pSpheremap[ offset + 2 ] = b >> 2; pSpheremap[ offset + 3 ] = a >> 2; offset += 4; } } } //----------------------------------------------------------------------------- // Generate spheremap based on the current images (only works for cubemaps) // The look dir indicates the direction of the center of the sphere //----------------------------------------------------------------------------- void CVTFTexture::GenerateSpheremap( LookDir_t lookDir ) { if (!IsCubeMap()) return; /* // HDRFIXME: Need to re-enable this. // Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); // We'll be doing our work in IMAGE_FORMAT_RGBA8888 mode 'cause it's easier unsigned char *pCubeMaps[6]; // Allocate the bits for the spheremap Assert( m_nDepth == 1 ); int iMemRequired = ComputeFaceSize( 0, IMAGE_FORMAT_RGBA8888 ); unsigned char *pSphereMapBits = (unsigned char *)MemAllocScratch(iMemRequired); // Generate a spheremap for each frame of the cubemap for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { // Point to our own textures (highest mip level) for (int iFace = 0; iFace < 6; ++iFace) { pCubeMaps[iFace] = ImageData( iFrame, iFace, 0 ); } // Compute the spheremap of the top LOD // HDRFIXME: Make this work? if( m_Format == IMAGE_FORMAT_RGBA8888 ) { ComputeSpheremapFrame( pCubeMaps, pSphereMapBits, lookDir ); } // Compute the mip levels of the spheremap, converting from RGBA8888 to our format unsigned char *pFinalSphereMapBits = ImageData( iFrame, CUBEMAP_FACE_SPHEREMAP, 0 ); ImageLoader::GenerateMipmapLevels( pSphereMapBits, pFinalSphereMapBits, m_nWidth, m_nHeight, m_nDepth, m_Format, 2.2, 2.2, m_nMipCount ); } // Free memory MemFreeScratch(); */ } void CVTFTexture::GenerateHemisphereMap( unsigned char *pSphereMapBitsRGBA, int targetWidth, int targetHeight, LookDir_t lookDir, int iFrame ) { Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); unsigned char *pCubeMaps[6]; // Point to our own textures (highest mip level) for (int iFace = 0; iFace < 6; ++iFace) { pCubeMaps[iFace] = ImageData( iFrame, iFace, 0 ); } // Compute the spheremap of the top LOD ComputeHemispheremapFrame( pCubeMaps, pSphereMapBitsRGBA, lookDir ); } //----------------------------------------------------------------------------- // Rotate the image depending on what iFace we've got... // We need to do this because we define the cube textures in a different // format from DX8. //----------------------------------------------------------------------------- static void FixCubeMapFacing( unsigned char* pImage, int cubeFaceID, int size, ImageFormat fmt ) { int retVal; switch( cubeFaceID ) { case CUBEMAP_FACE_RIGHT: // +x retVal = ImageLoader::RotateImageLeft( pImage, pImage, size, fmt ); Assert( retVal ); retVal = ImageLoader::FlipImageVertically( pImage, pImage, size, size, fmt ); Assert( retVal ); break; case CUBEMAP_FACE_LEFT: // -x retVal = ImageLoader::RotateImageLeft( pImage, pImage, size, fmt ); Assert( retVal ); retVal = ImageLoader::FlipImageHorizontally( pImage, pImage, size, size, fmt ); Assert( retVal ); break; case CUBEMAP_FACE_BACK: // +y retVal = ImageLoader::RotateImage180( pImage, pImage, size, fmt ); Assert( retVal ); retVal = ImageLoader::FlipImageHorizontally( pImage, pImage, size, size, fmt ); Assert( retVal ); break; case CUBEMAP_FACE_FRONT: // -y retVal = ImageLoader::FlipImageHorizontally( pImage, pImage, size, size, fmt ); Assert( retVal ); break; case CUBEMAP_FACE_UP: // +z retVal = ImageLoader::RotateImageLeft( pImage, pImage, size, fmt ); Assert( retVal ); retVal = ImageLoader::FlipImageVertically( pImage, pImage, size, size, fmt ); Assert( retVal ); break; case CUBEMAP_FACE_DOWN: // -z retVal = ImageLoader::FlipImageHorizontally( pImage, pImage, size, size, fmt ); Assert( retVal ); retVal = ImageLoader::RotateImageLeft( pImage, pImage, size, fmt ); Assert( retVal ); break; } } //----------------------------------------------------------------------------- // Fixes the cubemap faces orientation from our standard to what the material system needs //----------------------------------------------------------------------------- void CVTFTexture::FixCubemapFaceOrientation( ) { if (!IsCubeMap()) return; Assert( !ImageLoader::IsCompressed( m_Format ) ); for (int iMipLevel = 0; iMipLevel < m_nMipCount; ++iMipLevel) { int iMipSize, iTemp, nDepth; ComputeMipLevelDimensions( iMipLevel, &iMipSize, &iTemp, &nDepth ); Assert( (iMipSize == iTemp) && (nDepth == 1) ); for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for (int iFace = 0; iFace < 6; ++iFace) { FixCubeMapFacing( ImageData( iFrame, iFace, iMipLevel ), iFace, iMipSize, m_Format ); } } } } void CVTFTexture::NormalizeTopMipLevel() { if( !( m_nFlags & TEXTUREFLAGS_NORMAL ) ) return; int nSrcWidth, nSrcHeight, nSrcDepth; int srcMipLevel = 0; ComputeMipLevelDimensions( srcMipLevel, &nSrcWidth, &nSrcHeight, &nSrcDepth ); for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for (int iFace = 0; iFace < m_nFaceCount; ++iFace) { unsigned char *pSrcLevel = ImageData( iFrame, iFace, srcMipLevel ); ImageLoader::NormalizeNormalMapRGBA8888( pSrcLevel, nSrcWidth * nSrcHeight * nSrcDepth ); } } } static inline int PixelOffset( int x, int y, int w, int h ) { x = ( x + w ) % w; y = ( y + h ) % h; return y * w + x; } void CVTFTexture::Compute2DGradient() { int nSrcWidth, nSrcHeight, nSrcDepth; int srcMipLevel = 0; ComputeMipLevelDimensions( srcMipLevel, &nSrcWidth, &nSrcHeight, &nSrcDepth ); for ( int iFrame = 0; iFrame < m_nFrameCount; ++iFrame ) { for ( int iFace = 0; iFace < m_nFaceCount; ++iFace ) { unsigned char *pSrcLevel = ImageData( iFrame, iFace, srcMipLevel ); for ( int iSlice = 0; iSlice < nSrcDepth; iSlice++ ) { uint8 *pSliceData = pSrcLevel + iSlice * nSrcWidth * nSrcHeight * 4; // Copy G to A channel for ( int iY = 0; iY < nSrcHeight; iY++ ) { for ( int iX = 0; iX < nSrcWidth; iX++ ) { // Copy green channel into alpha pSliceData[ PixelOffset( iX, iY, nSrcWidth, nSrcHeight ) * 4 + 3 ] = pSliceData[ PixelOffset( iX, iY, nSrcWidth, nSrcHeight ) * 4 + 1 ]; } } for ( int iY = 0; iY < nSrcHeight; iY++ ) { for ( int iX = 0; iX < nSrcWidth; iX++ ) { float flSobelX, flSobelY; flSobelX = pSliceData[ PixelOffset( iX - 1, iY - 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * -1.0f; // Apply Sobel filter in X, with wrapping texel accesses flSobelX += pSliceData[ PixelOffset( iX - 1, iY + 0, nSrcWidth, nSrcHeight ) * 4 + 3 ] * -2.0f; // flSobelX += pSliceData[ PixelOffset( iX - 1, iY + 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * -1.0f; // [ -1 0 1 ] flSobelX += pSliceData[ PixelOffset( iX + 1, iY - 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * 1.0f; // [ -2 0 2 ] flSobelX += pSliceData[ PixelOffset( iX + 1, iY + 0, nSrcWidth, nSrcHeight ) * 4 + 3 ] * 2.0f; // [ -1 0 1 ] flSobelX += pSliceData[ PixelOffset( iX + 1, iY + 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * 1.0f; // flSobelX /= 255.0f; flSobelY = pSliceData[ PixelOffset( iX - 1, iY - 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * -1.0f; // Apply Sobel filter in Y, with wrapping texel accesses flSobelY += pSliceData[ PixelOffset( iX + 0, iY - 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * -2.0f; // flSobelY += pSliceData[ PixelOffset( iX + 1, iY - 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * -1.0f; // [ -1 -2 -1 ] flSobelY += pSliceData[ PixelOffset( iX - 1, iY + 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * 1.0f; // [ 0 0 0 ] flSobelY += pSliceData[ PixelOffset( iX + 0, iY + 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * 2.0f; // [ 1 2 1 ] flSobelY += pSliceData[ PixelOffset( iX + 1, iY + 1, nSrcWidth, nSrcHeight ) * 4 + 3 ] * 1.0f; // flSobelY /= 255.0f; float flLength = sqrtf( ( flSobelX * flSobelX ) + ( flSobelY * flSobelY ) ); float flX = ( flLength == 0.0f ) ? 0.5f : ( ( flSobelX / flLength ) + 1.0f ) / 2.0f; // Normalized, since we care about direction, but not magnitude float flY = ( flLength == 0.0f ) ? 0.5f : ( ( -flSobelY / flLength ) + 1.0f ) / 2.0f; // Inverting Y to match source1-style normals // 2D gradient story in R and G pSliceData[ PixelOffset( iX, iY, nSrcWidth, nSrcHeight ) * 4 + 0 ] = uint8( flX*255.0f ); pSliceData[ PixelOffset( iX, iY, nSrcWidth, nSrcHeight ) * 4 + 1 ] = uint8( flY*255.0f ); pSliceData[ PixelOffset( iX, iY, nSrcWidth, nSrcHeight ) * 4 + 2 ] = 0; } } } } } } //----------------------------------------------------------------------------- // Converts PC Gamma to 360 Gamma //----------------------------------------------------------------------------- static void ConvertPcTo360SrgbRGBA8888( unsigned char *pImageData, int iWidth, int iHeight ) { int nBytesPerPixel = 4; for ( int32 h = 0; h < iHeight; h++ ) // For each row { for ( int32 w = 0; w < iWidth; w++ ) // For each texel in this row { unsigned char *pRGB[3] = { NULL, NULL, NULL }; pRGB[0] = &( pImageData[ ( h * iWidth * nBytesPerPixel ) + ( w * nBytesPerPixel ) + 0 ] ); // Red pRGB[1] = &( pImageData[ ( h * iWidth * nBytesPerPixel ) + ( w * nBytesPerPixel ) + 1 ] ); // Green pRGB[2] = &( pImageData[ ( h * iWidth * nBytesPerPixel ) + ( w * nBytesPerPixel ) + 2 ] ); // Blue // Modify RGB data in place for ( int j = 0; j < 3; j++ ) // For red, green, blue { float flSrgbGamma = float( *( pRGB[j] ) ) / 255.0f; float fl360Gamma = SrgbGammaTo360Gamma( flSrgbGamma ); fl360Gamma = clamp( fl360Gamma, 0.0f, 1.0f ); *( pRGB[j] ) = ( unsigned char ) ( clamp( ( ( fl360Gamma * 255.0f ) + 0.5f ), 0.0f, 255.0f ) ); } } } } //----------------------------------------------------------------------------- // Generates mipmaps from the base mip levels //----------------------------------------------------------------------------- void CVTFTexture::GenerateMipmaps() { // Go ahead and generate mipmaps even if we don't want 'em in the vtf. // if( ( Flags() & ( TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_NOLOD ) ) == ( TEXTUREFLAGS_NOMIP | TEXTUREFLAGS_NOLOD ) ) // { // return; // } Assert( m_Format == IMAGE_FORMAT_RGBA8888 || m_Format == IMAGE_FORMAT_RGB323232F || m_Format == IMAGE_FORMAT_RGBA32323232F ); // FIXME: Should we be doing anything special for normalmaps other than a final normalization pass? ImageLoader::ResampleInfo_t info; info.m_nSrcWidth = m_nWidth; info.m_nSrcHeight = m_nHeight; info.m_nSrcDepth = m_nDepth; info.m_flSrcGamma = 2.2f; info.m_flDestGamma = 2.2f; info.m_nFlags = 0; bool bNormalMap = ( Flags() & TEXTUREFLAGS_NORMAL ) || ( m_Options.flags0 & VtfProcessingOptions::OPT_NORMAL_DUDV ); bool bAlphaTest = ( ( m_Options.flags0 & VtfProcessingOptions::OPT_MIP_ALPHATEST ) != 0 ); if ( bAlphaTest ) { info.m_nFlags |= ImageLoader::RESAMPLE_ALPHATEST; if ( m_flAlphaThreshhold >= 0 ) { info.m_flAlphaThreshhold = m_flAlphaThreshhold; } if ( m_flAlphaHiFreqThreshhold >= 0 ) { info.m_flAlphaHiFreqThreshhold = m_flAlphaHiFreqThreshhold; } } if ( m_Options.flags0 & VtfProcessingOptions::OPT_FILTER_NICE ) { info.m_nFlags |= ImageLoader::RESAMPLE_NICE_FILTER; } if ( Flags() & TEXTUREFLAGS_CLAMPS ) { info.m_nFlags |= ImageLoader::RESAMPLE_CLAMPS; } if ( Flags() & TEXTUREFLAGS_CLAMPT ) { info.m_nFlags |= ImageLoader::RESAMPLE_CLAMPT; } if ( Flags() & TEXTUREFLAGS_CLAMPU ) { info.m_nFlags |= ImageLoader::RESAMPLE_CLAMPU; } // Compute how many mips are above "visible mip0" int numMipsClampedLod = 0; if ( TextureLODControlSettings_t const *pLodSettings = ( TextureLODControlSettings_t const * ) GetResourceData( VTF_RSRC_TEXTURE_LOD_SETTINGS, NULL ) ) { int iClampX = 1 << MIN( pLodSettings->m_ResolutionClampX, pLodSettings->m_ResolutionClampX_360 ); int iClampY = 1 << MIN( pLodSettings->m_ResolutionClampX, pLodSettings->m_ResolutionClampX_360 ); while ( iClampX < m_nWidth || iClampY < m_nHeight ) { ++ numMipsClampedLod; iClampX <<= 1; iClampY <<= 1; } } if ( m_Options.flags0 & VtfProcessingOptions::OPT_SRGB_PC_TO_360 ) { int iMipLevel = 0; // Perform srgb-correction on mip0, it will be propagated down the mip chain for ( int iFrame = 0; iFrame < m_nFrameCount; ++iFrame ) { for ( int iFace = 0; iFace < m_nFaceCount; ++iFace ) { unsigned char *pImageData = ImageData( iFrame, iFace, iMipLevel ); int w, h, d; ComputeMipLevelDimensions( iMipLevel, &w, &h, &d ); if ( m_Format == IMAGE_FORMAT_RGB323232F ) { // We only convert 8-bit art, so do nothing for 32f? } else { ConvertPcTo360SrgbRGBA8888( pImageData, w, h ); } } } // Mark as pwl corrected, used as a debugging aid in VXConsole m_nFlags |= TEXTUREFLAGS_PWL_CORRECTED; } else { // Mark not pwl m_nFlags &= ~TEXTUREFLAGS_PWL_CORRECTED; } for ( int iMipLevel = 1; iMipLevel < m_nMipCount; ++iMipLevel ) { ComputeMipLevelDimensions( iMipLevel, &info.m_nDestWidth, &info.m_nDestHeight, &info.m_nDestDepth ); if ( m_Options.flags0 & VtfProcessingOptions::OPT_PREMULT_COLOR_ONEOVERMIP ) { for ( int ch = 0; ch < 3; ++ ch ) info.m_flColorScale[ch] = 1.0f / ( float )( 1 << iMipLevel ); } // don't use the 0th mip level since NICE filtering blows up! int nSrcMipLevel = iMipLevel - 4; if ( nSrcMipLevel < 0 ) nSrcMipLevel = 0; // Decay options bool bMipBlendActive = false; char chChannels[4] = { 'R', 'G', 'B', 'A' }; for ( int ch = 0; ch < 4; ++ ch ) { int iLastNonDecayMip = numMipsClampedLod + int( m_Options.numNotDecayMips[ch] ); if ( iLastNonDecayMip > m_nMipCount ) iLastNonDecayMip = m_nMipCount - 1; int numDecayMips = m_nMipCount - iLastNonDecayMip - 1; if ( numDecayMips < 1 ) numDecayMips = 1; // Decay is only active starting from numDecayMips if ( !( ( ( iMipLevel == m_nMipCount - 1 ) || ( iMipLevel > iLastNonDecayMip ) ) && // last 1x1 mip or past clamped and skipped ( m_Options.flags0 & ( VtfProcessingOptions::OPT_DECAY_R << ch ) ) ) ) // the channel has decay continue; // Color goal info.m_flColorGoal[ch] = m_Options.clrDecayGoal[ch]; // Color scale if ( iMipLevel == m_nMipCount - 1 ) { info.m_flColorScale[ch] = 0.0f; } else if ( m_Options.flags0 & ( VtfProcessingOptions::OPT_DECAY_EXP_R << ch ) ) { info.m_flColorScale[ch] = pow( m_Options.fDecayExponentBase[ch], iMipLevel - iLastNonDecayMip ); } else { info.m_flColorScale[ch] = 1.0f - float( iMipLevel - iLastNonDecayMip ) / float( numDecayMips ); } if ( !bMipBlendActive ) { bMipBlendActive = true; printf( "Blending mip%d %dx%d to", iMipLevel, info.m_nDestWidth, info.m_nDestHeight ); } printf( " %c=%d ~%d%%", chChannels[ch], m_Options.clrDecayGoal[ch], int( (1.f - info.m_flColorScale[ch]) * 100.0f + 0.5f ) ); } if ( bMipBlendActive ) printf( "\n" ); if ( bNormalMap ) { info.m_nFlags |= ImageLoader::RESAMPLE_NORMALMAP; // Normal maps xyz decays to 127.f for ( int ch = 0; ch < 3; ++ ch ) info.m_flColorGoal[ch] = 127.0f; } for ( int iFrame = 0; iFrame < m_nFrameCount; ++iFrame ) { for ( int iFace = 0; iFace < m_nFaceCount; ++iFace ) { unsigned char *pSrcLevel = ImageData( iFrame, iFace, nSrcMipLevel ); unsigned char *pDstLevel = ImageData( iFrame, iFace, iMipLevel ); info.m_pSrc = pSrcLevel; info.m_pDest = pDstLevel; ComputeMipLevelDimensions( nSrcMipLevel, &info.m_nSrcWidth, &info.m_nSrcHeight, &info.m_nSrcDepth ); if( m_Format == IMAGE_FORMAT_RGBA32323232F ) { ImageLoader::ResampleRGBA32323232F( info ); } else if( m_Format == IMAGE_FORMAT_RGB323232F ) { ImageLoader::ResampleRGB323232F( info ); } else { ImageLoader::ResampleRGBA8888( info ); } if ( Flags() & TEXTUREFLAGS_NORMAL ) { ImageLoader::NormalizeNormalMapRGBA8888( pDstLevel, info.m_nDestWidth * info.m_nDestHeight * info.m_nDestDepth ); } } } } } void CVTFTexture::PutOneOverMipLevelInAlpha() { Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); for (int iMipLevel = 0; iMipLevel < m_nMipCount; ++iMipLevel) { int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); int size = nMipWidth * nMipHeight * nMipDepth; unsigned char ooMipLevel = ( unsigned char )( 255.0f * ( 1.0f / ( float )( 1 << iMipLevel ) ) ); for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for (int iFace = 0; iFace < m_nFaceCount; ++iFace) { unsigned char *pDstLevel = ImageData( iFrame, iFace, iMipLevel ); unsigned char *pDst; for( pDst = pDstLevel; pDst < pDstLevel + size * 4; pDst += 4 ) { pDst[3] = ooMipLevel; } } } } } void CVTFTexture::PremultAlphaWithMipFraction() { Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); for (int iMipLevel = 0; iMipLevel < m_nMipCount; ++iMipLevel) { int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); int size = nMipWidth * nMipHeight * nMipDepth; int maxAlphaAtMip = clamp ( m_Options.fullAlphaAtMipLevel, 1, m_nMipCount ); float flMipScale = ( float )( iMipLevel ) / ( float )( maxAlphaAtMip ); flMipScale = clamp ( flMipScale, 0.0f, 1.0f ); for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for (int iFace = 0; iFace < m_nFaceCount; ++iFace) { unsigned char *pDstLevel = ImageData( iFrame, iFace, iMipLevel ); unsigned char *pDst; for( pDst = pDstLevel; pDst < pDstLevel + size * 4; pDst += 4 ) { pDst[3] = ( unsigned char )( ( float )( pDst[3] ) * flMipScale ); pDst[3] = ( unsigned char )clamp( pDst[3], m_Options.minAlpha, 255 ); } } } } } //----------------------------------------------------------------------------- // Computes the reflectivity //----------------------------------------------------------------------------- void CVTFTexture::ComputeReflectivity( ) { // HDRFIXME: fix this when we ahve a new intermediate format if( m_Format != IMAGE_FORMAT_RGBA8888 ) { m_vecReflectivity.Init( 0.2f, 0.2f, 0.2f ); return; } Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); int divisor = 0; m_vecReflectivity.Init( 0.0f, 0.0f, 0.0f ); for( int iFrame = 0; iFrame < m_nFrameCount; ++iFrame ) { for( int iFace = 0; iFace < m_nFaceCount; ++iFace ) { Vector vecFaceReflect; unsigned char* pSrc = ImageData( iFrame, iFace, 0 ); int nNumPixels = m_nWidth * m_nHeight * m_nDepth; VectorClear( vecFaceReflect ); for (int i = 0; i < nNumPixels; ++i, pSrc += 4 ) { vecFaceReflect[0] += TextureToLinear( pSrc[0] ); vecFaceReflect[1] += TextureToLinear( pSrc[1] ); vecFaceReflect[2] += TextureToLinear( pSrc[2] ); } vecFaceReflect /= nNumPixels; m_vecReflectivity += vecFaceReflect; ++divisor; } } m_vecReflectivity /= divisor; } //----------------------------------------------------------------------------- // Computes the alpha flags //----------------------------------------------------------------------------- void CVTFTexture::ComputeAlphaFlags() { // HDRFIXME: hack hack hack if( m_Format != IMAGE_FORMAT_RGBA8888 ) { m_nFlags &= ~( TEXTUREFLAGS_EIGHTBITALPHA | TEXTUREFLAGS_ONEBITALPHA ); m_Options.flags0 &= ~( VtfProcessingOptions::OPT_MIP_ALPHATEST ); return; } Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); m_nFlags &= ~(TEXTUREFLAGS_EIGHTBITALPHA | TEXTUREFLAGS_ONEBITALPHA); if( m_Options.flags0 & VtfProcessingOptions::OPT_SET_ALPHA_ONEOVERMIP ) { m_nFlags |= TEXTUREFLAGS_EIGHTBITALPHA; return; } for( int iFrame = 0; iFrame < m_nFrameCount; ++iFrame ) { for( int iFace = 0; iFace < m_nFaceCount; ++iFace ) { for( int iMipLevel = 0; iMipLevel < m_nMipCount; ++iMipLevel ) { // If we're all 0 or all 255, assume it's opaque bool bHasZero = false; bool bHas255 = false; unsigned char* pSrcBits = ImageData( iFrame, iFace, iMipLevel ); int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); int nNumPixels = nMipWidth * nMipHeight * nMipDepth; while ( --nNumPixels >= 0 ) { if ( pSrcBits[3] == 0 ) { bHasZero = true; } else if ( pSrcBits[3] == 255 ) { bHas255 = true; } else { // Have grey at all? 8 bit alpha baby m_nFlags &= ~TEXTUREFLAGS_ONEBITALPHA; m_nFlags |= TEXTUREFLAGS_EIGHTBITALPHA; return; } pSrcBits += 4; } // If we have both 0 at 255, we're at least one-bit alpha if ( bHasZero && bHas255 ) { m_nFlags |= TEXTUREFLAGS_ONEBITALPHA; } } } } } //----------------------------------------------------------------------------- // Gets the texture all internally consistent assuming you've loaded // mip 0 of all faces of all frames //----------------------------------------------------------------------------- void CVTFTexture::PostProcess( bool bGenerateSpheremap, LookDir_t lookDir, bool bAllowFixCubemapOrientation, bool bLoadedMiplevels ) { // HDRFIXME: Make sure that all of the below functions check for the proper formats if we get rid of this assert. // Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); // Set up the cube map faces if (IsCubeMap()) { // Rotate the cubemaps so they're appropriate for the material system if ( bAllowFixCubemapOrientation ) FixCubemapFaceOrientation(); // FIXME: We could theoretically not compute spheremap mip levels // in generate spheremaps; should we? The trick is when external // clients can be expected to call it // Compute the spheremap fallback for cubemaps if we weren't able to load up one... if (bGenerateSpheremap) GenerateSpheremap(lookDir); } if ( m_Options.flags0 & VtfProcessingOptions::OPT_COMPUTE_GRADIENT ) { Compute2DGradient(); } // Normalize the top mip level if necessary. NormalizeTopMipLevel(); // Generate mipmap levels if ( !bLoadedMiplevels ) { GenerateMipmaps(); } if( m_Options.flags0 & VtfProcessingOptions::OPT_SET_ALPHA_ONEOVERMIP ) { PutOneOverMipLevelInAlpha(); } if( m_Options.flags0 & VtfProcessingOptions::OPT_PREMULT_ALPHA_MIPFRACTION ) { PremultAlphaWithMipFraction(); } // Compute reflectivity ComputeReflectivity(); // Are we 8-bit or 1-bit alpha? // NOTE: We have to do this *after* computing the spheremap fallback for // cubemaps or it'll throw the flags off ComputeAlphaFlags(); } void CVTFTexture::SetPostProcessingSettings( VtfProcessingOptions const *pOptions ) { memset( &m_Options, 0, sizeof( m_Options ) ); memcpy( &m_Options, pOptions, MIN( sizeof( m_Options ), pOptions->cbSize ) ); m_Options.cbSize = sizeof( m_Options ); // Optionally perform the fixups } //----------------------------------------------------------------------------- // Generate the low-res image bits //----------------------------------------------------------------------------- bool CVTFTexture::ConstructLowResImage() { // HDRFIXME: hack hack hack if( m_Format != IMAGE_FORMAT_RGBA8888 ) { return true; } Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); Assert( m_pLowResImageData ); CUtlMemory lowResSizeImage; lowResSizeImage.EnsureCapacity( m_nLowResImageWidth * m_nLowResImageHeight * 4 ); ImageLoader::ResampleInfo_t info; info.m_pSrc = ImageData(0, 0, 0); info.m_pDest = lowResSizeImage.Base(); info.m_nSrcWidth = m_nWidth; info.m_nSrcHeight = m_nHeight; info.m_nDestWidth = m_nLowResImageWidth; info.m_nDestHeight = m_nLowResImageHeight; info.m_flSrcGamma = 2.2f; info.m_flDestGamma = 2.2f; info.m_nFlags = ImageLoader::RESAMPLE_NICE_FILTER; if( !ImageLoader::ResampleRGBA8888( info ) ) return false; // convert to the low-res size version with the correct image format unsigned char *tmpImage = lowResSizeImage.Base(); return ImageLoader::ConvertImageFormat( tmpImage, IMAGE_FORMAT_RGBA8888, m_pLowResImageData, m_LowResImageFormat, m_nLowResImageWidth, m_nLowResImageHeight ); } // ----------------------------------------------------------------------------- // Cubemap edge-filtering functions. // ----------------------------------------------------------------------------- void CVTFTexture::SetupFaceVert( int iMipLevel, int iVert, CEdgePos &out ) { int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); out.x = out.y = 0; if ( iVert == 0 || iVert == 3 ) { out.y = nMipHeight - 1; } if ( iVert == 2 || iVert == 3 ) { out.x = nMipWidth - 1; } } void CVTFTexture::SetupEdgeIncrement( CEdgePos &start, CEdgePos &end, CEdgePos &inc ) { inc.x = inc.y = 0; if ( start.x != end.x ) { Assert( start.y == end.y ); inc.x = (start.x < end.x) ? 1 : -1; } else if ( start.y != end.y ) { Assert( start.x == end.x ); inc.y = (start.y < end.y) ? 1 : -1; } else { Assert( false ); } } void CVTFTexture::SetupTextureEdgeIncrements( int iMipLevel, int iFace1Edge, int iFace2Edge, bool bFlipFace2Edge, CEdgeIncrements *incs ) { // Figure out the coordinates of the verts we're blending. SetupFaceVert( iMipLevel, iFace1Edge, incs->iFace1Start ); SetupFaceVert( iMipLevel, (iFace1Edge+1)%4, incs->iFace1End ); if ( bFlipFace2Edge ) { SetupFaceVert( iMipLevel, (iFace2Edge+1)%4, incs->iFace2Start ); SetupFaceVert( iMipLevel, iFace2Edge, incs->iFace2End ); } else { SetupFaceVert( iMipLevel, iFace2Edge, incs->iFace2Start ); SetupFaceVert( iMipLevel, (iFace2Edge+1)%4, incs->iFace2End ); } // Figure out the increments from start to end. SetupEdgeIncrement( incs->iFace1Start, incs->iFace1End, incs->iFace1Inc ); SetupEdgeIncrement( incs->iFace2Start, incs->iFace2End, incs->iFace2Inc ); } void BlendTexels( unsigned char **texels, int nTexels ) { int sum[4] = { 0, 0, 0, 0 }; int i; for ( i=0; i < nTexels; i++ ) { sum[0] += texels[i][0]; sum[1] += texels[i][1]; sum[2] += texels[i][2]; sum[3] += texels[i][3]; } for ( i=0; i < nTexels; i++ ) { texels[i][0] = (unsigned char)( sum[0] / nTexels ); texels[i][1] = (unsigned char)( sum[1] / nTexels ); texels[i][2] = (unsigned char)( sum[2] / nTexels ); texels[i][3] = (unsigned char)( sum[3] / nTexels ); } } void CVTFTexture::BlendCubeMapFaceEdges( int iFrame, int iMipLevel, const CEdgeMatch *pMatch ) { int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); Assert( nMipDepth == 1 ); if ( nMipWidth <= 1 || nMipHeight <= 1 ) return; unsigned char *pFace1Data = ImageData( iFrame, pMatch->m_iFaces[0], iMipLevel ); unsigned char *pFace2Data = ImageData( iFrame, pMatch->m_iFaces[1], iMipLevel ); CEdgeIncrements incs; SetupTextureEdgeIncrements( iMipLevel, pMatch->m_iEdges[0], pMatch->m_iEdges[1], pMatch->m_bFlipFace2Edge, &incs ); // Do all pixels but the first and the last one (those will be handled when blending corners). CEdgePos iFace1Cur = incs.iFace1Start + incs.iFace1Inc; CEdgePos iFace2Cur = incs.iFace2Start + incs.iFace2Inc; if ( m_Format == IMAGE_FORMAT_DXT1 || m_Format == IMAGE_FORMAT_DXT5 ) { if ( iFace1Cur != incs.iFace1End ) { while ( iFace1Cur != incs.iFace1End ) { // Copy the palette index from image 1 to image 2. S3PaletteIndex paletteIndex = S3TC_GetPaletteIndex( pFace1Data, m_Format, nMipWidth, iFace1Cur.x, iFace1Cur.y ); S3TC_SetPaletteIndex( pFace2Data, m_Format, nMipWidth, iFace2Cur.x, iFace2Cur.y, paletteIndex ); iFace1Cur += incs.iFace1Inc; iFace2Cur += incs.iFace2Inc; } } } else if ( m_Format == IMAGE_FORMAT_RGBA8888 ) { if ( iFace1Cur != incs.iFace1End ) { while ( iFace1Cur != incs.iFace1End ) { // Now we know the 2 pixels. Average them and copy the averaged value to both pixels. unsigned char *texels[2] = { pFace1Data + ((iFace1Cur.y * nMipWidth) + iFace1Cur.x) * 4, pFace2Data + ((iFace2Cur.y * nMipWidth) + iFace2Cur.x) * 4 }; BlendTexels( texels, 2 ); iFace1Cur += incs.iFace1Inc; iFace2Cur += incs.iFace2Inc; } } } else { Error( "BlendCubeMapFaceEdges: unsupported image format (%d)", (int)m_Format ); } } void CVTFTexture::BlendCubeMapFaceCorners( int iFrame, int iMipLevel, const CCornerMatch *pMatch ) { int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); Assert( nMipDepth == 1 ); // Setup the coordinates of each texel. CEdgePos texelPos[3]; unsigned char *pImageData[3]; int iEdge; for ( iEdge=0; iEdge < 3; iEdge++ ) { SetupFaceVert( iMipLevel, pMatch->m_iFaceEdges[iEdge], texelPos[iEdge] ); pImageData[iEdge] = ImageData( iFrame, pMatch->m_iFaces[iEdge], iMipLevel ); } if ( m_Format == IMAGE_FORMAT_DXT1 || m_Format == IMAGE_FORMAT_DXT5 ) { if ( nMipWidth < 4 || nMipHeight < 4 ) return; // Copy the first palette index to the other blocks. S3PaletteIndex paletteIndex = S3TC_GetPaletteIndex( pImageData[0], m_Format, nMipWidth, texelPos[0].x, texelPos[0].y ); S3TC_SetPaletteIndex( pImageData[1], m_Format, nMipWidth, texelPos[1].x, texelPos[1].y, paletteIndex ); S3TC_SetPaletteIndex( pImageData[2], m_Format, nMipWidth, texelPos[2].x, texelPos[2].y, paletteIndex ); } else if ( m_Format == IMAGE_FORMAT_RGBA8888 ) { // Setup pointers to the 3 corner texels. unsigned char *texels[3]; for ( iEdge=0; iEdge < 3; iEdge++ ) { CEdgePos facePos; SetupFaceVert( iMipLevel, pMatch->m_iFaceEdges[iEdge], facePos ); texels[iEdge] = pImageData[iEdge]; texels[iEdge] += (facePos.y * nMipWidth + facePos.x) * 4; } // Now blend the texels. BlendTexels( texels, 3 ); } else { Assert( false ); } } void CVTFTexture::BuildCubeMapMatchLists( CEdgeMatch edgeMatches[NUM_EDGE_MATCHES], CCornerMatch cornerMatches[NUM_CORNER_MATCHES], bool bSkybox ) { int **faceVertsList = bSkybox ? g_skybox_FaceVerts : g_FaceVerts; // For each face, look for matching edges on other faces. int nTotalEdgesMatched = 0; for ( int iFace = 0; iFace < 6; iFace++ ) { for ( int iEdge=0; iEdge < 4; iEdge++ ) { int i1 = faceVertsList[iFace][iEdge]; int i2 = faceVertsList[iFace][(iEdge+1)%4]; // Only look for faces with indices < what we have so we don't do each edge twice. for ( int iOtherFace=0; iOtherFace < iFace; iOtherFace++ ) { for ( int iOtherEdge=0; iOtherEdge < 4; iOtherEdge++ ) { int o1 = faceVertsList[iOtherFace][iOtherEdge]; int o2 = faceVertsList[iOtherFace][(iOtherEdge+1)%4]; if ( (i1 == o1 && i2 == o2) || (i2 == o1 && i1 == o2) ) { CEdgeMatch *pMatch = &edgeMatches[nTotalEdgesMatched]; pMatch->m_iFaces[0] = iFace; pMatch->m_iEdges[0] = iEdge; pMatch->m_iFaces[1] = iOtherFace; pMatch->m_iEdges[1] = iOtherEdge; pMatch->m_iCubeVerts[0] = o1; pMatch->m_iCubeVerts[1] = o2; pMatch->m_bFlipFace2Edge = i1 != o1; ++nTotalEdgesMatched; } } } } } Assert( nTotalEdgesMatched == 12 ); // For each corner vert, find the 3 edges touching it. for ( int iVert=0; iVert < NUM_CORNER_MATCHES; iVert++ ) { int iTouchingFace = 0; for ( int iFace=0; iFace < 6; iFace++ ) { for ( int iFaceVert=0; iFaceVert < 4; iFaceVert++ ) { if ( faceVertsList[iFace][iFaceVert] == iVert ) { cornerMatches[iVert].m_iFaces[iTouchingFace] = iFace; cornerMatches[iVert].m_iFaceEdges[iTouchingFace] = iFaceVert; ++iTouchingFace; } } } Assert( iTouchingFace == 3 ); } } void CVTFTexture::BlendCubeMapEdgePalettes( int iFrame, int iMipLevel, const CEdgeMatch *pMatch ) { Assert( m_Format == IMAGE_FORMAT_DXT1 || m_Format == IMAGE_FORMAT_DXT5 ); int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); Assert( nMipDepth == 1 ); if ( nMipWidth <= 8 || nMipHeight <= 8 ) return; unsigned char *pFace1Data = ImageData( iFrame, pMatch->m_iFaces[0], iMipLevel ); unsigned char *pFace2Data = ImageData( iFrame, pMatch->m_iFaces[1], iMipLevel ); S3RGBA *pFace1Original = &m_OriginalData[ GetImageOffset( iFrame, pMatch->m_iFaces[0], iMipLevel, IMAGE_FORMAT_RGBA8888 ) / 4 ]; S3RGBA *pFace2Original = &m_OriginalData[ GetImageOffset( iFrame, pMatch->m_iFaces[1], iMipLevel, IMAGE_FORMAT_RGBA8888 ) / 4 ]; CEdgeIncrements incs; SetupTextureEdgeIncrements( iMipLevel, pMatch->m_iEdges[0], pMatch->m_iEdges[1], pMatch->m_bFlipFace2Edge, &incs ); // Divide the coordinates by 4 since we're dealing with S3 blocks here. incs.iFace1Start /= 4; incs.iFace1End /= 4; incs.iFace2Start /= 4; incs.iFace2End /= 4; // Now walk along the edges, blending the edge pixels. CEdgePos iFace1Cur = incs.iFace1Start + incs.iFace1Inc; CEdgePos iFace2Cur = incs.iFace2Start + incs.iFace2Inc; while ( iFace1Cur != incs.iFace1End ) // We intentionally want to not process the last block here.. { // Merge the palette of these two blocks. char *blocks[2] = { S3TC_GetBlock( pFace1Data, m_Format, nMipWidth>>2, iFace1Cur.x, iFace1Cur.y ), S3TC_GetBlock( pFace2Data, m_Format, nMipWidth>>2, iFace2Cur.x, iFace2Cur.y ) }; S3RGBA *originals[2] = { &pFace1Original[(iFace1Cur.y * 4 * nMipWidth) + iFace1Cur.x * 4], &pFace2Original[(iFace2Cur.y * 4 * nMipWidth) + iFace2Cur.x * 4] }; S3TC_MergeBlocks( blocks, originals, 2, nMipWidth*4, m_Format ); iFace1Cur += incs.iFace1Inc; iFace2Cur += incs.iFace2Inc; } } void CVTFTexture::BlendCubeMapCornerPalettes( int iFrame, int iMipLevel, const CCornerMatch *pMatch ) { int nMipWidth, nMipHeight, nMipDepth; ComputeMipLevelDimensions( iMipLevel, &nMipWidth, &nMipHeight, &nMipDepth ); Assert( nMipDepth == 1 ); if ( nMipWidth < 4 || nMipHeight < 4 ) return; // Now setup an S3TC block pointer for each of the corner blocks on each face. char *blocks[3]; S3RGBA *originals[3]; for ( int iEdge=0; iEdge < 3; iEdge++ ) { CEdgePos facePos; SetupFaceVert( iMipLevel, pMatch->m_iFaceEdges[iEdge], facePos ); facePos /= 4; // To get the S3 block index. int iFaceIndex = pMatch->m_iFaces[iEdge]; unsigned char *pFaceData = ImageData( iFrame, iFaceIndex, iMipLevel ); S3RGBA *pFaceOriginal = &m_OriginalData[ GetImageOffset( iFrame, iFaceIndex, iMipLevel, IMAGE_FORMAT_RGBA8888 ) / 4 ]; blocks[iEdge] = S3TC_GetBlock( pFaceData, m_Format, nMipWidth>>2, facePos.x, facePos.y ); originals[iEdge] = &pFaceOriginal[ (facePos.y * 4 * nMipWidth) + facePos.x * 4 ]; } S3TC_MergeBlocks( blocks, originals, 3, nMipWidth*4, m_Format ); } void CVTFTexture::MatchCubeMapS3TCPalettes( CEdgeMatch edgeMatches[NUM_EDGE_MATCHES], CCornerMatch cornerMatches[NUM_CORNER_MATCHES] ) { for (int iMipLevel = 0; iMipLevel < m_nMipCount; ++iMipLevel) { for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { // First, match all the edge palettes (this part skips the first and last 4 texels // along the edge since those S3 blocks are handled in the corner case). for ( int iEdgeMatch=0; iEdgeMatch < NUM_EDGE_MATCHES; iEdgeMatch++ ) { BlendCubeMapEdgePalettes( iFrame, iMipLevel, &edgeMatches[iEdgeMatch] ); } for ( int iCornerMatch=0; iCornerMatch < NUM_CORNER_MATCHES; iCornerMatch++ ) { BlendCubeMapCornerPalettes( iFrame, iMipLevel, &cornerMatches[iCornerMatch] ); } } } } void CVTFTexture::MatchCubeMapBorders( int iStage, ImageFormat finalFormat, bool bSkybox ) { // HDRFIXME: hack hack hack if( m_Format != IMAGE_FORMAT_RGBA8888 ) { //Assert( 0 ); return; } if ( !IsCubeMap() ) return; Assert( IsCubeMap() ); Assert( m_nFaceCount >= 6 ); if ( iStage == 1 ) { // Stage 1 is while the image is still RGBA8888. If we're not going to S3 compress the image, // then it is easiest to match the borders now. Assert( m_Format == IMAGE_FORMAT_RGBA8888 ); if ( finalFormat == IMAGE_FORMAT_DXT1 || finalFormat == IMAGE_FORMAT_DXT5 ) { // If we're going to S3 compress the image eventually, then store off the original version // because we can use that while matching the S3 compressed edges (we have to do some tricky // repalettizing). int nTotalBytes = ComputeTotalSize(); m_OriginalData.SetSize( nTotalBytes / 4 ); memcpy( m_OriginalData.Base(), ImageData(), nTotalBytes ); // Swap R and B in these because IMAGE_FORMAT_RGBA8888 is swapped from the way S3RGBAs are. for ( int i=0; i < nTotalBytes/4; i++ ) V_swap( m_OriginalData[i].r, m_OriginalData[i].b ); return; } else { // Drop down below and do the edge matching. } } else { if ( finalFormat == IMAGE_FORMAT_DXT1 || finalFormat == IMAGE_FORMAT_DXT5 ) { Assert( m_Format == finalFormat ); } else { // If we're not winding up S3 compressed, then we already fixed the cubemap borders. return; } } // Figure out CEdgeMatch edgeMatches[NUM_EDGE_MATCHES]; CCornerMatch cornerMatches[NUM_CORNER_MATCHES]; BuildCubeMapMatchLists( edgeMatches, cornerMatches, bSkybox ); // If we're S3 compressed, then during the first pass, we need to match the palettes of all // bordering S3 blocks. if ( m_Format == IMAGE_FORMAT_DXT1 || m_Format == IMAGE_FORMAT_DXT5 ) { MatchCubeMapS3TCPalettes( edgeMatches, cornerMatches ); } for (int iMipLevel = 0; iMipLevel < m_nMipCount; ++iMipLevel) { for (int iFrame = 0; iFrame < m_nFrameCount; ++iFrame) { for ( int iEdgeMatch=0; iEdgeMatch < NUM_EDGE_MATCHES; iEdgeMatch++ ) { BlendCubeMapFaceEdges( iFrame, iMipLevel, &edgeMatches[iEdgeMatch] ); } for ( int iCornerMatch=0; iCornerMatch < NUM_CORNER_MATCHES; iCornerMatch++ ) { BlendCubeMapFaceCorners( iFrame, iMipLevel, &cornerMatches[iCornerMatch] ); } } } } /* Test code used to draw the cubemap into a scratchpad file. Useful for debugging, or at least it was once. IScratchPad3D *pPad = ScratchPad3D_Create(); int nMipWidth, nMipHeight; ComputeMipLevelDimensions( 0, &nMipWidth, &nMipHeight ); CUtlVector data; data.SetSize( nMipWidth*nMipHeight ); float cubeSize = 200; Vector vertPositions[8] = { Vector( 0, cubeSize, 0 ), Vector( 0, cubeSize, cubeSize ), Vector( cubeSize, 0, 0 ), Vector( cubeSize, 0, cubeSize ), Vector( 0, 0, 0 ), Vector( 0, 0, cubeSize ), Vector( cubeSize, cubeSize, 0 ), Vector( cubeSize, cubeSize, cubeSize ) }; char *faceNames[6] = { "right","left","back","front","up","down" }; for ( int iVert=0; iVert < 8; iVert++ ) { char str[512]; Q_snprintf( str, sizeof( str ), "%d", iVert ); CTextParams params; params.m_flLetterWidth = 20; params.m_vPos = vertPositions[iVert]; pPad->DrawText( str, params ); } for ( int iFace=0; iFace < 6; iFace++ ) { unsigned char *pFace1Data = ImageData( 0, iFace, 0 ); for ( int y=0; y < nMipHeight; y++ ) { for( int x=0; x < nMipWidth; x++ ) { S3PaletteIndex index = S3TC_GetPaletteIndex( pFace1Data, m_Format, nMipWidth, x, y ); const char *pBlock = S3TC_GetBlock( pFace1Data, m_Format, nMipWidth/4, x/4, y/4 ); unsigned char a0 = pBlock[0]; unsigned char a1 = pBlock[1]; if ( index.m_AlphaIndex == 0 ) { data[y*nMipWidth+x] = a0; } else if ( index.m_AlphaIndex == 1 ) { data[y*nMipWidth+x] = a1; } else if ( a0 > a1 ) { data[y*nMipWidth+x] = ((8-(int)index.m_AlphaIndex)*a0 + ((int)index.m_AlphaIndex-1)*a1) / 7; } else { if ( index.m_AlphaIndex == 6 ) data[y*nMipWidth+x] = 0; else if ( index.m_AlphaIndex == 7 ) data[y*nMipWidth+x] = 255; else data[y*nMipWidth+x] = ((6-(int)index.m_AlphaIndex)*a0 + ((int)index.m_AlphaIndex-1)*a1) / 5; } } } Vector vCorners[4]; for ( int iCorner=0; iCorner < 4; iCorner++ ) vCorners[iCorner] = vertPositions[g_FaceVerts[iFace][iCorner]]; pPad->DrawImageBW( data.Base(), nMipWidth, nMipHeight, nMipWidth, false, true, vCorners ); CTextParams params; params.m_vPos = (vCorners[0] + vCorners[1] + vCorners[2] + vCorners[3]) / 4; params.m_bCentered = true; params.m_vColor.Init( 1, 0, 0 ); params.m_bTwoSided = true; params.m_flLetterWidth = 10; Vector vNormal = (vCorners[1] - vCorners[0]).Cross( vCorners[2] - vCorners[1] ); VectorNormalize( vNormal ); params.m_vPos += vNormal*5; VectorAngles( vNormal, params.m_vAngles ); pPad->DrawText( faceNames[iFace], params ); pPad->Flush(); } */