Procházet zdrojové kódy

Font characters now get neatly packed using a texture atlas
Support for multiple sizes in a Font
Added all character description data to a font (untested)

Marko Pintera před 12 roky
rodič
revize
d5a54ec7b4

+ 12 - 1
CamelotClient/CamelotClient.cpp

@@ -19,6 +19,7 @@
 #include "CmMesh.h"
 #include "CmGpuProgInclude.h" // DEBUG ONLY
 #include "CmGpuProgramImportOptions.h"
+#include "CmFontImportOptions.h"
 
 #include "CmDebugCamera.h"
 
@@ -61,7 +62,17 @@ int CALLBACK WinMain(
 	RenderablePtr testRenderable = testModelGO->addComponent<Renderable>();
 
 	// Debug test fonts
-	FontHandle font = Importer::instance().import("C:\\arial.ttf");
+	ImportOptionsPtr fontImportOptions = Importer::instance().createImportOptions("C:\\arial.ttf");
+	if(rtti_is_of_type<FontImportOptions>(fontImportOptions))
+	{
+		FontImportOptions* importOptions = static_cast<FontImportOptions*>(fontImportOptions.get());
+
+		vector<CamelotEngine::UINT32>::type fontSizes;
+		fontSizes.push_back(12);
+		importOptions->setFontSizes(fontSizes);
+	}
+
+	FontHandle font = Importer::instance().import("C:\\arial.ttf", fontImportOptions);
 
 #if defined DX9
 	///////////////// HLSL 9 SHADERS //////////////////////////

+ 18 - 4
CamelotCore/Include/CmFont.h

@@ -6,6 +6,21 @@
 
 namespace CamelotEngine
 {
+	struct CM_EXPORT FontData : public IReflectable
+	{
+		UINT32 size;
+		FONT_DESC fontDesc;
+		vector<TexturePtr>::type texturePages;
+
+		/************************************************************************/
+		/* 								SERIALIZATION                      		*/
+		/************************************************************************/
+	public:
+		friend class FontDataRTTI;
+		static RTTITypeBase* getRTTIStatic();
+		virtual RTTITypeBase* getRTTI() const;
+	};
+
 	// TODO - When saved on disk font currently stores a copy of the texture pages. This should be acceptable
 	// if you import a new TrueType or OpenType font since the texture will be generated on the spot
 	// but if you use a bitmap texture to initialize the font manually, then you will potentially have duplicate textures.
@@ -16,7 +31,7 @@ namespace CamelotEngine
 	public:
 		virtual ~Font();
 
-		void initialize(const FONT_DESC& fontDesc, vector<TexturePtr>::type texturePages);
+		void initialize(vector<FontData>::type& fontData);
 
 	protected:
 		friend class FontManager;
@@ -24,8 +39,7 @@ namespace CamelotEngine
 		Font();
 
 	private:
-		vector<TexturePtr>::type mTexturePages;
-		FONT_DESC mFontDesc;
+		map<UINT32, FontData>::type mFontDataPerSize;
 
 		/************************************************************************/
 		/* 								SERIALIZATION                      		*/
@@ -40,6 +54,6 @@ namespace CamelotEngine
 		/************************************************************************/
 		
 	public:
-		static FontHandle create(const FONT_DESC& fontDesc, vector<TexturePtr>::type texturePages);
+		static FontHandle create(vector<FontData>::type& fontInitData);
 	};
 }

+ 1 - 2
CamelotCore/Include/CmFontDesc.h

@@ -6,8 +6,7 @@ namespace CamelotEngine
 {
 	struct KerningPair
 	{
-		UINT32 leftCharId;
-		UINT32 rightCharId;
+		UINT32 otherCharId;
 		INT32 amount;
 	};
 

+ 1 - 1
CamelotCore/Include/CmFontManager.h

@@ -8,7 +8,7 @@ namespace CamelotEngine
 	class CM_EXPORT FontManager : public Module<FontManager>
 	{
 	public:
-		FontPtr create(const FONT_DESC& fontDesc, vector<TexturePtr>::type texturePages) const;
+		FontPtr create(vector<FontData>::type& fontData) const;
 		FontPtr createEmpty() const;
 	};
 }

+ 84 - 22
CamelotCore/Include/CmFontRTTI.h

