1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
|
#pragma once
#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);
virtual ~TextureAtlasSet() = default;
// 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 {
virtual ~TextureEntry() = default;
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 = std::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;
}
}
|