diff options
author | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
---|---|---|
committer | Kae <80987908+Novaenia@users.noreply.github.com> | 2023-06-20 14:33:09 +1000 |
commit | 6352e8e3196f78388b6c771073f9e03eaa612673 (patch) | |
tree | e23772f79a7fbc41bc9108951e9e136857484bf4 /source/application/StarTextureAtlas.hpp | |
parent | 6741a057e5639280d85d0f88ba26f000baa58f61 (diff) |
everything everywhere
all at once
Diffstat (limited to 'source/application/StarTextureAtlas.hpp')
-rw-r--r-- | source/application/StarTextureAtlas.hpp | 417 |
1 files changed, 417 insertions, 0 deletions
diff --git a/source/application/StarTextureAtlas.hpp b/source/application/StarTextureAtlas.hpp new file mode 100644 index 0000000..db5a163 --- /dev/null +++ b/source/application/StarTextureAtlas.hpp @@ -0,0 +1,417 @@ +#ifndef STAR_TEXTURE_ATLAS_HPP +#define STAR_TEXTURE_ATLAS_HPP + +#include "StarRect.hpp" +#include "StarImage.hpp" +#include "StarCasting.hpp" + +namespace Star { + +STAR_EXCEPTION(TextureAtlasException, StarException); + +// Implements a set of "texture atlases" or, sets of smaller textures grouped +// as a larger texture. +template <typename AtlasTextureHandle> +class TextureAtlasSet { +public: + struct Texture { + virtual Vec2U imageSize() const = 0; + + virtual AtlasTextureHandle const& atlasTexture() const = 0; + virtual RectU atlasTextureCoordinates() const = 0; + + // A locked texture will never be moved during compression, so its + // atlasTexture and textureCoordinates will not change. + virtual void setLocked(bool locked) = 0; + + // Returns true if this texture has been freed or the parent + // TextureAtlasSet has been destructed. + virtual bool expired() const = 0; + }; + + typedef shared_ptr<Texture> TextureHandle; + + TextureAtlasSet(unsigned cellSize, unsigned atlasNumCells); + + // The constant square size of all atlas textures + Vec2U atlasTextureSize() const; + + // Removes all existing textures and destroys all texture atlases. + void reset(); + + // Adds texture to some TextureAtlas. Texture must fit in a single atlas + // texture, otherwise an exception is thrown. Returns a pointer to the new + // texture. If borderPixels is true, then fills a 1px border around the + // given image in the atlas with the nearest color value, to prevent + // bleeding. + TextureHandle addTexture(Image const& image, bool borderPixels = true); + + // Removes the given texture from the TextureAtlasSet and invalidates the + // pointer. + void freeTexture(TextureHandle const& texture); + + unsigned totalAtlases() const; + unsigned totalTextures() const; + float averageFillLevel() const; + + // Takes images from sparsely filled atlases and moves them to less sparsely + // filled atlases in an effort to free up room. This method tages the atlas + // with the lowest fill level and picks a texture from it, removes it, and + // re-adds it to the AtlasSet. It does this up to textureCount textures, + // until it finds a texture where re-adding it to the texture atlas simply + // moves the texture into the same atlas, at which point it stops. + void compressionPass(size_t textureCount = NPos); + + // The number of atlases that the AtlasSet will attempt to fit a texture in + // before giving up and creating a new atlas. Tries in order of least full + // to most full. Defaults to 3. + unsigned textureFitTries() const; + void setTextureFitTries(unsigned textureFitTries); + +protected: + virtual AtlasTextureHandle createAtlasTexture(Vec2U const& size, PixelFormat pixelFormat) = 0; + virtual void destroyAtlasTexture(AtlasTextureHandle const& atlasTexture) = 0; + virtual void copyAtlasPixels(AtlasTextureHandle const& atlasTexture, Vec2U const& bottomLeft, Image const& image) = 0; + +private: + struct TextureAtlas { + AtlasTextureHandle atlasTexture; + unique_ptr<bool[]> usedCells; + unsigned usedCellCount; + }; + + struct AtlasPlacement { + TextureAtlas* atlas; + bool borderPixels = false; + RectU occupiedCells; + RectU textureCoords; + }; + + struct TextureEntry : Texture { + Vec2U imageSize() const override; + + AtlasTextureHandle const& atlasTexture() const override; + RectU atlasTextureCoordinates() const override; + + // A locked texture will never be moved during compression, so its + // atlasTexture and textureCoordinates will not change. + void setLocked(bool locked) override; + + bool expired() const override; + + Image textureImage; + AtlasPlacement atlasPlacement; + bool placementLocked = false; + bool textureExpired = false; + }; + + void setAtlasRegionUsed(TextureAtlas* extureAtlas, RectU const& region, bool used) const; + + Maybe<AtlasPlacement> addTextureToAtlas(TextureAtlas* atlas, Image const& image, bool borderPixels); + void sortAtlases(); + + unsigned m_atlasCellSize; + unsigned m_atlasNumCells; + unsigned m_textureFitTries; + + List<shared_ptr<TextureAtlas>> m_atlases; + HashSet<shared_ptr<TextureEntry>> m_textures; +}; + +template <typename AtlasTextureHandle> +TextureAtlasSet<AtlasTextureHandle>::TextureAtlasSet(unsigned cellSize, unsigned atlasNumCells) + : m_atlasCellSize(cellSize), m_atlasNumCells(atlasNumCells), m_textureFitTries(3) {} + +template <typename AtlasTextureHandle> +Vec2U TextureAtlasSet<AtlasTextureHandle>::atlasTextureSize() const { + return Vec2U::filled(m_atlasCellSize * m_atlasNumCells); +} + +template <typename AtlasTextureHandle> +void TextureAtlasSet<AtlasTextureHandle>::reset() { + for (auto const& texture : m_textures) + texture->textureExpired = true; + + for (auto const& atlas : m_atlases) + destroyAtlasTexture(atlas->atlasTexture); + + m_atlases.clear(); + m_textures.clear(); +} + +template <typename AtlasTextureHandle> +auto TextureAtlasSet<AtlasTextureHandle>::addTexture(Image const& image, bool borderPixels) -> TextureHandle { + if (image.empty()) + throw TextureAtlasException("Empty image given in TextureAtlasSet::addTexture"); + + Image finalImage; + if (borderPixels) { + Vec2U imageSize = image.size(); + Vec2U finalImageSize = imageSize + Vec2U(2, 2); + finalImage = Image(finalImageSize, PixelFormat::RGBA32); + + // Fill 1px border on all sides of the image + for (unsigned y = 0; y < finalImageSize[1]; ++y) { + for (unsigned x = 0; x < finalImageSize[0]; ++x) { + unsigned xind = clamp<unsigned>(x, 1, imageSize[0]) - 1; + unsigned yind = clamp<unsigned>(y, 1, imageSize[1]) - 1; + finalImage.set32(x, y, image.getrgb(xind, yind)); + } + } + } else { + finalImage = image; + } + + auto tryAtlas = [&](TextureAtlas* atlas) -> TextureHandle { + auto placement = addTextureToAtlas(atlas, finalImage, borderPixels); + if (!placement) + return nullptr; + + auto textureEntry = make_shared<TextureEntry>(); + textureEntry->textureImage = move(finalImage); + textureEntry->atlasPlacement = *placement; + + m_textures.add(textureEntry); + sortAtlases(); + return textureEntry; + }; + + // Try the first 'm_textureFitTries' atlases to see if we can fit a given + // texture in an existing atlas. Do this from the most full to the least + // full atlas to maximize compression. + size_t startAtlas = m_atlases.size() - min<size_t>(m_atlases.size(), m_textureFitTries); + for (size_t i = startAtlas; i < m_atlases.size(); ++i) { + if (auto texturePtr = tryAtlas(m_atlases[i].get())) + return texturePtr; + } + + // If we have not found an existing atlas to put the texture, need to create + // a new atlas + m_atlases.append(make_shared<TextureAtlas>(TextureAtlas{ + createAtlasTexture(Vec2U::filled(m_atlasCellSize * m_atlasNumCells), PixelFormat::RGBA32), + unique_ptr<bool[]>(new bool[m_atlasNumCells * m_atlasNumCells]()), 0 + })); + + if (auto texturePtr = tryAtlas(m_atlases.last().get())) + return texturePtr; + + // If it can't fit in a brand new empty atlas, it will not fit in any atlas + destroyAtlasTexture(m_atlases.last()->atlasTexture); + m_atlases.removeLast(); + throw TextureAtlasException("Could not add texture to new atlas in TextureAtlasSet::addTexture, too large"); +} + +template <typename AtlasTextureHandle> +void TextureAtlasSet<AtlasTextureHandle>::freeTexture(TextureHandle const& texture) { + auto textureEntry = convert<TextureEntry>(texture); + + setAtlasRegionUsed(textureEntry->atlasPlacement.atlas, textureEntry->atlasPlacement.occupiedCells, false); + sortAtlases(); + + textureEntry->textureExpired = true; + m_textures.remove(textureEntry); +} + +template <typename AtlasTextureHandle> +unsigned TextureAtlasSet<AtlasTextureHandle>::totalAtlases() const { + return m_atlases.size(); +} + +template <typename AtlasTextureHandle> +unsigned TextureAtlasSet<AtlasTextureHandle>::totalTextures() const { + return m_textures.size(); +} + +template <typename AtlasTextureHandle> +float TextureAtlasSet<AtlasTextureHandle>::averageFillLevel() const { + if (m_atlases.empty()) + return 0.0f; + + float atlasFillLevelSum = 0.0f; + for (auto const& atlas : m_atlases) + atlasFillLevelSum += atlas->usedCellCount / (float)square(m_atlasNumCells); + return atlasFillLevelSum / m_atlases.size(); +} + +template <typename AtlasTextureHandle> +void TextureAtlasSet<AtlasTextureHandle>::compressionPass(size_t textureCount) { + while (m_atlases.size() > 1 && textureCount > 0) { + // Find the least full atlas, If it is empty, remove it and start at the + // next atlas. + auto const& smallestAtlas = m_atlases.last(); + if (smallestAtlas->usedCellCount == 0) { + destroyAtlasTexture(smallestAtlas->atlasTexture); + m_atlases.removeLast(); + continue; + } + + // Loop over the currently loaded textures to find the smallest texture in + // the smallest atlas that is not locked. + TextureEntry* smallestTexture = nullptr; + for (auto const& texture : m_textures) { + if (texture->atlasPlacement.atlas == m_atlases.last().get()) { + if (!texture->placementLocked) { + if (!smallestTexture || texture->atlasPlacement.occupiedCells.volume() < smallestTexture->atlasPlacement.occupiedCells.volume()) + smallestTexture = texture.get(); + } + } + } + + // If we were not able to find a smallest texture because the texture is + // locked, then simply stop. TODO: This could be done better, this will + // prevent compressing textures that are not from the smallest atlas if the + // smallest atlas has only locked textures. + if (!smallestTexture) + break; + + // Try to add the texture to any atlas that isn't the last (most empty) one + size_t startAtlas = m_atlases.size() - 1 - min<size_t>(m_atlases.size() - 1, m_textureFitTries); + for (size_t i = startAtlas; i < m_atlases.size() - 1; ++i) { + if (auto placement = addTextureToAtlas(m_atlases[i].get(), smallestTexture->textureImage, smallestTexture->atlasPlacement.borderPixels)) { + setAtlasRegionUsed(smallestTexture->atlasPlacement.atlas, smallestTexture->atlasPlacement.occupiedCells, false); + smallestTexture->atlasPlacement = *placement; + smallestTexture = nullptr; + sortAtlases(); + break; + } + } + + // If we have not managed to move the smallest texture into any other + // atlas, assume the atlas set is compressed enough and quit. + if (smallestTexture) + break; + + --textureCount; + } +} + +template <typename AtlasTextureHandle> +unsigned TextureAtlasSet<AtlasTextureHandle>::textureFitTries() const { + return m_textureFitTries; +} + +template <typename AtlasTextureHandle> +void TextureAtlasSet<AtlasTextureHandle>::setTextureFitTries(unsigned textureFitTries) { + m_textureFitTries = textureFitTries; +} + +template <typename AtlasTextureHandle> +Vec2U TextureAtlasSet<AtlasTextureHandle>::TextureEntry::imageSize() const { + if (atlasPlacement.borderPixels) + return textureImage.size() - Vec2U(2, 2); + else + return textureImage.size(); +} + +template <typename AtlasTextureHandle> +AtlasTextureHandle const& TextureAtlasSet<AtlasTextureHandle>::TextureEntry::atlasTexture() const { + return atlasPlacement.atlas->atlasTexture; +} + +template <typename AtlasTextureHandle> +RectU TextureAtlasSet<AtlasTextureHandle>::TextureEntry::atlasTextureCoordinates() const { + return atlasPlacement.textureCoords; +} + +template <typename AtlasTextureHandle> +void TextureAtlasSet<AtlasTextureHandle>::TextureEntry::setLocked(bool locked) { + placementLocked = locked; +} + +template <typename AtlasTextureHandle> +bool TextureAtlasSet<AtlasTextureHandle>::TextureEntry::expired() const { + return textureExpired; +} + +template <typename AtlasTextureHandle> +void TextureAtlasSet<AtlasTextureHandle>::setAtlasRegionUsed(TextureAtlas* textureAtlas, RectU const& region, bool used) const { + for (unsigned y = region.yMin(); y < region.yMax(); ++y) { + for (unsigned x = region.xMin(); x < region.xMax(); ++x) { + auto& val = textureAtlas->usedCells[y * m_atlasNumCells + x]; + bool oldVal = val; + val = used; + if (oldVal && !val) { + starAssert(textureAtlas->usedCellCount != 0); + textureAtlas->usedCellCount -= 1; + } else if (!oldVal && used) { + textureAtlas->usedCellCount += 1; + starAssert(textureAtlas->usedCellCount <= square(m_atlasNumCells)); + } + } + } +} + +template <typename AtlasTextureHandle> +void TextureAtlasSet<AtlasTextureHandle>::sortAtlases() { + sort(m_atlases, [](auto const& a1, auto const& a2) { + return a1->usedCellCount > a2->usedCellCount; + }); +} + +template <typename AtlasTextureHandle> +auto TextureAtlasSet<AtlasTextureHandle>::addTextureToAtlas(TextureAtlas* atlas, Image const& image, bool borderPixels) -> Maybe<AtlasPlacement> { + bool found = false; + // Minimum cell indexes where this texture fits in this atlas. + unsigned fitCellX = 0; + unsigned fitCellY = 0; + + Vec2U imageSize = image.size(); + + // Number of cells this image will take. + size_t numCellsX = (imageSize[0] + m_atlasCellSize - 1) / m_atlasCellSize; + size_t numCellsY = (imageSize[1] + m_atlasCellSize - 1) / m_atlasCellSize; + + if (numCellsX > m_atlasNumCells || numCellsY > m_atlasNumCells) + return {}; + + for (size_t cellY = 0; cellY <= m_atlasNumCells - numCellsY; ++cellY) { + for (size_t cellX = 0; cellX <= m_atlasNumCells - numCellsX; ++cellX) { + // Check this box of numCellsX x numCellsY for fit. + found = true; + size_t fx; + size_t fy; + for (fy = cellY; fy < cellY + numCellsY; ++fy) { + for (fx = cellX; fx < cellX + numCellsX; ++fx) { + if (atlas->usedCells[fy * m_atlasNumCells + fx]) { + found = false; + break; + } + } + if (!found) + break; + } + if (!found) { + // If it does not fit, then we can skip to the block past the first + // horizontal used block; + cellX = fx; + } else { + fitCellX = cellX; + fitCellY = cellY; + break; + } + } + if (found) + break; + } + + if (!found) + return {}; + + setAtlasRegionUsed(atlas, RectU::withSize({fitCellX, fitCellY}, {(unsigned)numCellsX, (unsigned)numCellsY}), true); + + copyAtlasPixels(atlas->atlasTexture, Vec2U(fitCellX * m_atlasCellSize, fitCellY * m_atlasCellSize), image); + + AtlasPlacement atlasPlacement; + atlasPlacement.atlas = atlas; + atlasPlacement.borderPixels = borderPixels; + atlasPlacement.occupiedCells = RectU::withSize(Vec2U(fitCellX, fitCellY), Vec2U(numCellsX, numCellsY)); + if (borderPixels) + atlasPlacement.textureCoords = RectU::withSize(Vec2U(fitCellX * m_atlasCellSize + 1, fitCellY * m_atlasCellSize + 1), imageSize - Vec2U(2, 2)); + else + atlasPlacement.textureCoords = RectU::withSize(Vec2U(fitCellX * m_atlasCellSize, fitCellY * m_atlasCellSize), imageSize); + return atlasPlacement; +} + +} + +#endif |