From b6c5668232624bd1cd144ea5f5ec3ab9c9c11c0e Mon Sep 17 00:00:00 2001 From: Lynix Date: Wed, 7 Jan 2015 23:46:26 +0100 Subject: [PATCH] Separated atlases from Font class First use of shared_ptr, yay! Former-commit-id: 4c22341fba5674746d5299cf0e8dbf6dc31d5362 --- .../Graphics/GuillotineTextureAtlas.hpp | 24 ++ include/Nazara/Utility/AbstractFontAtlas.hpp | 40 +++ include/Nazara/Utility/Font.hpp | 77 +++-- .../Nazara/Utility/GuillotineImageAtlas.hpp | 68 +++++ .../Graphics/GuillotineTextureAtlas.cpp | 57 ++++ src/Nazara/Utility/Font.cpp | 265 ++++-------------- src/Nazara/Utility/GuillotineImageAtlas.cpp | 244 ++++++++++++++++ 7 files changed, 522 insertions(+), 253 deletions(-) create mode 100644 include/Nazara/Graphics/GuillotineTextureAtlas.hpp create mode 100644 include/Nazara/Utility/AbstractFontAtlas.hpp create mode 100644 include/Nazara/Utility/GuillotineImageAtlas.hpp create mode 100644 src/Nazara/Graphics/GuillotineTextureAtlas.cpp create mode 100644 src/Nazara/Utility/GuillotineImageAtlas.cpp diff --git a/include/Nazara/Graphics/GuillotineTextureAtlas.hpp b/include/Nazara/Graphics/GuillotineTextureAtlas.hpp new file mode 100644 index 000000000..3eb14dd68 --- /dev/null +++ b/include/Nazara/Graphics/GuillotineTextureAtlas.hpp @@ -0,0 +1,24 @@ +// Copyright (C) 2015 Jérôme Leclercq +// This file is part of the "Nazara Engine - Graphics module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_GUILLOTINETEXTUREATLAS_HPP +#define NAZARA_GUILLOTINETEXTUREATLAS_HPP + +#include +#include + +class NAZARA_API NzGuillotineTextureAtlas : public NzGuillotineImageAtlas +{ + public: + NzGuillotineTextureAtlas() = default; + ~NzGuillotineTextureAtlas() = default; + + private: + unsigned int GetMaxAtlasSize() const override; + bool ResizeImage(Layer& layer, const NzVector2ui& size) const override; +}; + +#endif // NAZARA_GUILLOTINETEXTUREATLAS_HPP diff --git a/include/Nazara/Utility/AbstractFontAtlas.hpp b/include/Nazara/Utility/AbstractFontAtlas.hpp new file mode 100644 index 000000000..f3f06346a --- /dev/null +++ b/include/Nazara/Utility/AbstractFontAtlas.hpp @@ -0,0 +1,40 @@ +// Copyright (C) 2015 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_ABSTRACTFONTATLAS_HPP +#define NAZARA_ABSTRACTFONTATLAS_HPP + +#include +#include +#include +#include + +class NzAbstractImage; +class NzFont; +class NzImage; + +class NAZARA_API NzAbstractFontAtlas +{ + public: + NzAbstractFontAtlas() = default; + virtual ~NzAbstractFontAtlas(); + + virtual void Clear() = 0; + virtual void Free(NzSparsePtr rects, NzSparsePtr layers, unsigned int count) = 0; + virtual NzAbstractImage* GetLayer(unsigned int layerIndex) const = 0; + virtual unsigned int GetLayerCount() const = 0; + virtual bool Insert(const NzImage& image, NzRectui* rect, bool* flipped, unsigned int* layerIndex) = 0; + void RegisterFont(NzFont* font); + void UnregisterFont(NzFont* font); + + protected: + void NotifyCleared(); + + private: + std::set m_registredFonts; +}; + +#endif // NAZARA_ABSTRACTFONTATLAS_HPP diff --git a/include/Nazara/Utility/Font.hpp b/include/Nazara/Utility/Font.hpp index 190d3c716..2264b7604 100644 --- a/include/Nazara/Utility/Font.hpp +++ b/include/Nazara/Utility/Font.hpp @@ -8,11 +8,10 @@ #define NAZARA_FONT_HPP #include -#include #include #include #include -#include +#include #include #include @@ -24,7 +23,7 @@ struct NAZARA_API NzFontParams class NzFont; class NzFontData; -struct NzFontGlyph; // TEMP +struct NzFontGlyph; using NzFontConstRef = NzResourceRef; using NzFontLoader = NzResourceLoader; @@ -32,31 +31,12 @@ using NzFontRef = NzResourceRef; class NAZARA_API NzFont : public NzResource, NzNonCopyable { + friend NzAbstractFontAtlas; friend NzFontLoader; public: - struct Atlas - { - NzGuillotineBinPack binPack; - NzImage image; - }; - - struct Glyph - { - NzRecti aabb; - NzRectui atlasRect; - bool flipped; - bool valid; - int advance; - unsigned int atlasIndex; - }; - - struct SizeInfo - { - unsigned int lineHeight; - float underlinePosition; - float underlineThickness; - }; + struct Glyph; + struct SizeInfo; NzFont(); NzFont(NzFont&& font) = default; @@ -71,14 +51,12 @@ class NAZARA_API NzFont : public NzResource, NzNonCopyable bool ExtractGlyph(unsigned int characterSize, char32_t character, nzUInt32 style, NzFontGlyph* glyph) const; - const Atlas& GetAtlas(unsigned int atlasIndex) const; - unsigned int GetAtlasCount() const; + const NzAbstractFontAtlas* GetAtlas() const; unsigned int GetCachedGlyphCount(unsigned int characterSize, nzUInt32 style) const; unsigned int GetCachedGlyphCount() const; NzString GetFamilyName() const; int GetKerning(unsigned int characterSize, char32_t first, char32_t second) const; const Glyph& GetGlyph(unsigned int characterSize, nzUInt32 style, char32_t character) const; - unsigned int GetMaxAtlasSize() const; unsigned int GetMinimumStepSize() const; const SizeInfo& GetSizeInfo(unsigned int characterSize) const; NzString GetStyleName() const; @@ -93,41 +71,50 @@ class NAZARA_API NzFont : public NzResource, NzNonCopyable bool OpenFromMemory(const void* data, std::size_t size, const NzFontParams& params = NzFontParams()); bool OpenFromStream(NzInputStream& stream, const NzFontParams& params = NzFontParams()); - void SetMaxAtlasSize(unsigned int maxAtlasSize); + void SetAtlas(std::shared_ptr atlas); void SetMinimumStepSize(unsigned int minimumSizeStep); NzFont& operator=(NzFont&& font) = default; - static unsigned int GetDefaultMaxAtlasSize(); - static void SetDefaultMaxAtlasSize(unsigned int maxAtlasSize); + enum ModicationCode + { + ModificationCode_GlyphCacheCleared, + ModificationCode_KerningCacheCleared, + ModificationCode_SizeInfoCacheCleared + }; + + struct Glyph + { + NzRecti aabb; + NzRectui atlasRect; + bool flipped; + bool valid; + int advance; + unsigned int layerIndex; + }; + + struct SizeInfo + { + unsigned int lineHeight; + float underlinePosition; + float underlineThickness; + }; private: using GlyphMap = std::unordered_map; - struct QueuedGlyph - { - char32_t codepoint; - NzImage image; - GlyphMap* map; - }; - nzUInt64 ComputeKey(unsigned int characterSize, nzUInt32 style) const; - unsigned int GetRealMaxAtlasSize() const; - unsigned int InsertRect(NzRectui* rect, bool* flipped) const; + void OnAtlasCleared(); const Glyph& PrecacheGlyph(GlyphMap& glyphMap, unsigned int characterSize, bool bold, char32_t character) const; - void ProcessGlyphQueue() const; + std::shared_ptr m_atlas; std::unique_ptr m_data; mutable std::unordered_map> m_kerningCache; mutable std::unordered_map m_glyphes; mutable std::unordered_map m_sizeInfoCache; - mutable std::vector m_atlases; - mutable std::vector m_glyphQueue; - unsigned int m_maxAtlasSize; unsigned int m_minimumSizeStep; static NzFontLoader::LoaderList s_loaders; - static unsigned int s_maxAtlasSize; }; #endif // NAZARA_FONT_HPP diff --git a/include/Nazara/Utility/GuillotineImageAtlas.hpp b/include/Nazara/Utility/GuillotineImageAtlas.hpp new file mode 100644 index 000000000..dc1542560 --- /dev/null +++ b/include/Nazara/Utility/GuillotineImageAtlas.hpp @@ -0,0 +1,68 @@ +// Copyright (C) 2015 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#pragma once + +#ifndef NAZARA_GUILLOTINEIMAGEATLAS_HPP +#define NAZARA_GUILLOTINEIMAGEATLAS_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +class NAZARA_API NzGuillotineImageAtlas : public NzAbstractFontAtlas +{ + public: + NzGuillotineImageAtlas(); + virtual ~NzGuillotineImageAtlas(); + + void Clear(); + void Free(NzSparsePtr rects, NzSparsePtr layers, unsigned int count); + + NzGuillotineBinPack::FreeRectChoiceHeuristic GetRectChoiceHeuristic() const; + NzGuillotineBinPack::GuillotineSplitHeuristic GetRectSplitHeuristic() const; + NzAbstractImage* GetLayer(unsigned int layerIndex) const; + unsigned int GetLayerCount() const; + + bool Insert(const NzImage& image, NzRectui* rect, bool* flipped, unsigned int* layerIndex); + + void SetRectChoiceHeuristic(NzGuillotineBinPack::FreeRectChoiceHeuristic heuristic); + void SetRectSplitHeuristic(NzGuillotineBinPack::GuillotineSplitHeuristic heuristic); + + protected: + struct Layer; + + virtual unsigned int GetMaxAtlasSize() const; + virtual bool ResizeImage(Layer& layer, const NzVector2ui& size) const; + + struct QueuedGlyph + { + NzImage image; + NzRectui rect; + bool flipped; + }; + + struct Layer + { + std::vector queuedGlyphs; + std::unique_ptr image; + NzGuillotineBinPack binPack; + unsigned int freedRectangles = 0; + }; + + private: + void ProcessGlyphQueue(Layer& layer) const; + + std::set m_fonts; + mutable std::vector m_layers; + NzGuillotineBinPack::FreeRectChoiceHeuristic m_rectChoiceHeuristic; + NzGuillotineBinPack::GuillotineSplitHeuristic m_rectSplitHeuristic; +}; + +#endif // NAZARA_GUILLOTINEIMAGEATLAS_HPP diff --git a/src/Nazara/Graphics/GuillotineTextureAtlas.cpp b/src/Nazara/Graphics/GuillotineTextureAtlas.cpp new file mode 100644 index 000000000..7ff8bfa0c --- /dev/null +++ b/src/Nazara/Graphics/GuillotineTextureAtlas.cpp @@ -0,0 +1,57 @@ +// Copyright (C) 2015 Jérôme Leclercq +// This file is part of the "Nazara Engine - Graphics module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include +#include + +bool NzGuillotineTextureAtlas::ResizeImage(Layer& layer, const NzVector2ui& size) const +{ + NzTexture newTexture; + if (newTexture.Create(nzImageType_2D, nzPixelFormat_A8, size.x, size.y, 1, 0xFF)) + { + newTexture.EnableMipmapping(true); + + if (layer.image) + { + NzTexture& texture = *static_cast(layer.image.get()); + + // Copie des anciennes données + ///TODO: Copie de texture à texture + NzImage image; + if (!texture.Download(&image)) + { + NazaraError("Failed to download old texture"); + return false; + } + + if (!newTexture.Update(image, NzRectui(0, 0, image.GetWidth(), image.GetHeight()))) + { + NazaraError("Failed to update texture"); + return false; + } + + texture = std::move(newTexture); + } + else + layer.image.reset(new NzTexture(std::move(newTexture))); + + return true; + } + else + { + NazaraError("Failed to create texture"); + return false; + } +} + +unsigned int NzGuillotineTextureAtlas::GetMaxAtlasSize() const +{ + ///FIXME: D'après la documentation OpenGL, cette valeur n'est qu'une approximation et les texture proxies sont une meilleure solution + /// Cependant le test ne se fait pas au même moment, penser à adapter le code pour gérer ce cas ? + /// (Cela permettrait au passage de gérer le cas où une image ne peut être allouée car il n'y a pas assez de mémoire contigüe pour la contenir) + + return NzRenderer::GetMaxTextureSize(); +} diff --git a/src/Nazara/Utility/Font.cpp b/src/Nazara/Utility/Font.cpp index c6a888929..e41d6b755 100644 --- a/src/Nazara/Utility/Font.cpp +++ b/src/Nazara/Utility/Font.cpp @@ -8,18 +8,12 @@ #include #include -namespace -{ - const unsigned int s_atlasStartSize = 512; -} - bool NzFontParams::IsValid() const { return true; // Rien à tester } NzFont::NzFont() : -m_maxAtlasSize(0), m_minimumSizeStep(1) { } @@ -31,25 +25,39 @@ NzFont::~NzFont() void NzFont::ClearGlyphCache() { - // Destruction des atlas et glyphes mémorisés - m_atlases.clear(); - m_glyphes.clear(); - m_glyphQueue.clear(); + if (m_atlas) + { + if (m_atlas.use_count() > 1) // Au moins une autre police utilise cet atlas, on vire nos glyphes + { + for (auto mapIt = m_glyphes.begin(); mapIt != m_glyphes.end(); ++mapIt) + { + GlyphMap& glyphMap = mapIt->second; + for (auto glyphIt = glyphMap.begin(); glyphIt != glyphMap.end(); ++glyphIt) + { + Glyph& glyph = glyphIt->second; + m_atlas->Free(&glyph.atlasRect, &glyph.layerIndex, 1); + } + } - // Création du premier atlas - m_atlases.resize(1); - Atlas& atlas = m_atlases.back(); - atlas.binPack.Reset(s_atlasStartSize, s_atlasStartSize); + // Destruction des glyphes mémorisés + m_glyphes.clear(); + NotifyModified(ModificationCode_GlyphCacheCleared); + } + else + m_atlas->Clear(); + } } void NzFont::ClearKerningCache() { m_kerningCache.clear(); + NotifyModified(ModificationCode_KerningCacheCleared); } void NzFont::ClearSizeInfoCache() { m_sizeInfoCache.clear(); + NotifyModified(ModificationCode_SizeInfoCacheCleared); } bool NzFont::Create(NzFontData* data) @@ -66,18 +74,15 @@ bool NzFont::Create(NzFontData* data) m_data.reset(data); - ClearGlyphCache(); // Création du premier atlas en mémoire - return true; } void NzFont::Destroy() { - m_atlases.clear(); + ClearGlyphCache(); + m_data.reset(); m_kerningCache.clear(); - m_glyphes.clear(); - m_glyphQueue.clear(); m_sizeInfoCache.clear(); } @@ -94,17 +99,9 @@ bool NzFont::ExtractGlyph(unsigned int characterSize, char32_t character, nzUInt return m_data->ExtractGlyph(characterSize, character, style & nzTextStyle_Bold, glyph); } -const NzFont::Atlas& NzFont::GetAtlas(unsigned int atlasIndex) const +const NzAbstractFontAtlas* NzFont::GetAtlas() const { - if (!m_glyphQueue.empty()) - ProcessGlyphQueue(); - - return m_atlases.at(atlasIndex); -} - -unsigned int NzFont::GetAtlasCount() const -{ - return m_atlases.size(); + return m_atlas.get(); } unsigned int NzFont::GetCachedGlyphCount(unsigned int characterSize, nzUInt32 style) const @@ -173,11 +170,6 @@ const NzFont::Glyph& NzFont::GetGlyph(unsigned int characterSize, nzUInt32 style return PrecacheGlyph(m_glyphes[key], characterSize, style, character); } -unsigned int NzFont::GetMaxAtlasSize() const -{ - return m_maxAtlasSize; -} - unsigned int NzFont::GetMinimumStepSize() const { return m_minimumSizeStep; @@ -266,26 +258,11 @@ bool NzFont::OpenFromStream(NzInputStream& stream, const NzFontParams& params) return NzFontLoader::LoadFromStream(this, stream, params); } -void NzFont::SetMaxAtlasSize(unsigned int maxAtlasSize) +void NzFont::SetAtlas(std::shared_ptr atlas) { - unsigned int oldMaxAtlasSize = GetRealMaxAtlasSize(); - m_maxAtlasSize = maxAtlasSize; + ClearGlyphCache(); - // Si l'un de nos atlas dépasse la nouvelle taille, on doit vider le cache - maxAtlasSize = GetRealMaxAtlasSize(); - if (maxAtlasSize < oldMaxAtlasSize) - { - for (Atlas& atlas : m_atlases) - { - unsigned int atlasSize = atlas.binPack.GetWidth(); - if (atlasSize > maxAtlasSize) - { - NazaraWarning("At least one atlas was over new max atlas size (" + NzString::Number(atlasSize) + " > " + NzString::Number(maxAtlasSize) + "), clearing glyph cache..."); - ClearGlyphCache(); - return; - } - } - } + m_atlas = atlas; } void NzFont::SetMinimumStepSize(unsigned int minimumStepSize) @@ -299,16 +276,7 @@ void NzFont::SetMinimumStepSize(unsigned int minimumStepSize) #endif m_minimumSizeStep = minimumStepSize; -} - -unsigned int NzFont::GetDefaultMaxAtlasSize() -{ - return s_maxAtlasSize; -} - -void NzFont::SetDefaultMaxAtlasSize(unsigned int maxAtlasSize) -{ - s_maxAtlasSize = maxAtlasSize; + ClearGlyphCache(); } nzUInt64 NzFont::ComputeKey(unsigned int characterSize, nzUInt32 style) const @@ -326,58 +294,12 @@ nzUInt64 NzFont::ComputeKey(unsigned int characterSize, nzUInt32 style) const return (stylePart << 32) | sizePart; } -unsigned int NzFont::GetRealMaxAtlasSize() const +void NzFont::OnAtlasCleared() { - unsigned int maxAtlasSize = (m_maxAtlasSize == 0) ? s_maxAtlasSize : m_maxAtlasSize; - if (maxAtlasSize == 0) - maxAtlasSize = std::numeric_limits::max(); + // Notre atlas vient d'être vidé, détruisons le cache de glyphe + m_glyphes.clear(); - return maxAtlasSize; -} - -unsigned int NzFont::InsertRect(NzRectui* rect, bool* flipped) const -{ - ///DOC: Tous les pointeurs doivent être valides - // Précondition: Un rectangle ne peut pas être plus grand dans une dimension que la taille maximale de l'atlas - - unsigned int maxAtlasSize = GetRealMaxAtlasSize(); - - // Cette fonction ne fait qu'insérer un rectangle de façon virtuelle, l'insertion des images se fait après - for (unsigned int i = 0; i < m_atlases.size(); ++i) - { - Atlas& atlas = m_atlases[i]; - if (atlas.binPack.Insert(rect, flipped, 1, false, NzGuillotineBinPack::RectBestAreaFit, NzGuillotineBinPack::SplitMinimizeArea)) - // Insertion réussie dans l'un des atlas, pas de question à se poser - return i; - else if (i == m_atlases.size() - 1) // Dernière itération ? - { - // Dernier atlas, et le glyphe ne rentre pas, peut-on agrandir la taille de l'atlas ? - unsigned int size = atlas.binPack.GetWidth(); // l'atlas étant carré, on ne teste qu'une dimension - if (size < maxAtlasSize) - { - // On peut encore agrandir l'atlas - size = std::min(size*2, maxAtlasSize); - atlas.binPack.Expand(size, size); - - // On relance la boucle sur le dernier atlas - i--; - } - else - { - // On ne peut plus agrandir le dernier atlas, il est temps d'en créer un nouveau - m_atlases.resize(m_atlases.size() + 1); - Atlas& newAtlas = m_atlases.back(); - - newAtlas.binPack.Reset(s_atlasStartSize, s_atlasStartSize); - - // On laisse la boucle insérer toute seule le rectangle à la prochaine itération - } - } - } - - // Si nous arrivons ici, c'est qu'une erreur a eu lieu en amont - NazaraInternalError("This shouldn't happen"); - return std::numeric_limits::max(); + NotifyModified(ModificationCode_GlyphCacheCleared); } const NzFont::Glyph& NzFont::PrecacheGlyph(GlyphMap& glyphMap, unsigned int characterSize, bool bold, char32_t character) const @@ -386,7 +308,7 @@ const NzFont::Glyph& NzFont::PrecacheGlyph(GlyphMap& glyphMap, unsigned int char if (it != glyphMap.end()) // Si le glyphe n'est pas déjà chargé return it->second; - Glyph glyph; + Glyph& glyph = glyphMap[character]; // Insertion du glyphe glyph.valid = false; // On extrait le glyphe depuis la police @@ -396,112 +318,39 @@ const NzFont::Glyph& NzFont::PrecacheGlyph(GlyphMap& glyphMap, unsigned int char glyph.atlasRect.width = fontGlyph.image.GetWidth(); glyph.atlasRect.height = fontGlyph.image.GetHeight(); - unsigned int maxAtlasSize = GetRealMaxAtlasSize(); - if (glyph.atlasRect.width <= maxAtlasSize && glyph.atlasRect.height <= maxAtlasSize) + // Insertion du rectangle dans l'un des atlas + if (glyph.atlasRect.width > 0 && glyph.atlasRect.height > 0) // Si l'image contient quelque chose { - // Insertion du rectangle dans l'un des atlas - glyph.aabb = fontGlyph.aabb; - glyph.advance = fontGlyph.advance; - glyph.valid = true; + // Padding (pour éviter le débordement lors du filtrage) + const unsigned int padding = 1; // Un pixel de contour - if (glyph.atlasRect.width > 0 && glyph.atlasRect.height > 0) // Si l'image contient quelque chose + glyph.atlasRect.width += padding*2; + glyph.atlasRect.height += padding*2; + + // Insertion du rectangle dans l'atlas virtuel + if (!m_atlas->Insert(fontGlyph.image, &glyph.atlasRect, &glyph.flipped, &glyph.layerIndex)) { - // Padding (pour éviter le débordement lors du filtrage) - const unsigned int padding = 1; // Un pixel entre chaque glyphe - - glyph.atlasRect.width += padding; - glyph.atlasRect.height += padding; - - // Insertion du rectangle dans l'atlas virtuel - glyph.atlasIndex = InsertRect(&glyph.atlasRect, &glyph.flipped); - - glyph.atlasRect.width -= padding; - glyph.atlasRect.height -= padding; - - // Mise en queue pour insertion dans l'atlas réel - m_glyphQueue.resize(m_glyphQueue.size()+1); - QueuedGlyph& queuedGlyph = m_glyphQueue.back(); - queuedGlyph.codepoint = character; - queuedGlyph.image = std::move(fontGlyph.image); - queuedGlyph.map = &glyphMap; + NazaraError("Failed to insert glyph into atlas"); + return glyph; } + + // Compensation du contour (centrage du glyphe) + glyph.atlasRect.x += padding; + glyph.atlasRect.y += padding; + glyph.atlasRect.width -= padding*2; + glyph.atlasRect.height -= padding*2; } - else - { - NazaraWarning("Glyph \"" + NzString::Unicode(character) + "\" is bigger than max atlas size"); - } + + glyph.aabb = fontGlyph.aabb; + glyph.advance = fontGlyph.advance; + glyph.valid = true; } else { NazaraWarning("Failed to extract glyph \"" + NzString::Unicode(character) + "\""); } - return glyphMap.insert(std::make_pair(character, std::move(glyph))).first->second; -} - -void NzFont::ProcessGlyphQueue() const -{ - for (QueuedGlyph& queuedGlyph : m_glyphQueue) - { - GlyphMap& glyphMap = *queuedGlyph.map; - auto glyphIt = glyphMap.find(queuedGlyph.codepoint); - if (glyphIt == glyphMap.end()) - continue; // Le glyphe a certainement été supprimé du cache avant la mise à jour de l'atlas - - Glyph& glyph = glyphIt->second; - Atlas& atlas = m_atlases[glyph.atlasIndex]; - - // On s'assure que l'atlas est de la bonne taille - NzVector2ui atlasSize(atlas.image.GetWidth(), atlas.image.GetHeight()); - NzVector2ui binPackSize = atlas.binPack.GetSize(); - if (atlasSize != binPackSize) - { - // Création d'une nouvelle image - NzImage newAtlas(nzImageType_2D, nzPixelFormat_A8, binPackSize.x, binPackSize.y); - if (atlas.image.IsValid()) - { - newAtlas.Copy(atlas.image, NzRectui(atlasSize), NzVector2ui(0, 0)); // On copie les anciennes données - - // On initialise les nouvelles régions - newAtlas.Fill(NzColor(255, 255, 255, 0), NzRectui(0, atlasSize.y, binPackSize.x, binPackSize.y - atlasSize.y)); - newAtlas.Fill(NzColor(255, 255, 255, 0), NzRectui(atlasSize.x, 0, binPackSize.x - atlasSize.x, atlasSize.y)); - } - else - newAtlas.Fill(NzColor(255, 255, 255, 0)); // On initialise les pixels - - atlas.image = std::move(newAtlas); // On déplace la nouvelle image vers l'atlas - } - - unsigned int glyphWidth = queuedGlyph.image.GetWidth(); - unsigned int glyphHeight = queuedGlyph.image.GetHeight(); - - // On copie le glyphe dans l'atlas - if (glyph.flipped) - { - // On tourne le glyphe pour qu'il rentre dans le rectangle - const nzUInt8* src = queuedGlyph.image.GetConstPixels(); - nzUInt8* ptr = atlas.image.GetPixels(glyph.atlasRect.x, glyph.atlasRect.y + glyphWidth - 1); - - unsigned int lineStride = atlas.image.GetWidth(); // BPP = 1 - for (unsigned int y = 0; y < glyphHeight; ++y) - { - for (unsigned int x = 0; x < glyphWidth; ++x) - { - *ptr = *src++; // On copie et on avance dans le glyphe - ptr -= lineStride; // On remonte d'une ligne - } - - ptr += lineStride*glyphWidth + 1; - } - } - else - atlas.image.Copy(queuedGlyph.image, NzRectui(glyphWidth, glyphHeight), glyph.atlasRect.GetPosition()); - - queuedGlyph.image.Destroy(); // On libère l'image dès que possible (pour réduire la consommation) - } - - m_glyphQueue.clear(); + return glyph; } NzFontLoader::LoaderList NzFont::s_loaders; -unsigned int NzFont::s_maxAtlasSize = 8192; // Valeur totalement arbitraire diff --git a/src/Nazara/Utility/GuillotineImageAtlas.cpp b/src/Nazara/Utility/GuillotineImageAtlas.cpp new file mode 100644 index 000000000..a04eed25a --- /dev/null +++ b/src/Nazara/Utility/GuillotineImageAtlas.cpp @@ -0,0 +1,244 @@ +// Copyright (C) 2015 Jérôme Leclercq +// This file is part of the "Nazara Engine - Utility module" +// For conditions of distribution and use, see copyright notice in Config.hpp + +#include +#include +#include + +namespace +{ + const unsigned int s_atlasStartSize = 512; +} + +NzGuillotineImageAtlas::NzGuillotineImageAtlas() : +m_rectChoiceHeuristic(NzGuillotineBinPack::RectBestAreaFit), +m_rectSplitHeuristic(NzGuillotineBinPack::SplitMinimizeArea) +{ +} + +NzGuillotineImageAtlas::~NzGuillotineImageAtlas() = default; + +void NzGuillotineImageAtlas::Clear() +{ + m_layers.clear(); + NotifyCleared(); +} + +void NzGuillotineImageAtlas::Free(NzSparsePtr rects, NzSparsePtr layers, unsigned int count) +{ + for (unsigned int i = 0; i < count; ++i) + { + #ifdef NAZARA_DEBUG + if (layers[i] >= m_layers.size()) + { + NazaraWarning("Rectangle #" + NzString::Number(i) + " belong to an out-of-bounds layer (" + NzString::Number(i) + " >= " + NzString::Number(m_layers.size()) + ")"); + continue; + } + #endif + + m_layers[layers[i]].binPack.FreeRectangle(rects[i]); + m_layers[layers[i]].freedRectangles++; + } +} + +NzGuillotineBinPack::FreeRectChoiceHeuristic NzGuillotineImageAtlas::GetRectChoiceHeuristic() const +{ + return m_rectChoiceHeuristic; +} + +NzGuillotineBinPack::GuillotineSplitHeuristic NzGuillotineImageAtlas::GetRectSplitHeuristic() const +{ + return m_rectSplitHeuristic; +} + +NzAbstractImage* NzGuillotineImageAtlas::GetLayer(unsigned int layerIndex) const +{ + #if NAZARA_UTILITY_SAFE + if (layerIndex >= m_layers.size()) + { + NazaraError("Layer index out of range (" + NzString::Number(layerIndex) + " >= " + NzString::Number(m_layers.size()) + ')'); + return nullptr; + } + #endif + + Layer& layer = m_layers[layerIndex]; + ProcessGlyphQueue(layer); + + return layer.image.get(); +} + +unsigned int NzGuillotineImageAtlas::GetLayerCount() const +{ + return m_layers.size(); +} + +bool NzGuillotineImageAtlas::Insert(const NzImage& image, NzRectui* rect, bool* flipped, unsigned int* layerIndex) +{ + unsigned int maxAtlasSize = GetMaxAtlasSize(); + + if (m_layers.empty()) + { + // On créé une première couche s'il n'y en a pas + m_layers.resize(1); + Layer& layer = m_layers.back(); + layer.binPack.Reset(s_atlasStartSize, s_atlasStartSize); + } + + // Cette fonction ne fait qu'insérer un rectangle de façon virtuelle, l'insertion des images se fait après + for (unsigned int i = 0; i < m_layers.size(); ++i) + { + Layer& layer = m_layers[i]; + + // Une fois qu'un certain nombre de rectangles ont étés libérés d'une couche, on fusionne les rectangles libres + if (layer.freedRectangles > 10) // Valeur totalement arbitraire + { + while (layer.binPack.MergeFreeRectangles()); // Tant qu'une fusion est possible + layer.freedRectangles = 0; // Et on repart de zéro + } + + if (layer.binPack.Insert(rect, flipped, 1, false, m_rectChoiceHeuristic, m_rectSplitHeuristic)) + { + // Insertion réussie dans l'une des couches, on place le glyphe en file d'attente + layer.queuedGlyphs.resize(layer.queuedGlyphs.size()+1); + QueuedGlyph& glyph = layer.queuedGlyphs.back(); + glyph.flipped = *flipped; + glyph.image = image; // Merci le Copy-On-Write + glyph.rect = *rect; + + *layerIndex = i; + return true; + } + else if (i == m_layers.size() - 1) // Dernière itération ? + { + // Dernière couche, et le glyphe ne rentre pas, peut-on agrandir la taille de l'image ? + unsigned int size = layer.binPack.GetWidth(); // l'image étant carrée, on ne teste qu'une dimension + if (size < maxAtlasSize) + { + // On peut encore agrandir la couche + size = std::min(size*2, maxAtlasSize); + layer.binPack.Expand(size, size); + + // On relance la boucle sur la nouvelle dernière couche + i--; + } + else + { + // On ne peut plus agrandir la dernière couche, il est temps d'en créer une nouvelle + m_layers.resize(m_layers.size() + 1); + Layer& newLayer = m_layers.back(); + + newLayer.binPack.Reset(s_atlasStartSize, s_atlasStartSize); + + // On laisse la boucle insérer toute seule le rectangle à la prochaine itération + } + } + } + + return false; // Normalement impossible +} + +void NzGuillotineImageAtlas::SetRectChoiceHeuristic(NzGuillotineBinPack::FreeRectChoiceHeuristic heuristic) +{ + m_rectChoiceHeuristic = heuristic; +} + +void NzGuillotineImageAtlas::SetRectSplitHeuristic(NzGuillotineBinPack::GuillotineSplitHeuristic heuristic) +{ + m_rectSplitHeuristic = heuristic; +} + +unsigned int NzGuillotineImageAtlas::GetMaxAtlasSize() const +{ + return 8192; // Valeur totalement arbitraire +} + +void NzGuillotineImageAtlas::ProcessGlyphQueue(Layer& layer) const +{ + std::vector pixelBuffer; + + // On s'assure que l'image est de la bonne taille + NzVector2ui binPackSize(layer.binPack.GetSize()); + NzVector2ui imageSize((layer.image) ? layer.image->GetSize() : NzVector3ui(0U)); + if (binPackSize != imageSize) + ResizeImage(layer, binPackSize); + + for (QueuedGlyph& glyph : layer.queuedGlyphs) + { + unsigned int glyphWidth = glyph.image.GetWidth(); + unsigned int glyphHeight = glyph.image.GetHeight(); + + // Calcul de l'éventuel padding (pixels de contour) + unsigned int paddingX; + unsigned int paddingY; + if (glyph.flipped) + { + paddingX = (glyph.rect.height - glyphWidth)/2; + paddingY = (glyph.rect.width - glyphHeight)/2; + } + else + { + paddingX = (glyph.rect.width - glyphWidth)/2; + paddingY = (glyph.rect.height - glyphHeight)/2; + } + + if (paddingX > 0 || paddingY > 0) + { + // On remplit les contours + pixelBuffer.resize(glyph.rect.width * glyph.rect.height); + std::memset(pixelBuffer.data(), 0, glyph.rect.width*glyph.rect.height*sizeof(nzUInt8)); + + layer.image->Update(pixelBuffer.data(), glyph.rect); + } + + const nzUInt8* pixels; + // On copie le glyphe dans l'atlas + if (glyph.flipped) + { + pixelBuffer.resize(glyphHeight * glyphWidth); + + // On tourne le glyphe pour qu'il rentre dans le rectangle + const nzUInt8* src = glyph.image.GetConstPixels(); + nzUInt8* ptr = pixelBuffer.data(); + + unsigned int lineStride = glyphWidth*sizeof(nzUInt8); // BPP = 1 + src += lineStride-1; // Départ en haut à droite + for (unsigned int x = 0; x < glyphWidth; ++x) + { + for (unsigned int y = 0; y < glyphHeight; ++y) + { + *ptr++ = *src; + src += lineStride; + } + + src -= glyphHeight*lineStride + 1; + } + + pixels = pixelBuffer.data(); + std::swap(glyphWidth, glyphHeight); + } + else + pixels = glyph.image.GetConstPixels(); + + layer.image->Update(pixels, NzRectui(glyph.rect.x + paddingX, glyph.rect.y + paddingY, glyphWidth, glyphHeight), 0, glyphWidth, glyphHeight); + glyph.image.Destroy(); // On libère l'image dès que possible (pour réduire la consommation) + } + + layer.queuedGlyphs.clear(); +} + +bool NzGuillotineImageAtlas::ResizeImage(Layer& layer, const NzVector2ui& size) const +{ + NzImage newImage(nzImageType_2D, nzPixelFormat_A8, size.x, size.y); + if (layer.image) + { + NzImage& image = *static_cast(layer.image.get()); + newImage.Copy(image, NzRectui(size), NzVector2ui(0, 0)); // Copie des anciennes données + + image = std::move(newImage); + } + else + layer.image.reset(new NzImage(std::move(newImage))); + + return true; +}