Browse Source

TextureTools: new, optimal, atlasArrayPowerOfTwo() algorithm.

pull/578/head
Vladimír Vondruš 4 years ago
parent
commit
b9f7bf6086
  1. 5
      doc/changelog.dox
  2. 82
      src/Magnum/TextureTools/Atlas.cpp
  3. 30
      src/Magnum/TextureTools/Atlas.h
  4. 21
      src/Magnum/TextureTools/CMakeLists.txt
  5. 210
      src/Magnum/TextureTools/Test/AtlasTest.cpp
  6. 2
      src/Magnum/TextureTools/Test/CMakeLists.txt

5
doc/changelog.dox

@ -255,6 +255,11 @@ See also:
@ref ShaderTools::AnyConverter "AnyShaderConverter" plugin and a @ref ShaderTools::AnyConverter "AnyShaderConverter" plugin and a
@ref magnum-shaderconverter "magnum-shaderconverter" utility @ref magnum-shaderconverter "magnum-shaderconverter" utility
@subsubsection changelog-latest-new-texturetools TextureTools library
- New @ref TextureTools::atlasArrayPowerOfTwo() utility for optimal packing
of power-of-two textures into a texture atlas array
@subsubsection changelog-latest-new-trade Trade library @subsubsection changelog-latest-new-trade Trade library
- A new, redesigned @ref Trade::MaterialData class allowing to store custom - A new, redesigned @ref Trade::MaterialData class allowing to store custom

82
src/Magnum/TextureTools/Atlas.cpp

@ -25,7 +25,10 @@
#include "Atlas.h" #include "Atlas.h"
#include <algorithm>
#include <vector> #include <vector>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/Pair.h>
#include "Magnum/Math/Functions.h" #include "Magnum/Math/Functions.h"
#include "Magnum/Math/Range.h" #include "Magnum/Math/Range.h"
@ -61,4 +64,83 @@ std::vector<Range2Di> atlas(const Vector2i& atlasSize, const std::vector<Vector2
return atlas; return atlas;
} }
Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::ArrayView<const Vector2i> sizes) {
CORRADE_ASSERT(layerSize.product() && layerSize.x() == layerSize.y() && (layerSize & (layerSize - Vector2i{1})).isZero(),
"TextureTools::atlasArrayPowerOfTwo(): expected layer size to be a non-zero power-of-two square, got" << Debug::packed << layerSize, {});
if(sizes.isEmpty())
return {};
Containers::Array<Vector3i> output{NoInit, sizes.size()};
/* Copy the input to a sorted array, together with a mapping to the
original order stored in Z. Can't really reuse the output allocation
as it would be overwritten in random order. */
Containers::Array<Vector3i> sortedSizes{NoInit, sizes.size()};
for(std::size_t i = 0; i != sizes.size(); ++i) {
const Vector2i size = sizes[i];
CORRADE_ASSERT(size.product() && size.x() == size.y() && (size & (size - Vector2i{1})).isZero(),
"TextureTools::atlasArrayPowerOfTwo(): expected size" << i << "to be a non-zero power-of-two square, got" << Debug::packed << size, {});
sortedSizes[i].xy() = sizes[i];
sortedSizes[i].z() = i;
}
/* Sort to have the biggest size first. Assuming the items are square,
which is checked below in the loop. It's highly likely there are many
textures of the same size, thus use a stable sort to have output
consistent across platforms */
/** @todo stable_sort allocates, would be great if i could make it reuse
the memory allocated for output */
std::stable_sort(sortedSizes.begin(), sortedSizes.end(), [](const Vector3i& a, const Vector3i& b) {
return a.x() > b.x();
});
/* Start with the whole first layer free */
Int layer = 0;
UnsignedInt free = 1;
Vector2i previousSize = layerSize;
for(const Vector3i& size: sortedSizes) {
/* No free slots left, go to the next layer. Then, what's free, is one
whole layer. */
if(!free) {
++layer;
free = 1;
previousSize = layerSize;
}
/* Multiply number of free slots based on area difference from previous
size. If the size is the same, nothing changes. */
/** @todo there's definitely some bit trick for dividing power-of-two
numbers, use it */
free *= (previousSize/size.xy()).product();
/* Slot index as if the whole layer was consisting just of slots of
this size. */
const UnsignedInt sideSlotCount = layerSize.x()/size.x();
const UnsignedInt layerDepth = Math::log2(sideSlotCount);
const UnsignedInt slotIndex = sideSlotCount*sideSlotCount - free;
/* Calculate coordinates out of the slot index */
Vector2i coordinates;
for(UnsignedInt i = 0; i < layerDepth; ++i) {
if(slotIndex & (1 << 2*(layerDepth - i - 1)))
coordinates.x() += layerSize.x() >> (i + 1);
if(slotIndex & (1 << (2*(layerDepth - i - 1) + 1)))
coordinates.y() += layerSize.y() >> (i + 1);
}
/* Save to the output in the original order */
output[size.z()] = {coordinates, layer};
previousSize = size.xy();
--free;
}
return {layer + 1, std::move(output)};
}
Containers::Pair<Int, Containers::Array<Vector3i>> atlasArrayPowerOfTwo(const Vector2i& layerSize, const std::initializer_list<Vector2i> sizes) {
return atlasArrayPowerOfTwo(layerSize, Containers::arrayView(sizes));
}
}} }}

