Skip to content

Commit

Permalink
TextureTools: new landfill atlas packer.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
mosra committed Sep 26, 2023
1 parent c63ae80 commit 66bf0b2
Show file tree
Hide file tree
Showing 15 changed files with 2,995 additions and 15 deletions.
38 changes: 38 additions & 0 deletions doc/generated/atlas.cpp
Expand Up @@ -25,6 +25,9 @@

#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/StringStl.h> /** @todo remove once formatString() isn't used */
#include <Corrade/Utility/FormatStl.h> /** @todo remove once growable String exists */
Expand Down Expand Up @@ -114,5 +117,40 @@ int main() {
)");

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 changes: 78 additions & 0 deletions doc/snippets/MagnumTextureTools.cpp
Expand Up @@ -24,6 +24,8 @@
*/

#include <Corrade/Containers/Array.h>
#include <Corrade/Containers/BitArray.h>
#include <Corrade/Containers/BitArrayView.h>
#include <Corrade/Containers/StridedArrayView.h>
#include <Corrade/Utility/Algorithms.h>

Expand All @@ -39,6 +41,82 @@
using namespace Magnum;

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] */
Containers::ArrayView<const ImageView2D> input;
Expand Down

0 comments on commit 66bf0b2

Please sign in to comment.