@@ -8,55 +8,117 @@
 
 namespace CamelotEngine
 {
-	class CM_EXPORT FontRTTI : public RTTIType<Font, Resource, FontRTTI>
+	class CM_EXPORT FontDataRTTI : public RTTIType<FontData, Resource, FontDataRTTI>
 	{
-		struct FontInitData
+	private:
+		UINT32& getSize(FontData* obj)
 		{
-			FONT_DESC desc;
-			vector<TexturePtr>::type texturePages;
-		};
+			return obj->size;
+		}
 
-	private:
-		FONT_DESC& getFontDesc(Font* obj)
+		void setSize(FontData* obj, UINT32& size)
 		{
-			return obj->mFontDesc;
+			obj->size = size;
 		}
 
-		void setFontDesc(Font* obj, FONT_DESC& val)
+		FONT_DESC& getFontDesc(FontData* obj)
 		{
-			FontInitData* initData = boost::any_cast<FontInitData*>(obj->mRTTIData);
-			initData->desc = val;
+			return obj->fontDesc;
+		}
+
+		void setFontDesc(FontData* obj, FONT_DESC& val)
+		{
+			obj->fontDesc = val;
+		}
+
+		TexturePtr getTexture(FontData* obj, UINT32 idx)
+		{
+			return obj->texturePages.at(idx);
 		}
 
-		TexturePtr getTexture(Font* obj, UINT32 idx)
+		void setTexture(FontData* obj, UINT32 idx, TexturePtr value)
 		{
-			return obj->mTexturePages.at(idx);
+			obj->texturePages[idx] = value;
+		}
+
+		UINT32 getTextureArraySize(FontData* obj)
+		{
+			return (UINT32)obj->texturePages.size();
+		}
+
+		void setTextureArraySize(FontData* obj, UINT32 size)
+		{
+			obj->texturePages.resize(size);
+		}
+
+	public:
+		FontDataRTTI()
+		{
+			addPlainField("size", 0, &FontDataRTTI::getSize, &FontDataRTTI::setSize);
+			addPlainField("fontDesc", 1, &FontDataRTTI::getFontDesc, &FontDataRTTI::setFontDesc);
+			addReflectablePtrArrayField("texturePages", 2, &FontDataRTTI::getTexture, &FontDataRTTI::getTextureArraySize, &FontDataRTTI::setTexture, &FontDataRTTI::setTextureArraySize);
+		}
+
+		virtual const String& getRTTIName()
+		{
+			static String name = "FontData";
+			return name;
+		}
+
+		virtual UINT32 getRTTIId()
+		{
+			return TID_FontData;
+		}
+
+		virtual std::shared_ptr<IReflectable> newRTTIObject()
+		{
+			return std::shared_ptr<FontData>(new FontData());
+		}
+	};
+
+	class CM_EXPORT FontRTTI : public RTTIType<Font, Resource, FontRTTI>
+	{
+		struct FontInitData
+		{
+			vector<FontData>::type fontDataPerSize;
+		};
+
+	private:
+		FontData& getFontData(Font* obj, UINT32 idx)
+		{
+			if(idx >= obj->mFontDataPerSize.size())
+				CM_EXCEPT(InternalErrorException, "Index out of range: " + toString(idx) + ". Valid range: 0 .. " + toString(obj->mFontDataPerSize.size()));
+
+			auto iter = obj->mFontDataPerSize.begin();
+			for(UINT32 i = 0; i < idx; i++, ++iter)
+			{ }
+
+			return iter->second;
 		}
 
-		void setTexture(Font* obj, UINT32 idx, TexturePtr value)
+		void setFontData(Font* obj, UINT32 idx, FontData& value)
 		{
 			FontInitData* initData = boost::any_cast<FontInitData*>(obj->mRTTIData);
 
-			initData->texturePages[idx] = value;
+			initData->fontDataPerSize[idx] = value;
 		}
 
-		UINT32 getTextureArraySize(Font* obj)
+		UINT32 getNumFontData(Font* obj)
 		{
-			return (UINT32)obj->mTexturePages.size();
+			return (UINT32)obj->mFontDataPerSize.size();
 		}
 
-		void setTextureArraySize(Font* obj, UINT32 size)
+		void setNumFontData(Font* obj, UINT32 size)
 		{
 			FontInitData* initData = boost::any_cast<FontInitData*>(obj->mRTTIData);
 
-			initData->texturePages.resize(size);
+			initData->fontDataPerSize.resize(size);
 		}
 
 	public:
 		FontRTTI()
 		{
-			addPlainField("mFontDesc", 0, &FontRTTI::getFontDesc, &FontRTTI::setFontDesc);
-			addReflectablePtrArrayField("mTexturePages", 1, &FontRTTI::getTexture, &FontRTTI::getTextureArraySize, &FontRTTI::setTexture, &FontRTTI::setTextureArraySize);
+			addReflectableArrayField("mFontDataPerSize", 0, &FontRTTI::getFontData, &FontRTTI::getNumFontData, &FontRTTI::setFontData, &FontRTTI::setNumFontData);
 		}
 
 		virtual const String& getRTTIName()
@@ -89,7 +151,7 @@ namespace CamelotEngine
 			Font* font = static_cast<Font*>(obj);
 			FontInitData* initData = boost::any_cast<FontInitData*>(font->mRTTIData);
 
-			font->initialize(initData->desc, initData->texturePages);
+			font->initialize(initData->fontDataPerSize);
 
 			delete initData;
 		}

