/// @copydoc ResourceHandler::CacheResource() bool FontResourceHandler::CacheResource( ObjectPreprocessor* pObjectPreprocessor, Resource* pResource, const String& rSourceFilePath ) { HELIUM_ASSERT( pObjectPreprocessor ); HELIUM_ASSERT( pResource ); Font* pFont = Reflect::AssertCast< Font >( pResource ); // Load the font into memory ourselves in order to make sure we properly support Unicode file names. FileStream* pFileStream = File::Open( rSourceFilePath, FileStream::MODE_READ ); if( !pFileStream ) { HELIUM_TRACE( TRACE_ERROR, TXT( "FontResourceHandler: Source file for font resource \"%s\" failed to open properly.\n" ), *rSourceFilePath ); return false; } uint64_t fileSize64 = static_cast< uint64_t >( pFileStream->GetSize() ); if( fileSize64 > SIZE_MAX ) { HELIUM_TRACE( TRACE_ERROR, ( TXT( "FontResourceHandler: Font file \"%s\" exceeds the maximum addressable size of data in memory for " ) TXT( "this platform and will not be cached.\n" ) ), *rSourceFilePath ); delete pFileStream; return false; } size_t fileSize = static_cast< size_t >( fileSize64 ); uint8_t* pFileData = new uint8_t [ fileSize ]; if( !pFileData ) { HELIUM_TRACE( TRACE_ERROR, ( TXT( "FontResourceHandler: Failed to allocate %" ) TPRIuSZ TXT( " bytes for resource data for font " ) TXT( "\"%s\".\n" ) ), fileSize, *rSourceFilePath ); delete pFileStream; return false; } size_t bytesRead = pFileStream->Read( pFileData, 1, fileSize ); delete pFileStream; if( bytesRead != fileSize ) { HELIUM_TRACE( TRACE_WARNING, ( TXT( "FontResourceHandler: Attempted to read %" ) TPRIuSZ TXT( " bytes from font resource file \"%s\", " ) TXT( "but only %" ) TPRIuSZ TXT( " bytes were read successfully.\n" ) ), fileSize, *rSourceFilePath, bytesRead ); } // Create the font face. FT_Library pLibrary = GetStaticLibrary(); HELIUM_ASSERT( pLibrary ); FT_Face pFace = NULL; FT_Error error = FT_New_Memory_Face( pLibrary, pFileData, static_cast< FT_Long >( bytesRead ), 0, &pFace ); if( error != 0 ) { HELIUM_TRACE( TRACE_ERROR, TXT( "FontResourceHandler: Failed to create font face from resource file \"%s\".\n" ), *rSourceFilePath ); delete [] pFileData; return false; } // Set the appropriate font size. int32_t pointSize = Font::Float32ToFixed26x6( pFont->GetPointSize() ); uint32_t dpi = pFont->GetDpi(); error = FT_Set_Char_Size( pFace, pointSize, pointSize, dpi, dpi ); if( error != 0 ) { HELIUM_TRACE( TRACE_ERROR, TXT( "FontResourceHandler: Failed to set size of font resource \"%s\".\n" ), *rSourceFilePath ); FT_Done_Face( pFace ); delete [] pFileData; return false; } // Get the general font size information. FT_Size pSize = pFace->size; HELIUM_ASSERT( pSize ); int32_t ascender = pSize->metrics.ascender; int32_t descender = pSize->metrics.descender; int32_t height = pSize->metrics.height; int32_t maxAdvance = pSize->metrics.max_advance; // Make sure that all characters in the font will fit on a single texture sheet (note that we also need at least a // pixel on each side in order to pad each glyph). uint16_t textureSheetWidth = Max< uint16_t >( pFont->GetTextureSheetWidth(), 1 ); uint16_t textureSheetHeight = Max< uint16_t >( pFont->GetTextureSheetHeight(), 1 ); int32_t integerHeight = ( height + ( 1 << 6 ) - 1 ) >> 6; if( integerHeight + 2 > textureSheetHeight ) { HELIUM_TRACE( TRACE_ERROR, ( TXT( "FontResourceHandler: Font height (%" ) TPRId32 TXT( ") exceeds the texture sheet height (%" ) TPRIu16 TXT( ") for font resource \"%s\".\n" ) ), integerHeight, textureSheetHeight, *pResource->GetPath().ToString() ); FT_Done_Face( pFace ); delete [] pFileData; return false; } int32_t integerMaxAdvance = ( maxAdvance + ( 1 << 6 ) - 1 ) >> 6; if( integerMaxAdvance + 2 > textureSheetWidth ) { HELIUM_TRACE( TRACE_ERROR, ( TXT( "FontResourceHandler: Maximum character advance (%" ) TPRId32 TXT( ") exceeds the texture sheet " ) TXT( "width (%" ) TPRIu16 TXT( ") for font resource \"%s\".\n" ) ), integerMaxAdvance, textureSheetWidth, *pResource->GetPath().ToString() ); FT_Done_Face( pFace ); delete [] pFileData; return false; } // Allocate a buffer for building our texture sheets. uint_fast32_t texturePixelCount = static_cast< uint_fast32_t >( textureSheetWidth ) * static_cast< uint_fast32_t >( textureSheetHeight ); uint8_t* pTextureBuffer = new uint8_t [ texturePixelCount ]; HELIUM_ASSERT( pTextureBuffer ); if( !pTextureBuffer ) { HELIUM_TRACE( TRACE_ERROR, ( TXT( "FontResourceHandler: Failed to allocate %" ) TPRIuFAST32 TXT( " bytes for texture resource " ) TXT( "buffer data while caching font resource \"%s\".\n" ) ), texturePixelCount, *pResource->GetPath().ToString() ); FT_Done_Face( pFace ); delete [] pFileData; return false; } MemoryZero( pTextureBuffer, texturePixelCount ); // Build the texture sheets for our glyphs. Font::ECompression textureCompression = pFont->GetTextureCompression(); bool bAntialiased = pFont->GetAntialiased(); DynArray< DynArray< uint8_t > > textureSheets; DynArray< Font::Character > characters; uint16_t penX = 1; uint16_t penY = 1; uint16_t lineHeight = 0; FT_Int32 glyphLoadFlags = FT_LOAD_RENDER; if( !bAntialiased ) { glyphLoadFlags |= FT_LOAD_TARGET_MONO; } for( uint_fast32_t codePoint = 0; codePoint <= UNICODE_CODE_POINT_MAX; ++codePoint ) { // Check whether the current code point is contained within the font. FT_UInt characterIndex = FT_Get_Char_Index( pFace, static_cast< FT_ULong >( codePoint ) ); if( characterIndex == 0 ) { continue; } // Load and render the glyph for the current character. HELIUM_VERIFY( FT_Load_Glyph( pFace, characterIndex, glyphLoadFlags ) == 0 ); FT_GlyphSlot pGlyph = pFace->glyph; HELIUM_ASSERT( pGlyph ); // Proceed to the next line in the texture sheet or the next sheet itself if we don't have enough room in the // current line/sheet. HELIUM_ASSERT( pGlyph->bitmap.rows >= 0 ); HELIUM_ASSERT( pGlyph->bitmap.width >= 0 ); uint_fast32_t glyphRowCount = static_cast< uint32_t >( pGlyph->bitmap.rows ); uint_fast32_t glyphWidth = static_cast< uint32_t >( pGlyph->bitmap.width ); if( penX + glyphWidth + 1 >= textureSheetWidth ) { penX = 1; if( penY + glyphRowCount + 1 >= textureSheetHeight ) { CompressTexture( pTextureBuffer, textureSheetWidth, textureSheetHeight, textureCompression, textureSheets ); MemoryZero( pTextureBuffer, texturePixelCount ); penY = 1; } else { penY += lineHeight + 1; } lineHeight = 0; } // Copy the character data from the glyph bitmap to the texture sheet. int_fast32_t glyphPitch = pGlyph->bitmap.pitch; const uint8_t* pGlyphBuffer = pGlyph->bitmap.buffer; HELIUM_ASSERT( pGlyphBuffer || glyphRowCount == 0 ); uint8_t* pTexturePixel = pTextureBuffer + static_cast< size_t >( penY ) * static_cast< size_t >( textureSheetWidth ) + penX; if( bAntialiased ) { // Anti-aliased fonts are rendered as 8-bit grayscale images, so just copy the data as-is. for( uint_fast32_t rowIndex = 0; rowIndex < glyphRowCount; ++rowIndex ) { MemoryCopy( pTexturePixel, pGlyphBuffer, glyphWidth ); pGlyphBuffer += glyphPitch; pTexturePixel += textureSheetWidth; } } else { // Fonts without anti-aliasing are rendered as 1-bit monochrome images, so we need to manually convert each // row to 8-bit grayscale. for( uint_fast32_t rowIndex = 0; rowIndex < glyphRowCount; ++rowIndex ) { const uint8_t* pGlyphPixelBlock = pGlyphBuffer; pGlyphBuffer += glyphPitch; uint8_t* pCurrentTexturePixel = pTexturePixel; pTexturePixel += textureSheetWidth; uint_fast32_t remainingPixelCount = glyphWidth; while( remainingPixelCount >= 8 ) { remainingPixelCount -= 8; uint8_t pixelBlock = *pGlyphPixelBlock; ++pGlyphPixelBlock; *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 7 ) ) ? 255 : 0 ); *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 6 ) ) ? 255 : 0 ); *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 5 ) ) ? 255 : 0 ); *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 4 ) ) ? 255 : 0 ); *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 3 ) ) ? 255 : 0 ); *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 2 ) ) ? 255 : 0 ); *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 1 ) ) ? 255 : 0 ); *( pCurrentTexturePixel++ ) = ( ( pixelBlock & ( 1 << 0 ) ) ? 255 : 0 ); } uint8_t pixelBlock = *pGlyphPixelBlock; uint8_t mask = ( 1 << 7 ); while( remainingPixelCount != 0 ) { *( pCurrentTexturePixel++ ) = ( ( pixelBlock & mask ) ? 255 : 0 ); mask >>= 1; --remainingPixelCount; } } } // Store the character information in our character array. Font::Character* pCharacter = characters.New(); HELIUM_ASSERT( pCharacter ); pCharacter->codePoint = static_cast< uint32_t >( codePoint ); pCharacter->imageX = penX; pCharacter->imageY = penY; pCharacter->imageWidth = static_cast< uint16_t >( glyphWidth ); pCharacter->imageHeight = static_cast< uint16_t >( glyphRowCount ); pCharacter->width = pGlyph->metrics.width; pCharacter->height = pGlyph->metrics.height; pCharacter->bearingX = pGlyph->metrics.horiBearingX; pCharacter->bearingY = pGlyph->metrics.horiBearingY; pCharacter->advance = pGlyph->metrics.horiAdvance; HELIUM_ASSERT( textureSheets.GetSize() < UINT8_MAX ); pCharacter->texture = static_cast< uint8_t >( static_cast< uint8_t >( textureSheets.GetSize() ) ); // Update the pen location as well as the maximum line height as appropriate based on the current line height. penX += static_cast< uint16_t >( glyphWidth ) + 1; HELIUM_ASSERT( glyphRowCount <= UINT16_MAX ); lineHeight = Max< uint16_t >( lineHeight, static_cast< uint16_t >( glyphRowCount ) ); } // Compress and store the last texture in the sheet. if( !characters.IsEmpty() ) { CompressTexture( pTextureBuffer, textureSheetWidth, textureSheetHeight, textureCompression, textureSheets ); } // Done processing the font itself, so free some resources. delete [] pTextureBuffer; FT_Done_Face( pFace ); delete [] pFileData; // Cache the font data. size_t characterCountActual = characters.GetSize(); HELIUM_ASSERT( characterCountActual <= UINT32_MAX ); uint32_t characterCount = static_cast< uint32_t >( characterCountActual ); size_t textureCountActual = textureSheets.GetSize(); HELIUM_ASSERT( textureCountActual < UINT8_MAX ); uint8_t textureCount = static_cast< uint8_t >( textureCountActual ); BinarySerializer persistentDataSerializer; for( size_t platformIndex = 0; platformIndex < static_cast< size_t >( Cache::PLATFORM_MAX ); ++platformIndex ) { PlatformPreprocessor* pPreprocessor = pObjectPreprocessor->GetPlatformPreprocessor( static_cast< Cache::EPlatform >( platformIndex ) ); if( !pPreprocessor ) { continue; } persistentDataSerializer.SetByteSwapping( pPreprocessor->SwapBytes() ); persistentDataSerializer.BeginSerialize(); persistentDataSerializer << ascender; persistentDataSerializer << descender; persistentDataSerializer << height; persistentDataSerializer << maxAdvance; persistentDataSerializer << characterCount; persistentDataSerializer << textureCount; for( size_t characterIndex = 0; characterIndex < characterCountActual; ++characterIndex ) { characters[ characterIndex ].Serialize( persistentDataSerializer ); } persistentDataSerializer.EndSerialize(); Resource::PreprocessedData& rPreprocessedData = pResource->GetPreprocessedData( static_cast< Cache::EPlatform >( platformIndex ) ); rPreprocessedData.persistentDataBuffer = persistentDataSerializer.GetPropertyStreamBuffer(); rPreprocessedData.subDataBuffers = textureSheets; rPreprocessedData.bLoaded = true; } return true; }
/// @copydoc ResourceHandler::CacheResource() bool MeshResourceHandler::CacheResource( ObjectPreprocessor* pObjectPreprocessor, Resource* pResource, const String& rSourceFilePath ) { HELIUM_ASSERT( pObjectPreprocessor ); HELIUM_ASSERT( pResource ); // Load and parse the mesh data. DynArray< StaticMeshVertex< 1 > > vertices; DynArray< uint16_t > indices; DynArray< uint16_t > sectionVertexCounts; DynArray< uint32_t > sectionTriangleCounts; DynArray< FbxSupport::BoneData > bones; DynArray< FbxSupport::BlendData > vertexBlendData; DynArray< uint8_t > skinningPaletteMap; bool bLoadSuccess = m_rFbxSupport.LoadMesh( rSourceFilePath, vertices, indices, sectionVertexCounts, sectionTriangleCounts, bones, vertexBlendData, skinningPaletteMap ); if( !bLoadSuccess ) { HELIUM_TRACE( TRACE_ERROR, TXT( "MeshResourceHandler::CacheResource(): Failed to build mesh from source file \"%s\".\n" ), *rSourceFilePath ); return false; } size_t vertexCountActual = vertices.GetSize(); HELIUM_ASSERT( vertexCountActual <= UINT32_MAX ); uint32_t vertexCount = static_cast< uint32_t >( vertexCountActual ); size_t indexCount = indices.GetSize(); size_t triangleCountActual = indexCount; HELIUM_ASSERT( triangleCountActual % 3 == 0 ); triangleCountActual /= 3; HELIUM_ASSERT( triangleCountActual <= UINT32_MAX ); uint32_t triangleCount = static_cast< uint32_t >( triangleCountActual ); size_t boneCountActual = bones.GetSize(); HELIUM_ASSERT( boneCountActual <= UINT8_MAX ); #if !HELIUM_USE_GRANNY_ANIMATION uint8_t boneCount = static_cast< uint8_t >( boneCountActual ); #endif // Compute the mesh bounding box. Simd::AaBox bounds; if( vertexCountActual != 0 ) { const float32_t* pPosition = vertices[ 0 ].position; Simd::Vector3 position( pPosition[ 0 ], pPosition[ 1 ], pPosition[ 2 ] ); bounds.Set( position, position ); for( size_t vertexIndex = 1; vertexIndex < vertexCountActual; ++vertexIndex ) { pPosition = vertices[ vertexIndex ].position; bounds.Expand( Simd::Vector3( pPosition[ 0 ], pPosition[ 1 ], pPosition[ 2 ] ) ); } } #if HELIUM_USE_GRANNY_ANIMATION Granny::MeshCachingData grannyMeshCachingData; grannyMeshCachingData.BuildResourceData( bones ); #endif // HELIUM_USE_GRANNY_ANIMATION // Cache the data for each supported platform. BinarySerializer serializer; for( size_t platformIndex = 0; platformIndex < static_cast< size_t >( Cache::PLATFORM_MAX ); ++platformIndex ) { PlatformPreprocessor* pPreprocessor = pObjectPreprocessor->GetPlatformPreprocessor( static_cast< Cache::EPlatform >( platformIndex ) ); if( !pPreprocessor ) { continue; } Resource::PreprocessedData& rPreprocessedData = pResource->GetPreprocessedData( static_cast< Cache::EPlatform >( platformIndex ) ); DynArray< DynArray< uint8_t > >& rSubDataBuffers = rPreprocessedData.subDataBuffers; rSubDataBuffers.Reserve( 2 ); rSubDataBuffers.Resize( 2 ); rSubDataBuffers.Trim(); serializer.SetByteSwapping( pPreprocessor->SwapBytes() ); // Serialize the buffer sizes and mesh bounds first. serializer.BeginSerialize(); serializer << Serializer::WrapDynArray( sectionVertexCounts ); serializer << Serializer::WrapDynArray( sectionTriangleCounts ); serializer << Serializer::WrapDynArray( skinningPaletteMap ); serializer << vertexCount; serializer << triangleCount; serializer << bounds; #if HELIUM_USE_GRANNY_ANIMATION grannyMeshCachingData.CachePlatformResourceData( pPreprocessor, serializer ); #else serializer << boneCount; for( size_t boneIndex = 0; boneIndex < boneCount; ++boneIndex ) { FbxSupport::BoneData& rBoneData = bones[ boneIndex ]; serializer << rBoneData.name; serializer << rBoneData.parentIndex; serializer << rBoneData.referenceTransform; } #endif serializer.EndSerialize(); rPreprocessedData.persistentDataBuffer = serializer.GetPropertyStreamBuffer(); // Serialize the vertex buffer. If the mesh is a skinned mesh, the vertices will need to be converted to // and serialized as an array of SkinnedMeshVertex structs. serializer.BeginSerialize(); if( boneCountActual == 0 ) { for( size_t vertexIndex = 0; vertexIndex < vertexCountActual; ++vertexIndex ) { vertices[ vertexIndex ].Serialize( serializer ); } } else { HELIUM_ASSERT( vertexBlendData.GetSize() == vertexCountActual ); SkinnedMeshVertex vertex; for( size_t vertexIndex = 0; vertexIndex < vertexCountActual; ++vertexIndex ) { const StaticMeshVertex< 1 >& rStaticVertex = vertices[ vertexIndex ]; const FbxSupport::BlendData& rBlendData = vertexBlendData[ vertexIndex ]; MemoryCopy( vertex.position, rStaticVertex.position, sizeof( vertex.position ) ); vertex.blendWeights[ 0 ] = static_cast< uint8_t >( Clamp( rBlendData.weights[ 0 ] * 255.0f + 0.5f, 0.0f, 255.0f ) ); vertex.blendWeights[ 1 ] = static_cast< uint8_t >( Clamp( rBlendData.weights[ 1 ] * 255.0f + 0.5f, 0.0f, 255.0f ) ); vertex.blendWeights[ 2 ] = static_cast< uint8_t >( Clamp( rBlendData.weights[ 2 ] * 255.0f + 0.5f, 0.0f, 255.0f ) ); vertex.blendWeights[ 3 ] = static_cast< uint8_t >( Clamp( rBlendData.weights[ 3 ] * 255.0f + 0.5f, 0.0f, 255.0f ) ); // Tweak the blend weights to ensure they still add up to 255 (1.0 when normalized by the GPU). size_t blendWeightTotal = static_cast< size_t >( vertex.blendWeights[ 0 ] ) + static_cast< size_t >( vertex.blendWeights[ 1 ] ) + static_cast< size_t >( vertex.blendWeights[ 2 ] ) + static_cast< size_t >( vertex.blendWeights[ 3 ] ); if( blendWeightTotal != 0 && blendWeightTotal != 255 ) { if( blendWeightTotal > 255 ) { // Total blend weight is too large, so decrease blend weights, starting from the lowest // non-zero weight. size_t weightAdjustIndex = 0; do { do { weightAdjustIndex = ( weightAdjustIndex + 3 ) % 4; } while( vertex.blendWeights[ weightAdjustIndex ] == 0 ); --vertex.blendWeights[ weightAdjustIndex ]; --blendWeightTotal; } while( blendWeightTotal > 255 ); } else { // Total blend weight is too small, so increase blend weights, starting from the highest // non-zero blend weight. Note that we should not have to check whether the blend weight is // already at its max, as that would mean our total blend weight would have to already be at // least 255. size_t weightAdjustIndex = 3; do { do { weightAdjustIndex = ( weightAdjustIndex + 1 ) % 4; } while( vertex.blendWeights[ weightAdjustIndex ] == 0 ); HELIUM_ASSERT( vertex.blendWeights[ weightAdjustIndex ] != 255 ); ++vertex.blendWeights[ weightAdjustIndex ]; ++blendWeightTotal; } while( blendWeightTotal < 255 ); } HELIUM_ASSERT( blendWeightTotal == 255 ); } MemoryCopy( vertex.blendIndices, rBlendData.indices, sizeof( vertex.blendIndices ) ); MemoryCopy( vertex.normal, rStaticVertex.normal, sizeof( vertex.normal ) ); MemoryCopy( vertex.tangent, rStaticVertex.tangent, sizeof( vertex.tangent ) ); MemoryCopy( vertex.texCoords, rStaticVertex.texCoords[ 0 ], sizeof( vertex.texCoords ) ); vertex.Serialize( serializer ); } } serializer.EndSerialize(); rSubDataBuffers[ 0 ] = serializer.GetPropertyStreamBuffer(); // Serialize the index buffer. serializer.BeginSerialize(); for( size_t indexIndex = 0; indexIndex < indexCount; ++indexIndex ) { serializer << indices[ indexIndex ]; } serializer.EndSerialize(); rSubDataBuffers[ 1 ] = serializer.GetPropertyStreamBuffer(); // Platform data is now loaded. rPreprocessedData.bLoaded = true; } return true; }