30
src/Magnum/TextureTools/Atlas.h

@ -26,7 +26,7 @@
*/ */
/** @file /** @file
* @brief Function @ref Magnum::TextureTools::atlas() * @brief Function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo()
*/ */
#include <Corrade/Utility/StlForwardVector.h> #include <Corrade/Utility/StlForwardVector.h>
@ -52,6 +52,34 @@ padding.
*/ */
std::vector<Range2Di> MAGNUM_TEXTURETOOLS_EXPORT atlas(const Vector2i& atlasSize, const std::vector<Vector2i>& sizes, const Vector2i& padding = Vector2i()); std::vector<Range2Di> MAGNUM_TEXTURETOOLS_EXPORT atlas(const Vector2i& atlasSize, const std::vector<Vector2i>& sizes, const Vector2i& padding = Vector2i());
/**
@brief Pack square power-of-two textures into a texture atlas array
@param layerSize Size of the texture layer
@param sizes Sizes of all textures in the atlas
@return Total layer count and offsets of all textures in the atlas, with the Z
coordinate being the layer index
@m_since_latest
Both @p layerSize and all items in @p sizes are expected to be non-zero, square
and power-of-two. With such constraints the packing is optimal with no wasted
space in all but the last layer. Setting @p layerSize to the size of the
largest texture in the set will lead to the least wasted space in the last
layer.
The algorithm first sorts the textures by size using @ref std::stable_sort(),
which is usually @f$ \mathcal{O}(n \log{} n) @f$, and then performs the actual
atlasing in a single @f$ \mathcal{O}(n) @f$ operation. Due to the sort
involved, a temporary allocation holds the sorted array and additionally
@ref std::stable_sort() performs its own allocation.
*/
Containers::Pair<Int, Containers::Array<Vector3i>> MAGNUM_TEXTURETOOLS_EXPORT atlasArrayPowerOfTwo(const Vector2i& layerSize, Containers::ArrayView<const Vector2i> sizes);
/**
* @overload
* @m_since_latest
*/
Containers::Pair<Int, Containers::Array<Vector3i>> MAGNUM_TEXTURETOOLS_EXPORT atlasArrayPowerOfTwo(const Vector2i& layerSize, std::initializer_list<Vector2i> sizes);
}} }}
#endif #endif

21
src/Magnum/TextureTools/CMakeLists.txt