+ 3 - 1
CamelotCore/Include/CmPrerequisites.h

@@ -146,6 +146,7 @@ namespace CamelotEngine {
 	class TextureView;
 	class CoreObject;
 	class ImportOptions;
+	struct FontData;
 	// Asset import
 	class SpecificImporter;
 	class Importer;
@@ -270,7 +271,8 @@ namespace CamelotEngine
 		TID_CHAR_DESC = 1053,
 		TID_STDVECTOR = 1054,
 		TID_STDMAP = 1055,
-		TID_FontImportOptions = 1056
+		TID_FontImportOptions = 1056,
+		TID_FontData = 1057
 	};
 
 	/**

+ 16 - 10
CamelotCore/Source/CmFont.cpp

@@ -4,28 +4,34 @@
 
 namespace CamelotEngine
 {
-	Font::Font()
-		:Resource(false)
+	RTTITypeBase* FontData::getRTTIStatic()
 	{
-
+		return FontDataRTTI::instance();
 	}
 
-	Font::~Font()
+	RTTITypeBase* FontData::getRTTI() const
 	{
-
+		return FontData::getRTTIStatic();
 	}
 
-	void Font::initialize(const FONT_DESC& fontDesc, vector<TexturePtr>::type texturePages)
+	Font::Font()
+		:Resource(false)
+	{ }
+
+	Font::~Font()
+	{ }
+
+	void Font::initialize(vector<FontData>::type& fontData)
 	{
-		mFontDesc = fontDesc;
-		mTexturePages = texturePages;
+		for(auto iter = fontData.begin(); iter != fontData.end(); ++iter)
+			mFontDataPerSize[iter->size] = *iter;
 
 		Resource::initialize();
 	}
 
-	FontHandle Font::create(const FONT_DESC& fontDesc, vector<TexturePtr>::type texturePages)
+	FontHandle Font::create(vector<FontData>::type& fontData)
 	{
-		FontPtr newFont = FontManager::instance().create(fontDesc, texturePages);
+		FontPtr newFont = FontManager::instance().create(fontData);
 
 		return Resource::_createResourceHandle(newFont);
 	}

+ 2 - 2
CamelotCore/Source/CmFontManager.cpp

@@ -3,11 +3,11 @@
 
 namespace CamelotEngine
 {
-	FontPtr FontManager::create(const FONT_DESC& fontDesc, vector<TexturePtr>::type texturePages) const
+	FontPtr FontManager::create(vector<FontData>::type& fontData) const
 	{
 		FontPtr newFont(new Font(), &CoreObject::_deleteDelayed);
 		newFont->setThisPtr(newFont);
-		newFont->initialize(fontDesc, texturePages);
+		newFont->initialize(fontData);
 
 		return newFont;
 	}

+ 2 - 0
CamelotFontImporter/Include/CmFontImporter.h

@@ -40,5 +40,7 @@ namespace CamelotEngine
 		virtual ImportOptionsPtr createImportOptions() const;
 	private:
 		vector<String>::type mExtensions;
+
+		const static int MAXIMUM_TEXTURE_SIZE = 2048;
 	};
 }

+ 121 - 73
CamelotFontImporter/Source/CmFontImporter.cpp

@@ -4,6 +4,7 @@
 #include "CmTexture.h"
 #include "CmResources.h"
 #include "CmDebug.h"
+#include "CmTexAtlasGenerator.h"
 
 #include <ft2build.h>
 #include <freetype/freetype.h>
@@ -67,107 +68,154 @@ namespace CamelotEngine
 		vector<std::pair<UINT32, UINT32>>::type charIndexRanges = gpuProgImportOptions->getCharIndexRanges();
 		vector<UINT32>::type fontSizes = gpuProgImportOptions->getFontSizes();
 		UINT32 dpi = gpuProgImportOptions->getDPI();
+
+		vector<FontData>::type dataPerSize;
 		for(size_t i = 0; i < fontSizes.size(); i++)
 		{
 			FT_F26Dot6 ftSize = (FT_F26Dot6)(fontSizes[i] * (1 << 6));
 			if(FT_Set_Char_Size( face, ftSize, 0, dpi, dpi))
 				CM_EXCEPT(InternalErrorException, "Could not set character size." );
 
-			
-		}
-
-		/************************************************************************/
-		/* 									DEBUG				         		*/
-		/************************************************************************/
-		
-		if(FT_Set_Char_Size(face, 12 * (1 << 6), 0, 96, 96))
-			CM_EXCEPT(InternalErrorException, "Could not set character size." );
+			FontData fontData;
 
