diff --git a/doc/changelog.dox b/doc/changelog.dox index 4b6b40451..b8fecf55d 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -376,6 +376,9 @@ See also: [mosra/magnum#2](https://github.com/mosra/magnum/issues/2)) - New @ref TextureTools::atlasArrayPowerOfTwo() utility for optimal packing of power-of-two textures into a texture atlas array +- New @ref TextureTools::atlasTextureCoordinateTransformation() helper for + creating an appropriate texture coordinate transformation matrix for + textures placed into an atlas - Added a @ref TextureTools::DistanceField::operator()() overload taking a @ref GL::Framebuffer instead of a @ref GL::Texture as an output for an easier ability to download the resulting image on OpenGL ES platforms; diff --git a/doc/snippets/MagnumTextureTools.cpp b/doc/snippets/MagnumTextureTools.cpp index 240848835..448af4033 100644 --- a/doc/snippets/MagnumTextureTools.cpp +++ b/doc/snippets/MagnumTextureTools.cpp @@ -34,7 +34,11 @@ #include "Magnum/PixelFormat.h" #include "Magnum/Math/Color.h" #include "Magnum/Math/FunctionsBatch.h" +#include "Magnum/Math/Matrix3.h" +#include "Magnum/MeshTools/Transform.h" #include "Magnum/TextureTools/Atlas.h" +#include "Magnum/Trade/MaterialData.h" +#include "Magnum/Trade/MeshData.h" #define DOXYGEN_ELLIPSIS(...) __VA_ARGS__ @@ -142,4 +146,40 @@ for(std::size_t i = 0; i != input.size(); ++i) { } /* [atlasArrayPowerOfTwo] */ } + +{ +Vector2i atlasSize; +Containers::StridedArrayView1D sizes; +Containers::StridedArrayView1D offsets; +Containers::BitArrayView rotations; +std::size_t i{}; +/* [atlasTextureCoordinateTransformation] */ +Matrix3 matrix = (rotations[i] ? + TextureTools::atlasTextureCoordinateTransformationRotatedCounterClockwise : + TextureTools::atlasTextureCoordinateTransformation +)(atlasSize, sizes[i], offsets[i]); +/* [atlasTextureCoordinateTransformation] */ +static_cast(matrix); +} + +{ +Matrix3 matrix; +/* [atlasTextureCoordinateTransformation-meshdata] */ +Trade::MeshData mesh = DOXYGEN_ELLIPSIS(Trade::MeshData{MeshPrimitive::Points, 0}); + +MeshTools::transformTextureCoordinates2DInPlace(mesh, matrix); +/* [atlasTextureCoordinateTransformation-meshdata] */ +} + +{ +Matrix3 matrix; +/* [atlasTextureCoordinateTransformation-materialdata] */ +Trade::MaterialData material = DOXYGEN_ELLIPSIS(Trade::MaterialData{{}, {}}); + +Matrix3& materialMatrix = + material.mutableAttribute(Trade::MaterialAttribute::TextureMatrix); +materialMatrix = matrix*materialMatrix; +/* [atlasTextureCoordinateTransformation-materialdata] */ +} + } diff --git a/src/Magnum/TextureTools/Atlas.cpp b/src/Magnum/TextureTools/Atlas.cpp index 66be41953..d6337c138 100644 --- a/src/Magnum/TextureTools/Atlas.cpp +++ b/src/Magnum/TextureTools/Atlas.cpp @@ -33,7 +33,7 @@ #include #include -#include "Magnum/Math/Vector3.h" +#include "Magnum/Math/Matrix3.h" #include "Magnum/Math/Functions.h" #include "Magnum/Math/FunctionsBatch.h" @@ -503,4 +503,33 @@ Containers::Pair> atlasArrayPowerOfTwo(const Ve } #endif +Matrix3 atlasTextureCoordinateTransformation(const Vector2i& atlasSize, const Vector2i& size, const Vector2i& offset) { + CORRADE_ASSERT((offset + size <= atlasSize).all(), + "TextureTools::atlasTextureCoordinateTransformation(): size" << Debug::packed << size << "and offset" << Debug::packed << offset << "doesn't fit into" << Debug::packed << atlasSize, {}); + const Vector2 atlasSizeF = Vector2{atlasSize}; + return Matrix3{{Float(size.x())/atlasSizeF.x(), 0.0f, 0.0f}, + {0.0f, Float(size.y())/atlasSizeF.y(), 0.0f}, + {Vector2{offset}/atlasSizeF, 1.0f}}; +} + +Matrix3 atlasTextureCoordinateTransformationRotatedCounterClockwise(const Vector2i& atlasSize, const Vector2i& size, const Vector2i& offset) { + CORRADE_ASSERT((offset + size.flipped() <= atlasSize).all(), + "TextureTools::atlasTextureCoordinateTransformationRotatedCounterClockwise(): (rotated) size" << Debug::packed << size.flipped() << "and offset" << Debug::packed << offset << "doesn't fit into" << Debug::packed << atlasSize, {}); + const Vector2 atlasSizeF = Vector2{atlasSize}; + return Matrix3{{0.0f, Float(size.x())/atlasSizeF.y(), 0.0f}, + {-Float(size.y())/atlasSizeF.x(), 0.0f, 0.0f}, + {Float(offset.x() + size.y())/atlasSizeF.x(), + Float(offset.y())/atlasSizeF.y(), 1.0f}}; +} + +Matrix3 atlasTextureCoordinateTransformationRotatedClockwise(const Vector2i& atlasSize, const Vector2i& size, const Vector2i& offset) { + CORRADE_ASSERT((offset + size.flipped() <= atlasSize).all(), + "TextureTools::atlasTextureCoordinateTransformationRotatedClockwise(): (rotated) size" << Debug::packed << size.flipped() << "and offset" << Debug::packed << offset << "doesn't fit into" << Debug::packed << atlasSize, {}); + const Vector2 atlasSizeF = Vector2{atlasSize}; + return Matrix3{{0.0f, -Float(size.x())/atlasSizeF.y(), 0.0f}, + {Float(size.y())/atlasSizeF.x(), 0.0f, 0.0f}, + {Float(offset.x())/atlasSizeF.x(), + Float(offset.y() + size.x())/atlasSizeF.y(), 1.0f}}; +} + }} diff --git a/src/Magnum/TextureTools/Atlas.h b/src/Magnum/TextureTools/Atlas.h index 11b5b94dd..dc1092802 100644 --- a/src/Magnum/TextureTools/Atlas.h +++ b/src/Magnum/TextureTools/Atlas.h @@ -137,6 +137,11 @@ efficiency while not making any difference for texture mapping. @snippet MagnumTextureTools.cpp AtlasLandfill-usage +Calculating a texture coordinate transformation matrix for a particular image +can then be done with @ref atlasTextureCoordinateTransformation(), see its +documentation for an example of how to calculate and apply the matrix to either +the mesh directly or to a material / shader. + 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 @@ -153,6 +158,10 @@ height. @snippet MagnumTextureTools.cpp AtlasLandfill-usage-array +The layer has to be taken into an account in addition to the texture coordinate +transformation matrix calculated with @ref atlasTextureCoordinateTransformation(), +for example by adding a texture layer attribute to @ref Trade::MaterialData. + @section TextureTools-AtlasLandfill-process Packing process On every @ref add(), the algorithm first makes all sizes the same orientation @@ -446,7 +455,11 @@ texture in the set will lead to the least wasted space in the last layer. @htmlinclude atlas-array-power-of-two.svg -Example usage is shown below. +Example usage is shown below. Calculating a texture coordinate transformation +matrix for a particular image can then be done with +@ref atlasTextureCoordinateTransformation(), see its documentation for how to +calculate and apply the matrix to either the mesh directly or to a material / +shader. @snippet MagnumTextureTools.cpp atlasArrayPowerOfTwo @@ -486,6 +499,63 @@ MAGNUM_TEXTURETOOLS_EXPORT CORRADE_DEPRECATED("use the overload taking offsets a MAGNUM_TEXTURETOOLS_EXPORT CORRADE_DEPRECATED("use the overload taking offsets as an output view instead") Containers::Pair> atlasArrayPowerOfTwo(const Vector2i& layerSize, std::initializer_list sizes); #endif +/** +@brief Calculate a texture coordinate transformation matrix for an atlas-packed item +@m_since_latest + +Together with @ref atlasTextureCoordinateTransformationRotatedCounterClockwise() +or @ref atlasTextureCoordinateTransformationRotatedClockwise() meant be used to +adjust mesh texture coordinate attributes after packing textures with +@ref AtlasLandfill or @ref atlasArrayPowerOfTwo(). Expects that @p size and +@p offset fit into the @p atlasSize, the rotated variants expect that @p size +with coordinates flipped and @p offset fit into the @p atlasSize. + +With a concrete `atlasSize`, `sizes` being the input sizes passed to +@ref AtlasLandfill::add() (i.e., without any potential rotations applied yet), +and `offsets` and `rotations` being the output, the usage is as follows: + +@snippet MagnumTextureTools.cpp atlasTextureCoordinateTransformation + +The resulting matrix can be then directly used to adjust texture coordinates, +like below with @ref MeshTools::transformTextureCoordinates2DInPlace() on a +@link Trade::MeshData @endlink: + +@snippet MagnumTextureTools.cpp atlasTextureCoordinateTransformation-meshdata + +Alternatively, for example in cases where a single mesh is used with several different textures, the transformation can be applied at draw time, such as +with @ref Shaders::FlatGL::setTextureMatrix(). In case there's already a +texture transformation matrix being applied when drawing, the new +transformation has to happen *after*, so multiplied from the left side. For +example with a @ref Trade::MaterialData that contains a +@link Trade::MaterialAttribute::TextureMatrix @endlink: + +@snippet MagnumTextureTools.cpp atlasTextureCoordinateTransformation-materialdata +*/ +MAGNUM_TEXTURETOOLS_EXPORT Matrix3 atlasTextureCoordinateTransformation(const Vector2i& atlasSize, const Vector2i& size, const Vector2i& offset); + +/** +@brief Calculate a texture coordinate transformation matrix for an atlas-packed item rotated counterclockwise +@m_since_latest + +Like @ref atlasTextureCoordinateTransformation(), but swaps X and Y of @p size +and produces a matrix that rotates the texture coordinates 90° +counterclockwise. The lower left corner of the input becomes a lower right +corner. See @ref atlasTextureCoordinateTransformationRotatedClockwise() for a +clockwise variant. +*/ +MAGNUM_TEXTURETOOLS_EXPORT Matrix3 atlasTextureCoordinateTransformationRotatedCounterClockwise(const Vector2i& atlasSize, const Vector2i& size, const Vector2i& offset); + +/** +@brief Calculate a texture coordinate transformation matrix for an atlas-packed item rotated clockwise +@m_since_latest + +Like @ref atlasTextureCoordinateTransformation(), but swaps X and Y of @p size +and produces a matrix that rotates the texture coordinates 90° clockwise. The lower left corner of the input becomes an upper left corner. See +@ref atlasTextureCoordinateTransformationRotatedClockwise() for a +counterclockwise variant. +*/ +MAGNUM_TEXTURETOOLS_EXPORT Matrix3 atlasTextureCoordinateTransformationRotatedClockwise(const Vector2i& atlasSize, const Vector2i& size, const Vector2i& offset); + }} #endif diff --git a/src/Magnum/TextureTools/Test/AtlasTest.cpp b/src/Magnum/TextureTools/Test/AtlasTest.cpp index e879d7bc3..36a0115dd 100644 --- a/src/Magnum/TextureTools/Test/AtlasTest.cpp +++ b/src/Magnum/TextureTools/Test/AtlasTest.cpp @@ -35,7 +35,7 @@ #include #include -#include "Magnum/Math/Vector3.h" +#include "Magnum/Math/Matrix3.h" #include "Magnum/TextureTools/Atlas.h" #ifdef MAGNUM_BUILD_DEPRECATED @@ -92,6 +92,9 @@ struct AtlasTest: TestSuite::Tester { #ifdef MAGNUM_BUILD_DEPRECATED void arrayPowerOfTwoDeprecated(); #endif + + void textureCoordinateTransformation(); + void textureCoordinateTransformationOutOfBounds(); }; const Vector2i LandfillSizes[]{ @@ -551,6 +554,9 @@ AtlasTest::AtlasTest() { #ifdef MAGNUM_BUILD_DEPRECATED addTests({&AtlasTest::arrayPowerOfTwoDeprecated}); #endif + + addTests({&AtlasTest::textureCoordinateTransformation, + &AtlasTest::textureCoordinateTransformationOutOfBounds}); } void AtlasTest::debugLandfillFlag() { @@ -1440,6 +1446,164 @@ void AtlasTest::arrayPowerOfTwoDeprecated() { } #endif +void AtlasTest::textureCoordinateTransformation() { + const Vector2i atlasSize{4, 5}; + const Vector2i size{2, 1}; + const Vector2i offset{1, 2}; + const Vector2 a{0.0f, 0.0f}; + const Vector2 b{1.0f, 0.0f}; + const Vector2 c{0.0f, 1.0f}; + const Vector2 d{1.0f, 1.0f}; + + /* Trivial rotation cases with no scaling or offset should return in exact + corner positions + c--d d--b a--c + | | | | | | + a--b c--a b--d */ + { + const Matrix3 transformation = atlasTextureCoordinateTransformation(atlasSize, atlasSize, {}); + CORRADE_COMPARE(transformation.transformPoint(a), (Vector2{0.0f, 0.0f})); + CORRADE_COMPARE(transformation.transformPoint(b), (Vector2{1.0f, 0.0f})); + CORRADE_COMPARE(transformation.transformPoint(c), (Vector2{0.0f, 1.0f})); + CORRADE_COMPARE(transformation.transformPoint(d), (Vector2{1.0f, 1.0f})); + CORRADE_COMPARE(transformation, (Matrix3{ + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 0.0f}, + {0.0f, 0.0f, 1.0f} + })); + } { + /* The item size is flipped, as otherwise with the rotation it'd mean + we want to put a {5, 4} item into an atlas of size {4, 5} */ + const Matrix3 transformation = atlasTextureCoordinateTransformationRotatedCounterClockwise(atlasSize, atlasSize.flipped(), {}); + CORRADE_COMPARE(transformation.transformPoint(a), (Vector2{1.0f, 0.0f})); + CORRADE_COMPARE(transformation.transformPoint(b), (Vector2{1.0f, 1.0f})); + CORRADE_COMPARE(transformation.transformPoint(c), (Vector2{0.0f, 0.0f})); + CORRADE_COMPARE(transformation.transformPoint(d), (Vector2{0.0f, 1.0f})); + CORRADE_COMPARE(transformation, (Matrix3{ + {0.0f, 1.0f, 0.0f}, + {-1.0f, 0.0f, 0.0f}, + {1.0f, 0.0f, 1.0f} + })); + } { + /* The item size is flipped, as otherwise with the rotation it'd mean + we want to put a {5, 4} item into an atlas of size {4, 5} */ + const Matrix3 transformation = atlasTextureCoordinateTransformationRotatedClockwise(atlasSize, atlasSize.flipped(), {}); + CORRADE_COMPARE(transformation.transformPoint(a), (Vector2{0.0f, 1.0f})); + CORRADE_COMPARE(transformation.transformPoint(b), (Vector2{0.0f, 0.0f})); + CORRADE_COMPARE(transformation.transformPoint(c), (Vector2{1.0f, 1.0f})); + CORRADE_COMPARE(transformation.transformPoint(d), (Vector2{1.0f, 0.0f})); + CORRADE_COMPARE(transformation, (Matrix3{ + {0.0f, -1.0f, 0.0f}, + {1.0f, 0.0f, 0.0f}, + {0.0f, 1.0f, 1.0f} + })); + + /* 5 +--------+ + | | + 3 | c----d | + | | | | + 2 | a----b | + | | + 0 +--------+ + 0 1 3 4 */ + } { + const Matrix3 transformation = atlasTextureCoordinateTransformation(atlasSize, size, offset); + CORRADE_COMPARE(transformation.transformPoint(a)*atlasSize, (Vector2i{1, 2})); + CORRADE_COMPARE(transformation.transformPoint(b)*atlasSize, (Vector2i{3, 2})); + CORRADE_COMPARE(transformation.transformPoint(c)*atlasSize, (Vector2i{1, 3})); + CORRADE_COMPARE(transformation.transformPoint(d)*atlasSize, (Vector2i{3, 3})); + CORRADE_COMPARE(transformation, (Matrix3{ + {0.5f, 0.0f, 0.0f}, + {0.0f, 0.2f, 0.0f}, + {0.25f, 0.4f, 1.0f} + })); + + /* 5 +--------+ + 4 | d--b | + | | | | + | | | | + 2 | c--a | + | | + 0 +--------+ + 0 1 2 4 */ + } { + const Matrix3 transformation = atlasTextureCoordinateTransformationRotatedCounterClockwise(atlasSize, size, offset); + CORRADE_COMPARE(transformation.transformPoint(a)*atlasSize, (Vector2i{2, 2})); + CORRADE_COMPARE(transformation.transformPoint(b)*atlasSize, (Vector2i{2, 4})); + CORRADE_COMPARE(transformation.transformPoint(c)*atlasSize, (Vector2i{1, 2})); + CORRADE_COMPARE(transformation.transformPoint(d)*atlasSize, (Vector2i{1, 4})); + CORRADE_COMPARE(transformation, (Matrix3{ + {0.0f, 0.4f, 0.0f}, + {-0.25f, 0.0f, 0.0f}, + {0.5f, 0.4f, 1.0f} + })); + + /* 5 +--------+ + 4 | a--c | + | | | | + | | | | + 2 | b--d | + | | + 0 +--------+ + 0 1 2 4 */ + } { + const Matrix3 transformation = atlasTextureCoordinateTransformationRotatedClockwise(atlasSize, size, offset); + CORRADE_COMPARE(transformation.transformPoint(a)*atlasSize, (Vector2i{1, 4})); + CORRADE_COMPARE(transformation.transformPoint(b)*atlasSize, (Vector2i{1, 2})); + CORRADE_COMPARE(transformation.transformPoint(c)*atlasSize, (Vector2i{2, 4})); + CORRADE_COMPARE(transformation.transformPoint(d)*atlasSize, (Vector2i{2, 2})); + CORRADE_COMPARE(transformation, (Matrix3{ + {0.0f, -0.4f, 0.0f}, + {0.25f, 0.0f, 0.0f}, + {0.25f, 0.8f, 1.0f} + })); + } +} + +void AtlasTest::textureCoordinateTransformationOutOfBounds() { + CORRADE_SKIP_IF_NO_ASSERT(); + + /* These should be fine */ + atlasTextureCoordinateTransformation({5, 4}, {5, 4}, {}); + atlasTextureCoordinateTransformationRotatedCounterClockwise({5, 4}, {4, 5}, {}); + atlasTextureCoordinateTransformationRotatedClockwise({5, 4}, {4, 5}, {}); + atlasTextureCoordinateTransformation({5, 4}, {3, 1}, {2, 3}); + atlasTextureCoordinateTransformationRotatedCounterClockwise({5, 4}, {1, 3}, {2, 3}); + atlasTextureCoordinateTransformationRotatedClockwise({5, 4}, {1, 3}, {2, 3}); + + std::ostringstream out; + Error redirectError{&out}; + /* Size too large in either dimension */ + atlasTextureCoordinateTransformation({5, 4}, {3, 5}, {}); + atlasTextureCoordinateTransformation({4, 5}, {5, 3}, {}); + atlasTextureCoordinateTransformationRotatedCounterClockwise({5, 4}, {5, 3}, {}); + atlasTextureCoordinateTransformationRotatedCounterClockwise({4, 5}, {3, 5}, {}); + atlasTextureCoordinateTransformationRotatedClockwise({5, 4}, {5, 3}, {}); + atlasTextureCoordinateTransformationRotatedClockwise({4, 5}, {3, 5}, {}); + /* Size + offset too large */ + atlasTextureCoordinateTransformation({5, 4}, {1, 2}, {2, 3}); + atlasTextureCoordinateTransformation({4, 5}, {2, 1}, {3, 2}); + atlasTextureCoordinateTransformationRotatedCounterClockwise({5, 4}, {2, 1}, {2, 3}); + atlasTextureCoordinateTransformationRotatedCounterClockwise({4, 5}, {1, 2}, {3, 2}); + atlasTextureCoordinateTransformationRotatedClockwise({5, 4}, {2, 1}, {2, 3}); + atlasTextureCoordinateTransformationRotatedClockwise({4, 5}, {1, 2}, {3, 2}); + CORRADE_COMPARE_AS(out.str(), + "TextureTools::atlasTextureCoordinateTransformation(): size {3, 5} and offset {0, 0} doesn't fit into {5, 4}\n" + "TextureTools::atlasTextureCoordinateTransformation(): size {5, 3} and offset {0, 0} doesn't fit into {4, 5}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedCounterClockwise(): (rotated) size {3, 5} and offset {0, 0} doesn't fit into {5, 4}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedCounterClockwise(): (rotated) size {5, 3} and offset {0, 0} doesn't fit into {4, 5}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedClockwise(): (rotated) size {3, 5} and offset {0, 0} doesn't fit into {5, 4}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedClockwise(): (rotated) size {5, 3} and offset {0, 0} doesn't fit into {4, 5}\n" + + "TextureTools::atlasTextureCoordinateTransformation(): size {1, 2} and offset {2, 3} doesn't fit into {5, 4}\n" + "TextureTools::atlasTextureCoordinateTransformation(): size {2, 1} and offset {3, 2} doesn't fit into {4, 5}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedCounterClockwise(): (rotated) size {1, 2} and offset {2, 3} doesn't fit into {5, 4}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedCounterClockwise(): (rotated) size {2, 1} and offset {3, 2} doesn't fit into {4, 5}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedClockwise(): (rotated) size {1, 2} and offset {2, 3} doesn't fit into {5, 4}\n" + "TextureTools::atlasTextureCoordinateTransformationRotatedClockwise(): (rotated) size {2, 1} and offset {3, 2} doesn't fit into {4, 5}\n", + TestSuite::Compare::String); +} + }}}} CORRADE_TEST_MAIN(Magnum::TextureTools::Test::AtlasTest)