From 10bc66884ae156d585018cac67be04e2a2a7c4c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Wed, 27 Sep 2023 13:53:19 +0200 Subject: [PATCH] TextureTools: support padding in the landfill atlas packer. This achieves feature parity with the silly old packer so I can deprecate it. --- src/Magnum/TextureTools/Atlas.cpp | 65 ++++++++-- src/Magnum/TextureTools/Atlas.h | 69 +++++++++- src/Magnum/TextureTools/Test/AtlasTest.cpp | 143 +++++++++++++++++++++ 3 files changed, 261 insertions(+), 16 deletions(-) diff --git a/src/Magnum/TextureTools/Atlas.cpp b/src/Magnum/TextureTools/Atlas.cpp index bba46a0d2..8086c77e8 100644 --- a/src/Magnum/TextureTools/Atlas.cpp +++ b/src/Magnum/TextureTools/Atlas.cpp @@ -81,13 +81,14 @@ struct AtlasLandfillState { /* X = MAX and z = 1 is for 2D unbounded, z = MAX is for 3D unbounded */ Vector3i size; AtlasLandfillFlags flags = AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst; + Vector2i padding; }; } namespace { -bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, const Int slice, const Containers::StridedArrayView1D> sortedFlippedSizes, const Containers::StridedArrayView1D offsets, const Containers::StridedArrayView1D zOffsets) { +bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, const Int slice, const Containers::StridedArrayView1D> sortedFlippedSizes, const Containers::StridedArrayView1D offsets, const Containers::StridedArrayView1D zOffsets, const Containers::BitArrayView rotations) { /* Add a new slice if not there yet, extend the yOffsets array */ if(UnsignedInt(slice) >= state.slices.size()) { CORRADE_INTERNAL_ASSERT(UnsignedInt(slice) == state.slices.size()); @@ -134,13 +135,25 @@ bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, co for(UnsignedShort& yOffset: placementYOffsets) yOffset = newYOffset; + /* Index of this item in the original array */ + const UnsignedInt index = sortedFlippedSizes[i].second(); + + /* Figure out padding of this item. If the size was rotated, rotate it + as well. If the rotations aren't even present, no rotations were + done. */ + const Vector2i padding = !rotations.isEmpty() && rotations[index] ? + state.padding.flipped() : state.padding; + /* Save the position (X-flip it in case we're in reverse direction), - advance to the next X offset */ - offsets[sortedFlippedSizes[i].second()] = { + add the (appropriately rotated) padding to it so it points to the + original unpadded size */ + offsets[index] = padding + Vector2i{ sliceState.direction > 0 ? sliceState.xOffset : state.size.x() - sliceState.xOffset - size.x(), placementYOffset }; + + /* Advance to the next X offset */ sliceState.xOffset += size.x(); } @@ -154,7 +167,7 @@ bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, co if(i < sortedFlippedSizes.size()) { if(slice + 1 == state.size.z()) return false; - return atlasLandfillAddSortedFlipped(state, slice + 1, sortedFlippedSizes.exceptPrefix(i), offsets, zOffsets); + return atlasLandfillAddSortedFlipped(state, slice + 1, sortedFlippedSizes.exceptPrefix(i), offsets, zOffsets, rotations); } /* Everything fit, success */ @@ -180,17 +193,31 @@ bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillSt Containers::Array> sortedFlippedSizes{NoInit, sizes.size()}; for(std::size_t i = 0; i != sizes.size(); ++i) { Vector2i size = sizes[i]; - if((state.flags & AtlasLandfillFlag::RotateLandscape && size.x() < size.y()) || - (state.flags & AtlasLandfillFlag::RotatePortrait && size.x() > size.y())) + #ifndef CORRADE_NO_ASSERT + Vector2i padding = state.padding; + #endif + Vector2i sizePadded = size + 2*state.padding; + if((state.flags & AtlasLandfillFlag::RotateLandscape && sizePadded.x() < sizePadded.y()) || + (state.flags & AtlasLandfillFlag::RotatePortrait && sizePadded.x() > sizePadded.y())) { + #ifndef CORRADE_NO_ASSERT size = size.flipped(); + padding = padding.flipped(); + #endif + sizePadded = sizePadded.flipped(); rotations.set(i); } - CORRADE_ASSERT(size.product() && size <= state.size.xy(), - messagePrefix << "expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size, {}); + #ifndef CORRADE_NO_ASSERT + if(state.padding.isZero()) + CORRADE_ASSERT(size.product() && sizePadded <= state.size.xy(), + messagePrefix << "expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size, {}); + else + CORRADE_ASSERT(size.product() && sizePadded <= state.size.xy(), + messagePrefix << "expected size" << i << "to be non-zero and not larger than" << Debug::packed << state.size.xy() << "but got" << Debug::packed << size << "and padding" << Debug::packed << padding, {}); + #endif - sortedFlippedSizes[i] = {size, UnsignedInt(i)}; + sortedFlippedSizes[i] = {sizePadded, UnsignedInt(i)}; } /* Sort to have the highest first. Assuming the items are square, @@ -216,7 +243,7 @@ bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillSt return a.first().y() > b.first().y(); }); - return atlasLandfillAddSortedFlipped(state, 0, sortedFlippedSizes, offsets, zOffsets); + return atlasLandfillAddSortedFlipped(state, 0, sortedFlippedSizes, offsets, zOffsets, rotations); } } @@ -249,6 +276,15 @@ Vector2i AtlasLandfill::filledSize() const { return {_state->size.x(), Math::max(_state->yOffsets)}; } +Vector2i AtlasLandfill::padding() const { + return _state->padding; +} + +AtlasLandfill& AtlasLandfill::setPadding(const Vector2i& padding) { + _state->padding = padding; + return *this; +} + AtlasLandfillFlags AtlasLandfill::flags() const { return _state->flags; } @@ -309,6 +345,15 @@ Vector3i AtlasLandfillArray::filledSize() const { return {_state->size.xy(), Int(_state->slices.size())}; } +Vector2i AtlasLandfillArray::padding() const { + return _state->padding; +} + +AtlasLandfillArray& AtlasLandfillArray::setPadding(const Vector2i& padding) { + _state->padding = padding; + return *this; +} + AtlasLandfillFlags AtlasLandfillArray::flags() const { return _state->flags; } diff --git a/src/Magnum/TextureTools/Atlas.h b/src/Magnum/TextureTools/Atlas.h index 67e54a39f..de8688f1f 100644 --- a/src/Magnum/TextureTools/Atlas.h +++ b/src/Magnum/TextureTools/Atlas.h @@ -254,6 +254,32 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { return setFlags(this->flags() & ~flags); } + /** + * @brief Padding around each texture + * + * Default is a zero vector. + */ + Vector2i padding() const; + + /** + * @brief Set padding around each texture + * + * Sizes are extended with twice the padding value before placement but + * the returned offsets are without padding again. In order to have + * @ref AtlasLandfillFlag::RotatePortrait and + * @relativeref{AtlasLandfillFlag,RotateLandscape} work well also + * with non-uniform padding, the padding is applied *before* a + * potential rotation. I.e., the horizontal padding value is always + * applied on input image width independently on how it's rotated + * after. If you need different behavior, disable rotations with + * @ref clearFlags() or pre-pad the input sizes directly instead of + * using this function. + * + * Can be called with different values before each particular + * @ref add(). + */ + AtlasLandfill& setPadding(const Vector2i& padding); + /** * @brief Add textures to the atlas * @param[in] sizes Texture sizes @@ -262,20 +288,22 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill { * * The @p sizes, @p offsets and @p rotations views are expected to have * the same size. The @p sizes are all expected to be non-zero and not - * larger than @ref size() after a rotation based on - * @ref AtlasLandfillFlag::RotatePortrait or + * larger than @ref size() after appying padding and then a rotation + * based on @ref AtlasLandfillFlag::RotatePortrait or * @relativeref{AtlasLandfillFlag,RotateLandscape} being set. If * neither @relativeref{AtlasLandfillFlag,RotatePortrait} nor * @relativeref{AtlasLandfillFlag,RotateLandscape} is set, the * @p rotations view can be also empty or you can use the * @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) - * overload. + * overload. The @p offsets always point to the original potentially + * rotated sizes without padding applied. * * On success returns @cpp true @ce and updates @ref filledSize(). If * @ref size() is bounded, can return @cpp false @ce if the items * didn't fit, in which case the internals and contents of @p offsets * and @p rotations are left in an undefined state. For an unbounded * @ref size() returns @cpp true @ce always. + * @see @ref setFlags(), @ref setPadding() */ bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); @@ -409,6 +437,33 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfillArray { return setFlags(this->flags() & ~flags); } + /** + * @brief Padding around each texture + * + * Default is a zero vector. + */ + Vector2i padding() const; + + /** + * @brief Set padding around each texture + * + * Sizes are extended with twice the padding value before placement but + * the returned offsets are without padding again. The third dimension + * isn't treated in any special way. In order to have + * @ref AtlasLandfillFlag::RotatePortrait and + * @relativeref{AtlasLandfillFlag,RotateLandscape} work well also + * with non-uniform padding, the padding is applied *before* a + * potential rotation. I.e., the horizontal padding value is always + * applied on input image width independently on how it's rotated + * after. If you need different behavior, disable rotations with + * @ref clearFlags() or pre-pad the input sizes directly instead of + * using this function. + * + * Can be called with different values before each particular + * @ref add(). + */ + AtlasLandfillArray& setPadding(const Vector2i& padding); + /** * @brief Add textures to the atlas * @param[in] sizes Texture sizes @@ -417,20 +472,22 @@ class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfillArray { * * The @p sizes, @p offsets and @p rotations views are expected to have * the same size. The @p sizes are all expected to be non-zero and not - * larger than @ref size() after a rotation based on - * @ref AtlasLandfillFlag::RotatePortrait or + * larger than @ref size() after applying padding and then a rotation + * based on @ref AtlasLandfillFlag::RotatePortrait or * @relativeref{AtlasLandfillFlag,RotateLandscape} being set. If * neither @relativeref{AtlasLandfillFlag,RotatePortrait} nor * @relativeref{AtlasLandfillFlag,RotateLandscape} is set, the * @p rotations view can be also empty or you can use the * @ref add(const Containers::StridedArrayView1D&, const Containers::StridedArrayView1D&) - * overload. + * overload. The @p offsets always point to the original potentially + * rotated sizes without padding applied. * * On success returns @cpp true @ce and updates @ref filledSize(). If * @ref size() is bounded, can return @cpp false @ce if the items * didn't fit, in which case the internals and contents of @p offsets * and @p rotations are left in an undefined state. For an unbounded * @ref size() returns @cpp true @ce always. + * @see @ref setFlags(), @ref setPadding() */ bool add(const Containers::StridedArrayView1D& sizes, const Containers::StridedArrayView1D& offsets, Containers::MutableBitArrayView rotations); diff --git a/src/Magnum/TextureTools/Test/AtlasTest.cpp b/src/Magnum/TextureTools/Test/AtlasTest.cpp index d0d29f4ff..c126b2d49 100644 --- a/src/Magnum/TextureTools/Test/AtlasTest.cpp +++ b/src/Magnum/TextureTools/Test/AtlasTest.cpp @@ -50,6 +50,7 @@ struct AtlasTest: TestSuite::Tester { void landfillFullFit(); void landfill(); void landfillIncremental(); + void landfillPadded(); void landfillNoFit(); void landfillCopy(); void landfillMove(); @@ -57,6 +58,7 @@ struct AtlasTest: TestSuite::Tester { void landfillArrayFullFit(); void landfillArray(); void landfillArrayIncremental(); + void landfillArrayPadded(); void landfillArrayNoFit(); void landfillArrayCopy(); void landfillArrayMove(); @@ -66,6 +68,7 @@ struct AtlasTest: TestSuite::Tester { void landfillAddMissingRotations(); void landfillAddInvalidViewSizes(); void landfillAddTooLargeElement(); + void landfillAddTooLargeElementPadded(); void basic(); void padding(); @@ -430,6 +433,7 @@ AtlasTest::AtlasTest() { Containers::arraySize(LandfillData)); addTests({&AtlasTest::landfillIncremental, + &AtlasTest::landfillPadded, &AtlasTest::landfillNoFit, &AtlasTest::landfillCopy, &AtlasTest::landfillMove, @@ -440,6 +444,7 @@ AtlasTest::AtlasTest() { Containers::arraySize(LandfillArrayData)); addTests({&AtlasTest::landfillArrayIncremental, + &AtlasTest::landfillArrayPadded, &AtlasTest::landfillArrayNoFit, &AtlasTest::landfillArrayCopy, &AtlasTest::landfillArrayMove, @@ -449,6 +454,7 @@ AtlasTest::AtlasTest() { &AtlasTest::landfillAddMissingRotations, &AtlasTest::landfillAddInvalidViewSizes, &AtlasTest::landfillAddTooLargeElement, + &AtlasTest::landfillAddTooLargeElementPadded, &AtlasTest::basic, &AtlasTest::padding, @@ -496,6 +502,7 @@ void AtlasTest::landfillFullFit() { CORRADE_COMPARE(atlas.size(), (Vector2i{4, 6})); CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 0})); CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(atlas.padding(), Vector2i{}); Vector2i offsets[4]; UnsignedByte rotationData[1]; @@ -639,6 +646,53 @@ void AtlasTest::landfillIncremental() { }), TestSuite::Compare::Container); } +void AtlasTest::landfillPadded() { + AtlasLandfill atlas{{15, 14}}; + atlas.setPadding({1, 2}); + CORRADE_COMPARE(atlas.padding(), (Vector2i{1, 2})); + + Vector2i offsets[6]; + UnsignedByte rotationData[1]; + Containers::MutableBitArrayView rotations{rotationData, 0, 6}; + CORRADE_VERIFY(atlas.add({ + {6, 2}, /* 0, padded to {8, 6}, flipped */ + {1, 3}, /* 1, padded to {3, 7} */ + {4, 1}, /* 2, padded to {6, 5}, flipped */ + {2, 2}, /* 3, padded to {4, 6} */ + {2, 1}, /* 4, padded to {4, 5}, not flipped as padded it's portrait */ + {1, 1}, /* 5, padded to {3, 5} */ + }, offsets, rotations)); + + CORRADE_COMPARE(atlas.filledSize(), (Vector2i{15, 13})); + CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ + true, false, true, false, false, false + }).sliceBit(0), TestSuite::Compare::Container); + + /* ... + ...----.... + 10 .5.----.... + 9 ...-44-.33. + 8 ...----.33. + ______ ----.... + __00__... .... + __00__..._____ + __00__.1.__2__ + __00__.1.__2__ + 2 __00__.1.__2__ + 1 __00__...__2__ + ______..._____ + + 2 5 78 12 */ + CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView({ + { 2, 1}, /* 0 */ + { 7, 2}, /* 1 */ + {11, 1}, /* 2 */ + {12, 8}, /* 3 */ + { 8, 9}, /* 4 */ + { 5, 10} /* 5 */ + }), TestSuite::Compare::Container); +} + void AtlasTest::landfillNoFit() { /* Same as landfill(portrait, widest first) (which is the default flags) which fits into {11, 10} but limiting height to 9 */ @@ -684,6 +738,7 @@ void AtlasTest::landfillArrayFullFit() { CORRADE_COMPARE(atlas.size(), (Vector3i{4, 5, 2})); CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 5, 0})); CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst); + CORRADE_COMPARE(atlas.padding(), Vector2i{}); Vector3i offsets[6]; UnsignedByte rotationData[1]; @@ -818,6 +873,54 @@ void AtlasTest::landfillArrayIncremental() { }), TestSuite::Compare::Container); } +void AtlasTest::landfillArrayPadded() { + /* Like landfillPadded(), but item 5 overlflowing to the next slice */ + + AtlasLandfillArray atlas{{15, 12, 3}}; + atlas.setPadding({1, 2}); + CORRADE_COMPARE(atlas.padding(), (Vector2i{1, 2})); + + Vector3i offsets[6]; + UnsignedByte rotationData[1]; + Containers::MutableBitArrayView rotations{rotationData, 0, 6}; + CORRADE_VERIFY(atlas.add({ + {6, 2}, /* 0, padded to {8, 6}, flipped */ + {1, 3}, /* 1, padded to {3, 7} */ + {4, 1}, /* 2, padded to {6, 5}, flipped */ + {2, 2}, /* 3, padded to {4, 6} */ + {2, 1}, /* 4, padded to {4, 5}, not flipped as padded it's portrait */ + {1, 1}, /* 5, padded to {3, 5} */ + }, offsets, rotations)); + + CORRADE_COMPARE(atlas.filledSize(), (Vector3i{15, 12, 2})); + CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({ + true, false, true, false, false, false + }).sliceBit(0), TestSuite::Compare::Container); + + /* ----.... + ----.... + 9 -44-.33. + 8 ----.33. + ______ ----.... + __00__... .... + __00__..._____ + __00__.1.__2__ ... + __00__.1.__2__ ... + 2 __00__.1.__2__ .5. + 1 __00__...__2__ ... + ______..._____ ... + + 2 5 78 12 1 */ + CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView({ + { 2, 1, 0}, /* 0 */ + { 7, 2, 0}, /* 1 */ + {11, 1, 0}, /* 2 */ + {12, 8, 0}, /* 3 */ + { 8, 9, 0}, /* 4 */ + { 1, 2, 1} /* 5 */ + }), TestSuite::Compare::Container); +} + void AtlasTest::landfillArrayNoFit() { /* Same as landfillArray(portrait, widest first) (which is the default flags) which fits into {11, 6, 2} but limiting depth to 1 */ @@ -983,6 +1086,46 @@ void AtlasTest::landfillAddTooLargeElement() { TestSuite::Compare::String); } +void AtlasTest::landfillAddTooLargeElementPadded() { + /* Sizes (except for zeros) are same as above minus padding */ + + CORRADE_SKIP_IF_NO_ASSERT(); + + /* The atlas makes the sizes portrait first, the array landscape instead */ + AtlasLandfill atlas{{16, 23}}; + AtlasLandfill atlas2{{16, 13}}; + AtlasLandfillArray array{{23, 16, 3}}; + AtlasLandfillArray array2{{13, 16, 3}}; + atlas.setPadding({2, 1}); + atlas2.setPadding({2, 1}); + array.setPadding({1, 2}) + .setFlags(AtlasLandfillFlag::RotateLandscape); + array2.setPadding({1, 2}) + .setFlags(AtlasLandfillFlag::RotateLandscape); + Vector2i offsets[2]; + Vector3i offsets3[2]; + UnsignedByte rotationsData[1]; + Containers::MutableBitArrayView rotations{rotationsData, 0, 2}; + + std::ostringstream out; + Error redirectError{&out}; + atlas.add({{12, 21}, {0, 21}}, offsets, rotations); + array.add({{21, 12}, {21, 0}}, offsets3, rotations); + atlas.add({{12, 21}, {13, 21}}, offsets, rotations); + array.add({{21, 12}, {21, 13}}, offsets3, rotations); + /* Sizes that fit but don't after a flip */ + atlas2.add({{9, 11}, {12, 11}}, offsets, rotations); + array2.add({{11, 9}, {11, 12}}, offsets3, rotations); + CORRADE_COMPARE_AS(out.str(), + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {0, 21} and padding {2, 1}\n" + "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 0} and padding {1, 2}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {13, 21} and padding {2, 1}\n" + "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {21, 13} and padding {1, 2}\n" + "TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 13} but got {11, 12} and padding {1, 2}\n" + "TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {12, 11} and padding {2, 1}\n", + TestSuite::Compare::String); +} + void AtlasTest::basic() { std::vector atlas = TextureTools::atlas({64, 64}, { {12, 18},