-		INT32 maxWidth = 0;
-		INT32 maxHeight = 0;
-		for(UINT32 i = 33; i < 166; i++)
-		{
-			error = FT_Load_Char(face, i, FT_LOAD_RENDER);
-
-			if(error)
+			// Get all char sizes so we can generate texture layout
+			vector<TexAtlasElementDesc>::type atlasElements;
+			map<UINT32, UINT32>::type seqIdxToCharIdx;
+			for(auto iter = charIndexRanges.begin(); iter != charIndexRanges.end(); ++iter)
 			{
-				CM_EXCEPT(InternalErrorException, "Failed to load a character");
-			}
-
-			FT_GlyphSlot slot = face->glyph;
-
-			if(slot->bitmap.width > maxWidth)
-				maxWidth = slot->bitmap.width;
+				for(UINT32 charIdx = iter->first; charIdx <= iter->second; charIdx++)
+				{
+					error = FT_Load_Char(face, (FT_ULong)charIdx, FT_LOAD_RENDER);
 
-			if(slot->bitmap.rows > maxHeight)
-				maxHeight = slot->bitmap.rows;
-		}
+					if(error)
+						CM_EXCEPT(InternalErrorException, "Failed to load a character");
 
-		UINT32 texWidth = 1024;
-		UINT32 texHeight = 1024;
+					FT_GlyphSlot slot = face->glyph;
 
-		UINT8* pixelBuffer = new UINT8[texWidth * texHeight * 2];
-		memset(pixelBuffer, 0, texWidth * texHeight * 2);
-		PixelData pixelData(texWidth, texHeight, 1, PF_R8G8, pixelBuffer, true);
+					TexAtlasElementDesc atlasElement;
+					atlasElement.input.width = slot->bitmap.width;
+					atlasElement.input.height = slot->bitmap.rows;
 
-		UINT32 curX = 0;
-		UINT32 curY = 0;
-		for(UINT32 i = 33; i < 166; i++)
-		{
-			if((curX + maxWidth) >= texWidth)
-			{
-				curX = 0;
-				curY += maxHeight;
-
-				if((curY + maxHeight) >= texHeight)
-					CM_EXCEPT(InternalErrorException, "Texture to small to fit all glyphs.");
+					atlasElements.push_back(atlasElement);
+					seqIdxToCharIdx[(UINT32)atlasElements.size() - 1] = charIdx;
+				}
 			}
 
-			error = FT_Load_Char(face, i, FT_LOAD_RENDER);
-			if(error)
+			// Create an optimal layout for character bitmaps
+			TexAtlasGenerator texAtlasGen(false, MAXIMUM_TEXTURE_SIZE, MAXIMUM_TEXTURE_SIZE);
+			vector<TexAtlasPageDesc>::type pages = texAtlasGen.createAtlasLayout(atlasElements);
+
+			// Create char bitmap atlas textures and load character information
+			UINT32 pageIdx = 0;
+			for(auto pageIter = pages.begin(); pageIter != pages.end(); ++pageIter)
 			{
-				CM_EXCEPT(InternalErrorException, "Failed to load a character");
-			}
+				UINT32 bufferSize = pageIter->width * pageIter->height * 2;
 
-			FT_GlyphSlot slot = face->glyph;
+				UINT8* pixelBuffer = new UINT8[bufferSize];
+				memset(pixelBuffer, 0, bufferSize);
 
-			if(slot->bitmap.buffer == nullptr && slot->bitmap.rows > 0 && slot->bitmap.width > 0)
-			{
-				CM_EXCEPT(InternalErrorException, "Failed to render glyph bitmap");
-			}
+				PixelData pixelData(pageIter->width, pageIter->height, 1, PF_R8G8, pixelBuffer, true);
 
-			UINT8* sourceBuffer = slot->bitmap.buffer;
-			UINT8* dstBuffer = pixelBuffer + (curY * texWidth * 2) + curX * 2;
-			for(INT32 bitmapRow = 0; bitmapRow < slot->bitmap.rows; bitmapRow++)
-			{
-				for(INT32 bitmapColumn = 0; bitmapColumn < slot->bitmap.width; bitmapColumn++)
+				for(size_t elementIdx = 0; elementIdx < atlasElements.size(); elementIdx++)
 				{
-					dstBuffer[bitmapColumn * 2 + 0] = sourceBuffer[bitmapColumn];
-					dstBuffer[bitmapColumn * 2 + 1] = sourceBuffer[bitmapColumn];
+					// Copy character bitmap
+					if(atlasElements[elementIdx].output.page != pageIdx)
+						continue;
+
+					TexAtlasElementDesc curElement = atlasElements[elementIdx];
+
+					UINT32 charIdx = seqIdxToCharIdx[(UINT32)elementIdx];
+					
+					error = FT_Load_Char(face, charIdx, FT_LOAD_RENDER);
+					if(error)
+						CM_EXCEPT(InternalErrorException, "Failed to load a character");
+
+					FT_GlyphSlot slot = face->glyph;
+
+					if(slot->bitmap.buffer == nullptr && slot->bitmap.rows > 0 && slot->bitmap.width > 0)
+						CM_EXCEPT(InternalErrorException, "Failed to render glyph bitmap");
+
+					UINT8* sourceBuffer = slot->bitmap.buffer;
+					UINT8* dstBuffer = pixelBuffer + (curElement.output.y * pageIter->width * 2) + curElement.output.x * 2;
+					for(INT32 bitmapRow = 0; bitmapRow < slot->bitmap.rows; bitmapRow++)
+					{
+						for(INT32 bitmapColumn = 0; bitmapColumn < slot->bitmap.width; bitmapColumn++)
+						{
+							dstBuffer[bitmapColumn * 2 + 0] = sourceBuffer[bitmapColumn];
+							dstBuffer[bitmapColumn * 2 + 1] = sourceBuffer[bitmapColumn];
+						}
+
+						dstBuffer += pageIter->width * 2;
+						sourceBuffer += slot->bitmap.pitch;
+					}
+
+					// Store character information
+					CHAR_DESC charDesc;
+
+					float invTexWidth = 1.0f / pageIter->width;
+					float invTexHeight = 1.0f / pageIter->height;
+
+					charDesc.charId = charIdx;
+					charDesc.width = curElement.input.width;
+					charDesc.height = curElement.input.height;
+					charDesc.page = curElement.output.page;
+					charDesc.uvWidth = invTexWidth * curElement.input.width;
+					charDesc.uvHeight = invTexHeight * curElement.input.height;
+					charDesc.uvX = invTexWidth * curElement.output.x;
+					charDesc.uvY = invTexHeight * curElement.output.y;
+					charDesc.xOffset = slot->bitmap_left;
+					charDesc.yOffset = slot->bitmap_top;
+					charDesc.xAdvance = slot->advance.x >> 6;
+					charDesc.yAdvance = slot->advance.y >> 6;
+
+					// Load kerning
+					FT_Vector resultKerning;
+					for(auto kerningIter = charIndexRanges.begin(); kerningIter != charIndexRanges.end(); ++kerningIter)
+					{
+						for(UINT32 kerningCharIdx = kerningIter->first; kerningCharIdx <= kerningIter->second; kerningCharIdx++)
+						{
+							if(kerningCharIdx == charIdx)
+								continue;
+
+							error = FT_Get_Kerning(face, charIdx, kerningCharIdx, FT_KERNING_DEFAULT, &resultKerning);
+
+							if(error)
+								CM_EXCEPT(InternalErrorException, "Failed to get kerning information for character: " + toString(charIdx));
+
+							INT32 kerningX = (INT32)(resultKerning.x >> 6); // Y kerning is ignored because it is so rare
+							if(kerningX == 0) // We don't store 0 kerning, this is assumed default
+								continue;
+
+							KerningPair pair;
+							pair.amount = kerningX;
+							pair.otherCharId = kerningCharIdx;
+
+							charDesc.kerningPairs.push_back(pair);
+						}
+					}
+
+					fontData.fontDesc.characters[charIdx] = charDesc;
 				}
 
-				dstBuffer += texWidth * 2;
-				sourceBuffer += slot->bitmap.pitch;
-			}
+				TextureHandle newTex = Texture::create(TEX_TYPE_2D, pageIter->width, pageIter->height, 0, PF_R8G8);
+				newTex->waitUntilInitialized();
+				newTex->setRawPixels(pixelData);
 
-			curX += maxWidth;
-		}
+				gDebug().writeAsBMP(pixelData, "C:\\FontTex.bmp");
 
