2024-02-25 14:46:47 +00:00
|
|
|
#pragma once
|
2023-06-20 04:33:09 +00:00
|
|
|
|
|
|
|
#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);
|
|
|
|
|
2024-02-19 15:55:19 +00:00
|
|
|
virtual ~TextureAtlasSet() = default;
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
// 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 {
|
2024-02-19 15:55:19 +00:00
|
|
|
virtual ~TextureEntry() = default;
|
|
|
|
|
2023-06-20 04:33:09 +00:00
|
|
|
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>();
|
2024-02-19 15:55:19 +00:00
|
|
|
textureEntry->textureImage = std::move(finalImage);
|
2023-06-20 04:33:09 +00:00
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|