Browse Source

TextureTools: new landfill atlas packer.

Just the dumbest possible idea I had, and it compares surprisingly well
in both efficiency (~comparable to stb_rect_pack) and speed
(significantly faster than stb_rect_pack with tons of tiny images,
slower with larger ones -- would probably need to SIMD Math::max() and
such, haha). It's the very first implementation without any additional
improvements I have in mind, so it'll likely improve further.

Includes a benchmark with a bunch of "datasets" extracted from both
fonts and large glTF models. The stb_rect_pack file isn't commited as
it's not useful apart from this single benchmark, put it to
AtlasTestFiles/ and recompile.
pull/168/head
Vladimír Vondruš 3 years ago
parent
commit
66bf0b264d
  1. 38
      doc/generated/atlas.cpp
  2. 78
      doc/snippets/MagnumTextureTools.cpp
  3. 669
      doc/snippets/atlas-landfill.svg
  4. 306
      src/Magnum/TextureTools/Atlas.cpp
  5. 424
      src/Magnum/TextureTools/Atlas.h
  6. 440
      src/Magnum/TextureTools/Test/AtlasBenchmark.cpp
  7. 868
      src/Magnum/TextureTools/Test/AtlasTest.cpp
  8. 1
      src/Magnum/TextureTools/Test/AtlasTestFiles/.gitignore
  9. 56
      src/Magnum/TextureTools/Test/AtlasTestFiles/extract-font-glyph-sizes.py
  10. 67
      src/Magnum/TextureTools/Test/AtlasTestFiles/extract-texture-sizes.py
  11. BIN
      src/Magnum/TextureTools/Test/AtlasTestFiles/fp-102344349-textures.bin
  12. BIN
      src/Magnum/TextureTools/Test/AtlasTestFiles/fp-103997718-171030855-textures.bin
  13. BIN
      src/Magnum/TextureTools/Test/AtlasTestFiles/noto-serif-tangut-glyphs.bin
  14. BIN
      src/Magnum/TextureTools/Test/AtlasTestFiles/oxygen-glyphs.bin
  15. 63
      src/Magnum/TextureTools/Test/CMakeLists.txt

38
doc/generated/atlas.cpp

@ -25,6 +25,9 @@
#include <random> #include <random>
#include <Corrade/Containers/Array.h> #include <Corrade/Containers/Array.h>
#include <Corrade/Containers/BitArray.h>
#include <Corrade/Containers/BitArrayView.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/StridedArrayView.h> #include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/StringStl.h> /** @todo remove once formatString() isn't used */ #include <Corrade/Containers/StringStl.h> /** @todo remove once formatString() isn't used */
#include <Corrade/Utility/FormatStl.h> /** @todo remove once growable String exists */ #include <Corrade/Utility/FormatStl.h> /** @todo remove once growable String exists */
@ -114,5 +117,40 @@ int main() {
)"); )");
CORRADE_INTERNAL_ASSERT_OUTPUT(Utility::Path::write("atlas-array-power-of-two.svg", Containers::StringView{out})); CORRADE_INTERNAL_ASSERT_OUTPUT(Utility::Path::write("atlas-array-power-of-two.svg", Containers::StringView{out}));
/* AtlasLandfill */
} {
constexpr Float displaySizeDivisor = 1.0f;
Containers::Optional<Containers::Array<char>> sizeData = Utility::Path::read(Utility::Path::join(Utility::Path::split(__FILE__).first(), "../../src/Magnum/TextureTools/Test/oxygen-glyphs.bin"));
CORRADE_INTERNAL_ASSERT(sizeData);
const auto sizes = Containers::arrayCast<const Vector2i>(*sizeData);
TextureTools::AtlasLandfill atlas{{512, 512}};
Containers::Array<Vector2i> offsets{NoInit, sizes.size()};
Containers::BitArray rotations{NoInit, sizes.size()};
CORRADE_INTERNAL_ASSERT(atlas.add(sizes, offsets, rotations));
Range2Di viewBox{{}, atlas.filledSize()};
std::string out;
Utility::formatInto(out, out.size(), R"(<svg class="m-image" style="width: {4}px; height: {5}px;" viewBox="{0} {1} {2} {3}" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
)",
viewBox.left(), viewBox.bottom(), viewBox.sizeX(), viewBox.sizeY(), viewBox.sizeX()/displaySizeDivisor, viewBox.sizeY()/displaySizeDivisor);
for(std::size_t i = 0; i != sizes.size(); ++i) {
const Vector2i size = rotations[i] ? sizes[i].flipped() : sizes[i];
const Vector2i offset = offsets[i];
const Color4ub color = DebugTools::ColorMap::turbo()[colorDist(rd)];
Utility::formatInto(out, out.size(), R"( <rect x="{}" y="{}" width="{}" height="{}" style="fill:#{:.2x}{:.2x}{:.2x}"/>
)",
offset.x(), viewBox.sizeY() - size.y() - offset.y(), size.x(), size.y(), color.r(), color.g(), color.b());
}
Utility::formatInto(out, out.size(), R"(</svg>
)");
CORRADE_INTERNAL_ASSERT_OUTPUT(Utility::Path::write("atlas-landfill.svg", Containers::StringView{out}));
} }
} }

78
doc/snippets/MagnumTextureTools.cpp