-		gDebug().writeAsBMP(pixelData, "C:\\FontTex.bmp");
+				fontData.texturePages.push_back(newTex.getInternalPtr());
 
-		//TextureHandle newTex = Texture::create(TEX_TYPE_2D, texWidth, texHeight, 0, PF_R8G8);
-		//newTex->waitUntilInitialized();
-		//newTex->setRawPixels(pixelData);
+				pageIdx++;
+			}
 
-		//Resources::instance().create(newTex, "C:\\FontTex.tex", true);
+			fontData.size = fontSizes[i];
+			dataPerSize.push_back(fontData);
+		}
 
-		/************************************************************************/
-		/* 								END DEBUG	                     		*/
-		/************************************************************************/
+		FontHandle newFont = Font::create(dataPerSize);
 
-		
 		FT_Done_FreeType(library);
 
-		return BaseResourceHandle();
+		return newFont;
 	}
 }

+ 2 - 0
CamelotUtility/CamelotUtility.vcxproj

@@ -164,6 +164,7 @@
     <ClCompile Include="Source\CmInt2.cpp" />
     <ClCompile Include="Source\CmManagedDataBlock.cpp" />
     <ClCompile Include="Source\CmPixelData.cpp" />
+    <ClCompile Include="Source\CmTexAtlasGenerator.cpp" />
     <ClCompile Include="Source\CmUUID.cpp" />
     <ClCompile Include="Source\CmWorkQueue.cpp" />
     <ClCompile Include="Source\Win32\CmTimer.cpp" />