@ -27,7 +27,7 @@
# property that would have to be set on each target separately. # property that would have to be set on each target separately.
set(CMAKE_FOLDER "Magnum/TextureTools") set(CMAKE_FOLDER "Magnum/TextureTools")
set(MagnumTextureTools_SRCS set(MagnumTextureTools_GracefulAssert_SRCS
Atlas.cpp) Atlas.cpp)
set(MagnumTextureTools_HEADERS set(MagnumTextureTools_HEADERS
@ -46,7 +46,7 @@ if(MAGNUM_TARGET_GL)
"CORRADE_AUTOMATIC_FINALIZER=CORRADE_NOOP") "CORRADE_AUTOMATIC_FINALIZER=CORRADE_NOOP")
endif() endif()
list(APPEND MagnumTextureTools_SRCS list(APPEND MagnumTextureTools_GracefulAssert_SRCS
DistanceField.cpp DistanceField.cpp
${MagnumTextureTools_RESOURCES}) ${MagnumTextureTools_RESOURCES})
@ -55,7 +55,7 @@ endif()
# TextureTools library # TextureTools library
add_library(MagnumTextureTools ${SHARED_OR_STATIC} add_library(MagnumTextureTools ${SHARED_OR_STATIC}
${MagnumTextureTools_SRCS} ${MagnumTextureTools_GracefulAssert_SRCS}
${MagnumTextureTools_HEADERS}) ${MagnumTextureTools_HEADERS})
set_target_properties(MagnumTextureTools PROPERTIES DEBUG_POSTFIX "-d") set_target_properties(MagnumTextureTools PROPERTIES DEBUG_POSTFIX "-d")
if(NOT MAGNUM_BUILD_STATIC) if(NOT MAGNUM_BUILD_STATIC)
@ -114,6 +114,21 @@ if(MAGNUM_WITH_DISTANCEFIELDCONVERTER)
endif() endif()
if(MAGNUM_BUILD_TESTS) if(MAGNUM_BUILD_TESTS)
# Library with graceful assert for testing
add_library(MagnumTextureToolsTestLib ${SHARED_OR_STATIC}
${MagnumTextureTools_GracefulAssert_SRCS})
set_target_properties(MagnumTextureToolsTestLib PROPERTIES DEBUG_POSTFIX "-d")
target_compile_definitions(MagnumTextureToolsTestLib PRIVATE
"CORRADE_GRACEFUL_ASSERT" "MagnumTextureTools_EXPORTS")
if(MAGNUM_BUILD_STATIC_PIC)
set_target_properties(MagnumTextureToolsTestLib PROPERTIES POSITION_INDEPENDENT_CODE ON)
endif()
target_link_libraries(MagnumTextureToolsTestLib PUBLIC
Magnum)
if(MAGNUM_TARGET_GL)
target_link_libraries(MagnumTextureToolsTestLib PUBLIC MagnumGL)
endif()
add_subdirectory(Test) add_subdirectory(Test)
endif() endif()

210
src/Magnum/TextureTools/Test/AtlasTest.cpp