@ -24,6 +24,8 @@
*/ */
#include <Corrade/Containers/Array.h> #include <Corrade/Containers/Array.h>
#include <Corrade/Containers/BitArray.h>
#include <Corrade/Containers/BitArrayView.h>
#include <Corrade/Containers/StridedArrayView.h> #include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Utility/Algorithms.h> #include <Corrade/Utility/Algorithms.h>
@ -39,6 +41,82 @@
using namespace Magnum; using namespace Magnum;
int main() { int main() {
{
/* [AtlasLandfill-usage] */
Containers::ArrayView<const ImageView2D> images = DOXYGEN_ELLIPSIS({});
Containers::Array<Vector2i> offsets{NoInit, images.size()};
Containers::BitArray rotations{NoInit, images.size()};
/* Fill the atlas with an unbounded height */
TextureTools::AtlasLandfill atlas{{1024, 0}};
atlas.add(stridedArrayView(images).slice(&ImageView2D::size), offsets, rotations);
/* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */
Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize(),
Containers::Array<char>{ValueInit, std::size_t(atlas.filledSize().product())}};
Containers::StridedArrayView2D<Color4ub> dst = output.pixels<Color4ub>();
for(std::size_t i = 0; i != images.size(); ++i) {
/* Rotate 90° counterclockwise if the image is rotated in the atlas */
Containers::StridedArrayView2D<const Color4ub> src = rotations[i] ?
images[i].pixels<Color4ub>().flipped<1>().transposed<0, 1>() :
images[i].pixels<Color4ub>();
Utility::copy(src, dst.sliceSize(
{std::size_t(offsets[i].y()),
std::size_t(offsets[i].x())}, src.size()));
}
/* [AtlasLandfill-usage] */
}
{
Containers::ArrayView<const ImageView2D> images;
Containers::Array<Vector2i> offsets{NoInit, images.size()};
TextureTools::AtlasLandfill atlas{{1024, 0}};
/* [AtlasLandfill-usage-no-rotation] */
atlas.clearFlags(TextureTools::AtlasLandfillFlag::RotatePortrait|
TextureTools::AtlasLandfillFlag::RotateLandscape)
.add(stridedArrayView(images).slice(&ImageView2D::size), offsets);
/* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */
Image2D output{PixelFormat::RGBA8Unorm, atlas.filledSize(),
Containers::Array<char>{ValueInit, std::size_t(atlas.filledSize().product())}};
Containers::StridedArrayView2D<Color4ub> dst = output.pixels<Color4ub>();
for(std::size_t i = 0; i != images.size(); ++i) {
Containers::StridedArrayView2D<const Color4ub> src = images[i].pixels<Color4ub>();
Utility::copy(src, dst.sliceSize(
{std::size_t(offsets[i].y()),
std::size_t(offsets[i].x())}, src.size()));
}
/* [AtlasLandfill-usage-no-rotation] */
}
{
/* [AtlasLandfillArray-usage] */
Containers::ArrayView<const ImageView2D> images = DOXYGEN_ELLIPSIS({});
Containers::Array<Vector3i> offsets{NoInit, images.size()};
Containers::BitArray rotations{NoInit, images.size()};
/* Fill the atlas with an unbounded depth */
TextureTools::AtlasLandfillArray atlas{{1024, 1024, 0}};
atlas.add(stridedArrayView(images).slice(&ImageView2D::size), offsets, rotations);
/* Copy the image data to the atlas, assuming all are RGBA8Unorm as well */
Vector3i outputSize = atlas.filledSize();
Image3D output{PixelFormat::RGBA8Unorm, outputSize,
Containers::Array<char>{ValueInit, std::size_t(outputSize.product())}};
Containers::StridedArrayView3D<Color4ub> dst = output.pixels<Color4ub>();
for(std::size_t i = 0; i != images.size(); ++i) {
/* Rotate 90° counterclockwise if the image is rotated in the atlas */
Containers::StridedArrayView3D<const Color4ub> src = rotations[i] ?
images[i].pixels<Color4ub>().flipped<1>().transposed<0, 1>() :
images[i].pixels<Color4ub>();
Utility::copy(src, dst.sliceSize(
{std::size_t(offsets[i].z()),
std::size_t(offsets[i].y()),
std::size_t(offsets[i].x())}, src.size()));
}
/* [AtlasLandfillArray-usage] */
}
{ {
/* [atlasArrayPowerOfTwo] */ /* [atlasArrayPowerOfTwo] */
Containers::ArrayView<const ImageView2D> input; Containers::ArrayView<const ImageView2D> input;

669
doc/snippets/atlas-landfill.svg

@ -0,0 +1,669 @@
<svg class="m-image" style="width: 512px; height: 151px;" viewBox="0 0 512 151" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<rect x="327" y="10" width="5" height="11" style="fill:#19d5cd"/>
<rect x="473" y="9" width="3" height="12" style="fill:#fea130"/>
<rect x="215" y="6" width="4" height="5" style="fill:#d2e935"/>
<rect x="65" y="31" width="9" height="12" style="fill:#c6f034"/>
<rect x="257" y="100" width="10" height="16" style="fill:#fa7b1f"/>
<rect x="254" y="85" width="12" height="15" style="fill:#27eea4"/>
<rect x="501" y="33" width="11" height="12" style="fill:#faba39"/>
<rect x="247" y="7" width="3" height="4" style="fill:#fcb136"/>
<rect x="413" y="57" width="4" height="14" style="fill:#ef5811"/>
<rect x="422" y="57" width="5" height="14" style="fill:#fb8122"/>
<rect x="101" y="3" width="8" height="8" style="fill:#d83706"/>
<rect x="109" y="3" width="7" height="8" style="fill:#3b2f80"/>
<rect x="271" y="7" width="2" height="4" style="fill:#19e2bb"/>
<rect x="273" y="7" width="2" height="4" style="fill:#dbe236"/>
<rect x="281" y="8" width="3" height="3" style="fill:#bef434"/>
<rect x="433" y="57" width="7" height="14" style="fill:#1ecbda"/>
<rect x="304" y="34" width="10" height="12" style="fill:#b9f635"/>
<rect x="501" y="9" width="5" height="12" style="fill:#fd8d27"/>
<rect x="243" y="21" width="8" height="12" style="fill:#fb7e21"/>
<rect x="251" y="21" width="8" height="12" style="fill:#e7490c"/>
<rect x="56" y="30" width="9" height="12" style="fill:#3aa3fc"/>
<rect x="47" y="30" width="9" height="12" style="fill:#38f491"/>
<rect x="38" y="30" width="9" height="12" style="fill:#455ccf"/>
<rect x="435" y="20" width="7" height="12" style="fill:#c1f334"/>
<rect x="29" y="30" width="9" height="12" style="fill:#3f3b97"/>
<rect x="20" y="30" width="9" height="12" style="fill:#4ef97d"/>
<rect x="92" y="1" width="3" height="9" style="fill:#3e3891"/>
<rect x="320" y="11" width="3" height="11" style="fill:#2eb4f2"/>
<rect x="238" y="11" width="8" height="10" style="fill:#f4c73a"/>
<rect x="123" y="3" width="6" height="8" style="fill:#99fe42"/>
<rect x="230" y="11" width="8" height="10" style="fill:#4af880"/>
<rect x="442" y="20" width="7" height="12" style="fill:#c32503"/>
<rect x="76" y="70" width="14" height="14" style="fill:#f56918"/>
<rect x="490" y="33" width="11" height="12" style="fill:#d9e436"/>
<rect x="294" y="33" width="10" height="12" style="fill:#f46617"/>
<rect x="284" y="33" width="10" height="12" style="fill:#467df4"/>
<rect x="479" y="33" width="11" height="12" style="fill:#f56918"/>
<rect x="259" y="21" width="8" height="12" style="fill:#cc2b04"/>
<rect x="267" y="21" width="8" height="12" style="fill:#df3f08"/>
<rect x="468" y="33" width="11" height="12" style="fill:#ce2d04"/>
<rect x="457" y="33" width="11" height="12" style="fill:#f6c33a"/>
<rect x="470" y="9" width="3" height="12" style="fill:#434eba"/>
<rect x="43" y="68" width="5" height="15" style="fill:#f46617"/>
<rect x="274" y="33" width="10" height="12" style="fill:#f9bc39"/>
<rect x="275" y="21" width="8" height="12" style="fill:#fa7b1f"/>
<rect x="332" y="58" width="12" height="13" style="fill:#dde037"/>
<rect x="446" y="33" width="11" height="12" style="fill:#ac1701"/>
<rect x="320" y="59" width="12" height="13" style="fill:#b7f735"/>
<rect x="11" y="30" width="9" height="12" style="fill:#25c0e7"/>
<rect x="0" y="98" width="13" height="16" style="fill:#e2430a"/>
<rect x="264" y="33" width="10" height="12" style="fill:#880802"/>
<rect x="254" y="33" width="10" height="12" style="fill:#1ecbda"/>
<rect x="283" y="21" width="8" height="12" style="fill:#e2430a"/>
<rect x="244" y="33" width="10" height="12" style="fill:#4099ff"/>
<rect x="435" y="32" width="11" height="12" style="fill:#18d9c8"/>
<rect x="207" y="133" width="12" height="18" style="fill:#cdec34"/>
<rect x="424" y="32" width="11" height="12" style="fill:#4249b1"/>
<rect x="413" y="32" width="11" height="12" style="fill:#33184a"/>
<rect x="2" y="30" width="9" height="12" style="fill:#18dbc5"/>
<rect x="48" y="68" width="5" height="15" style="fill:#99fe42"/>
<rect x="329" y="45" width="7" height="13" style="fill:#b71d02"/>
<rect x="53" y="68" width="5" height="15" style="fill:#d4e735"/>
<rect x="116" y="11" width="8" height="9" style="fill:#fc8725"/>
<rect x="169" y="5" width="2" height="7" style="fill:#fe992c"/>
<rect x="231" y="6" width="3" height="5" style="fill:#cdec34"/>
<rect x="108" y="11" width="8" height="9" style="fill:#27eea4"/>
<rect x="13" y="55" width="9" height="13" style="fill:#341b51"/>
<rect x="100" y="11" width="8" height="9" style="fill:#b91e02"/>
<rect x="4" y="55" width="9" height="13" style="fill:#2cb7f0"/>
<rect x="205" y="12" width="9" height="9" style="fill:#af1801"/>
<rect x="470" y="21" width="6" height="12" style="fill:#20eaac"/>
<rect x="0" y="42" width="9" height="13" style="fill:#b71d02"/>
<rect x="9" y="42" width="9" height="13" style="fill:#1ecbda"/>
<rect x="467" y="9" width="3" height="12" style="fill:#980e01"/>
<rect x="369" y="85" width="5" height="16" style="fill:#46f884"/>
<rect x="225" y="45" width="8" height="13" style="fill:#cbed34"/>
<rect x="406" y="44" width="5" height="13" style="fill:#22c5e2"/>
<rect x="335" y="71" width="9" height="14" style="fill:#b9f635"/>
<rect x="196" y="12" width="9" height="9" style="fill:#2eb4f2"/>
<rect x="291" y="11" width="9" height="10" style="fill:#fa7b1f"/>
<rect x="18" y="42" width="9" height="13" style="fill:#b4f836"/>
<rect x="27" y="42" width="9" height="13" style="fill:#4ef97d"/>
<rect x="78" y="1" width="5" height="9" style="fill:#36215f"/>
<rect x="92" y="10" width="8" height="9" style="fill:#351e58"/>
<rect x="476" y="21" width="6" height="12" style="fill:#32f298"/>
<rect x="187" y="12" width="9" height="9" style="fill:#e4450a"/>
<rect x="282" y="11" width="9" height="10" style="fill:#455ed3"/>
<rect x="36" y="42" width="9" height="13" style="fill:#33184a"/>
<rect x="84" y="10" width="8" height="9" style="fill:#fcb136"/>
<rect x="233" y="45" width="8" height="13" style="fill:#f76f1a"/>
<rect x="60" y="0" width="6" height="9" style="fill:#dc3b07"/>
<rect x="364" y="85" width="5" height="16" style="fill:#4294ff"/>
<rect x="338" y="85" width="3" height="16" style="fill:#fb8122"/>
<rect x="359" y="85" width="5" height="16" style="fill:#e14109"/>
<rect x="88" y="1" width="4" height="9" style="fill:#4146ac"/>
<rect x="464" y="9" width="3" height="12" style="fill:#e9d539"/>
<rect x="291" y="21" width="8" height="12" style="fill:#f8721c"/>
<rect x="299" y="21" width="8" height="12" style="fill:#4771e9"/>
<rect x="418" y="9" width="9" height="11" style="fill:#a4fc3c"/>
<rect x="0" y="18" width="9" height="12" style="fill:#b1f936"/>
<rect x="73" y="69" width="3" height="15" style="fill:#3f3b97"/>
<rect x="336" y="45" width="7" height="13" style="fill:#3e3891"/>
<rect x="201" y="6" width="2" height="6" style="fill:#d2e935"/>
<rect x="90" y="70" width="14" height="14" style="fill:#3aa3fc"/>
<rect x="176" y="6" width="4" height="6" style="fill:#4143a7"/>
<rect x="135" y="4" width="6" height="7" style="fill:#88ff4e"/>
<rect x="217" y="11" width="5" height="10" style="fill:#b21a01"/>
<rect x="129" y="3" width="2" height="8" style="fill:#fe962b"/>
<rect x="104" y="70" width="14" height="14" style="fill:#1ad2d2"/>
<rect x="203" y="6" width="2" height="6" style="fill:#f15d13"/>
<rect x="141" y="4" width="6" height="7" style="fill:#9b0f01"/>
<rect x="222" y="11" width="8" height="10" style="fill:#351e58"/>
<rect x="158" y="5" width="4" height="7" style="fill:#eecf3a"/>
<rect x="162" y="5" width="4" height="7" style="fill:#4682f8"/>
<rect x="250" y="7" width="3" height="4" style="fill:#2fb2f4"/>
<rect x="479" y="71" width="8" height="14" style="fill:#d43305"/>
<rect x="284" y="8" width="3" height="3" style="fill:#e84b0c"/>
<rect x="205" y="7" width="5" height="5" style="fill:#30123b"/>
<rect x="166" y="5" width="3" height="7" style="fill:#9e1001"/>
<rect x="171" y="6" width="5" height="6" style="fill:#1ad4d0"/>
<rect x="147" y="5" width="6" height="7" style="fill:#f05b12"/>
<rect x="308" y="59" width="12" height="13" style="fill:#99fe42"/>
<rect x="118" y="71" width="13" height="14" style="fill:#7a0403"/>
<rect x="296" y="59" width="12" height="13" style="fill:#fea732"/>
<rect x="449" y="21" width="7" height="12" style="fill:#efcd3a"/>
<rect x="389" y="134" width="11" height="17" style="fill:#cbed34"/>
<rect x="400" y="134" width="11" height="17" style="fill:#affa37"/>
<rect x="411" y="134" width="11" height="17" style="fill:#75fe5c"/>
<rect x="183" y="85" width="11" height="15" style="fill:#20eaac"/>
<rect x="242" y="85" width="12" height="15" style="fill:#a71401"/>
<rect x="172" y="85" width="11" height="15" style="fill:#950d01"/>
<rect x="13" y="98" width="13" height="16" style="fill:#4685fa"/>
<rect x="267" y="100" width="10" height="16" style="fill:#a71401"/>
<rect x="171" y="116" width="8" height="17" style="fill:#ce2d04"/>
<rect x="163" y="116" width="8" height="17" style="fill:#8b0902"/>
<rect x="155" y="116" width="8" height="17" style="fill:#8b0902"/>
<rect x="13" y="83" width="8" height="15" style="fill:#a1fd3d"/>
<rect x="44" y="114" width="5" height="17" style="fill:#458cfd"/>
<rect x="39" y="114" width="5" height="17" style="fill:#458afc"/>
<rect x="34" y="114" width="5" height="17" style="fill:#455ccf"/>
<rect x="31" y="68" width="6" height="15" style="fill:#d83706"/>
<rect x="439" y="45" width="12" height="12" style="fill:#18dbc5"/>
<rect x="161" y="85" width="11" height="15" style="fill:#2fb2f4"/>
<rect x="286" y="134" width="13" height="17" style="fill:#a4fc3c"/>
<rect x="299" y="134" width="13" height="17" style="fill:#df3f08"/>
<rect x="312" y="134" width="13" height="17" style="fill:#71fe5f"/>
<rect x="292" y="86" width="13" height="15" style="fill:#a41301"/>
<rect x="279" y="86" width="13" height="15" style="fill:#a91601"/>
<rect x="116" y="3" width="7" height="8" style="fill:#35f394"/>
<rect x="325" y="134" width="13" height="17" style="fill:#4af880"/>
<rect x="414" y="117" width="10" height="17" style="fill:#3ff68a"/>
<rect x="404" y="117" width="10" height="17" style="fill:#27bee9"/>
<rect x="394" y="117" width="10" height="17" style="fill:#d02f05"/>
<rect x="118" y="85" width="10" height="15" style="fill:#27eea4"/>
<rect x="422" y="134" width="11" height="17" style="fill:#4682f8"/>
<rect x="234" y="33" width="10" height="12" style="fill:#e2430a"/>
<rect x="224" y="33" width="10" height="12" style="fill:#ec530f"/>
<rect x="241" y="45" width="8" height="13" style="fill:#4778f0"/>
<rect x="249" y="45" width="8" height="13" style="fill:#a41301"/>
<rect x="257" y="45" width="8" height="13" style="fill:#ce2d04"/>
<rect x="307" y="22" width="8" height="12" style="fill:#3e9bfe"/>
<rect x="315" y="22" width="8" height="12" style="fill:#df3f08"/>
<rect x="265" y="45" width="8" height="13" style="fill:#f7c13a"/>
<rect x="45" y="42" width="9" height="13" style="fill:#3d358b"/>
<rect x="343" y="45" width="7" height="13" style="fill:#4af880"/>
<rect x="344" y="71" width="9" height="14" style="fill:#e7490c"/>
<rect x="353" y="71" width="9" height="14" style="fill:#3b2f80"/>
<rect x="362" y="71" width="9" height="14" style="fill:#372466"/>
<rect x="9" y="18" width="9" height="12" style="fill:#3e9bfe"/>
<rect x="411" y="44" width="5" height="13" style="fill:#acfb38"/>
<rect x="416" y="44" width="5" height="13" style="fill:#ebd339"/>
<rect x="421" y="44" width="5" height="13" style="fill:#d7e535"/>
<rect x="496" y="9" width="5" height="12" style="fill:#fbb637"/>
<rect x="92" y="57" width="10" height="13" style="fill:#f46617"/>
<rect x="18" y="18" width="9" height="12" style="fill:#2eb4f2"/>
<rect x="255" y="71" width="10" height="14" style="fill:#eecf3a"/>
<rect x="265" y="71" width="10" height="14" style="fill:#a9fb39"/>
<rect x="275" y="71" width="10" height="14" style="fill:#28bceb"/>
<rect x="214" y="33" width="10" height="12" style="fill:#4143a7"/>
<rect x="204" y="33" width="10" height="12" style="fill:#980e01"/>
<rect x="76" y="10" width="8" height="9" style="fill:#458cfd"/>
<rect x="82" y="57" width="10" height="13" style="fill:#f8be39"/>
<rect x="371" y="71" width="9" height="14" style="fill:#1fc9dd"/>
<rect x="380" y="71" width="9" height="14" style="fill:#23c3e4"/>
<rect x="389" y="71" width="9" height="14" style="fill:#4778f0"/>
<rect x="27" y="18" width="9" height="12" style="fill:#4294ff"/>
<rect x="147" y="116" width="8" height="17" style="fill:#458afc"/>
<rect x="387" y="101" width="9" height="16" style="fill:#434eba"/>
<rect x="486" y="85" width="8" height="16" style="fill:#ed5510"/>
<rect x="183" y="71" width="12" height="14" style="fill:#f15d13"/>
<rect x="356" y="10" width="8" height="11" style="fill:#18dec0"/>
<rect x="143" y="100" width="12" height="16" style="fill:#d9e436"/>
<rect x="323" y="21" width="8" height="12" style="fill:#880802"/>
<rect x="191" y="100" width="11" height="16" style="fill:#3e3891"/>
<rect x="273" y="45" width="8" height="13" style="fill:#e3db38"/>
<rect x="384" y="117" width="10" height="17" style="fill:#fcb136"/>
<rect x="487" y="71" width="8" height="14" style="fill:#e7490c"/>
<rect x="374" y="117" width="10" height="17" style="fill:#7dff56"/>
<rect x="495" y="71" width="8" height="14" style="fill:#fea130"/>
<rect x="277" y="101" width="10" height="16" style="fill:#fb7e21"/>
<rect x="331" y="21" width="8" height="12" style="fill:#4040a2"/>
<rect x="364" y="117" width="10" height="17" style="fill:#880802"/>
<rect x="503" y="71" width="8" height="14" style="fill:#b91e02"/>
<rect x="433" y="134" width="11" height="17" style="fill:#27eea4"/>
<rect x="451" y="45" width="12" height="12" style="fill:#33184a"/>
<rect x="463" y="45" width="12" height="12" style="fill:#27eea4"/>
<rect x="54" y="42" width="9" height="13" style="fill:#2cb7f0"/>
<rect x="504" y="57" width="8" height="14" style="fill:#4661d6"/>
<rect x="409" y="9" width="9" height="11" style="fill:#28bceb"/>
<rect x="478" y="85" width="8" height="16" style="fill:#b41b01"/>
<rect x="36" y="18" width="9" height="12" style="fill:#8b0902"/>
<rect x="470" y="85" width="8" height="16" style="fill:#fe932a"/>
<rect x="45" y="18" width="9" height="12" style="fill:#d2e935"/>
<rect x="396" y="101" width="9" height="16" style="fill:#3d9efe"/>
<rect x="63" y="43" width="9" height="13" style="fill:#d4e735"/>
<rect x="139" y="116" width="8" height="17" style="fill:#e2430a"/>
<rect x="398" y="71" width="9" height="14" style="fill:#d63506"/>
<rect x="444" y="134" width="11" height="17" style="fill:#1bd0d5"/>
<rect x="215" y="116" width="9" height="17" style="fill:#1fc9dd"/>
<rect x="202" y="100" width="11" height="16" style="fill:#1ae4b6"/>
<rect x="405" y="101" width="9" height="16" style="fill:#4454c3"/>
<rect x="213" y="100" width="11" height="16" style="fill:#ebd339"/>
<rect x="414" y="101" width="9" height="16" style="fill:#f56918"/>
<rect x="455" y="134" width="11" height="17" style="fill:#cc2b04"/>
<rect x="243" y="133" width="9" height="18" style="fill:#476ee6"/>
<rect x="466" y="134" width="11" height="17" style="fill:#19e3b9"/>
<rect x="407" y="71" width="9" height="14" style="fill:#df3f08"/>
<rect x="402" y="32" width="11" height="12" style="fill:#b71d02"/>
<rect x="54" y="18" width="9" height="12" style="fill:#bc2002"/>
<rect x="58" y="69" width="5" height="15" style="fill:#f2c93a"/>
<rect x="482" y="21" width="6" height="12" style="fill:#affa37"/>
<rect x="409" y="57" width="4" height="14" style="fill:#fe9e2f"/>
<rect x="323" y="10" width="4" height="11" style="fill:#2eb4f2"/>
<rect x="354" y="85" width="5" height="16" style="fill:#9e1001"/>
<rect x="491" y="9" width="5" height="12" style="fill:#8e0a01"/>
<rect x="345" y="85" width="4" height="16" style="fill:#1ccdd8"/>
<rect x="341" y="85" width="4" height="16" style="fill:#8fff49"/>
<rect x="335" y="85" width="3" height="16" style="fill:#c6f034"/>
<rect x="95" y="1" width="3" height="9" style="fill:#eecf3a"/>
<rect x="24" y="68" width="7" height="15" style="fill:#52fa7a"/>
<rect x="399" y="85" width="7" height="16" style="fill:#9ffd3f"/>
<rect x="42" y="131" width="6" height="20" style="fill:#1ad2d2"/>
<rect x="61" y="115" width="6" height="17" style="fill:#3e3891"/>
<rect x="354" y="117" width="10" height="17" style="fill:#18ddc2"/>
<rect x="252" y="133" width="8" height="18" style="fill:#4099ff"/>
<rect x="68" y="9" width="8" height="9" style="fill:#9ffd3f"/>
<rect x="131" y="116" width="8" height="17" style="fill:#a4fc3c"/>
<rect x="55" y="115" width="6" height="17" style="fill:#950d01"/>
<rect x="123" y="116" width="8" height="17" style="fill:#dde037"/>
<rect x="260" y="133" width="5" height="18" style="fill:#d23105"/>
<rect x="339" y="21" width="8" height="12" style="fill:#a71401"/>
<rect x="488" y="21" width="6" height="12" style="fill:#2eb4f2"/>
<rect x="347" y="21" width="8" height="12" style="fill:#2cb7f0"/>
<rect x="456" y="21" width="7" height="12" style="fill:#22c5e2"/>
<rect x="63" y="18" width="9" height="12" style="fill:#4294ff"/>
<rect x="364" y="45" width="6" height="13" style="fill:#bef434"/>
<rect x="477" y="134" width="11" height="17" style="fill:#25eca7"/>
<rect x="72" y="43" width="9" height="13" style="fill:#18ddc2"/>
<rect x="488" y="134" width="11" height="17" style="fill:#19e3b9"/>
<rect x="416" y="71" width="9" height="14" style="fill:#3cf58e"/>
<rect x="499" y="134" width="11" height="17" style="fill:#f5c53a"/>
<rect x="81" y="43" width="9" height="13" style="fill:#4773eb"/>
<rect x="108" y="84" width="10" height="15" style="fill:#30123b"/>
<rect x="72" y="19" width="9" height="12" style="fill:#950d01"/>
<rect x="266" y="85" width="13" height="15" style="fill:#ce2d04"/>
<rect x="437" y="9" width="10" height="11" style="fill:#8b0902"/>
<rect x="26" y="98" width="13" height="16" style="fill:#980e01"/>
<rect x="194" y="33" width="10" height="12" style="fill:#9e1001"/>
<rect x="39" y="98" width="13" height="16" style="fill:#920b01"/>
<rect x="72" y="56" width="10" height="13" style="fill:#38f491"/>
<rect x="230" y="85" width="12" height="15" style="fill:#476ee6"/>
<rect x="39" y="83" width="9" height="15" style="fill:#c6f034"/>
<rect x="344" y="117" width="10" height="17" style="fill:#ed5510"/>
<rect x="370" y="45" width="6" height="13" style="fill:#cc2b04"/>
<rect x="334" y="117" width="10" height="17" style="fill:#a4fc3c"/>
<rect x="417" y="57" width="5" height="14" style="fill:#455ed3"/>
<rect x="324" y="117" width="10" height="17" style="fill:#ed5510"/>
<rect x="376" y="45" width="6" height="13" style="fill:#1ecbda"/>
<rect x="314" y="117" width="10" height="17" style="fill:#bef434"/>
<rect x="496" y="57" width="8" height="14" style="fill:#f15d13"/>
<rect x="304" y="117" width="10" height="17" style="fill:#2eb4f2"/>
<rect x="488" y="57" width="8" height="14" style="fill:#18d9c8"/>
<rect x="287" y="101" width="10" height="16" style="fill:#f66c19"/>
<rect x="281" y="45" width="8" height="13" style="fill:#476ee6"/>
<rect x="294" y="117" width="10" height="17" style="fill:#2ab9ee"/>
<rect x="480" y="57" width="8" height="14" style="fill:#f5c53a"/>
<rect x="462" y="85" width="8" height="16" style="fill:#f7c13a"/>
<rect x="386" y="85" width="6" height="16" style="fill:#bcf534"/>
<rect x="115" y="116" width="8" height="17" style="fill:#1fe9af"/>
<rect x="81" y="19" width="9" height="12" style="fill:#e7490c"/>
<rect x="355" y="21" width="8" height="12" style="fill:#4454c3"/>
<rect x="494" y="21" width="6" height="12" style="fill:#3aa3fc"/>
<rect x="98" y="84" width="10" height="15" style="fill:#392a73"/>
<rect x="90" y="19" width="9" height="12" style="fill:#9cfe40"/>
<rect x="88" y="84" width="10" height="15" style="fill:#2fb2f4"/>
<rect x="400" y="9" width="9" height="11" style="fill:#fb7e21"/>
<rect x="297" y="101" width="10" height="16" style="fill:#e5d938"/>
<rect x="99" y="20" width="9" height="12" style="fill:#351e58"/>
<rect x="284" y="117" width="10" height="17" style="fill:#f66c19"/>
<rect x="425" y="71" width="9" height="14" style="fill:#27eea4"/>
<rect x="307" y="101" width="10" height="16" style="fill:#46f884"/>
<rect x="90" y="44" width="9" height="13" style="fill:#a1fd3d"/>
<rect x="317" y="101" width="10" height="16" style="fill:#b4f836"/>
<rect x="99" y="44" width="9" height="13" style="fill:#ecd13a"/>
<rect x="102" y="133" width="17" height="18" style="fill:#ebd339"/>
<rect x="396" y="58" width="13" height="13" style="fill:#23c3e4"/>
<rect x="501" y="117" width="11" height="17" style="fill:#d9e436"/>
<rect x="107" y="116" width="8" height="17" style="fill:#20c7df"/>
<rect x="78" y="84" width="10" height="15" style="fill:#27bee9"/>
<rect x="206" y="116" width="9" height="17" style="fill:#1ce6b4"/>
<rect x="350" y="45" width="7" height="13" style="fill:#4451bf"/>
<rect x="423" y="101" width="9" height="16" style="fill:#1ecbda"/>
<rect x="500" y="21" width="6" height="12" style="fill:#bcf534"/>
<rect x="197" y="116" width="9" height="17" style="fill:#ec530f"/>
<rect x="382" y="45" width="6" height="13" style="fill:#8b0902"/>
<rect x="475" y="45" width="12" height="12" style="fill:#efcd3a"/>
<rect x="392" y="85" width="7" height="16" style="fill:#27eea4"/>
<rect x="195" y="71" width="12" height="14" style="fill:#eb500e"/>
<rect x="391" y="9" width="9" height="11" style="fill:#27bee9"/>
<rect x="284" y="58" width="12" height="13" style="fill:#a11201"/>
<rect x="382" y="10" width="9" height="11" style="fill:#59fb73"/>
<rect x="13" y="131" width="17" height="20" style="fill:#61fc6c"/>
<rect x="168" y="133" width="13" height="18" style="fill:#448ffe"/>
<rect x="52" y="99" width="13" height="16" style="fill:#3b2f80"/>
<rect x="150" y="85" width="11" height="15" style="fill:#fa7b1f"/>
<rect x="224" y="100" width="11" height="16" style="fill:#e5470b"/>
<rect x="99" y="115" width="8" height="17" style="fill:#dfdf37"/>
<rect x="320" y="86" width="15" height="15" style="fill:#f1cb3a"/>
<rect x="2" y="114" width="15" height="16" style="fill:#f5c53a"/>
<rect x="155" y="100" width="12" height="16" style="fill:#af1801"/>
<rect x="490" y="117" width="11" height="17" style="fill:#f5c53a"/>
<rect x="289" y="45" width="8" height="13" style="fill:#69fd66"/>
<rect x="29" y="114" width="5" height="17" style="fill:#ef5811"/>
<rect x="426" y="44" width="5" height="13" style="fill:#466be3"/>
<rect x="338" y="134" width="13" height="17" style="fill:#f9781e"/>
<rect x="285" y="72" width="10" height="14" style="fill:#28bceb"/>
<rect x="274" y="117" width="10" height="17" style="fill:#3ba0fd"/>
<rect x="434" y="71" width="9" height="14" style="fill:#43f787"/>
<rect x="264" y="116" width="10" height="17" style="fill:#4666dd"/>
<rect x="443" y="71" width="9" height="14" style="fill:#f76f1a"/>
<rect x="72" y="132" width="10" height="19" style="fill:#faba39"/>
<rect x="432" y="101" width="9" height="16" style="fill:#e7490c"/>
<rect x="82" y="132" width="10" height="19" style="fill:#b4f836"/>
<rect x="441" y="101" width="9" height="16" style="fill:#b4f836"/>
<rect x="92" y="132" width="10" height="19" style="fill:#a4fc3c"/>
<rect x="450" y="101" width="9" height="16" style="fill:#ce2d04"/>
<rect x="377" y="134" width="12" height="17" style="fill:#f1cb3a"/>
<rect x="472" y="57" width="8" height="14" style="fill:#43f787"/>
<rect x="305" y="86" width="15" height="15" style="fill:#38f491"/>
<rect x="201" y="58" width="11" height="13" style="fill:#a4fc3c"/>
<rect x="479" y="117" width="11" height="17" style="fill:#d63506"/>
<rect x="188" y="116" width="9" height="17" style="fill:#fe992c"/>
<rect x="254" y="116" width="10" height="17" style="fill:#3aa3fc"/>
<rect x="454" y="85" width="8" height="16" style="fill:#4040a2"/>
<rect x="65" y="99" width="13" height="16" style="fill:#3e3891"/>
<rect x="62" y="56" width="10" height="13" style="fill:#bcf534"/>
<rect x="181" y="133" width="13" height="18" style="fill:#424bb5"/>
<rect x="68" y="84" width="10" height="15" style="fill:#2aefa1"/>
<rect x="24" y="114" width="5" height="17" style="fill:#71fe5f"/>
<rect x="30" y="131" width="12" height="20" style="fill:#f6c33a"/>
<rect x="219" y="133" width="12" height="18" style="fill:#b4f836"/>
<rect x="78" y="99" width="13" height="16" style="fill:#18d7ca"/>
<rect x="468" y="117" width="11" height="17" style="fill:#e5470b"/>
<rect x="179" y="116" width="9" height="17" style="fill:#fcb336"/>
<rect x="457" y="117" width="11" height="17" style="fill:#4682f8"/>
<rect x="108" y="44" width="9" height="13" style="fill:#b71d02"/>
<rect x="61" y="132" width="11" height="19" style="fill:#477bf2"/>
<rect x="91" y="115" width="8" height="17" style="fill:#3a2d79"/>
<rect x="270" y="134" width="16" height="17" style="fill:#c3f134"/>
<rect x="131" y="71" width="13" height="14" style="fill:#d0ea34"/>
<rect x="0" y="130" width="13" height="21" style="fill:#8bff4b"/>
<rect x="244" y="116" width="10" height="17" style="fill:#4666dd"/>
<rect x="235" y="100" width="11" height="16" style="fill:#affa37"/>
<rect x="297" y="46" width="8" height="13" style="fill:#75fe5c"/>
<rect x="218" y="85" width="12" height="15" style="fill:#2eb4f2"/>
<rect x="363" y="21" width="8" height="12" style="fill:#4040a2"/>
<rect x="446" y="85" width="8" height="16" style="fill:#c82803"/>
<rect x="117" y="44" width="9" height="13" style="fill:#a1fd3d"/>
<rect x="5" y="83" width="8" height="15" style="fill:#4ef97d"/>
<rect x="108" y="20" width="9" height="12" style="fill:#392a73"/>
<rect x="349" y="85" width="5" height="16" style="fill:#1ecbda"/>
<rect x="388" y="45" width="6" height="13" style="fill:#f26014"/>
<rect x="63" y="69" width="5" height="15" style="fill:#3d9efe"/>
<rect x="486" y="9" width="5" height="12" style="fill:#810602"/>
<rect x="91" y="99" width="13" height="16" style="fill:#ac1701"/>
<rect x="52" y="55" width="10" height="13" style="fill:#96fe44"/>
<rect x="104" y="99" width="13" height="16" style="fill:#434eba"/>
<rect x="184" y="33" width="10" height="12" style="fill:#e5470b"/>
<rect x="327" y="101" width="10" height="16" style="fill:#3f3e9c"/>
<rect x="394" y="45" width="6" height="13" style="fill:#1fe9af"/>
<rect x="58" y="84" width="10" height="15" style="fill:#a1fd3d"/>
<rect x="481" y="9" width="5" height="12" style="fill:#980e01"/>
<rect x="337" y="101" width="10" height="16" style="fill:#3d9efe"/>
<rect x="126" y="45" width="9" height="13" style="fill:#acfb38"/>
<rect x="347" y="101" width="10" height="16" style="fill:#466be3"/>
<rect x="117" y="20" width="9" height="12" style="fill:#4669e0"/>
<rect x="234" y="116" width="10" height="17" style="fill:#1ae4b6"/>
<rect x="464" y="57" width="8" height="14" style="fill:#1ccdd8"/>
<rect x="83" y="115" width="8" height="17" style="fill:#18d7ca"/>
<rect x="49" y="115" width="6" height="17" style="fill:#4391fe"/>
<rect x="446" y="117" width="11" height="17" style="fill:#4249b1"/>
<rect x="459" y="101" width="9" height="16" style="fill:#fe962b"/>
<rect x="167" y="100" width="12" height="16" style="fill:#4454c3"/>
<rect x="371" y="21" width="8" height="12" style="fill:#f8721c"/>
<rect x="438" y="85" width="8" height="16" style="fill:#4685fa"/>
<rect x="135" y="45" width="9" height="13" style="fill:#4040a2"/>
<rect x="351" y="134" width="13" height="17" style="fill:#ecd13a"/>
<rect x="295" y="72" width="10" height="14" style="fill:#fdae35"/>
<rect x="364" y="134" width="13" height="17" style="fill:#3f3e9c"/>
<rect x="305" y="72" width="10" height="14" style="fill:#b4f836"/>
<rect x="117" y="100" width="13" height="16" style="fill:#31aff5"/>
<rect x="174" y="33" width="10" height="12" style="fill:#efcd3a"/>
<rect x="194" y="133" width="13" height="18" style="fill:#4661d6"/>
<rect x="315" y="72" width="10" height="14" style="fill:#ec530f"/>
<rect x="325" y="72" width="10" height="14" style="fill:#f46617"/>
<rect x="0" y="68" width="8" height="15" style="fill:#35f394"/>
<rect x="431" y="44" width="5" height="13" style="fill:#2cf09e"/>
<rect x="178" y="12" width="9" height="9" style="fill:#80ff53"/>
<rect x="275" y="7" width="2" height="4" style="fill:#fe9e2f"/>
<rect x="192" y="6" width="3" height="6" style="fill:#980e01"/>
<rect x="195" y="6" width="3" height="6" style="fill:#dd3d08"/>
<rect x="253" y="7" width="3" height="4" style="fill:#acfb38"/>
<rect x="287" y="8" width="3" height="3" style="fill:#3ba0fd"/>
<rect x="219" y="6" width="4" height="5" style="fill:#43f787"/>
<rect x="239" y="7" width="4" height="4" style="fill:#99fe42"/>
<rect x="237" y="6" width="2" height="5" style="fill:#455ed3"/>
<rect x="234" y="6" width="3" height="5" style="fill:#ec530f"/>
<rect x="290" y="8" width="3" height="3" style="fill:#4771e9"/>
<rect x="198" y="6" width="3" height="6" style="fill:#69fd66"/>
<rect x="256" y="7" width="3" height="4" style="fill:#fd8d27"/>
<rect x="243" y="7" width="4" height="4" style="fill:#fe962b"/>
<rect x="259" y="7" width="3" height="4" style="fill:#c52603"/>
<rect x="293" y="8" width="3" height="3" style="fill:#f9751d"/>
<rect x="296" y="8" width="3" height="3" style="fill:#476ee6"/>
<rect x="299" y="8" width="3" height="3" style="fill:#7a0403"/>
<rect x="302" y="8" width="3" height="3" style="fill:#f56918"/>
<rect x="305" y="8" width="3" height="3" style="fill:#4143a7"/>
<rect x="308" y="8" width="3" height="3" style="fill:#65fd69"/>
<rect x="311" y="9" width="3" height="3" style="fill:#fe992c"/>
<rect x="314" y="9" width="3" height="3" style="fill:#96fe44"/>
<rect x="317" y="9" width="3" height="3" style="fill:#b9f635"/>
<rect x="98" y="1" width="3" height="9" style="fill:#ec530f"/>
<rect x="190" y="58" width="11" height="13" style="fill:#f15d13"/>
<rect x="42" y="55" width="10" height="13" style="fill:#a41301"/>
<rect x="305" y="46" width="8" height="13" style="fill:#4669e0"/>
<rect x="179" y="58" width="11" height="13" style="fill:#fe9e2f"/>
<rect x="456" y="57" width="8" height="14" style="fill:#4559cb"/>
<rect x="144" y="45" width="9" height="13" style="fill:#faba39"/>
<rect x="168" y="58" width="11" height="13" style="fill:#4664da"/>
<rect x="320" y="8" width="3" height="3" style="fill:#3cf58e"/>
<rect x="436" y="44" width="3" height="13" style="fill:#fe932a"/>
<rect x="32" y="55" width="10" height="13" style="fill:#18d7ca"/>
<rect x="157" y="58" width="11" height="13" style="fill:#22c5e2"/>
<rect x="383" y="58" width="13" height="13" style="fill:#f5c53a"/>
<rect x="146" y="58" width="11" height="13" style="fill:#dde037"/>
<rect x="323" y="7" width="3" height="3" style="fill:#1fc9dd"/>
<rect x="144" y="71" width="13" height="14" style="fill:#d0ea34"/>
<rect x="135" y="58" width="11" height="13" style="fill:#fe932a"/>
<rect x="153" y="45" width="9" height="13" style="fill:#ce2d04"/>
<rect x="448" y="57" width="8" height="14" style="fill:#65fd69"/>
<rect x="313" y="46" width="8" height="13" style="fill:#18d7ca"/>
<rect x="124" y="58" width="11" height="13" style="fill:#372466"/>
<rect x="370" y="58" width="13" height="13" style="fill:#d2e935"/>
<rect x="113" y="57" width="11" height="13" style="fill:#d23105"/>
<rect x="326" y="7" width="3" height="3" style="fill:#e84b0c"/>
<rect x="157" y="71" width="13" height="14" style="fill:#f6c33a"/>
<rect x="380" y="85" width="6" height="16" style="fill:#65fd69"/>
<rect x="357" y="101" width="10" height="16" style="fill:#e5470b"/>
<rect x="329" y="7" width="3" height="3" style="fill:#c82803"/>
<rect x="332" y="7" width="3" height="3" style="fill:#6dfe62"/>
<rect x="335" y="7" width="3" height="3" style="fill:#ed5510"/>
<rect x="338" y="7" width="3" height="3" style="fill:#4776ee"/>
<rect x="341" y="7" width="3" height="3" style="fill:#980e01"/>
<rect x="344" y="7" width="3" height="3" style="fill:#38276d"/>
<rect x="347" y="7" width="3" height="3" style="fill:#fc8725"/>
<rect x="350" y="7" width="3" height="3" style="fill:#950d01"/>
<rect x="353" y="7" width="3" height="3" style="fill:#424bb5"/>
<rect x="356" y="7" width="3" height="3" style="fill:#d23105"/>
<rect x="359" y="7" width="3" height="3" style="fill:#b1f936"/>
<rect x="362" y="7" width="3" height="3" style="fill:#a4fc3c"/>
<rect x="365" y="7" width="3" height="3" style="fill:#efcd3a"/>
<rect x="368" y="7" width="3" height="3" style="fill:#2ff19b"/>
<rect x="60" y="9" width="8" height="9" style="fill:#25eca7"/>
<rect x="371" y="7" width="3" height="3" style="fill:#4451bf"/>
<rect x="374" y="7" width="3" height="3" style="fill:#d2e935"/>
<rect x="377" y="7" width="3" height="3" style="fill:#84ff51"/>
<rect x="380" y="7" width="3" height="3" style="fill:#33184a"/>
<rect x="310" y="12" width="10" height="10" style="fill:#a7fc3a"/>
<rect x="300" y="11" width="10" height="10" style="fill:#477bf2"/>
<rect x="162" y="45" width="9" height="13" style="fill:#d4e735"/>
<rect x="383" y="7" width="3" height="3" style="fill:#b41b01"/>
<rect x="386" y="7" width="3" height="3" style="fill:#affa37"/>
<rect x="389" y="6" width="3" height="3" style="fill:#25eca7"/>
<rect x="392" y="6" width="3" height="3" style="fill:#455ed3"/>
<rect x="395" y="6" width="3" height="3" style="fill:#fe9029"/>
<rect x="52" y="9" width="8" height="9" style="fill:#8bff4b"/>
<rect x="398" y="6" width="3" height="3" style="fill:#ec530f"/>
<rect x="401" y="6" width="3" height="3" style="fill:#466be3"/>
<rect x="66" y="0" width="6" height="9" style="fill:#36215f"/>
<rect x="404" y="6" width="3" height="3" style="fill:#b21a01"/>
<rect x="407" y="6" width="3" height="3" style="fill:#18d9c8"/>
<rect x="410" y="6" width="3" height="3" style="fill:#2cb7f0"/>
<rect x="413" y="6" width="3" height="3" style="fill:#e9d539"/>
<rect x="75" y="115" width="8" height="17" style="fill:#99fe42"/>
<rect x="440" y="57" width="8" height="14" style="fill:#477bf2"/>
<rect x="379" y="21" width="8" height="12" style="fill:#43f787"/>
<rect x="387" y="21" width="8" height="12" style="fill:#84ff51"/>
<rect x="164" y="33" width="10" height="12" style="fill:#19d5cd"/>
<rect x="154" y="33" width="10" height="12" style="fill:#8fff49"/>
<rect x="461" y="9" width="3" height="12" style="fill:#4249b1"/>
<rect x="427" y="57" width="6" height="14" style="fill:#dd3d08"/>
<rect x="68" y="69" width="5" height="15" style="fill:#25c0e7"/>
<rect x="206" y="85" width="12" height="15" style="fill:#e7d739"/>
<rect x="272" y="58" width="12" height="13" style="fill:#cdec34"/>
<rect x="395" y="20" width="8" height="12" style="fill:#b1f936"/>
<rect x="224" y="116" width="10" height="17" style="fill:#424bb5"/>
<rect x="435" y="117" width="11" height="17" style="fill:#321543"/>
<rect x="48" y="83" width="10" height="15" style="fill:#a11201"/>
<rect x="391" y="33" width="11" height="12" style="fill:#4249b1"/>
<rect x="380" y="33" width="11" height="12" style="fill:#1fe9af"/>
<rect x="126" y="20" width="9" height="12" style="fill:#c3f134"/>
<rect x="144" y="33" width="10" height="12" style="fill:#af1801"/>
<rect x="403" y="20" width="8" height="12" style="fill:#ecd13a"/>
<rect x="207" y="71" width="12" height="14" style="fill:#20eaac"/>
<rect x="411" y="20" width="8" height="12" style="fill:#1ce6b4"/>
<rect x="194" y="85" width="12" height="15" style="fill:#18dec0"/>
<rect x="135" y="21" width="9" height="12" style="fill:#a4fc3c"/>
<rect x="369" y="33" width="11" height="12" style="fill:#43f787"/>
<rect x="139" y="85" width="11" height="15" style="fill:#3f3e9c"/>
<rect x="134" y="33" width="10" height="12" style="fill:#1ce6b4"/>
<rect x="124" y="32" width="10" height="12" style="fill:#be2102"/>
<rect x="260" y="58" width="12" height="13" style="fill:#6dfe62"/>
<rect x="358" y="33" width="11" height="12" style="fill:#1de7b2"/>
<rect x="248" y="58" width="12" height="13" style="fill:#2aefa1"/>
<rect x="347" y="33" width="11" height="12" style="fill:#22c5e2"/>
<rect x="144" y="21" width="9" height="12" style="fill:#455ccf"/>
<rect x="114" y="32" width="10" height="12" style="fill:#ce2d04"/>
<rect x="419" y="20" width="8" height="12" style="fill:#a91601"/>
<rect x="336" y="33" width="11" height="12" style="fill:#fcb136"/>
<rect x="236" y="58" width="12" height="13" style="fill:#810602"/>
<rect x="325" y="33" width="11" height="12" style="fill:#99fe42"/>
<rect x="219" y="71" width="12" height="14" style="fill:#3d9efe"/>
<rect x="153" y="21" width="9" height="12" style="fill:#9ffd3f"/>
<rect x="224" y="58" width="12" height="13" style="fill:#bcf534"/>
<rect x="170" y="71" width="13" height="14" style="fill:#dbe236"/>
<rect x="487" y="45" width="12" height="12" style="fill:#f5c53a"/>
<rect x="499" y="45" width="12" height="12" style="fill:#bef434"/>
<rect x="162" y="21" width="9" height="12" style="fill:#ce2d04"/>
<rect x="104" y="32" width="10" height="12" style="fill:#ed5510"/>
<rect x="179" y="100" width="12" height="16" style="fill:#cbed34"/>
<rect x="94" y="32" width="10" height="12" style="fill:#6dfe62"/>
<rect x="44" y="9" width="8" height="9" style="fill:#e4450a"/>
<rect x="36" y="9" width="8" height="9" style="fill:#b41b01"/>
<rect x="28" y="9" width="8" height="9" style="fill:#f9bc39"/>
<rect x="83" y="1" width="5" height="9" style="fill:#b71d02"/>
<rect x="427" y="9" width="10" height="11" style="fill:#e9d539"/>
<rect x="169" y="12" width="9" height="9" style="fill:#1ad4d0"/>
<rect x="171" y="45" width="9" height="13" style="fill:#f6c33a"/>
<rect x="32" y="0" width="7" height="9" style="fill:#dc3b07"/>
<rect x="160" y="12" width="9" height="9" style="fill:#3e9bfe"/>
<rect x="171" y="21" width="9" height="12" style="fill:#e4450a"/>
<rect x="20" y="9" width="8" height="9" style="fill:#dc3b07"/>
<rect x="151" y="12" width="9" height="9" style="fill:#e14109"/>
<rect x="142" y="12" width="9" height="9" style="fill:#52fa7a"/>
<rect x="133" y="11" width="9" height="9" style="fill:#9b0f01"/>
<rect x="273" y="11" width="9" height="10" style="fill:#4661d6"/>
<rect x="124" y="11" width="9" height="9" style="fill:#3d9efe"/>
<rect x="180" y="45" width="9" height="13" style="fill:#3b2f80"/>
<rect x="12" y="9" width="8" height="9" style="fill:#3aa3fc"/>
<rect x="72" y="0" width="6" height="9" style="fill:#a7fc3a"/>
<rect x="189" y="45" width="9" height="13" style="fill:#467df4"/>
<rect x="102" y="57" width="11" height="13" style="fill:#fe962b"/>
<rect x="4" y="9" width="8" height="9" style="fill:#33184a"/>
<rect x="373" y="10" width="9" height="11" style="fill:#ca2a04"/>
<rect x="0" y="0" width="8" height="9" style="fill:#3ba0fd"/>
<rect x="364" y="10" width="9" height="11" style="fill:#810602"/>
<rect x="447" y="9" width="11" height="11" style="fill:#1ae4b6"/>
<rect x="264" y="11" width="9" height="10" style="fill:#c8ef34"/>
<rect x="255" y="11" width="9" height="10" style="fill:#3aa3fc"/>
<rect x="8" y="0" width="8" height="9" style="fill:#c12302"/>
<rect x="39" y="0" width="7" height="9" style="fill:#d83706"/>
<rect x="180" y="21" width="9" height="12" style="fill:#f9781e"/>
<rect x="16" y="0" width="8" height="9" style="fill:#2fb2f4"/>
<rect x="452" y="71" width="9" height="14" style="fill:#477bf2"/>
<rect x="189" y="21" width="9" height="12" style="fill:#9b0f01"/>
<rect x="30" y="83" width="9" height="15" style="fill:#af1801"/>
<rect x="400" y="45" width="6" height="13" style="fill:#455ed3"/>
<rect x="46" y="0" width="7" height="9" style="fill:#8b0902"/>
<rect x="24" y="0" width="8" height="9" style="fill:#f36315"/>
<rect x="458" y="9" width="3" height="12" style="fill:#9b0f01"/>
<rect x="476" y="9" width="5" height="12" style="fill:#3c3286"/>
<rect x="19" y="114" width="5" height="17" style="fill:#fe9b2d"/>
<rect x="461" y="71" width="9" height="14" style="fill:#27eea4"/>
<rect x="198" y="45" width="9" height="13" style="fill:#fe992c"/>
<rect x="198" y="21" width="9" height="12" style="fill:#25c0e7"/>
<rect x="321" y="46" width="8" height="13" style="fill:#2aefa1"/>
<rect x="207" y="45" width="9" height="13" style="fill:#434eba"/>
<rect x="430" y="85" width="8" height="16" style="fill:#d0ea34"/>
<rect x="348" y="10" width="8" height="11" style="fill:#ea4e0d"/>
<rect x="367" y="101" width="10" height="16" style="fill:#88ff4e"/>
<rect x="468" y="101" width="9" height="16" style="fill:#f9bc39"/>
<rect x="246" y="100" width="11" height="16" style="fill:#a7fc3a"/>
<rect x="477" y="101" width="9" height="16" style="fill:#ebd339"/>
<rect x="422" y="85" width="8" height="16" style="fill:#affa37"/>
<rect x="374" y="85" width="6" height="16" style="fill:#fea130"/>
<rect x="130" y="100" width="13" height="16" style="fill:#b1f936"/>
<rect x="231" y="71" width="12" height="14" style="fill:#acfb38"/>
<rect x="486" y="101" width="9" height="16" style="fill:#af1801"/>
<rect x="495" y="101" width="9" height="16" style="fill:#ecd13a"/>
<rect x="377" y="101" width="10" height="16" style="fill:#59fb73"/>
<rect x="427" y="20" width="8" height="12" style="fill:#dd3d08"/>
<rect x="414" y="85" width="8" height="16" style="fill:#e2430a"/>
<rect x="37" y="68" width="6" height="15" style="fill:#da3907"/>
<rect x="119" y="133" width="17" height="18" style="fill:#455ed3"/>
<rect x="357" y="58" width="13" height="13" style="fill:#bcf534"/>
<rect x="136" y="133" width="17" height="18" style="fill:#f05b12"/>
<rect x="344" y="58" width="13" height="13" style="fill:#2fb2f4"/>
<rect x="153" y="133" width="15" height="18" style="fill:#b41b01"/>
<rect x="243" y="71" width="12" height="14" style="fill:#4773eb"/>
<rect x="8" y="68" width="8" height="15" style="fill:#35f394"/>
<rect x="207" y="21" width="9" height="12" style="fill:#65fd69"/>
<rect x="424" y="117" width="11" height="17" style="fill:#da3907"/>
<rect x="67" y="115" width="8" height="17" style="fill:#d02f05"/>
<rect x="128" y="85" width="11" height="15" style="fill:#25c0e7"/>
<rect x="503" y="85" width="9" height="16" style="fill:#38276d"/>
<rect x="131" y="3" width="2" height="8" style="fill:#dde037"/>
<rect x="17" y="114" width="2" height="17" style="fill:#4af880"/>
<rect x="277" y="7" width="2" height="4" style="fill:#fea732"/>
<rect x="279" y="7" width="2" height="4" style="fill:#4143a7"/>
<rect x="262" y="7" width="3" height="4" style="fill:#ecd13a"/>
<rect x="223" y="6" width="4" height="5" style="fill:#c1f334"/>
<rect x="227" y="6" width="4" height="5" style="fill:#fe9b2d"/>
<rect x="180" y="6" width="4" height="6" style="fill:#fea732"/>
<rect x="463" y="21" width="7" height="12" style="fill:#e7d739"/>
<rect x="506" y="21" width="6" height="12" style="fill:#4454c3"/>
<rect x="210" y="6" width="5" height="5" style="fill:#476ee6"/>
<rect x="214" y="11" width="3" height="10" style="fill:#35abf8"/>
<rect x="231" y="133" width="12" height="18" style="fill:#fe992c"/>
<rect x="184" y="6" width="4" height="6" style="fill:#88ff4e"/>
<rect x="188" y="6" width="4" height="6" style="fill:#79fe59"/>
<rect x="216" y="21" width="9" height="12" style="fill:#f05b12"/>
<rect x="153" y="5" width="5" height="7" style="fill:#c8ef34"/>
<rect x="225" y="21" width="9" height="12" style="fill:#4451bf"/>
<rect x="48" y="132" width="13" height="19" style="fill:#b21a01"/>
<rect x="357" y="45" width="7" height="13" style="fill:#fea431"/>
<rect x="470" y="71" width="9" height="14" style="fill:#9ffd3f"/>
<rect x="22" y="55" width="10" height="13" style="fill:#434eba"/>
<rect x="84" y="31" width="10" height="12" style="fill:#351e58"/>
<rect x="21" y="83" width="9" height="15" style="fill:#e4450a"/>
<rect x="133" y="3" width="2" height="8" style="fill:#18d9c8"/>
<rect x="494" y="85" width="9" height="16" style="fill:#4666dd"/>
<rect x="506" y="9" width="6" height="12" style="fill:#341b51"/>
<rect x="265" y="133" width="5" height="18" style="fill:#75fe5c"/>
<rect x="53" y="0" width="7" height="9" style="fill:#a91601"/>
<rect x="246" y="11" width="9" height="10" style="fill:#9e1001"/>
<rect x="340" y="10" width="8" height="11" style="fill:#3c3286"/>
<rect x="332" y="10" width="8" height="11" style="fill:#1ad4d0"/>
<rect x="216" y="45" width="9" height="13" style="fill:#fe962b"/>
<rect x="314" y="34" width="11" height="12" style="fill:#b9f635"/>
<rect x="234" y="21" width="9" height="12" style="fill:#f46617"/>
<rect x="74" y="31" width="10" height="12" style="fill:#9e1001"/>
<rect x="212" y="58" width="12" height="13" style="fill:#18ddc2"/>
<rect x="406" y="85" width="8" height="16" style="fill:#3f3e9c"/>
<rect x="265" y="7" width="3" height="4" style="fill:#9ffd3f"/>
<rect x="268" y="7" width="3" height="4" style="fill:#23c3e4"/>
<rect x="416" y="6" width="3" height="3" style="fill:#38a5fb"/>
<rect x="16" y="68" width="8" height="15" style="fill:#2ab9ee"/>
</svg>

After

Width:  |  Height:  |  Size: 44 KiB

306
src/Magnum/TextureTools/Atlas.cpp

@ -28,14 +28,320 @@
#include <algorithm> #include <algorithm>
#include <vector> #include <vector>
#include <Corrade/Containers/Array.h> #include <Corrade/Containers/Array.h>
#include <Corrade/Containers/BitArrayView.h>
#include <Corrade/Containers/EnumSet.hpp>
#include <Corrade/Containers/GrowableArray.h>
#include <Corrade/Containers/Pair.h> #include <Corrade/Containers/Pair.h>
#include <Corrade/Containers/StridedArrayView.h> #include <Corrade/Containers/StridedArrayView.h>
#include "Magnum/Math/Functions.h" #include "Magnum/Math/Functions.h"
#include "Magnum/Math/FunctionsBatch.h"
#include "Magnum/Math/Range.h" #include "Magnum/Math/Range.h"
namespace Magnum { namespace TextureTools { namespace Magnum { namespace TextureTools {
Debug& operator<<(Debug& debug, const AtlasLandfillFlag value) {
debug << "TextureTools::AtlasLandfillFlag" << Debug::nospace;
switch(value) {
/* LCOV_EXCL_START */
#define _c(v) case AtlasLandfillFlag::v: return debug << "::" #v;
_c(RotatePortrait)
_c(RotateLandscape)
_c(WidestFirst)
_c(NarrowestFirst)
#undef _c
/* LCOV_EXCL_STOP */
}
return debug << "(" << Debug::nospace << reinterpret_cast<void*>(UnsignedInt(value)) << Debug::nospace << ")";
}
Debug& operator<<(Debug& debug, const AtlasLandfillFlags value) {
return Containers::enumSetDebugOutput(debug, value, "TextureTools::AtlasLandfillFlags{}", {
AtlasLandfillFlag::RotatePortrait,
AtlasLandfillFlag::RotateLandscape,
AtlasLandfillFlag::WidestFirst,
AtlasLandfillFlag::NarrowestFirst,
});
}
namespace Implementation {
struct AtlasLandfillState {
struct Slice {
Int direction = +1; /* +1 left-to-right, -1 right-to-left */
/* If direction is left-to-right, it's offset from the left, otherwise
from the right */
Int xOffset = 0;
};
Containers::Array<Slice> slices;
/* One entry for every size.x() */
Containers::Array<UnsignedShort> yOffsets;
/* X = MAX and z = 1 is for 2D unbounded, z = MAX is for 3D unbounded */
Vector3i size;
AtlasLandfillFlags flags = AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst;
};
}
namespace {
bool atlasLandfillAddSortedFlipped(Implementation::AtlasLandfillState& state, const Int slice, const Containers::StridedArrayView1D<const Containers::Pair<Vector2i, UnsignedInt>> sortedFlippedSizes, const Containers::StridedArrayView1D<Vector2i> offsets, const Containers::StridedArrayView1D<Int> zOffsets) {
/* 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());
CORRADE_INTERNAL_ASSERT(state.yOffsets.size() == state.slices.size()*state.size.x());
arrayAppend(state.slices, InPlaceInit);
/** @todo have an option to always start at the last tile so it doesn't
use a ton of memory when not filling incrementally and doesn't take
ages when incrementally filling a deep array */
/** @todo Utility::fill() */
for(UnsignedShort& i: arrayAppend(state.yOffsets, NoInit, state.size.x()))
i = 0;
}
Implementation::AtlasLandfillState::Slice& sliceState = state.slices[slice];
/* View on the Y offsets in current slice and in current fill direction */
Containers::StridedArrayView1D<UnsignedShort> sliceYOffsets = state.yOffsets.sliceSize(slice*state.size.x(), state.size.x());
if(sliceState.direction == -1)
sliceYOffsets = sliceYOffsets.flipped<0>();
std::size_t i;
for(i = 0; i != sortedFlippedSizes.size(); ++i) {
const Vector2i size = sortedFlippedSizes[i].first();
/* If the width cannnot fit into current offset, start a new row in
the opposite direction */
if(sliceState.xOffset + size.x() > state.size.x()) {
sliceState.xOffset = 0;
sliceState.direction *= -1;
sliceYOffsets = sliceYOffsets.flipped<0>();
}
/* Find the lowest Y offset where the width can be placed. If the
height cannot fit in there, bail. */
const Containers::StridedArrayView1D<UnsignedShort> placementYOffsets = sliceYOffsets.sliceSize(sliceState.xOffset, size.x());
const Int placementYOffset = Math::max(placementYOffsets);
/** @todo skip it until some smaller fits, and then continue with the
skipped rest to the next slice */
if(placementYOffset + size.y() > state.size.y())
break;
/** @todo Utility::fill() */
const UnsignedShort newYOffset = placementYOffset + size.y();
for(UnsignedShort& yOffset: placementYOffsets)
yOffset = newYOffset;
/* Save the position (X-flip it in case we're in reverse direction),
advance to the next X offset */
offsets[sortedFlippedSizes[i].second()] = {
sliceState.direction > 0 ? sliceState.xOffset :
state.size.x() - sliceState.xOffset - size.x(),
placementYOffset
};
sliceState.xOffset += size.x();
}
/* If the Z offset array is present, fill it with current slice index for
all items that fit */
if(zOffsets) for(std::size_t j = 0; j != i; ++j)
zOffsets[sortedFlippedSizes[j].second()] = slice;
/* If there are items that didn't fit, recurse to the next slice. This
should only happen if the Y size is bounded. */
if(i < sortedFlippedSizes.size()) {
if(slice + 1 == state.size.z())
return false;
return atlasLandfillAddSortedFlipped(state, slice + 1, sortedFlippedSizes.exceptPrefix(i), offsets, zOffsets);
}
/* Everything fit, success */
return true;
}
bool atlasLandfillAdd(const char* messagePrefix, Implementation::AtlasLandfillState& state, const Containers::StridedArrayView1D<const Vector2i> sizes, const Containers::StridedArrayView1D<Vector2i> offsets, const Containers::StridedArrayView1D<Int> zOffsets, const Containers::MutableBitArrayView rotations) {
CORRADE_ASSERT(offsets.size() == sizes.size(),
messagePrefix << "expected sizes and offsets views to have the same size, got" << sizes.size() << "and" << offsets.size(), {});
CORRADE_ASSERT((!(state.flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) && rotations.isEmpty()) || rotations.size() == sizes.size(),
messagePrefix << "expected sizes and rotations views to have the same size, got" << sizes.size() << "and" << rotations.size(), {});
/* These are sliced internally from a Vector3i input, so should match */
CORRADE_INTERNAL_ASSERT(!zOffsets || zOffsets.size() == sizes.size());
#ifdef CORRADE_NO_ASSERT
static_cast<void>(messagePrefix);
#endif
/* Nothing is flipped by default */
rotations.resetAll();
/* Copy all input sizes to a mutable array, flip them if not portrait,
and remember their original order for sorting */
Containers::Array<Containers::Pair<Vector2i, UnsignedInt>> 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()))
{
size = size.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, {});
sortedFlippedSizes[i] = {size, UnsignedInt(i)};
}
/* Sort to have the highest 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 */
if(state.flags & AtlasLandfillFlag::NarrowestFirst)
std::stable_sort(sortedFlippedSizes.begin(), sortedFlippedSizes.end(), [](const Containers::Pair<Vector2i, UnsignedInt>& a, const Containers::Pair<Vector2i, UnsignedInt>& b) {
return a.first().y() == b.first().y() ?
a.first().x() < b.first().x() :
a.first().y() > b.first().y();
});
else if(state.flags & AtlasLandfillFlag::WidestFirst)
std::stable_sort(sortedFlippedSizes.begin(), sortedFlippedSizes.end(), [](const Containers::Pair<Vector2i, UnsignedInt>& a, const Containers::Pair<Vector2i, UnsignedInt>& b) {
return a.first().y() == b.first().y() ?
a.first().x() > b.first().x() :
a.first().y() > b.first().y();
});
else
std::stable_sort(sortedFlippedSizes.begin(), sortedFlippedSizes.end(), [](const Containers::Pair<Vector2i, UnsignedInt>& a, const Containers::Pair<Vector2i, UnsignedInt>& b) {
return a.first().y() > b.first().y();
});
return atlasLandfillAddSortedFlipped(state, 0, sortedFlippedSizes, offsets, zOffsets);
}
}
AtlasLandfill::AtlasLandfill(const Vector2i& size):_state{InPlaceInit} {
CORRADE_ASSERT(size.x(), "TextureTools::AtlasLandfill: expected non-zero width, got" << Debug::packed << size, );
CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfill: expected width to fit into 16 bits, got" << Debug::packed << size, );
/* Change y = 0 to y = MAX so the algorithm doesn't need to branch on that
internally */
_state->size = {size.x(),
size.y() ? size.y() : 0x7fffffff,
1};
}
AtlasLandfill::AtlasLandfill(AtlasLandfill&&) noexcept = default;
AtlasLandfill::~AtlasLandfill() = default;
AtlasLandfill& AtlasLandfill::operator=(AtlasLandfill&&) noexcept = default;
Vector2i AtlasLandfill::size() const {
/* Change y = MAX (that's there so the algorithm doesn't need to branch on
that internally) back to y = 0 */
return {_state->size.x(),
_state->size.y() == 0x7fffffff ? 0 : _state->size.y()};
}
Vector2i AtlasLandfill::filledSize() const {
return {_state->size.x(), Math::max(_state->yOffsets)};
}
AtlasLandfillFlags AtlasLandfill::flags() const {
return _state->flags;
}
AtlasLandfill& AtlasLandfill::setFlags(AtlasLandfillFlags flags) {
CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) ||
!(flags & AtlasLandfillFlag::RotateLandscape),
"TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this);
CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) ||
!(flags & AtlasLandfillFlag::NarrowestFirst),
"TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this);
_state->flags = flags;
return *this;
}
bool AtlasLandfill::add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector2i>& offsets, Containers::MutableBitArrayView flips) {
return atlasLandfillAdd("TextureTools::AtlasLandfill::add():", *_state, sizes, offsets, nullptr, flips);
}
bool AtlasLandfill::add(const std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector2i>& offsets, Containers::MutableBitArrayView flips) {
return add(Containers::stridedArrayView(sizes), offsets, flips);
}
bool AtlasLandfill::add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector2i>& offsets) {
CORRADE_ASSERT(!(_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)),
"TextureTools::AtlasLandfill::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {});
return add(sizes, offsets, nullptr);
}
bool AtlasLandfill::add(const std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector2i>& offsets) {
return add(Containers::stridedArrayView(sizes), offsets);
}
AtlasLandfillArray::AtlasLandfillArray(const Vector3i& size):_state{InPlaceInit} {
CORRADE_ASSERT(size.xy().product(), "TextureTools::AtlasLandfillArray: expected non-zero width and height, got" << Debug::packed << size, );
CORRADE_ASSERT(size.x() <= 65536, "TextureTools::AtlasLandfillArray: expected width to fit into 16 bits, got" << Debug::packed << size, );
/* Change z = 0 to z = MAX so the algorithm doesn't need to branch on that
internally */
_state->size = {size.xy(),
size.z() ? size.z() : 0x7fffffff};
}
AtlasLandfillArray::AtlasLandfillArray(AtlasLandfillArray&&) noexcept = default;
AtlasLandfillArray::~AtlasLandfillArray() = default;
AtlasLandfillArray& AtlasLandfillArray::operator=(AtlasLandfillArray&&) noexcept = default;
Vector3i AtlasLandfillArray::size() const {
/* Change z = MAX (that's there so the algorithm doesn't need to branch on
that internally) back to z = 0 */
return {_state->size.xy(),
_state->size.z() == 0x7fffffff ? 0 : _state->size.z()};
}
Vector3i AtlasLandfillArray::filledSize() const {
return {_state->size.xy(), Int(_state->slices.size())};
}
AtlasLandfillFlags AtlasLandfillArray::flags() const {
return _state->flags;
}
AtlasLandfillArray& AtlasLandfillArray::setFlags(AtlasLandfillFlags flags) {
CORRADE_ASSERT(!(flags & AtlasLandfillFlag::RotatePortrait) ||
!(flags & AtlasLandfillFlag::RotateLandscape),
"TextureTools::AtlasLandfillArray::setFlags(): only one of RotatePortrait and RotateLandscape can be set", *this);
CORRADE_ASSERT(!(flags & AtlasLandfillFlag::WidestFirst) ||
!(flags & AtlasLandfillFlag::NarrowestFirst),
"TextureTools::AtlasLandfillArray::setFlags(): only one of WidestFirst and NarrowestFirst can be set", *this);
_state->flags = flags;
return *this;
}
bool AtlasLandfillArray::add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets, Containers::MutableBitArrayView flips) {
return atlasLandfillAdd("TextureTools::AtlasLandfillArray::add():", *_state, sizes, offsets.slice(&Vector3i::xy), offsets.slice(&Vector3i::z), flips);
}
bool AtlasLandfillArray::add(const std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector3i>& offsets, Containers::MutableBitArrayView flips) {
return add(Containers::stridedArrayView(sizes), offsets, flips);
}
bool AtlasLandfillArray::add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets) {
CORRADE_ASSERT(!(_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)),
"TextureTools::AtlasLandfillArray::add():" << (_state->flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)) << "set, expected a rotations view", {});
return add(sizes, offsets, nullptr);
}
bool AtlasLandfillArray::add(const std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector3i>& offsets) {
return add(Containers::stridedArrayView(sizes), offsets);
}
std::vector<Range2Di> atlas(const Vector2i& atlasSize, const std::vector<Vector2i>& sizes, const Vector2i& padding) { std::vector<Range2Di> atlas(const Vector2i& atlasSize, const std::vector<Vector2i>& sizes, const Vector2i& padding) {
if(sizes.empty()) return {}; if(sizes.empty()) return {};

424
src/Magnum/TextureTools/Atlas.h

@ -26,17 +26,435 @@
*/ */
/** @file /** @file
* @brief Function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo() * @brief Class @ref Magnum::TextureTools::AtlasLandfill, @ref Magnum::TextureTools::AtlasLandfillArray, enum @ref Magnum::TextureTools::AtlasLandfillFlag, enum set @ref Magnum::TextureTools::AtlasLandfillFlags, function @ref Magnum::TextureTools::atlas(), @ref Magnum::TextureTools::atlasArrayPowerOfTwo()
*/ */
#include <Corrade/Containers/Pointer.h>
#include <Corrade/Utility/StlForwardVector.h> #include <Corrade/Utility/StlForwardVector.h>
#include "Magnum/Magnum.h" #include "Magnum/Magnum.h"
#include "Magnum/Math/Vector2.h" #include "Magnum/Math/Vector2.h"
#include "Magnum/TextureTools/visibility.h" #include "Magnum/TextureTools/visibility.h"
#ifdef MAGNUM_BUILD_DEPRECATED
#include <Corrade/Utility/Macros.h>
#endif
namespace Magnum { namespace TextureTools { namespace Magnum { namespace TextureTools {
namespace Implementation {
struct AtlasLandfillState;
}
/**
@brief Landfill texture atlas packer behavior flag
@m_since_latest
@see @ref AtlasLandfillFlags, @ref AtlasLandfill::setFlags(),
@ref AtlasLandfill::addFlags(), @ref AtlasLandfill::clearFlags(),
@ref AtlasLandfillArray::setFlags(), @ref AtlasLandfillArray::addFlags(),
@ref AtlasLandfillArray::clearFlags()
*/
enum class AtlasLandfillFlag {
/**
* Rotate all textures to a portrait orientation. Only one of
* @ref AtlasLandfillFlag::RotatePortrait and
* @relativeref{AtlasLandfillFlag,RotateLandscape} can be set. If neither
* is set, keeps the original orientation.
*/
RotatePortrait = 1 << 0,
/**
* Rotate all textures to a landscape orientation. Only one of
* @ref AtlasLandfillFlag::RotatePortrait and
* @relativeref{AtlasLandfillFlag,RotateLandscape} can be set. If neither
* is set, keeps the original orientation.
*/
RotateLandscape = 1 << 1,
/**
* Sort same-height textures widest first. Only one of
* @ref AtlasLandfillFlag::WidestFirst and
* @relativeref{AtlasLandfillFlag,NarrowestFirst} can be set. If neither is
* set, textures of the same height keep their original order.
*/
WidestFirst = 1 << 2,
/**
* Sort same-height textures narrowest first. Only one of
* @ref AtlasLandfillFlag::WidestFirst and
* @relativeref{AtlasLandfillFlag,NarrowestFirst} can be set. If neither is
* set, textures of the same height keep their original order.
*/
NarrowestFirst = 1 << 3
};
/** @debugoperatorenum{AtlasLandfillFlag} */
MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlag value);
/**
@brief Landfill texture atlas packer behavior flags
@m_since_latest
@see @ref Flags, @ref AtlasLandfill::setFlags(), @ref AtlasLandfill::addFlags(),
@ref AtlasLandfill::clearFlags(), @ref AtlasLandfillArray::setFlags(),
@ref AtlasLandfillArray::addFlags(), @ref AtlasLandfillArray::clearFlags()
*/
typedef Containers::EnumSet<AtlasLandfillFlag> AtlasLandfillFlags;
CORRADE_ENUMSET_OPERATORS(AtlasLandfillFlags)
/** @debugoperatorenum{AtlasLandfillFlags} */
MAGNUM_TEXTURETOOLS_EXPORT Debug& operator<<(Debug& output, AtlasLandfillFlags value);
/**
@brief Landfill texture atlas packer
@m_since_latest
Keeps track of currently filled height at every pixel with the aim to fill the
available space bottom-up as evenly as possible. Packs to a 2D texture with the
height optionally unbounded. See @ref AtlasLandfillArray for a variant that
works with 2D texture arrays, and @ref atlasArrayPowerOfTwo() for a variant
that always provides optimal packing for power-of-two sizes.
@htmlinclude atlas-landfill.svg
* *The Trash Algorithm.* Naming credit goes to [\@lacyyy](https://github.com/lacyyy).
@section TextureTools-AtlasLandfill-usage Example usage
The following snippets shows packing a list of images into an atlas with the
width set to 1024 and height unbounded. The algorithm by default makes all
images the same orientation as that significantly improves the layout
efficiency while not making any difference for texture mapping.
@snippet MagnumTextureTools.cpp AtlasLandfill-usage
If rotations are undesirable, for example if the resulting atlas is used by a
linear rasterizer later, they can be disabled by clearing appropriate
@ref AtlasLandfillFlags. The process can then also use the
@ref add(const Containers::StridedArrayView1D<const Vector2i>&, const Containers::StridedArrayView1D<Vector2i>&)
overload without the rotations argument.
@snippet MagnumTextureTools.cpp AtlasLandfill-usage-no-rotation
@section TextureTools-AtlasLandfill-process Packing process
On every @ref add(), the algorithm first makes all sizes the same orientation
depending on @ref AtlasLandfillFlag::RotatePortrait or
@relativeref{AtlasLandfillFlag,RotateLandscape} being set and sorts the sizes
highest first and then depending on @ref AtlasLandfillFlag::WidestFirst or
@relativeref{AtlasLandfillFlag,NarrowestFirst} being set.
A per-pixel array of currently filled `heights`, initially all @cpp 0 @ce, and
a horizontal insertion `cursor`, initially @cpp 0 @ce, is maintained. An item
of given `size` gets placed at a `height` that's
@cpp max(heights[cursor], heights[cursor + size.x]) @ce, this range gets then
set to `height + size.y` and the cursor is updated to `cursor + size.x`. If
cursor reaches the edge that an item cannot fit there anymore, it's reset to
@cpp 0 @ce and the process continues again in the opposite direction. With the
assumption that the texture sizes are uniformly distributed, this results in a
fairly leveled out height. The process is aborted if the atlas height is
bounded and the next item cannot fit there anymore.
The sort is performed using @ref std::stable_sort(), which is usually
@f$ \mathcal{O}(n \log{} n) @f$, the actual atlasing is a single
@f$ \mathcal{O}(n) @f$ operation. Memory complexity is
@f$ \mathcal{O}(n + w) @f$ with @f$ n @f$ being a sorted copy of the input size
array and @f$ w @f$ being a 16-bit integer for every pixel of atlas width,
additionally @ref std::stable_sort() performs its own allocation.
@section TextureTools-AtlasLandfill-incremental Incremental population
It's possible to call @ref add() multiple times in order to incrementally fill
the atlas with new data as much as the atlas height (if bounded) allows. In an
ideal scenario, if the previous fill resulted in an uniform height the newly
added data will be added in an optimal way as well, but in practice calling
@ref add() with all data just once will always result in a more optimal
packing than an incremental one.
*/
class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfill {
public:
/**
* @brief Constructor
*
* The @p size is expected to have non-zero width, and height not
* larger than 65536. If height is zero, the dimension is treated as
* unbounded, i.e. @ref add() never fails.
*/
explicit AtlasLandfill(const Vector2i& size);
/** @brief Copying is not allowed */
AtlasLandfill(const AtlasLandfill&) = delete;
/** @brief Move constructor */
AtlasLandfill(AtlasLandfill&&) noexcept;
~AtlasLandfill();
/** @brief Copying is not allowed */
AtlasLandfill& operator=(const AtlasLandfill&) = delete;
/** @brief Move assignment */
AtlasLandfill& operator=(AtlasLandfill&&) noexcept;
/**
* @brief Atlas size specified in constructor
*
* @see @ref filledSize()
*/
Vector2i size() const;
/**
* @brief Currently filled size
*
* Width is always taken from @ref size(). The height is @cpp 0 @ce
* initially, and at most the height of @ref size() if it's bounded.
* The size is calculated with a @f$ \mathcal{O}(w) @f$ complexity,
* with @f$ w @f$ being the atlas width.
*/
Vector2i filledSize() const;
/**
* @brief Behavior flags
*
* Default is @ref AtlasLandfillFlag::RotatePortrait and
* @relativeref{AtlasLandfillFlag,WidestFirst}.
*/
AtlasLandfillFlags flags() const;
/**
* @brief Set behavior flags
*
* Can be called with different values before each particular
* @ref add().
* @see @ref addFlags(), @ref clearFlags()
*/
AtlasLandfill& setFlags(AtlasLandfillFlags flags);
/**
* @brief Add behavior flags
*
* Calls @ref setFlags() with the existing flags ORed with @p flags.
* Useful for preserving the defaults.
* @see @ref clearFlags()
*/
AtlasLandfill& addFlags(AtlasLandfillFlags flags) {
return setFlags(this->flags()|flags);
}
/**
* @brief Clear behavior flags
*
* Calls @ref setFlags() with the existing flags ANDed with the inverse
* of @p flags. Useful for preserving the defaults.
* @see @ref addFlags()
*/
AtlasLandfill& clearFlags(AtlasLandfillFlags flags) {
return setFlags(this->flags() & ~flags);
}
/**
* @brief Add textures to the atlas
* @param[in] sizes Texture sizes
* @param[out] offsets Resulting offsets in the atlas
* @param[out] rotations Which textures got rotated
*
* 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
* @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 Vector2i>&, const Containers::StridedArrayView1D<Vector2i>&)
* overload.
*
* 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.
*/
bool add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector2i>& offsets, Containers::MutableBitArrayView rotations);
/** @overload */
bool add(std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector2i>& offsets, Containers::MutableBitArrayView rotations);
/**
* @brief Add textures to the atlas with rotations disabled
*
* Equivalent to calling @ref add(const Containers::StridedArrayView1D<const Vector2i>&, const Containers::StridedArrayView1D<Vector2i>&, Containers::MutableBitArrayView)
* with the @p rotations view being empty. Can be called only if
* neither @ref AtlasLandfillFlag::RotatePortrait nor
* @relativeref{AtlasLandfillFlag,RotateLandscape} is set.
* @see @ref clearFlags()
*/
bool add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector2i>& offsets);
/** @overload */
bool add(std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector2i>& offsets);
private:
Containers::Pointer<Implementation::AtlasLandfillState> _state;
};
/**
@brief Landfill texture atlas packer
@m_since_latest
Extends @ref AtlasLandfill to a third dimension. Instead of expanding to an
unbounded height, on overflow a new texture slice is made. See also
@ref atlasArrayPowerOfTwo() for a variant that always provides optimal packing
for power-of-two sizes.
@section TextureTools-AtlasLandfillArray-usage Example usage
Compared to the @ref TextureTools-AtlasLandfill-usage "2D usage" it's extended
to three dimensions:
@snippet MagnumTextureTools.cpp AtlasLandfillArray-usage
@section TextureTools-AtlasLandfillArray-process Packing process
Apart from expanding to new slices on height overflow, the underlying process
is @ref TextureTools-AtlasLandfill-process "the same as in AtlasLandfill".
In this case, memory complexity is @f$ \mathcal{O}(n + wd) @f$ with @f$ n @f$
being a sorted copy of the input size array and @f$ wd @f$ being a 16-bit
integer for every pixel of atlas width times atlas depth.
@section TextureTools-AtlasLandfillArray-incremental Incremental population
Compared to the @ref TextureTools-AtlasLandfill-incremental "2D incremental population",
the incremental process always starts from the first slice, finding the first
that can fit the first (sorted) item. Then it attempts to place as many items
as possible and on overflow continues searching for the next slice that can fit
the first remaining item. If all slices are exhausted, adds a new one.
*/
class MAGNUM_TEXTURETOOLS_EXPORT AtlasLandfillArray {
public:
/**
* @brief Constructor
*
* The @p size has to have non-zero width and height. If depth is
* @cpp 0 @ce, the dimension is treated as unbounded, i.e. @ref add()
* never fails. If depth is @cpp 1 @ce, behaves the same as
* @ref AtlasLandfillArray with a bounded height.
*/
explicit AtlasLandfillArray(const Vector3i& size);
/** @brief Copying is not allowed */
AtlasLandfillArray(const AtlasLandfillArray&) = delete;
/** @brief Move constructor */
AtlasLandfillArray(AtlasLandfillArray&&) noexcept;
~AtlasLandfillArray();
/** @brief Copying is not allowed */
AtlasLandfillArray& operator=(const AtlasLandfillArray&) = delete;
/** @brief Move assignment */
AtlasLandfillArray& operator=(AtlasLandfillArray&&) noexcept;
/**
* @brief Atlas size specified in constructor
*
* @see @ref filledSize()
*/
Vector3i size() const;
/**
* @brief Currently filled size
*
* Width and height is always taken from @ref size(). The depth is
* @cpp 0 @ce initially, and at most @ref size() depth if the size is
* bounded.
*/
Vector3i filledSize() const;
/** @brief Behavior flags */
AtlasLandfillFlags flags() const;
/**
* @brief Set behavior flags
*
* Can be called with different values before each particular
* @ref add(). Default is @ref AtlasLandfillFlag::RotatePortrait.
* @see @ref addFlags(), @ref clearFlags()
*/
AtlasLandfillArray& setFlags(AtlasLandfillFlags flags);
/**
* @brief Add behavior flags
*
* Calls @ref setFlags() with the existing flags ORed with @p flags.
* Useful for preserving the defaults.
* @see @ref clearFlags()
*/
AtlasLandfillArray& addFlags(AtlasLandfillFlags flags) {
return setFlags(this->flags()|flags);
}
/**
* @brief Clear behavior flags
*
* Calls @ref setFlags() with the existing flags ANDed with the inverse
* of @p flags. Useful for preserving the defaults.
* @see @ref addFlags()
*/
AtlasLandfillArray& clearFlags(AtlasLandfillFlags flags) {
return setFlags(this->flags() & ~flags);
}
/**
* @brief Add textures to the atlas
* @param[in] sizes Texture sizes
* @param[out] offsets Resulting offsets in the atlas
* @param[out] rotations Which textures got rotated
*
* 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
* @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 Vector2i>&, const Containers::StridedArrayView1D<Vector3i>&)
* overload.
*
* 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.
*/
bool add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets, Containers::MutableBitArrayView rotations);
/** @overload */
bool add(std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector3i>& offsets, Containers::MutableBitArrayView rotations);
/**
* @brief Add textures to the atlas with rotations disabled
*
* Equivalent to calling @ref add(const Containers::StridedArrayView1D<const Vector2i>&, const Containers::StridedArrayView1D<Vector3i>&, Containers::MutableBitArrayView)
* with the @p rotations view being empty. Can be called only if
* neither @ref AtlasLandfillFlag::RotatePortrait nor
* @relativeref{AtlasLandfillFlag,RotateLandscape} is set.
* @see @ref clearFlags()
*/
bool add(const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets);
/** @overload */
bool add(std::initializer_list<Vector2i> sizes, const Containers::StridedArrayView1D<Vector3i>& offsets);
private:
Containers::Pointer<Implementation::AtlasLandfillState> _state;
};
/** /**
@brief Pack textures into texture atlas @brief Pack textures into texture atlas
@param atlasSize Size of resulting atlas @param atlasSize Size of resulting atlas
@ -80,6 +498,10 @@ atlasing in a single @f$ \mathcal{O}(n) @f$ operation. Memory complexity is
array, additionally @ref std::stable_sort() performs its own allocation. See array, additionally @ref std::stable_sort() performs its own allocation. See
the [Zero-waste single-pass packing of power-of-two textures](https://blog.magnum.graphics/backstage/pot-array-packing/) the [Zero-waste single-pass packing of power-of-two textures](https://blog.magnum.graphics/backstage/pot-array-packing/)
article for a detailed description of the algorithm. article for a detailed description of the algorithm.
See the @ref AtlasLandfill and @ref AtlasLandfillArray classes for an
alternative that isn't restricted to power-of-two sizes and can be used in an
incremental way but doesn't always produce optimal packing.
*/ */
MAGNUM_TEXTURETOOLS_EXPORT Int atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets); MAGNUM_TEXTURETOOLS_EXPORT Int atlasArrayPowerOfTwo(const Vector2i& layerSize, const Containers::StridedArrayView1D<const Vector2i>& sizes, const Containers::StridedArrayView1D<Vector3i>& offsets);

440
src/Magnum/TextureTools/Test/AtlasBenchmark.cpp

@ -0,0 +1,440 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023 Vladimír Vondruš <mosra@centrum.cz>
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
*/
#include <algorithm>
#include <random>
#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/BitArray.h>
#include <Corrade/Containers/BitArrayView.h>
#include <Corrade/Containers/Optional.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/String.h>
#include <Corrade/Containers/StringIterable.h>
#include <Corrade/PluginManager/Manager.h>
#include <Corrade/TestSuite/Tester.h>
#include <Corrade/Utility/Algorithms.h>
#include <Corrade/Utility/Path.h>
#include "Magnum/Image.h"
#include "Magnum/ImageView.h"
#include "Magnum/PixelFormat.h"
#include "Magnum/DebugTools/ColorMap.h"
#include "Magnum/Math/Color.h"
#include "Magnum/Math/PackingBatch.h"
#include "Magnum/Math/Range.h"
#include "Magnum/TextureTools/Atlas.h"
#include "Magnum/Trade/AbstractImageConverter.h"
#include "configure.h"
#ifdef __has_include
#if __has_include("AtlasTestFiles/stb_rect_pack.h")
#ifdef CORRADE_TARGET_GCC
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#endif
#include "AtlasTestFiles/stb_rect_pack.h"
#ifdef CORRADE_TARGET_GCC
#pragma GCC diagnostic pop
#endif
inline void stbSort(stbrp_rect* rects, std::size_t count, std::size_t, int(*compare)(const void*, const void*)) {
std::sort(rects, rects + count, [compare](const stbrp_rect& a, const stbrp_rect& b) {
/* It returns -1 or 1, and -1 is if a dimension is higher, which is
descending, which is what should return true here */
return compare(&a, &b) < 0;
});
}
#define STB_RECT_PACK_IMPLEMENTATION
/* Comment this out to test with qsort instead (considerably slower as the
comparator function call isn't inlined) */
#define STBRP_SORT stbSort
#ifdef CORRADE_TARGET_GCC
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wold-style-cast"
#endif
#include "AtlasTestFiles/stb_rect_pack.h"
#ifdef CORRADE_TARGET_GCC
#pragma GCC diagnostic pop
#endif
#endif
#endif
namespace Magnum { namespace TextureTools { namespace Test { namespace {
struct AtlasBenchmark: TestSuite::Tester {
explicit AtlasBenchmark();
/* A bit chaotic here -- the benchmarkBegin() / benchmarkEnd() rely on the
actual case filling _sizes and _filledArea before the
CORRADE_BENCHMARK() ends. Then, the actual verification of the output
(that there is exactly as many filled pixels as was in the input) is
done by a custom CompareAtlasPacking comparator that's implemented
below, and this comparator also produces details about the packing with
--verbose and saves a TGA visualization with --save-diagnostic */
void benchmarkBegin();
std::uint64_t benchmarkEnd();
void landfill();
void stbRectPack();
private:
Containers::ArrayView<Vector2i> _sizes;
UnsignedInt _filledArea;
};
const struct {
const char* name;
const char* filename;
const char* image;
Vector2i size;
Containers::Optional<AtlasLandfillFlags> flags;
} LandfillData[]{
{"Oxygen.ttf, portrait, widest first",
"oxygen-glyphs.bin",
"oxygen-glyphs-landfill-portrait-widest-first.tga",
{512, 256}, {}},
{"Oxygen.ttf, portrait, narrowest first",
"oxygen-glyphs.bin",
"oxygen-glyphs-landfill-portrait-narrowest-first.tga",
{512, 256},
~~AtlasLandfillFlag::NarrowestFirst},
{"Oxygen.ttf, landscape, widest first",
"oxygen-glyphs.bin",
"oxygen-glyphs-landfill-landscape-widest-first.tga",
{512, 256},
AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst},
{"Oxygen.ttf, landscape, narrowest first",
"oxygen-glyphs.bin",
"oxygen-glyphs-landfill-landscape-narrowest-first.tga",
{512, 256},
AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst},
{"Noto Serif Tangut, portrait, widest first",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-landfill-portrait-widest-first.tga",
{2048, 800},
{}},
{"Noto Serif Tangut, portrait, narrowest first",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-landfill-portrait-narrowest-first.tga",
{2048, 800},
~~AtlasLandfillFlag::NarrowestFirst},
{"Noto Serif Tangut, landscape, widest first",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-landfill-landscape-widest-first.tga",
{2048, 800},
AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst},
{"Noto Serif Tangut, landscape, narrowest first",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-landfill-landscape-narrowest-first.tga",
{2048, 800},
AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst},
{"FP 102344349, landscape, widest first",
"fp-102344349-textures.bin",
"fp-102344349-textures-landfill-portrait-widest-first.tga",
{2048, 2048}, AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst},
{"FP 103997718_171030855, portrait, widest first",
"fp-103997718-171030855-textures.bin",
"fp-103997718-171030855-textures-landfill-portrait-widest-first.tga",
{8192, 8192}, {}},
};
const struct {
const char* name;
const char* filename;
const char* image;
Vector2i size;
Int rotate;
bool allowOOM;
} StbRectPackData[]{
{"Oxygen.ttf",
"oxygen-glyphs.bin",
"oxygen-glyph-stb.tga",
{512, 256}, 0, false},
{"Oxygen.ttf, portrait",
"oxygen-glyphs.bin",
"oxygen-glyphs-stb-portrait.tga",
{512, 256}, 1, false},
{"Oxygen.ttf, landscape",
"oxygen-glyphs.bin",
"oxygen-glyphs-stb-lanscape.tga",
{512, 256}, -1, false},
{"Oxygen.ttf, allow OOM",
"oxygen-glyphs.bin",
"oxygen-glyph-stb.tga",
{512, 256}, 0, true},
{"Noto Serif Tangut",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-stb.tga",
{2048, 800}, 0, false},
{"Noto Serif Tangut, portrait",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-stb-portrait.tga",
{2048, 800}, 1, false},
{"Noto Serif Tangut, landscape",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-stb-lanscape.tga",
{2048, 800}, -1, false},
{"Noto Serif Tangut, allow OOM",
"noto-serif-tangut-glyphs.bin",
"noto-serif-tangut-glyphs-stb.tga",
{2048, 800}, 0, true},
{"FP 102344349",
"fp-102344349-textures.bin",
"fp-102344349-textures-stb.tga",
{2048, 2048}, 0, false},
{"FP 103997718_171030855",
"fp-103997718-171030855-textures.bin",
"fp-103997718-171030855-textures-stb.tga",
{8192, 8192}, 0, false},
};
AtlasBenchmark::AtlasBenchmark() {
addCustomInstancedBenchmarks({&AtlasBenchmark::landfill}, 1,
Containers::arraySize(LandfillData),
&AtlasBenchmark::benchmarkBegin,
&AtlasBenchmark::benchmarkEnd,
BenchmarkUnits::PercentageThousandths);
addCustomInstancedBenchmarks({&AtlasBenchmark::stbRectPack}, 1,
Containers::arraySize(StbRectPackData),
&AtlasBenchmark::benchmarkBegin,
&AtlasBenchmark::benchmarkEnd,
BenchmarkUnits::PercentageThousandths);
/* Run all benchmarks again but with time measurement instead of
efficiency */
addInstancedBenchmarks({&AtlasBenchmark::landfill}, 10,
Containers::arraySize(LandfillData));
addInstancedBenchmarks({&AtlasBenchmark::stbRectPack}, 10,
Containers::arraySize(StbRectPackData));
}
class CompareAtlasPacking;
}}}}
namespace Corrade { namespace TestSuite {
using namespace Magnum;
template<> class Comparator<TextureTools::Test::CompareAtlasPacking> {
public:
explicit Comparator(Containers::StringView filename, const Vector2i& filledSize): _filename{filename}, _image{PixelFormat::RGBA8Unorm, filledSize, Containers::Array<char>{ValueInit, std::size_t(filledSize.product())*4}} {}
ComparisonStatusFlags operator()(const Containers::Pair<Containers::StridedArrayView1D<const Vector2i>, Containers::BitArrayView>& offsetsRotations, Containers::ArrayView<const Vector2i> sizes) {
_count = sizes.size();
Containers::StridedArrayView2D<Color4ub> pixels = _image.pixels<Color4ub>();
/* Generate a random set of colors. Have the same set every time,
so location of corresponding entries can be compared across
different algorithms. */
std::mt19937 rd;
std::uniform_int_distribution<Int> colorDist{0, 255};
/* Fill pixels where the items are placed */
for(std::size_t i = 0; i != sizes.size(); ++i) {
const Color4ub color[]{DebugTools::ColorMap::turbo()[colorDist(rd)]};
const Containers::StridedArrayView2D<const Color4ub> src{color, {1, 1}};
const Vector2i size = !offsetsRotations.second().isEmpty() && offsetsRotations.second()[i] ? sizes[i].flipped() : sizes[i];
const Containers::StridedArrayView2D<Color4ub> dst =
pixels.sliceSize(
{std::size_t(offsetsRotations.first()[i].y()),
std::size_t(offsetsRotations.first()[i].x())},
{std::size_t(size.y()),
std::size_t(size.x())});
Utility::copy(src.broadcasted<0>(dst.size()[0])
.broadcasted<1>(dst.size()[1]), dst);
}
/* Calculate expected area of the input sizes */
_expectedTotal = 0;
for(const Vector2i& i: sizes)
_expectedTotal += i.product();
/* Calculate the actual filled area */
_actualTotal = 0;
for(Containers::StridedArrayView1D<const Color4ub> row: pixels)
for(Color4ub pixel: row)
if(pixel != Color4ub{})
++_actualTotal;
return (_actualTotal == _expectedTotal ? ComparisonStatusFlags{} :
ComparisonStatusFlag::Failed)|ComparisonStatusFlag::Diagnostic|ComparisonStatusFlag::Verbose;
}
void printMessage(ComparisonStatusFlags flags, Debug& out, const char* actual, const char* expected) const {
if(flags & ComparisonStatusFlag::Failed)
out << "Packing" << actual << "from" << expected << "was lossy," << _actualTotal << "filled pixels but expected" << _expectedTotal;
else if(flags & ComparisonStatusFlag::Verbose)
out << "Packed" << _count << "images into" << Debug::packed << _image.size();
else CORRADE_INTERNAL_ASSERT_UNREACHABLE();
}
void saveDiagnostic(CORRADE_UNUSED ComparisonStatusFlags flags, Debug& out, Containers::StringView path) {
CORRADE_INTERNAL_ASSERT(flags & ComparisonStatusFlag::Diagnostic);
PluginManager::Manager<Trade::AbstractImageConverter> imageConverterManager;
Containers::Pointer<Trade::AbstractImageConverter> imageConverter = imageConverterManager.loadAndInstantiate("TgaImageConverter");
Containers::String filename = Utility::Path::join(path, _filename);
if(imageConverter->convertToFile(_image, filename))
out << "->" << filename;
}
private:
Containers::StringView _filename;
Image2D _image;
UnsignedInt _count, _actualTotal, _expectedTotal;
};
}}
namespace Magnum { namespace TextureTools { namespace Test { namespace {
class CompareAtlasPacking {
public:
explicit CompareAtlasPacking(Containers::StringView filename, const Vector2i& filledSize): _c{filename, filledSize} {}
TestSuite::Comparator<CompareAtlasPacking>& comparator() {
return _c;
}
private:
TestSuite::Comparator<CompareAtlasPacking> _c;
};
void AtlasBenchmark::benchmarkBegin() {
setBenchmarkName("efficiency");
_filledArea = 0;
}
std::uint64_t AtlasBenchmark::benchmarkEnd() {
/* If the test failed, exit early as continuing would cause a division by
zero. */
if(!_filledArea) return {};
UnsignedInt total = 0;
for(const Vector2i& i: _sizes)
total += i.product();
return total*100000ull/_filledArea;
}
void AtlasBenchmark::landfill() {
auto&& data = LandfillData[testCaseInstanceId()];
setTestCaseDescription(data.name);
Containers::Optional<Containers::Array<char>> sizeData = Utility::Path::read(Utility::Path::join({TEXTURETOOLS_TEST_DIR, "AtlasTestFiles", data.filename}));
CORRADE_VERIFY(sizeData);
auto sizes16 = Containers::arrayCast<Vector2s>(*sizeData);
Containers::Array<Vector2i> sizes{NoInit, sizes16.size()};
Math::castInto(
Containers::arrayCast<2, const Short>(stridedArrayView(sizes16)),
Containers::arrayCast<2, Int>(stridedArrayView(sizes)));
_sizes = sizes;
AtlasLandfill atlas{data.size};
if(data.flags)
atlas.setFlags(*data.flags);
Containers::Array<Vector2i> offsets{NoInit, _sizes.size()};
Containers::BitArray flips{NoInit, _sizes.size()};
CORRADE_BENCHMARK(1) {
CORRADE_VERIFY(atlas.add(_sizes, offsets, flips));
_filledArea = atlas.filledSize().product();
}
CORRADE_COMPARE_WITH(
Containers::pair(Containers::StridedArrayView1D<const Vector2i>{offsets}, Containers::BitArrayView{flips}),
_sizes,
(CompareAtlasPacking{data.image, atlas.filledSize()}));
}
void AtlasBenchmark::stbRectPack() {
auto&& data = StbRectPackData[testCaseInstanceId()];
setTestCaseDescription(data.name);
#ifdef STB_RECT_PACK_VERSION
Containers::Optional<Containers::Array<char>> sizeData = Utility::Path::read(Utility::Path::join({TEXTURETOOLS_TEST_DIR, "AtlasTestFiles", data.filename}));
CORRADE_VERIFY(sizeData);
auto sizes16 = Containers::arrayCast<Vector2s>(*sizeData);
Containers::Array<Vector2i> sizes{NoInit, sizes16.size()};
Math::castInto(
Containers::arrayCast<2, const Short>(stridedArrayView(sizes16)),
Containers::arrayCast<2, Int>(stridedArrayView(sizes)));
_sizes = sizes;
if(data.rotate) for(Vector2i& size: _sizes) {
if((data.rotate < 0 && size.x() < size.y()) ||
(data.rotate > 0 && size.x() > size.y()))
size = size.flipped();
}
stbrp_context ctx;
Containers::Array<stbrp_node> nodes{NoInit, _sizes.size()};
stbrp_init_target(&ctx, data.size.x(), data.size.y(), nodes.data(), nodes.size());
stbrp_setup_allow_out_of_mem(&ctx, data.allowOOM);
struct MyRect {
int:32;
Vector2i size;
Vector2i offset;
int:32;
};
static_assert(sizeof(MyRect) == sizeof(stbrp_rect), "failed to fake a rect struct");
Containers::Array<MyRect> rects{NoInit, _sizes.size()};
Utility::copy(_sizes, stridedArrayView(rects).slice(&MyRect::size));
Int height = 0;
CORRADE_BENCHMARK(1) {
CORRADE_VERIFY(stbrp_pack_rects(&ctx, reinterpret_cast<stbrp_rect*>(rects.data()), rects.size()));
for(const MyRect& i: rects)
height = Math::max(i.size.y() + i.offset.y(), height);
_filledArea = height*data.size.x();
}
Vector2i filledSize{data.size.x(), height};
CORRADE_COMPARE_WITH(
Containers::pair(Containers::StridedArrayView1D<const MyRect>{rects}.slice(&MyRect::offset), Containers::BitArrayView{}),
_sizes,
(CompareAtlasPacking{data.image, filledSize}));
#else
CORRADE_SKIP("stb_rect_pack.h not found, place it next to the test to benchmark it");
#endif
}
}}}}
CORRADE_TEST_MAIN(Magnum::TextureTools::Test::AtlasBenchmark)

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

@ -26,10 +26,13 @@
#include <sstream> #include <sstream>
#include <vector> #include <vector>
#include <Corrade/Containers/Array.h> #include <Corrade/Containers/Array.h>
#include <Corrade/Containers/StridedBitArrayView.h>
#include <Corrade/Containers/Pair.h> #include <Corrade/Containers/Pair.h>
#include <Corrade/Containers/StridedArrayView.h> #include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Containers/StringStl.h> /** @todo remove once Debug is stream-free */
#include <Corrade/TestSuite/Tester.h> #include <Corrade/TestSuite/Tester.h>
#include <Corrade/TestSuite/Compare/Container.h> #include <Corrade/TestSuite/Compare/Container.h>
#include <Corrade/TestSuite/Compare/String.h>
#include <Corrade/Utility/DebugStl.h> #include <Corrade/Utility/DebugStl.h>
#include <Corrade/Utility/FormatStl.h> #include <Corrade/Utility/FormatStl.h>
@ -41,6 +44,29 @@ namespace Magnum { namespace TextureTools { namespace Test { namespace {
struct AtlasTest: TestSuite::Tester { struct AtlasTest: TestSuite::Tester {
explicit AtlasTest(); explicit AtlasTest();
void debugLandfillFlag();
void debugLandfillFlags();
void landfillFullFit();
void landfill();
void landfillIncremental();
void landfillNoFit();
void landfillCopy();
void landfillMove();
void landfillArrayFullFit();
void landfillArray();
void landfillArrayIncremental();
void landfillArrayNoFit();
void landfillArrayCopy();
void landfillArrayMove();
void landfillInvalidSize();
void landfillSetFlagsInvalid();
void landfillAddMissingRotations();
void landfillAddInvalidViewSizes();
void landfillAddTooLargeElement();
void basic(); void basic();
void padding(); void padding();
void empty(); void empty();
@ -59,6 +85,305 @@ struct AtlasTest: TestSuite::Tester {
#endif #endif
}; };
const Vector2i LandfillSizes[]{
{3, 6}, /* 0 */
{2, 5}, /* 1 */
{4, 2}, /* 2 */
{3, 3}, /* 3 */
{2, 3}, /* 4 */
{3, 3}, /* 5 */
{2, 2}, /* 6 */
{2, 1}, /* 7 */
{2, 2}, /* 8 */
{2, 2}, /* 9 */
{2, 1}, /* a */
{1, 2}, /* b */
{1, 1}, /* c */
};
const struct {
const char* name;
AtlasLandfillFlags flags;
Vector2i size;
Vector2i filledSize;
Containers::Pair<Vector2i, bool> offsetsFlips[Containers::arraySize(LandfillSizes)];
} LandfillData[]{
/* In all of these, rectangles with the same size should keep their order.
5 after 3, 9 after 8 after 6 (and b after a after 7 if they're rotated
to the same orientation) */
{"no rotation, no width sorting", {}, {11, 12}, {11, 10}, {
/* 99b
99b77
8866 aac
88662222
000 2222555
00011 555
00011 555
0001133344
0001133344
0001133344 */
{{0, 0}, false}, /* 0 */
{{3, 0}, false}, /* 1 */
{{4, 5}, false}, /* 2 */
{{5, 0}, false}, /* 3 */
{{8, 0}, false}, /* 4 */
{{8, 3}, false}, /* 5 */
{{2, 6}, false}, /* 6 */
{{3, 8}, false}, /* 7 */
{{0, 6}, false}, /* 8 */
{{0, 8}, false}, /* 9 */
{{5, 7}, false}, /* a */
{{2, 8}, false}, /* b */
{{7, 7}, false}}}, /* c */
/* No rotation with width sorting omitted, not interesting */
{"portrait, no width sorting", AtlasLandfillFlag::RotatePortrait, {11, 12}, {11, 10}, {
/* 99a
99ab
88bc
88766555
00076655544
00011 55544
0001122 44
0001122333
0001122333
0001122333 */
{{0, 0}, false}, /* 0 */
{{3, 0}, false}, /* 1 */
{{5, 0}, true}, /* 2 */
{{7, 0}, false}, /* 3 */
{{9, 3}, false}, /* 4 */
{{6, 4}, false}, /* 5 */
{{4, 5}, false}, /* 6 */
{{3, 5}, true}, /* 7 */
{{1, 6}, false}, /* 8 */
{{0, 8}, false}, /* 9 */
{{2, 8}, true}, /* a */
{{3, 7}, false}, /* b */
{{4, 7}, false}}}, /* c */
{"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 10}, {
/* 7ab
7abc
9988
99886644
000 6644555
00011 44555
0001122 555
0001122333
0001122333
0001122333 */
{{0, 0}, false}, /* 0 */
{{3, 0}, false}, /* 1 */
{{5, 0}, true}, /* 2 */
{{7, 0}, false}, /* 3 */
{{6, 4}, false}, /* 4 */
{{8, 3}, false}, /* 5 */
{{4, 5}, false}, /* 6 */
{{0, 8}, true}, /* 7 */
{{2, 6}, false}, /* 8 */
{{0, 6}, false}, /* 9 */
{{1, 8}, true}, /* a */
{{2, 8}, false}, /* b */
{{3, 8}, false}}}, /* c */
{"portrait, widest first, unbounded height", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 0}, {11, 10}, {
/* Should have the same result as above
7ab
7abc
9988
99886644
000 6644555
00011 44555
0001122 555
0001122333
0001122333
0001122333 */
{{0, 0}, false}, /* 0 */
{{3, 0}, false}, /* 1 */
{{5, 0}, true}, /* 2 */
{{7, 0}, false}, /* 3 */
{{6, 4}, false}, /* 4 */
{{8, 3}, false}, /* 5 */
{{4, 5}, false}, /* 6 */
{{0, 8}, true}, /* 7 */
{{2, 6}, false}, /* 8 */
{{0, 6}, false}, /* 9 */
{{1, 8}, true}, /* a */
{{2, 8}, false}, /* b */
{{3, 8}, false}}}, /* c */
{"portrait, narrowest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 10}, {
/* 8899
8899
66b c
66ba7555
000a7555333
00011555333
0001122 333
000112244
000112244
000112244 */
{{0, 0}, false}, /* 0 */
{{3, 0}, false}, /* 1 */
{{5, 0}, true}, /* 2 */
{{8, 3}, false}, /* 3 */
{{7, 0}, false}, /* 4 */
{{5, 4}, false}, /* 5 */
{{0, 6}, false}, /* 6 */
{{4, 5}, true}, /* 7 */
{{0, 8}, false}, /* 8 */
{{2, 8}, false}, /* 9 */
{{3, 5}, true}, /* a */
{{2, 6}, false}, /* b */
{{4, 7}, false}}}, /* c */
{"landscape, no width sorting", AtlasLandfillFlag::RotateLandscape, {11, 12}, {11, 10}, {
/* 99
7799
cbbaa6688
22224446688
2222444 555
11111555
11111555
000000333
000000333
000000333 */
{{0, 0}, true}, /* 0 */
{{3, 3}, true}, /* 1 */
{{0, 5}, false}, /* 2 */
{{6, 0}, false}, /* 3 */
{{4, 5}, true}, /* 4 */
{{8, 3}, false}, /* 5 */
{{7, 6}, false}, /* 6 */
{{7, 8}, false}, /* 7 */
{{9, 6}, false}, /* 8 */
{{9, 8}, false}, /* 9 */
{{5, 7}, false}, /* a */
{{3, 7}, true}, /* b */
{{2, 7}, false}}}, /* c */
{"landscape, widest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::WidestFirst, {11, 12}, {11, 10}, {
/* No change compared to "no width sorting" in this case
99
7799
cbbaa6688
22224446688
2222444 555
11111555
11111555
000000333
000000333
000000333 */
{{0, 0}, true}, /* 0 */
{{3, 3}, true}, /* 1 */
{{0, 5}, false}, /* 2 */
{{6, 0}, false}, /* 3 */
{{4, 5}, true}, /* 4 */
{{8, 3}, false}, /* 5 */
{{7, 6}, false}, /* 6 */
{{7, 8}, false}, /* 7 */
{{9, 6}, false}, /* 8 */
{{9, 8}, false}, /* 9 */
{{5, 7}, false}, /* a */
{{3, 7}, true}, /* b */
{{2, 7}, false}}}, /* c */
{"landscape, narrowest first", AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst, {11, 12}, {11, 10}, {
/* 11111
bb c11111
aa772222
994442222
99444000000
8866000000
8866000000
333555
333555
333555 */
{{5, 3}, true}, /* 0 */
{{6, 8}, true}, /* 1 */
{{5, 6}, false}, /* 2 */
{{0, 0}, false}, /* 3 */
{{2, 5}, true}, /* 4 */
{{3, 0}, false}, /* 5 */
{{3, 3}, false}, /* 6 */
{{3, 7}, false}, /* 7 */
{{1, 3}, false}, /* 8 */
{{0, 5}, false}, /* 9 */
{{1, 7}, false}, /* a */
{{0, 8}, true}, /* b */
{{5, 8}, false}}}, /* c */
};
const Vector2i LandfillArraySizes[]{
{3, 6}, /* 0 */
{2, 5}, /* 1 */
{4, 2}, /* 2 */
{3, 3}, /* 3 */
{3, 3}, /* 4 */
{2, 2}, /* 5 */
{2, 2}, /* 6 */
{2, 1}, /* 7 */
{2, 2}, /* 8 */
{2, 2}, /* 9 */
};
const struct {
const char* name;
AtlasLandfillFlags flags;
Vector3i size;
Vector3i filledSize;
Containers::Pair<Vector3i, bool> offsetsFlips[Containers::arraySize(LandfillArraySizes)];
} LandfillArrayData[]{
/* Various sorting aspects are tested in landfill() already, this just
checks the array-specific behaviors and the rotation-less overload */
{"no rotation", {}, {11, 6, 3}, {11, 6, 2}, {
/* 000
00011552222
00011552222
00011333444
00011333444 668899
00011333444 66889977 */
{{0, 0, 0}, false}, /* 0 */
{{3, 0, 0}, false}, /* 1 */
{{7, 3, 0}, false}, /* 2 */
{{5, 0, 0}, false}, /* 3 */
{{8, 0, 0}, false}, /* 4 */
{{5, 3, 0}, false}, /* 5 */
{{0, 0, 1}, false}, /* 6 */
{{6, 0, 1}, false}, /* 7 */
{{2, 0, 1}, false}, /* 8 */
{{4, 0, 1}, false}}}, /* 9 */
{"portrait, widest first", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 6, 3}, {11, 6, 2}, {
/* 000 55444
00011 55444
0001122 444
0001122333
0001122333 6688997
0001122333 6688997 */
{{0, 0, 0}, false}, /* 0 */
{{3, 0, 0}, false}, /* 1 */
{{5, 0, 0}, true}, /* 2 */
{{7, 0, 0}, false}, /* 3 */
{{8, 3, 0}, false}, /* 4 */
{{6, 4, 0}, false}, /* 5 */
{{0, 0, 1}, false}, /* 6 */
{{6, 0, 1}, true}, /* 7 */
{{2, 0, 1}, false}, /* 8 */
{{4, 0, 1}, false}}}, /* 9 */
{"portrait, widest first, unbounded", AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst, {11, 6, 3}, {11, 6, 2}, {
/* Should have the same result as above
000 55444
00011 55444
0001122 444
0001122333
0001122333 6688997
0001122333 6688997 */
{{0, 0, 0}, false}, /* 0 */
{{3, 0, 0}, false}, /* 1 */
{{5, 0, 0}, true}, /* 2 */
{{7, 0, 0}, false}, /* 3 */
{{8, 3, 0}, false}, /* 4 */
{{6, 4, 0}, false}, /* 5 */
{{0, 0, 1}, false}, /* 6 */
{{6, 0, 1}, true}, /* 7 */
{{2, 0, 1}, false}, /* 8 */
{{4, 0, 1}, false}}}, /* 9 */
};
/* Could make order[15] and then Containers::arraySize(), but then it won't /* Could make order[15] and then Containers::arraySize(), but then it won't
work on MSVC2015 and cause overly complicated code elsewhere */ work on MSVC2015 and cause overly complicated code elsewhere */
constexpr std::size_t ArrayPowerOfTwoOneLayerImageCount = 15; constexpr std::size_t ArrayPowerOfTwoOneLayerImageCount = 15;
@ -96,7 +421,36 @@ const struct {
}; };
AtlasTest::AtlasTest() { AtlasTest::AtlasTest() {
addTests({&AtlasTest::basic, addTests({&AtlasTest::debugLandfillFlag,
&AtlasTest::debugLandfillFlags,
&AtlasTest::landfillFullFit});
addInstancedTests({&AtlasTest::landfill},
Containers::arraySize(LandfillData));
addTests({&AtlasTest::landfillIncremental,
&AtlasTest::landfillNoFit,
&AtlasTest::landfillCopy,
&AtlasTest::landfillMove,
&AtlasTest::landfillArrayFullFit});
addInstancedTests({&AtlasTest::landfillArray},
Containers::arraySize(LandfillArrayData));
addTests({&AtlasTest::landfillArrayIncremental,
&AtlasTest::landfillArrayNoFit,
&AtlasTest::landfillArrayCopy,
&AtlasTest::landfillArrayMove,
&AtlasTest::landfillInvalidSize,
&AtlasTest::landfillSetFlagsInvalid,
&AtlasTest::landfillAddMissingRotations,
&AtlasTest::landfillAddInvalidViewSizes,
&AtlasTest::landfillAddTooLargeElement,
&AtlasTest::basic,
&AtlasTest::padding, &AtlasTest::padding,
&AtlasTest::empty, &AtlasTest::empty,
&AtlasTest::tooSmall, &AtlasTest::tooSmall,
@ -122,6 +476,518 @@ AtlasTest::AtlasTest() {
#endif #endif
} }
void AtlasTest::debugLandfillFlag() {
std::ostringstream out;
Debug{&out} << AtlasLandfillFlag::RotatePortrait << AtlasLandfillFlag(0xcafedead);
CORRADE_COMPARE(out.str(), "TextureTools::AtlasLandfillFlag::RotatePortrait TextureTools::AtlasLandfillFlag(0xcafedead)\n");
}
void AtlasTest::debugLandfillFlags() {
std::ostringstream out;
Debug{&out} << (AtlasLandfillFlag::RotateLandscape|AtlasLandfillFlag::NarrowestFirst|AtlasLandfillFlag(0xdead0000)) << AtlasLandfillFlags{};
CORRADE_COMPARE(out.str(), "TextureTools::AtlasLandfillFlag::RotateLandscape|TextureTools::AtlasLandfillFlag::NarrowestFirst|TextureTools::AtlasLandfillFlag(0xdead0000) TextureTools::AtlasLandfillFlags{}\n");
}
void AtlasTest::landfillFullFit() {
/* Trivial case to verify there are no off-by-one errors that would prevent
a tight fit */
AtlasLandfill atlas{{4, 6}};
CORRADE_COMPARE(atlas.size(), (Vector2i{4, 6}));
CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 0}));
CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst);
Vector2i offsets[4];
UnsignedByte rotationData[1];
Containers::MutableBitArrayView rotations{rotationData, 0, 4};
/* Testing the init list overload here as all others test the view */
CORRADE_VERIFY(atlas.add({
{2, 4}, /* 0 */
{2, 3}, /* 1 */
{2, 3}, /* 2 */
{2, 2}, /* 3 */
}, offsets, rotations));
CORRADE_COMPARE(atlas.filledSize(), (Vector2i{4, 6}));
CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({
false, false, false, false
}).sliceBit(0), TestSuite::Compare::Container);
/* 3322
3322
0022
0011
0011
0011 */
CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView<Vector2i>({
{0, 0}, /* 0 */
{2, 0}, /* 1 */
{2, 3}, /* 2 */
{0, 4}, /* 3 */
}), TestSuite::Compare::Container);
}
void AtlasTest::landfill() {
auto&& data = LandfillData[testCaseInstanceId()];
setTestCaseDescription(data.name);
AtlasLandfill atlas{data.size};
/* For unbounded sizes it should return 0 again */
CORRADE_COMPARE(atlas.size(), data.size);
Vector2i offsets[Containers::arraySize(LandfillSizes)];
/* In case rotations aren't enabled, this isn't zero-initialized by
add() */
UnsignedByte rotationData[2]{};
Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillSizes)};
atlas.setFlags(data.flags);
/* Test the rotations-less overload if no rotations are enabled */
if(!(data.flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)))
CORRADE_VERIFY(atlas.add(LandfillSizes, offsets));
else
CORRADE_VERIFY(atlas.add(LandfillSizes, offsets, rotations));
CORRADE_COMPARE(atlas.filledSize(), data.filledSize);
CORRADE_COMPARE_AS(rotations,
Containers::stridedArrayView(data.offsetsFlips)
.slice(&Containers::Pair<Vector2i, bool>::second)
.sliceBit(0),
TestSuite::Compare::Container);
CORRADE_COMPARE_AS(Containers::arrayView(offsets),
Containers::stridedArrayView(data.offsetsFlips)
.slice(&Containers::Pair<Vector2i, bool>::first),
TestSuite::Compare::Container);
}
void AtlasTest::landfillIncremental() {
/* Same as landfill(portrait, widest first) (which is the default flags)
but with the data split into three parts (0 to 4, 5 to 8, 9 to c), and
shuffled to verify the sort works as it should */
Vector2i sizeData[]{
{4, 2}, /* 0, rotated */
{3, 6}, /* 1 */
{3, 3}, /* 2 */
{5, 2}, /* 3, rotated */
{3, 3}, /* 4 */
{2, 2}, /* 5 */
{2, 2}, /* 6 */
{2, 2}, /* 7 */
{3, 2}, /* 8, rotated */
{1, 1}, /* 9 */
{1, 2}, /* a */
{2, 1}, /* b, rotated */
{1, 2}, /* c */
};
auto sizes = Containers::arrayView(sizeData);
Vector2i offsetData[Containers::arraySize(sizeData)];
auto offsets = Containers::arrayView(offsetData);
UnsignedByte rotationData[2];
Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)};
AtlasLandfill atlas{{11, 10}};
CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 0}));
CORRADE_VERIFY(atlas.add(
sizes.prefix(5),
offsets.prefix(5),
rotations.prefix(5)));
CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 6}));
CORRADE_VERIFY(atlas.add(
sizes.slice(5, 9),
offsets.slice(5, 9),
rotations.slice(5, 9)));
CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 8}));
CORRADE_VERIFY(atlas.add(
sizes.exceptPrefix(9),
offsets.exceptPrefix(9),
rotations.exceptPrefix(9)));
CORRADE_COMPARE(atlas.filledSize(), (Vector2i{11, 10}));
CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({
true, false, false, true, false, false, false, false, true, false,
false, true, false
}).sliceBit(0), TestSuite::Compare::Container);
/* abc
abc9
7766
77665588
111 5588444
11133 88444
1113300 444
1113300222
1113300222
1113300222 */
CORRADE_COMPARE_AS(offsets, Containers::arrayView<Vector2i>({
{5, 0}, /* 0 */
{0, 0}, /* 1 */
{7, 0}, /* 2 */
{3, 0}, /* 3 */
{8, 3}, /* 4 */
{4, 5}, /* 5 */
{2, 6}, /* 6 */
{0, 6}, /* 7 */
{6, 4}, /* 8 */
{3, 8}, /* 9 */
{0, 8}, /* a */
{1, 8}, /* b */
{2, 8}, /* c */
}), 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 */
AtlasLandfill atlas{{11, 9}};
Vector2i offsets[Containers::arraySize(LandfillSizes)];
UnsignedByte rotationData[2];
Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillSizes)};
CORRADE_VERIFY(!atlas.add(LandfillSizes, offsets, rotations));
}
void AtlasTest::landfillCopy() {
CORRADE_VERIFY(!std::is_copy_constructible<AtlasLandfill>{});
CORRADE_VERIFY(!std::is_copy_assignable<AtlasLandfill>{});
}
void AtlasTest::landfillMove() {
AtlasLandfill a{{16, 24}};
Vector2i offsets[2];
UnsignedByte rotations[1];
CORRADE_VERIFY(a.add({{15, 17}, {2, 3}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2}));
AtlasLandfill b = Utility::move(a);
CORRADE_COMPARE(b.size(), (Vector2i{16, 24}));
CORRADE_COMPARE(b.filledSize(), (Vector2i{16, 20}));
AtlasLandfill c{{16, 12}};
c = Utility::move(b);
CORRADE_COMPARE(c.size(), (Vector2i{16, 24}));
CORRADE_COMPARE(c.filledSize(), (Vector2i{16, 20}));
CORRADE_VERIFY(std::is_nothrow_move_constructible<AtlasLandfill>::value);
CORRADE_VERIFY(std::is_nothrow_move_assignable<AtlasLandfill>::value);
}
void AtlasTest::landfillArrayFullFit() {
/* Trivial case to verify there are no off-by-one errors that would prevent
a tight fit */
AtlasLandfillArray atlas{{4, 5, 2}};
CORRADE_COMPARE(atlas.size(), (Vector3i{4, 5, 2}));
CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 5, 0}));
CORRADE_COMPARE(atlas.flags(), AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::WidestFirst);
Vector3i offsets[6];
UnsignedByte rotationData[1];
Containers::MutableBitArrayView rotations{rotationData, 0, 6};
/* Testing the init list overload as all others test the view */
CORRADE_VERIFY(atlas.add({
{3, 5}, /* 0 */
{1, 5}, /* 1 */
{3, 3}, /* 2 */
{1, 3}, /* 3 */
{2, 2}, /* 4 */
{2, 2}, /* 5 */
}, offsets, rotations));
CORRADE_COMPARE(atlas.filledSize(), (Vector3i{4, 5, 2}));
CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({
false, false, false, false, false, false
}).sliceBit(0), TestSuite::Compare::Container);
/* 0001 5544
0001 5544
0001 2223
0001 2223
0001 2223 */
CORRADE_COMPARE_AS(Containers::arrayView(offsets), Containers::arrayView<Vector3i>({
{0, 0, 0}, /* 0 */
{3, 0, 0}, /* 1 */
{0, 0, 1}, /* 2 */
{3, 0, 1}, /* 3 */
{2, 3, 1}, /* 4 */
{0, 3, 1}, /* 5 */
}), TestSuite::Compare::Container);
}
void AtlasTest::landfillArray() {
auto&& data = LandfillArrayData[testCaseInstanceId()];
setTestCaseDescription(data.name);
AtlasLandfillArray atlas{data.size};
/* For unbounded sizes it should return 0 again */
CORRADE_COMPARE(atlas.size(), data.size);
Vector3i offsets[Containers::arraySize(LandfillArraySizes)];
/* In case rotations aren't enabled, this isn't zero-initialized by
add() */
UnsignedByte rotationData[2]{};
Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillArraySizes)};
atlas.setFlags(data.flags);
/* Test the rotations-less overload if no rotations are enabled */
if(!(data.flags & (AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape)))
CORRADE_VERIFY(atlas.add(LandfillArraySizes, offsets));
else
CORRADE_VERIFY(atlas.add(LandfillArraySizes, offsets, rotations));
CORRADE_COMPARE(atlas.filledSize(), data.filledSize);
CORRADE_COMPARE_AS(rotations,
Containers::stridedArrayView(data.offsetsFlips)
.slice(&Containers::Pair<Vector3i, bool>::second)
.sliceBit(0),
TestSuite::Compare::Container);
CORRADE_COMPARE_AS(Containers::arrayView(offsets),
Containers::stridedArrayView(data.offsetsFlips)
.slice(&Containers::Pair<Vector3i, bool>::first),
TestSuite::Compare::Container);
}
void AtlasTest::landfillArrayIncremental() {
/* 000 55444
00011 55444
0001122 444
0001122333
0001122333 6688997
0001122333 6688997 */
Vector2i sizeData[]{
{4, 2}, /* 0, rotated */
{3, 6}, /* 1 */
{3, 3}, /* 2 */
{5, 2}, /* 3, rotated */
{2, 2}, /* 4 */
{2, 2}, /* 5 */
{3, 3}, /* 6 */
{2, 2}, /* 7 */
{2, 1}, /* 8, rotated */
{2, 2}, /* 9 */
};
auto sizes = Containers::arrayView(sizeData);
Vector3i offsetData[Containers::arraySize(sizeData)];
auto offsets = Containers::arrayView(offsetData);
UnsignedByte rotationData[2];
Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(sizeData)};
AtlasLandfillArray atlas{{11, 6, 2}};
CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 0}));
CORRADE_VERIFY(atlas.add(
sizes.prefix(4),
offsets.prefix(4),
rotations.prefix(4)));
CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 1}));
CORRADE_VERIFY(atlas.add(
sizes.slice(4, 7),
offsets.slice(4, 7),
rotations.slice(4, 7)));
CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 2}));
CORRADE_VERIFY(atlas.add(
sizes.exceptPrefix(7),
offsets.exceptPrefix(7),
rotations.exceptPrefix(7)));
CORRADE_COMPARE(atlas.filledSize(), (Vector3i{11, 6, 2}));
CORRADE_COMPARE_AS(rotations, Containers::stridedArrayView({
true, false, false, true, false, false, false, false, true, false
}).sliceBit(0), TestSuite::Compare::Container);
/* 111 44666
11133 44666
1113300 666
1113300222
1113300222 5577998
1113300222 5577998 */
CORRADE_COMPARE_AS(offsets, Containers::arrayView<Vector3i>({
{5, 0, 0}, /* 0 */
{0, 0, 0}, /* 1 */
{7, 0, 0}, /* 2 */
{3, 0, 0}, /* 3 */
{6, 4, 0}, /* 4 */
{0, 0, 1}, /* 5 */
{8, 3, 0}, /* 6 */
{2, 0, 1}, /* 7 */
{6, 0, 1}, /* 8 */
{4, 0, 1}, /* 9 */
}), 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 */
AtlasLandfillArray atlas{{11, 6, 1}};
Vector3i offsets[Containers::arraySize(LandfillArraySizes)];
UnsignedByte rotationData[2];
Containers::MutableBitArrayView rotations{rotationData, 0, Containers::arraySize(LandfillArraySizes)};
CORRADE_VERIFY(!atlas.add(LandfillArraySizes, offsets, rotations));
}
void AtlasTest::landfillArrayCopy() {
CORRADE_VERIFY(!std::is_copy_constructible<AtlasLandfillArray>{});
CORRADE_VERIFY(!std::is_copy_assignable<AtlasLandfillArray>{});
}
void AtlasTest::landfillArrayMove() {
AtlasLandfillArray a{{16, 24, 8}};
Vector3i offsets[2];
UnsignedByte rotations[1];
CORRADE_VERIFY(a.add({{12, 17}, {5, 12}}, offsets, Containers::MutableBitArrayView{rotations, 0, 2}));
AtlasLandfillArray b = Utility::move(a);
CORRADE_COMPARE(b.size(), (Vector3i{16, 24, 8}));
CORRADE_COMPARE(b.filledSize(), (Vector3i{16, 24, 2}));
AtlasLandfillArray c{{16, 12, 1}};
c = Utility::move(b);
CORRADE_COMPARE(c.size(), (Vector3i{16, 24, 8}));
CORRADE_COMPARE(c.filledSize(), (Vector3i{16, 24, 2}));
CORRADE_VERIFY(std::is_nothrow_move_constructible<AtlasLandfillArray>::value);
CORRADE_VERIFY(std::is_nothrow_move_assignable<AtlasLandfillArray>::value);
}
void AtlasTest::landfillInvalidSize() {
CORRADE_SKIP_IF_NO_ASSERT();
/* These are fine */
AtlasLandfill{{16, 0}};
AtlasLandfill{{65536, 16}};
AtlasLandfillArray{{16, 16, 0}};
AtlasLandfillArray{{65536, 16, 16}};
std::ostringstream out;
Error redirectError{&out};
AtlasLandfill{{0, 16}};
AtlasLandfill{{65537, 16}};
AtlasLandfillArray{{0, 16, 16}};
AtlasLandfillArray{{16, 0, 16}};
AtlasLandfillArray{{65537, 16, 16}};
CORRADE_COMPARE_AS(out.str(),
"TextureTools::AtlasLandfill: expected non-zero width, got {0, 16}\n"
"TextureTools::AtlasLandfill: expected width to fit into 16 bits, got {65537, 16}\n"
"TextureTools::AtlasLandfillArray: expected non-zero width and height, got {0, 16, 16}\n"
"TextureTools::AtlasLandfillArray: expected non-zero width and height, got {16, 0, 16}\n"
"TextureTools::AtlasLandfillArray: expected width to fit into 16 bits, got {65537, 16, 16}\n",
TestSuite::Compare::String);
}
void AtlasTest::landfillSetFlagsInvalid() {
CORRADE_SKIP_IF_NO_ASSERT();
AtlasLandfill atlas{{16, 16}};
AtlasLandfillArray array{{16, 16, 1}};
std::ostringstream out;
Error redirectError{&out};
atlas.setFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape);
array.setFlags(AtlasLandfillFlag::RotatePortrait|AtlasLandfillFlag::RotateLandscape);
atlas.setFlags(AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::NarrowestFirst);
array.setFlags(AtlasLandfillFlag::WidestFirst|AtlasLandfillFlag::NarrowestFirst);
CORRADE_COMPARE_AS(out.str(),
"TextureTools::AtlasLandfill::setFlags(): only one of RotatePortrait and RotateLandscape can be set\n"
"TextureTools::AtlasLandfillArray::setFlags(): only one of RotatePortrait and RotateLandscape can be set\n"
"TextureTools::AtlasLandfill::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n"
"TextureTools::AtlasLandfillArray::setFlags(): only one of WidestFirst and NarrowestFirst can be set\n",
TestSuite::Compare::String);
}
void AtlasTest::landfillAddMissingRotations() {
CORRADE_SKIP_IF_NO_ASSERT();
AtlasLandfill atlasPortrait{{16, 23}};
AtlasLandfill atlasLandscape{{16, 23}};
AtlasLandfillArray arrayPortrait{{16, 23, 2}};
AtlasLandfillArray arrayLandscape{{16, 23, 2}};
atlasPortrait.setFlags(AtlasLandfillFlag::RotatePortrait);
arrayPortrait.setFlags(AtlasLandfillFlag::RotatePortrait);
atlasLandscape.setFlags(AtlasLandfillFlag::RotateLandscape);
arrayLandscape.setFlags(AtlasLandfillFlag::RotateLandscape);
Vector2i sizes[2];
Vector2i offsets[2];
Vector3i offsets3[2];
std::ostringstream out;
Error redirectError{&out};
atlasPortrait.add(sizes, offsets);
arrayPortrait.add(sizes, offsets3);
/* "Testing" the rotation-less init list variants too */
atlasLandscape.add({{}, {}}, offsets);
arrayLandscape.add({{}, {}}, offsets3);
CORRADE_COMPARE(out.str(),
"TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n"
"TextureTools::AtlasLandfillArray::add(): TextureTools::AtlasLandfillFlag::RotatePortrait set, expected a rotations view\n"
"TextureTools::AtlasLandfill::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n"
"TextureTools::AtlasLandfillArray::add(): TextureTools::AtlasLandfillFlag::RotateLandscape set, expected a rotations view\n");
}
void AtlasTest::landfillAddInvalidViewSizes() {
CORRADE_SKIP_IF_NO_ASSERT();
AtlasLandfill atlas{{16, 23}};
Vector2i sizes[2];
Vector2i offsets[2];
Vector2i offsetsInvalid[3];
UnsignedByte rotationsData[1];
Containers::MutableBitArrayView rotations{rotationsData, 0, 2};
Containers::MutableBitArrayView rotationsInvalid{rotationsData, 0, 3};
std::ostringstream out;
Error redirectError{&out};
atlas.add(sizes, offsetsInvalid, rotations);
atlas.add(sizes, offsets, rotationsInvalid);
CORRADE_COMPARE(out.str(),
"TextureTools::AtlasLandfill::add(): expected sizes and offsets views to have the same size, got 2 and 3\n"
"TextureTools::AtlasLandfill::add(): expected sizes and rotations views to have the same size, got 2 and 3\n");
}
void AtlasTest::landfillAddTooLargeElement() {
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}};
array.setFlags(AtlasLandfillFlag::RotateLandscape);
array2.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({{16, 23}, {0, 23}}, offsets, rotations);
array.add({{23, 16}, {23, 0}}, offsets3, rotations);
atlas.add({{16, 23}, {17, 23}}, offsets, rotations);
array.add({{23, 16}, {23, 17}}, offsets3, rotations);
/* Sizes that fit but don't after a flip */
atlas2.add({{13, 13}, {15, 13}}, offsets, rotations);
array2.add({{13, 13}, {13, 15}}, 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, 23}\n"
"TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 0}\n"
"TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 23} but got {17, 23}\n"
"TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {23, 16} but got {23, 17}\n"
"TextureTools::AtlasLandfill::add(): expected size 1 to be non-zero and not larger than {16, 13} but got {13, 15}\n"
"TextureTools::AtlasLandfillArray::add(): expected size 1 to be non-zero and not larger than {13, 16} but got {15, 13}\n",
TestSuite::Compare::String);
}
void AtlasTest::basic() { void AtlasTest::basic() {
std::vector<Range2Di> atlas = TextureTools::atlas({64, 64}, { std::vector<Range2Di> atlas = TextureTools::atlas({64, 64}, {
{12, 18}, {12, 18},

1
src/Magnum/TextureTools/Test/AtlasTestFiles/.gitignore vendored

@ -0,0 +1 @@
stb_rect_pack.h

56
src/Magnum/TextureTools/Test/AtlasTestFiles/extract-font-glyph-sizes.py

@ -0,0 +1,56 @@
#!/usr/bin/env python3
#
# This file is part of Magnum.
#
# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
# 2020, 2021, 2022, 2023 Vladimír Vondruš <mosra@centrum.cz>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
# Loads a font and saves sizes of all its glyphs to a binary file that's then
# used by AtlasBenchmark.cpp
import argparse
import array
from magnum import math, text
from magnum import *
parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('output')
parser.add_argument('--size', default=16)
args = parser.parse_args()
font = text.FontManager().load_and_instantiate('FreeTypeFont')
font.open_file(args.input, args.size)
sizes = []
for i in range(font.glyph_count):
size = Vector2i(math.ceil(font.glyph_size(i)))
if not size.product():
continue
sizes += list(size)
print("Writing {} glyph sizes, {} non-empty".format(font.glyph_count, len(sizes)//2))
with open(args.output, 'wb') as output:
array.array('h', sizes).tofile(output)

67
src/Magnum/TextureTools/Test/AtlasTestFiles/extract-texture-sizes.py

@ -0,0 +1,67 @@
#!/usr/bin/env python3
#
# This file is part of Magnum.
#
# Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
# 2020, 2021, 2022, 2023 Vladimír Vondruš <mosra@centrum.cz>
#
# Permission is hereby granted, free of charge, to any person obtaining a
# copy of this software and associated documentation files (the "Software"),
# to deal in the Software without restriction, including without limitation
# the rights to use, copy, modify, merge, publish, distribute, sublicense,
# and/or sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included
# in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
#
# Loads a font and saves sizes of all its glyphs to a binary file that's then
# used by AtlasBenchmark.cpp
import argparse
import array
from magnum import math, trade
from magnum import *
parser = argparse.ArgumentParser()
parser.add_argument('input')
parser.add_argument('output')
parser.add_argument('--max', type=int, default=1024)
parser.add_argument('--ratio', type=int, default=1)
args = parser.parse_args()
importer = trade.ImporterManager().load_and_instantiate('AnySceneImporter')
importer.open_file(args.input)
print("Importing {} images".format(importer.image2d_count))
sizes = []
total = 0
max = Vector2i()
for i in range(importer.image2d_count):
image = importer.image2d(i)
size = image.size/args.ratio;
if (size > Vector2i(args.max)).any():
continue
sizes += list(size)
total += size.product()
max = math.max(size, max)
print("Remains {0} images not larger than {1}x{1}".format(len(sizes)//2, args.max))
print("Total area: {} ({:.1f}**2), max: {}x{}".format(total, math.sqrt(total), max.x, max.y))
with open(args.output, 'wb') as output:
array.array('h', sizes).tofile(output)

BIN
src/Magnum/TextureTools/Test/AtlasTestFiles/fp-102344349-textures.bin

Binary file not shown.

BIN
src/Magnum/TextureTools/Test/AtlasTestFiles/fp-103997718-171030855-textures.bin

Binary file not shown.

BIN
src/Magnum/TextureTools/Test/AtlasTestFiles/noto-serif-tangut-glyphs.bin

Binary file not shown.

BIN
src/Magnum/TextureTools/Test/AtlasTestFiles/oxygen-glyphs.bin

Binary file not shown.

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

@ -27,14 +27,6 @@
# 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 MagnumTextureToolsTestLib)
if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID)
set(TEXTURETOOLS_TEST_DIR .)
else()
set(TEXTURETOOLS_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR})
endif()
if(MAGNUM_BUILD_GL_TESTS) if(MAGNUM_BUILD_GL_TESTS)
# Otherwise CMake complains that Corrade::PluginManager is not found, wtf # Otherwise CMake complains that Corrade::PluginManager is not found, wtf
find_package(Corrade REQUIRED PluginManager) find_package(Corrade REQUIRED PluginManager)
@ -47,12 +39,57 @@ if(MAGNUM_BUILD_GL_TESTS)
set(TGAIMPORTER_PLUGIN_FILENAME $<TARGET_FILE:TgaImporter>) set(TGAIMPORTER_PLUGIN_FILENAME $<TARGET_FILE:TgaImporter>)
endif() endif()
endif() endif()
endif()
# First replace ${} variables, then $<> generator expressions if(CORRADE_TARGET_EMSCRIPTEN OR CORRADE_TARGET_ANDROID)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake set(TEXTURETOOLS_TEST_DIR .)
${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) else()
file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/configure.h set(TEXTURETOOLS_TEST_DIR ${CMAKE_CURRENT_SOURCE_DIR})
INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in) endif()
# First replace ${} variables, then $<> generator expressions
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/configure.h.cmake
${CMAKE_CURRENT_BINARY_DIR}/configure.h.in)
file(GENERATE OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/configure.h
INPUT ${CMAKE_CURRENT_BINARY_DIR}/configure.h.in)
corrade_add_test(TextureToolsAtlasTest AtlasTest.cpp LIBRARIES MagnumTextureToolsTestLib)
corrade_add_test(TextureToolsAtlasBenchmark AtlasBenchmark.cpp
LIBRARIES
MagnumDebugTools
MagnumTextureTools
MagnumTrade
FILES
# ./extract-texture-sizes.py ~/Data/fp-scenes/scenes/original/102344349.glb fp-102344349-textures.bin --ratio 9
AtlasTestFiles/fp-102344349-textures.bin
# ./extract-texture-sizes.py ~/Data/fp-scenes/scenes/original/103997718_171030855.glb fp-103997718-171030855-textures.bin --ratio 8
AtlasTestFiles/fp-103997718-171030855-textures.bin
# ./extract-font-glyph-sizes.py /usr/share/fonts/noto/NotoSerifTangut-Regular.ttf noto-serif-tangut-glyphs.bin
AtlasTestFiles/noto-serif-tangut-glyphs.bin
# ./extract-font-glyph-sizes.py ~/Code/magnum-plugins/src/MagnumPlugins/FreeTypeFont/Test/Oxygen.ttf oxygen-glyphs.bin
AtlasTestFiles/oxygen-glyphs.bin)
target_include_directories(TextureToolsAtlasBenchmark PRIVATE ${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>)
if(CORRADE_TARGET_EMSCRIPTEN)
if(CMAKE_VERSION VERSION_LESS 3.13)
message(FATAL_ERROR "CMake 3.13+ is required in order to specify Emscripten linker options")
endif()
# It allocates an 8K image in order to verify no overlaps
target_link_options(TextureToolsAtlasBenchmark PRIVATE "SHELL:-s ALLOW_MEMORY_GROWTH=1")
endif()
if(MAGNUM_BUILD_PLUGINS_STATIC)
if(MAGNUM_WITH_TGAIMAGECONVERTER)
target_link_libraries(TextureToolsAtlasBenchmark PRIVATE TgaImageConverter)
endif()
else()
# So the plugins get properly built when building the test
if(MAGNUM_WITH_TGAIMAGECONVERTER)
add_dependencies(TextureToolsAtlasBenchmark TgaImageConverter)
endif()
endif()
if(MAGNUM_BUILD_GL_TESTS)
# Otherwise CMake complains that Corrade::PluginManager is not found, wtf
find_package(Corrade REQUIRED PluginManager)
set(TextureToolsDistanceFieldGLTest_SRCS DistanceFieldGLTest.cpp) set(TextureToolsDistanceFieldGLTest_SRCS DistanceFieldGLTest.cpp)
if(CORRADE_TARGET_IOS) if(CORRADE_TARGET_IOS)

Loading…
Cancel
Save