@@ -230,6 +231,7 @@
     <ClCompile Include="Source\CmRTTIField.cpp" />
     <ClCompile Include="Source\CmRTTIType.cpp" />
     <ClInclude Include="Include\CmWorkQueue.h" />
+    <ClInclude Include="Include\CmTexAtlasGenerator.h" />
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="Include\CmAxisAlignedBox.cpp" />

+ 6 - 0
CamelotUtility/CamelotUtility.vcxproj.filters

@@ -219,6 +219,9 @@
     <ClInclude Include="Include\CmBitmapWriter.h">
       <Filter>Header Files\Debug</Filter>
     </ClInclude>
+    <ClInclude Include="Include\CmTexAtlasGenerator.h">
+      <Filter>Header Files</Filter>
+    </ClInclude>
   </ItemGroup>
   <ItemGroup>
     <ClCompile Include="Include\CmAxisAlignedBox.cpp">
@@ -317,5 +320,8 @@
     <ClCompile Include="Source\CmBitmapWriter.cpp">
       <Filter>Source Files\Debug</Filter>
     </ClCompile>
+    <ClCompile Include="Source\CmTexAtlasGenerator.cpp">
+      <Filter>Source Files</Filter>
+    </ClCompile>
   </ItemGroup>
 </Project>

+ 4 - 0
CamelotUtility/Include/CmRect.h

