diff --git a/doc/changelog.dox b/doc/changelog.dox index 563495594..a206e7593 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -207,6 +207,9 @@ See also: - Added @ref MeshTools::subdivideInPlace() that operates on a partially filled array view instead of a @ref std::vector +- Added @ref MeshTools::removeDuplicatesIndexedInPlace() that operates + in-place on an indexed array view and a STL-less + @ref MeshTools::removeDuplicatesInPlace() variant @subsubsection changelog-latest-changes-platform Platform libraries diff --git a/doc/snippets/MagnumMeshTools.cpp b/doc/snippets/MagnumMeshTools.cpp index c57ee65b8..a34dcc122 100644 --- a/doc/snippets/MagnumMeshTools.cpp +++ b/doc/snippets/MagnumMeshTools.cpp @@ -85,16 +85,7 @@ auto data = MeshTools::interleave(positions, weights, 2, vertexColors, 1); } { -/* [removeDuplicates1] */ -std::vector indices; -std::vector positions; - -indices = MeshTools::duplicate(indices, MeshTools::removeDuplicates(positions)); -/* [removeDuplicates1] */ -} - -{ -/* [removeDuplicates2] */ +/* [removeDuplicates-multiple] */ std::vector positions; std::vector texCoords; @@ -105,7 +96,7 @@ std::vector indices = MeshTools::combineIndexedArrays( std::make_pair(std::cref(positionIndices), std::ref(positions)), std::make_pair(std::cref(texCoordIndices), std::ref(texCoords)) ); -/* [removeDuplicates2] */ +/* [removeDuplicates-multiple] */ } { diff --git a/src/Magnum/MeshTools/GenerateNormals.h b/src/Magnum/MeshTools/GenerateNormals.h index 5d73a461c..b515d4f95 100644 --- a/src/Magnum/MeshTools/GenerateNormals.h +++ b/src/Magnum/MeshTools/GenerateNormals.h @@ -47,7 +47,7 @@ namespace Magnum { namespace MeshTools { All vertices in each triangle face get the same normal vector. Expects that the position count is divisible by 3. If you need to generate flat normals for an indexed mesh, @ref duplicate() the vertices first, after the operation you -might want to remove the duplicates again using @ref removeDuplicates(). +might want to remove the duplicates again using @ref removeDuplicatesInPlace(). Example usage: @snippet MagnumMeshTools.cpp generateFlatNormals diff --git a/src/Magnum/MeshTools/RemoveDuplicates.h b/src/Magnum/MeshTools/RemoveDuplicates.h index dc9524d2e..a256bf412 100644 --- a/src/Magnum/MeshTools/RemoveDuplicates.h +++ b/src/Magnum/MeshTools/RemoveDuplicates.h @@ -26,13 +26,14 @@ */ /** @file - * @brief Function @ref Magnum::MeshTools::removeDuplicates() + * @brief Function @ref Magnum::MeshTools::removeDuplicatesInPlace(), @ref Magnum::MeshTools::removeDuplicatesIndexedInPlace(), @ref Magnum::MeshTools::removeDuplicates() */ #include #include #include #include +#include #include #include @@ -51,11 +52,14 @@ namespace Implementation { } /** -@brief Remove duplicate floating-point vector data from given array -@param[in,out] data Input data array -@param[in] epsilon Epsilon value, vertices nearer than this distance will be +@brief Remove duplicate floating-point vector data from given array in-place +@param[in,out] data Data array, duplicate items will be cut away with order + preserved +@param[in] epsilon Epsilon value, vertices closer than this distance will be melt together -@return Index array and unique data +@return Size of unique prefix in the cleaned up @p data array and the resulting + index array +@m_since_latest Removes duplicate data from the array by collapsing them into buckets of size @p epsilon. First vector in given bucket is used, other ones are thrown away, @@ -63,57 +67,82 @@ no interpolation is done. Note that this function is meant to be used for floating-point data (or generally with non-zero @p epsilon), for discrete data the usual sorting method is much more efficient. -If you want to remove duplicate data from an already indexed array, first -remove duplicates as if the array wasn't indexed at all and then use -@ref duplicate() to combine the two index arrays: +If you want to remove duplicate data from an already indexed array, use +@ref removeDuplicatesIndexedInPlace() instead. See also +@ref removeDuplicates(std::vector&, typename Vector::Type) for a +variant operating on a STL vector. +*/ +template std::pair, std::size_t> removeDuplicatesInPlace(const Containers::StridedArrayView1D& data, typename Vector::Type epsilon = Math::TypeTraits::epsilon()); + +/** +@brief Remove duplicate floating-point vector data from a STL vector in-place +@param[in,out] data Data array, duplicate items will be cut away with order + preserved and the size shrunk to just the unique prefix +@param[in] epsilon Epsilon value, vertices closer than this distance will be + melt together +@return Resulting index array -@snippet MagnumMeshTools.cpp removeDuplicates1 +Similar to the above, except that it's operating on a @ref std::vector, which +gets shrunk as a result (instead of the prefix size being returned). This +variant is useful together with @ref combineIndexedArrays() to remove +duplicates in multiple incidental arrays --- first remove duplicates in each +array separately and then combine the resulting index arrays to single index +array, and reorder the data accordingly: -Removing duplicates in multiple indcidental arrays is also possible --- first -remove duplicates in each array separately and then use @ref combineIndexedArrays() -to combine the resulting index arrays to single index array, and reorder the -data accordingly: +@snippet MagnumMeshTools.cpp removeDuplicates-multiple +*/ +template std::vector removeDuplicates(std::vector& data, typename Vector::Type epsilon = Math::TypeTraits::epsilon()); -@snippet MagnumMeshTools.cpp removeDuplicates2 +/** +@brief Remove duplicates from indexed floating-point vector data in-place +@param[in,out] indices Index array, which will get remapped to list just + unique vertices +@param[in,out] data Data array, duplicate items will be cut away with order + preserved +@param[in] epsilon Epsilon value, vertices closer than this distance will + be melt together +@return Size of unique prefix in the cleaned up @p data array +@m_since_latest + +Compared to @ref removeDuplicatesInPlace() this variant is more suited for data +that is already indexed as it works on the existing index array instead of +allocating a new one. */ -template std::vector removeDuplicates(std::vector& data, typename Vector::Type epsilon = Math::TypeTraits::epsilon()) { +template std::size_t removeDuplicatesIndexedInPlace(const Containers::StridedArrayView1D& indices, const Containers::StridedArrayView1D& data, typename Vector::Type epsilon = Math::TypeTraits::epsilon()) { + /* Somehow ~IndexType{} doesn't work for < 4byte types, as the result is + int(-1) instead of the type I want */ + CORRADE_ASSERT(data.size() <= IndexType(-1), "MeshTools::removeDuplicatesIndexedInPlace(): a" << sizeof(IndexType) << Debug::nospace << "-byte index type is too small for" << data.size() << "vertices", {}); + /* Get bounds. When NaNs appear, those will get collapsed together when you're lucky, or cause the whole data to disappear when you're not -- it needs a much more specialized handling to be robust. */ - std::pair minmax = Math::minmax(data); + std::pair minmax = Math::minmax(data); /* Make epsilon so large that std::size_t can index all vectors inside the bounds. */ epsilon = Math::max(epsilon, typename Vector::Type((minmax.second-minmax.first).max()/~std::size_t{})); - /* Resulting index array. Because we'll be remapping these, we need to - start from a 0..n sequence. */ - std::vector indices(data.size()); - std::iota(indices.begin(), indices.end(), 0); - /* Table containing original vector index for each discretized vector. Reserving more buckets than necessary (i.e. as if each vector was unique). */ - std::unordered_map, UnsignedInt, Implementation::VectorHash> table(data.size()); + std::size_t dataSize = data.size(); + std::unordered_map, UnsignedInt, Implementation::VectorHash> table(dataSize); /* Index array that'll be filled in each pass and then used for remapping the `indices` */ - std::vector remapping(data.size()); + Containers::Array remapping{Containers::NoInit, dataSize}; /* First go with original coordinates, then move them by epsilon/2 in each direction. */ Vector moved; for(std::size_t moving = 0; moving <= Vector::Size; ++moving) { - /* Clear the table for this pass */ - table.clear(); - /* Go through all vectors */ - for(std::size_t i = 0; i != data.size(); ++i) { + for(std::size_t i = 0; i != dataSize; ++i) { /* Try to insert new vertex into the table */ const Math::Vector v{(data[i] + moved - minmax.first)/epsilon}; const auto result = table.emplace(v, table.size()); - /* Add the (either new or already existing) index into index array */ + /* Add the (either new or already existing) index into the array */ remapping[i] = result.first->second; /* If this is a new combination, copy the data to new (earlier) @@ -121,13 +150,9 @@ template std::vector removeDuplicates(std::vector= table.size()); - data.resize(table.size()); - /* Remap the resulting index array */ for(auto& i: indices) i = remapping[i]; @@ -137,8 +162,31 @@ template std::vector removeDuplicates(std::vector= dataSize); + return dataSize; +} + +template std::pair, std::size_t> removeDuplicatesInPlace(const Containers::StridedArrayView1D& data, typename Vector::Type epsilon) { + /* A trivial index array that'll be remapped and returned after */ + Containers::Array indices{Containers::NoInit, data.size()}; + std::iota(indices.begin(), indices.end(), 0); + const std::size_t size = removeDuplicatesIndexedInPlace(Containers::stridedArrayView(indices), data, epsilon); + return {std::move(indices), size}; +} + +template std::vector removeDuplicates(std::vector& data, typename Vector::Type epsilon) { + /* A trivial index array that'll be remapped and returned after */ + std::vector indices(data.size()); + std::iota(indices.begin(), indices.end(), 0); + const std::size_t size = removeDuplicatesIndexedInPlace(Containers::stridedArrayView(indices), Containers::stridedArrayView(data), epsilon); + data.resize(size); return indices; } diff --git a/src/Magnum/MeshTools/Test/CMakeLists.txt b/src/Magnum/MeshTools/Test/CMakeLists.txt index 10e54e114..e03c7c893 100644 --- a/src/Magnum/MeshTools/Test/CMakeLists.txt +++ b/src/Magnum/MeshTools/Test/CMakeLists.txt @@ -40,6 +40,7 @@ set_property(TARGET MeshToolsCombineIndexedArraysTest MeshToolsDuplicateTest MeshToolsInterleaveTest + MeshToolsRemoveDuplicatesTest MeshToolsSubdivideTest APPEND PROPERTY COMPILE_DEFINITIONS "CORRADE_GRACEFUL_ASSERT") diff --git a/src/Magnum/MeshTools/Test/RemoveDuplicatesTest.cpp b/src/Magnum/MeshTools/Test/RemoveDuplicatesTest.cpp index e10373f8e..b349addd1 100644 --- a/src/Magnum/MeshTools/Test/RemoveDuplicatesTest.cpp +++ b/src/Magnum/MeshTools/Test/RemoveDuplicatesTest.cpp @@ -23,7 +23,10 @@ DEALINGS IN THE SOFTWARE. */ +#include #include +#include +#include #include "Magnum/Math/Vector2.h" #include "Magnum/MeshTools/RemoveDuplicates.h" @@ -33,17 +36,47 @@ namespace Magnum { namespace MeshTools { namespace Test { namespace { struct RemoveDuplicatesTest: TestSuite::Tester { explicit RemoveDuplicatesTest(); - void removeDuplicates(); + void removeDuplicatesInPlace(); + void removeDuplicatesStl(); + template void removeDuplicatesIndexedInPlace(); + void removeDuplicatesIndexedInPlaceSmallType(); + void removeDuplicatesIndexedInPlaceEmptyIndices(); + void removeDuplicatesIndexedInPlaceEmptyIndicesVertices(); }; RemoveDuplicatesTest::RemoveDuplicatesTest() { - addTests({&RemoveDuplicatesTest::removeDuplicates}); + addTests({&RemoveDuplicatesTest::removeDuplicatesInPlace, + &RemoveDuplicatesTest::removeDuplicatesStl, + &RemoveDuplicatesTest::removeDuplicatesIndexedInPlace, + &RemoveDuplicatesTest::removeDuplicatesIndexedInPlace, + &RemoveDuplicatesTest::removeDuplicatesIndexedInPlace, + &RemoveDuplicatesTest::removeDuplicatesIndexedInPlaceSmallType, + &RemoveDuplicatesTest::removeDuplicatesIndexedInPlaceEmptyIndices, + &RemoveDuplicatesTest::removeDuplicatesIndexedInPlaceEmptyIndicesVertices}); } -void RemoveDuplicatesTest::removeDuplicates() { +void RemoveDuplicatesTest::removeDuplicatesInPlace() { /* Numbers with distance 1 should be merged, numbers with distance 2 should be kept. Testing both even-odd and odd-even sequence to verify that half-epsilon translations are applied properly. */ + Vector2i data[]{ + {1, 0}, + {2, 1}, + {0, 4}, + {1, 5} + }; + + std::pair, std::size_t> result = MeshTools::removeDuplicatesInPlace(Containers::stridedArrayView(data), 2); + CORRADE_COMPARE_AS(Containers::arrayView(result.first), + Containers::arrayView({0, 0, 1, 1}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(data).prefix(result.second), + Containers::arrayView({{1, 0}, {0, 4}}), + TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::removeDuplicatesStl() { + /* Same but with implicit bloat. HEH HEH */ std::vector data{ {1, 0}, {2, 1}, @@ -52,11 +85,67 @@ void RemoveDuplicatesTest::removeDuplicates() { }; const std::vector indices = MeshTools::removeDuplicates(data, 2); - CORRADE_COMPARE(indices, (std::vector{0, 0, 1, 1})); - CORRADE_COMPARE(data, (std::vector{ + CORRADE_COMPARE_AS(indices, + (std::vector{0, 0, 1, 1}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(data, + (std::vector{{1, 0}, {0, 4}}), + TestSuite::Compare::Container); +} + +template void RemoveDuplicatesTest::removeDuplicatesIndexedInPlace() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + /* Same as above, but with an explicit index buffer */ + T indices[]{3, 2, 0, 1, 2, 3}; + Vector2i data[]{ {1, 0}, - {0, 4} - })); + {2, 1}, + {0, 4}, + {1, 5} + }; + + std::size_t count = MeshTools::removeDuplicatesIndexedInPlace( + Containers::stridedArrayView(indices), + Containers::stridedArrayView(data), 2); + CORRADE_COMPARE_AS(Containers::arrayView(indices), + Containers::arrayView({1, 1, 0, 0, 1, 1}), + TestSuite::Compare::Container); + CORRADE_COMPARE_AS(Containers::arrayView(data).prefix(count), + Containers::arrayView({{1, 0}, {0, 4}}), + TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::removeDuplicatesIndexedInPlaceSmallType() { + std::stringstream out; + Error redirectError{&out}; + + UnsignedByte indices[1]; + Vector2i data[256]{}; + MeshTools::removeDuplicatesIndexedInPlace( + Containers::stridedArrayView(indices), + Containers::stridedArrayView(data), 2); + CORRADE_COMPARE(out.str(), "MeshTools::removeDuplicatesIndexedInPlace(): a 1-byte index type is too small for 256 vertices\n"); +} + +void RemoveDuplicatesTest::removeDuplicatesIndexedInPlaceEmptyIndices() { + Vector2i data[]{ + {1, 0}, + {2, 1}, + {0, 4}, + {1, 5} + }; + + std::size_t count = MeshTools::removeDuplicatesIndexedInPlace( + Containers::StridedArrayView1D{}, + Containers::stridedArrayView(data), 2); + CORRADE_COMPARE_AS(Containers::arrayView(data).prefix(count), + Containers::arrayView({{1, 0}, {0, 4}}), + TestSuite::Compare::Container); +} + +void RemoveDuplicatesTest::removeDuplicatesIndexedInPlaceEmptyIndicesVertices() { + CORRADE_COMPARE((MeshTools::removeDuplicatesIndexedInPlace({}, {}, 2)), 0); } }}}}