@ -25,8 +25,12 @@
#include <sstream> #include <sstream>
#include <vector> #include <vector>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/Pair.h>
#include <Corrade/TestSuite/Tester.h> #include <Corrade/TestSuite/Tester.h>
#include <Corrade/TestSuite/Compare/Container.h>
#include <Corrade/Utility/DebugStl.h> #include <Corrade/Utility/DebugStl.h>
#include <Corrade/Utility/FormatStl.h>
#include "Magnum/Math/Range.h" #include "Magnum/Math/Range.h"
#include "Magnum/TextureTools/Atlas.h" #include "Magnum/TextureTools/Atlas.h"
@ -40,13 +44,56 @@ struct AtlasTest: TestSuite::Tester {
void padding(); void padding();
void empty(); void empty();
void tooSmall(); void tooSmall();
void arrayPowerOfTwoEmpty();
void arrayPowerOfTwoSingleElement();
void arrayPowerOfTwoAllSameElements();
void arrayPowerOfTwoOneLayer();
void arrayPowerOfTwoMoreLayers();
void arrayPowerOfTwoWrongLayerSize();
void arrayPowerOfTwoWrongSize();
};
const struct {
const char* name;
std::size_t order[15];
} ArrayPowerOfTwoOneLayerData[]{
{"sorted",
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14}},
{"",
/* Because there are duplicate sizes, the shuffling needs to preserve
the original order of duplicates to match the output */
{0, 2, 7, 13, 11, 3, 4, 5, 8, 14, 1, 9, 6, 12, 10}},
};
const struct {
const char* name;
Vector2i size;
const char* message;
} ArrayPowerOfTwoWrongSizeData[]{
{"non-power-of-two", {128, 127}, "{128, 127}"},
{"non-square", {128, 256}, "{128, 256}"},
{"zero", {1024, 0}, "{1024, 0}"}
}; };
AtlasTest::AtlasTest() { AtlasTest::AtlasTest() {
addTests({&AtlasTest::basic, addTests({&AtlasTest::basic,
&AtlasTest::padding, &AtlasTest::padding,
&AtlasTest::empty, &AtlasTest::empty,
&AtlasTest::tooSmall}); &AtlasTest::tooSmall,
&AtlasTest::arrayPowerOfTwoEmpty,
&AtlasTest::arrayPowerOfTwoSingleElement,
&AtlasTest::arrayPowerOfTwoAllSameElements});
addInstancedTests({&AtlasTest::arrayPowerOfTwoOneLayer},
Containers::arraySize(ArrayPowerOfTwoOneLayerData));
addTests({&AtlasTest::arrayPowerOfTwoMoreLayers});
addInstancedTests({&AtlasTest::arrayPowerOfTwoWrongLayerSize,
&AtlasTest::arrayPowerOfTwoWrongSize},
Containers::arraySize(ArrayPowerOfTwoWrongSizeData));
} }
void AtlasTest::basic() { void AtlasTest::basic() {
@ -95,6 +142,167 @@ void AtlasTest::tooSmall() {
CORRADE_COMPARE(o.str(), "TextureTools::atlas(): requested atlas size Vector(64, 32) is too small to fit 3 Vector(25, 31) textures. Generated atlas will be empty.\n"); CORRADE_COMPARE(o.str(), "TextureTools::atlas(): requested atlas size Vector(64, 32) is too small to fit 3 Vector(25, 31) textures. Generated atlas will be empty.\n");
} }
void AtlasTest::arrayPowerOfTwoEmpty() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({128, 128}, {});
CORRADE_COMPARE(out.first(), 0);
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
}), TestSuite::Compare::Container);
}
void AtlasTest::arrayPowerOfTwoSingleElement() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({128, 128}, {{128, 128}});
CORRADE_COMPARE(out.first(), 1);
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
{0, 0, 0}
}), TestSuite::Compare::Container);
}
void AtlasTest::arrayPowerOfTwoAllSameElements() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({128, 128}, {
{64, 64},
{64, 64},
{64, 64},
{64, 64},
});
CORRADE_COMPARE(out.first(), 1);
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
{0, 0, 0},
{64, 0, 0},
{0, 64, 0},
{64, 64, 0}
}), TestSuite::Compare::Container);
}
void AtlasTest::arrayPowerOfTwoOneLayer() {
auto&& data = ArrayPowerOfTwoOneLayerData[testCaseInstanceId()];
setTestCaseDescription(data.name);
constexpr std::size_t count = Containers::arraySize(ArrayPowerOfTwoOneLayerData->order);
const Vector2i inputSorted[count]{
{1024, 1024}, /* 0 */
{1024, 1024}, /* 1 */
{512, 512}, /* 2 */
{512, 512}, /* 3 */
{512, 512}, /* 4 */
{512, 512}, /* 5 */
{512, 512}, /* 6 */
{256, 256}, /* 7 */
{256, 256}, /* 8 */
{256, 256}, /* 9 */
{256, 256}, /* 10 */
{128, 128}, /* 11 */
{128, 128}, /* 12 */
{32, 32}, /* 13 */
{32, 32} /* 14 */
};
const Vector3i expectedSorted[count]{
{0, 0, 0},
{1024, 0, 0},
{0, 1024, 0},
{512, 1024, 0},
{0, 1536, 0},
{512, 1536, 0},
{1024, 1024, 0},
{1536, 1024, 0},
{1792, 1024, 0},
{1536, 1280, 0},
{1792, 1280, 0},
{1024, 1536, 0},
{1152, 1536, 0},
{1024, 1664, 0},
{1056, 1664, 0}
};
Vector2i input[count];
Vector3i expected[count];
for(std::size_t i = 0; i != count; ++i) {
input[i] = inputSorted[data.order[i]];
expected[i] = expectedSorted[data.order[i]];
}
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({2048, 2048}, input);
CORRADE_COMPARE(out.first(), 1);
CORRADE_COMPARE_AS(out.second(),
Containers::ArrayView<const Vector3i>{expected},
TestSuite::Compare::Container);
}
void AtlasTest::arrayPowerOfTwoMoreLayers() {
Containers::Pair<Int, Containers::Array<Vector3i>> out = atlasArrayPowerOfTwo({2048, 2048}, {
{2048, 2048},
{1024, 1024},
{1024, 1024},
{1024, 1024},
{512, 512},
{512, 512},
{512, 512},
{512, 512},
{512, 512},
{256, 256},
{256, 256}
});
CORRADE_COMPARE(out.first(), 3);
CORRADE_COMPARE_AS(out.second(), Containers::arrayView<Vector3i>({
{0, 0, 0},
{0, 0, 1},
{1024, 0, 1},
{0, 1024, 1},
{1024, 1024, 1},
{1536, 1024, 1},
{1024, 1536, 1},
{1536, 1536, 1},
{0, 0, 2},
{512, 0, 2},
{768, 0, 2}
}), TestSuite::Compare::Container);
}
void AtlasTest::arrayPowerOfTwoWrongLayerSize() {
auto&& data = ArrayPowerOfTwoWrongSizeData[testCaseInstanceId()];
setTestCaseDescription(data.name);
#ifdef CORRADE_NO_ASSERT
CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions");
#endif
std::ostringstream out;
Error redirectError{&out};
atlasArrayPowerOfTwo(data.size, {});
CORRADE_COMPARE(out.str(), Utility::formatString("TextureTools::atlasArrayPowerOfTwo(): expected layer size to be a non-zero power-of-two square, got {}\n", data.message));
}
void AtlasTest::arrayPowerOfTwoWrongSize() {
auto&& data = ArrayPowerOfTwoWrongSizeData[testCaseInstanceId()];
setTestCaseDescription(data.name);
#ifdef CORRADE_NO_ASSERT
CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions");
#endif
std::ostringstream out;
Error redirectError{&out};
atlasArrayPowerOfTwo({256, 256}, {
{64, 64},
{128, 128},
data.size
});
CORRADE_COMPARE(out.str(), Utility::formatString("TextureTools::atlasArrayPowerOfTwo(): expected size 2 to be a non-zero power-of-two square, got {}\n", data.message));
}
}}}} }}}}
CORRADE_TEST_MAIN(Magnum::TextureTools::Test::AtlasTest) CORRADE_TEST_MAIN(Magnum::TextureTools::Test::AtlasTest)

2
src/Magnum/TextureTools/Test/CMakeLists.txt

@ -27,7 +27,7 @@
# property that would have to be set on each target separately. # property that would have to be set on each target separately.
set(CMAKE_FOLDER "Magnum/TextureTools/Test") set(CMAKE_FOLDER "Magnum/TextureTools/Test")
corrade_add_test(TextureToolsAtlasTest AtlasTest.cpp LIBRARIES MagnumTextureTools) corrade_add_test(TextureToolsAtlasTest AtlasTest.cpp LIBRARIES MagnumTextureToolsTestLib)
if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID) if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID)
set(DISTANCEFIELDGLTEST_FILES_DIR "DistanceFieldGLTestFiles") set(DISTANCEFIELDGLTEST_FILES_DIR "DistanceFieldGLTestFiles")

Loading…
Cancel
Save