@@ -13,6 +13,10 @@ namespace CamelotEngine
 	class Rect
 	{
 	public:
+		Rect()
+			:x(0), y(0), width(0), height(0)
+		{ }
+
 		Rect(int _x, int _y, int _width, int _height)
 			:x(_x), y(_y), width(_width), height(_height)
 		{ }

+ 83 - 0
CamelotUtility/Include/CmTexAtlasGenerator.h

@@ -0,0 +1,83 @@
+#pragma once
+
+#include "CmPrerequisitesUtil.h"
+#include "CmVector2.h"
+
+namespace CamelotEngine
+{
+	/**
+	 * @brief	Represents a single element used as in input to
+	 * 			TexAtlasGenerator. Usually represents a single texture.
+	 * 			
+	 * @note	input is required to be filled in before passing it to
+	 * 			TexAtlasGenerator
+	 * 			output will be filled in by TexAtlasGenerator after a call to
+	 * 			TexAtlasGenerator::createAtlasLayout
+	 */
+	struct TexAtlasElementDesc
+	{
+		struct
+		{
+			UINT32 width, height;
+		} input;
+		
+		struct
+		{
+			UINT32 x, y;
+			INT32 page;
+		} output;
+	};
+
+	/**
+	 * @brief	A single page of the texture atlas.
+	 */
+	struct TexAtlasPageDesc
+	{
+		UINT32 width, height;
+	};
+
+	class TexAtlasNode;
+
+	class CM_UTILITY_EXPORT TexAtlasGenerator
+	{
+	public:
+		/**
+		 * @brief	Constructor.
+		 *
+		 * @param	square			(optional) Should the returned texture always be square. (width == height)
+		 * 							This option is only used if "fixedSize" parameter is set to false.
+		 * @param	maxTexWidth 	(optional) Maximum width of the texture. 
+		 * @param	maxTexHeight	(optional) Maximum height of the texture. 
+		 * @param	fixedSize   	(optional) If this field is false, algorithm will try to reduce the size of the texture
+		 * 							if possible. If it is true, the algorithm will always produce textures of the specified
+		 * 							"maxTexWidth", "maxTexHeight" size.
+		 */
+		TexAtlasGenerator(bool square = false, UINT32 maxTexWidth = 2048, UINT32 maxTexHeight = 2048, bool fixedSize = false);
+
+		/**
+		 * @brief	Creates an optimal texture layout by packing texture elements in order to end up with
+		 * 			as little empty space as possible.
+		 *
+		 * @param	elements	Elements to process. They need to have their "input" structures filled in,
+		 * 						and this method will fill "output" when it returns.
+		 * 						
+		 * @note	Algorithm will split elements over multiple textures if they don't fit
+		 * 						in a single texture (Determined by maximum texture size)
+		 *
+		 * @return	One or more descriptors that determine the size of the final atlas textures. Texture elements
+		 * 			will reference these pages with their "output.page" parameter.
+		 */
+		vector<TexAtlasPageDesc>::type createAtlasLayout(vector<TexAtlasElementDesc>::type& elements) const;
+
+	private:
+		bool mSquare;
+		bool mFixedSize;
+		UINT32 mMaxTexWidth;
+		UINT32 mMaxTexHeight;
+
+		int generatePagesForSize(vector<TexAtlasElementDesc>::type& elements, UINT32 width, UINT32 height, UINT32 startPage = 0) const;
+		int addLargestTextureWithoutPageThatFits(vector<TexAtlasElementDesc>::type& elements, TexAtlasNode& node) const;
+		int findLargestTextureWithoutPage(const vector<TexAtlasElementDesc>::type& elements) const;
+		void sortBySize(vector<TexAtlasElementDesc>::type& elements) const;
+	};
+}

+ 309 - 0
CamelotUtility/Source/CmTexAtlasGenerator.cpp

@@ -0,0 +1,309 @@
+#include "CmTexAtlasGenerator.h"
+#include "CmDebug.h"
+
+namespace CamelotEngine
+{
+	class TexAtlasNode
+	{
+	public:
+		TexAtlasNode()
+			:x(0), y(0), width(0), height(0), children(nullptr), nodeFull(false)
+		{ }
+
+		TexAtlasNode(UINT32 _x, UINT32 _y, UINT32 _width, UINT32 _height)
+			:x(_x), y(_y), width(_width), height(_height), children(nullptr), nodeFull(false)
+		{ }
+
+		~TexAtlasNode()
+		{
+			if(children != nullptr)
+				delete[] children;
+		}
+
+		UINT32 x, y, width, height;
+		TexAtlasNode* children;
+		bool nodeFull;
+
+		bool insert(TexAtlasElementDesc& element)
+		{
+			float aspect = width / (float)height;
+
+			return insert(element, aspect);
+		}
+
+		bool insert(TexAtlasElementDesc& element, float aspect)
+		{
+			if (children != nullptr)
+			{
+				if (children[0].insert(element))
+					return true;
+
+				return children[1].insert(element);
+			}
+			else
+			{
+				if(nodeFull)
+					return false;
+
+				if (element.input.width > width || element.input.height > height)
+					return false;
+
+				if (element.input.width == width && element.input.height == height)
+				{
+					element.output.x = x;
+					element.output.y = y;
+					nodeFull = true;
+
+					return true;
+				}
+
+				float dw = (float)(width - element.input.width);
+				float dh = (height - element.input.height) * aspect;
+
+				children = new TexAtlasNode[2];
+
+				if (dw > dh)
+				{
+					children[0].x = x;
+					children[0].y = y;
+					children[0].width = element.input.width;
+					children[0].height = height;
+
+					children[1].x = x + element.input.width;
+					children[1].y = y;
+					children[1].width = width - element.input.width;
+					children[1].height = height;
+				}
+				else
+				{
+					children[0].x = x;
+					children[0].y = y;
+					children[0].width = width;
+					children[0].height = element.input.height;
+
+					children[1].x = x;
+					children[1].y = y + element.input.height;
+					children[1].width = width;
+					children[1].height = height - element.input.height;
+				}
+
+				return children[0].insert(element);
+			}
+		}
+	};
+
+	TexAtlasGenerator::TexAtlasGenerator(bool square, UINT32 maxTexWidth, UINT32 maxTexHeight, bool fixedSize)
+		:mSquare(square), mMaxTexWidth(maxTexWidth), mMaxTexHeight(maxTexHeight), mFixedSize(fixedSize)
+	{
+		if(square)
+		{
+			if(maxTexWidth > maxTexHeight)
+				maxTexWidth = maxTexHeight;
+
+			if(maxTexHeight > maxTexWidth)
+				maxTexHeight = maxTexWidth;
+		}
+	}
+
+	vector<TexAtlasPageDesc>::type TexAtlasGenerator::createAtlasLayout(vector<TexAtlasElementDesc>::type& elements) const
+	{
+		for(size_t i = 0; i < elements.size(); i++)
+			elements[i].output.page = -1;
+
+		//sortBySize(elements);
+		int numPages = generatePagesForSize(elements, mMaxTexWidth, mMaxTexHeight);
+
+		if(numPages == -1)
+		{
+			LOGWRN("Some of the provided elements don't fit in an atlas of provided size. Returning empty array of pages.");
+			return vector<TexAtlasPageDesc>::type();
+		}
+
+		if(numPages == 0)
+			return vector<TexAtlasPageDesc>::type();
+
+		UINT32 lastPageWidth = mMaxTexWidth;
+		UINT32 lastPageHeight = mMaxTexHeight;
+		UINT32 lastPageIdx = numPages - 1;
+
+		// If size isn't fixed, try to reduce the size of the last page
+		if(!mFixedSize)
+		{
+			while (true)
+			{
+				if (lastPageWidth <= 1 || lastPageHeight <= 1)
+					break;
+
+				int newLastPageWidth = lastPageWidth;
+				int newLastPageHeight = lastPageHeight;
+
+				if(mSquare)
+				{
+					if (newLastPageWidth > newLastPageHeight)
+						newLastPageWidth /= 2;
+					else
+						newLastPageHeight /= 2;
+				}
+				else
+				{
+					if (newLastPageWidth > newLastPageHeight)
+						newLastPageWidth /= 2;
+					else
+						newLastPageHeight /= 2;
+				}
+
+				// Clear page indexes so we know which pages to process
+				for(size_t i = 0; i < elements.size(); i++)
+				{
+					if(elements[i].output.page == lastPageIdx)
+						elements[i].output.page = -1;
+				}
+
+				if(generatePagesForSize(elements, newLastPageWidth, newLastPageHeight, lastPageIdx) == 1)
+				{
+					lastPageWidth = newLastPageWidth;
+					lastPageHeight = newLastPageHeight;
+				}
+				else
+					break;
+			}
+		}
+
+		// Create page descriptors and return
+		vector<TexAtlasPageDesc>::type pages;
+		for(int i = 0; i < numPages - 1; i++)
+		{
+			TexAtlasPageDesc pageDesc;
+			pageDesc.width = mMaxTexWidth;
+			pageDesc.height = mMaxTexHeight;
+
+			pages.push_back(pageDesc);
+		}
+
+		TexAtlasPageDesc lastPageDesc;
+		lastPageDesc.width = lastPageWidth;
+		lastPageDesc.height = lastPageHeight;
+
+		pages.push_back(lastPageDesc);
+
+		return pages;
+	}
+
+	int TexAtlasGenerator::generatePagesForSize(vector<TexAtlasElementDesc>::type& elements, UINT32 width, UINT32 height, UINT32 startPage) const
+	{
+		if(elements.size() == 0)
+			return 0;
+
+		int currentPage = startPage;
+		int numPages = 0;
+		while (true)
+		{
+			int largestTexId = findLargestTextureWithoutPage(elements); // Start with the largest available texture
+
+			if (largestTexId == -1) // No more textures, we're done
+				return numPages;
+
+			TexAtlasElementDesc& currentElem = elements[largestTexId];
+
+			// If the texture is larger than the atlas size then it can never fit
+			while (width < currentElem.input.width || height < currentElem.input.height)
+				return -1;
+
+			TexAtlasNode atlasNode(0, 0, width, height);
+			atlasNode.insert(elements[largestTexId]);
+			elements[largestTexId].output.page = currentPage;
+
+			// Now that the first (largest) texture has been added, do the same for every other texture until atlas is full
+			while (true)
+			{
+				int addedTextureId = addLargestTextureWithoutPageThatFits(elements, atlasNode); // Try to add next largest texture
+				if (addedTextureId == -1)
+					break;
+
+				elements[addedTextureId].output.page = currentPage;
+			}
+
+			currentPage++;
+			numPages++;
+		}
+	}
+
+	int TexAtlasGenerator::addLargestTextureWithoutPageThatFits(vector<TexAtlasElementDesc>::type& elements, TexAtlasNode& node) const
+	{
+		UINT32 sizeLimit = std::numeric_limits<UINT32>::max();
+		while (true)
+		{
+			UINT32 largestSize = 0;
+			INT32 largestId = -1;
+
+			for (size_t i = 0; i < elements.size(); i++)
+			{
+				if (elements[i].output.page == -1) // Signifies that we haven't processed this node yet
+				{
+					UINT32 size = elements[i].input.width * elements[i].input.height;
+					if (size > largestSize && size < sizeLimit)
+					{
+						largestSize = size;
+						largestId = (INT32)i;
+					}
+				}
+			}
+
+			if (largestId == -1)
+				return -1;
+
+			if (node.insert(elements[largestId]))
+				return largestId;
+			else
+				sizeLimit = largestSize;
+		}
+	}
+
+	int TexAtlasGenerator::findLargestTextureWithoutPage(const vector<TexAtlasElementDesc>::type& elements) const
+	{
+		INT32 largestId = -1;
+		UINT32 largestSize = 0;
+
+		for (size_t i = 0; i < elements.size(); i++)
+		{
+			if (elements[i].output.page == -1) // Signifies that we haven't processed this node yet
+			{
+				UINT32 size = elements[i].input.width * elements[i].input.height;
+				if (size > largestSize)
+				{
+					largestSize = size;
+					largestId = (INT32)i;
+				}
+			}
+		}
+
+		return largestId;
+	}
+
+	void TexAtlasGenerator::sortBySize(vector<TexAtlasElementDesc>::type& elements) const
+	{
+		for (size_t i = 0; i < elements.size(); i++)
+		{
+			INT32 largestId = -1;
+			UINT32 largestSize = 0;
+
+			for (size_t j = i; j < elements.size(); j++)
+			{
+				UINT32 size = elements[j].input.width * elements[j].input.height;
+
+				if (size > largestSize)
+				{
+					largestSize = size;
+					largestId = (INT32)j;
+				}
+			}
+
+			if(largestId != -1)
+			{
+				TexAtlasElementDesc temp = elements[i];
+				elements[i] = elements[largestId];
+				elements[largestId] = temp;
+			}
+		}
+	}
+}