diff --git a/doc/changelog.dox b/doc/changelog.dox index 7c942f586..aa6e4b914 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -128,6 +128,8 @@ See also: subdivision - New @ref MeshTools::removeDuplicatesInPlace() variant that works on discrete data in addition to floating-point +- New @ref MeshTools::combineIndexedAttributes() tool for combining + differently indexed attributes into a single index buffer @subsubsection changelog-latest-new-platform Platform libraries diff --git a/src/Magnum/MeshTools/CMakeLists.txt b/src/Magnum/MeshTools/CMakeLists.txt index efbf610b1..ebc59e94f 100644 --- a/src/Magnum/MeshTools/CMakeLists.txt +++ b/src/Magnum/MeshTools/CMakeLists.txt @@ -29,6 +29,7 @@ set(MagnumMeshTools_SRCS # Files compiled with different flags for main library and unit test library set(MagnumMeshTools_GracefulAssert_SRCS + Combine.cpp CombineIndexedArrays.cpp CompressIndices.cpp Duplicate.cpp @@ -38,6 +39,7 @@ set(MagnumMeshTools_GracefulAssert_SRCS RemoveDuplicates.cpp) set(MagnumMeshTools_HEADERS + Combine.h CombineIndexedArrays.h CompressIndices.h Duplicate.h diff --git a/src/Magnum/MeshTools/Combine.cpp b/src/Magnum/MeshTools/Combine.cpp new file mode 100644 index 000000000..d099a90e3 --- /dev/null +++ b/src/Magnum/MeshTools/Combine.cpp @@ -0,0 +1,143 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + 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 "Combine.h" + +#include +#include +#include +#include + +#include "Magnum/MeshTools/Duplicate.h" +#include "Magnum/MeshTools/RemoveDuplicates.h" +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace MeshTools { + +Trade::MeshData combineIndexedAttributes(const Containers::ArrayView> data) { + CORRADE_ASSERT(!data.empty(), "MeshTools::combineIndexedAttributes(): no meshes passed", (Trade::MeshData{MeshPrimitive{}, 0})); + + /* Decide on the output primitive and index count, calculated total + combined index type size and also the count and stride of all + attributes */ + MeshPrimitive primitive; + UnsignedInt indexCount; + std::size_t indexStride = 0; + std::size_t attributeCount = 0; + UnsignedInt vertexStride = 0; + for(std::size_t i = 0; i != data.size(); ++i) { + CORRADE_ASSERT(data[i]->isIndexed(), + "MeshTools::combineIndexedAttributes(): data" << i << "is not indexed", + (Trade::MeshData{MeshPrimitive{}, 0})); + if(i == 0) { + primitive = data[i]->primitive(); + indexCount = data[i]->indexCount(); + } else { + CORRADE_ASSERT(data[i]->primitive() == primitive, + "MeshTools::combineIndexedAttributes(): data" << i << "is" << data[i]->primitive() << "but expected" << primitive, (Trade::MeshData{MeshPrimitive{}, 0})); + CORRADE_ASSERT(data[i]->indexCount() == indexCount, + "MeshTools::combineIndexedAttributes(): data" << i << "has" << data[i]->indexCount() << "indices but expected" << indexCount, (Trade::MeshData{MeshPrimitive{}, 0})); + } + indexStride += meshIndexTypeSize(data[i]->indexType()); + attributeCount += data[i]->attributeCount(); + for(std::size_t j = 0; j != data[i]->attributeCount(); ++j) + vertexStride += vertexFormatSize(data[i]->attributeFormat(j)); + } + + /* Create a combined index array */ + Containers::Array combinedIndices{Containers::NoInit, + indexCount*indexStride}; + { + std::size_t indexOffset = 0; + for(const Trade::MeshData& mesh: data) { + const UnsignedInt indexSize = meshIndexTypeSize(mesh.indexType()); + Containers::StridedArrayView2D dst{combinedIndices, + combinedIndices.data() + indexOffset, + {indexCount, indexSize}, + {std::ptrdiff_t(indexStride), 1}}; + Utility::copy(mesh.indices(), dst); + indexOffset += indexSize; + } + + /* Check we pre-calculated correctly */ + CORRADE_INTERNAL_ASSERT(indexOffset == indexStride); + } + + /** @todo handle alignment in the above somehow (duplicate() will fail when + reading 32-bit values from odd addresses on some platforms) */ + + /* Make the combined index array unique */ + Containers::Array indexData{indexCount*sizeof(UnsignedInt)}; + const auto indexDataI = Containers::arrayCast(indexData); + const std::size_t vertexCount = removeDuplicatesInPlaceInto( + Containers::StridedArrayView2D{combinedIndices, {indexCount, indexStride}}, + indexDataI); + + /* Allocate resulting attribute and vertex data and duplicate the + attributes there according to the combined index buffer */ + Containers::Array vertexData{Containers::NoInit, + vertexStride*vertexCount}; + Containers::Array attributeData{attributeCount}; + { + std::size_t indexOffset = 0; + std::size_t attributeOffset = 0; + std::size_t vertexOffset = 0; + for(const Trade::MeshData& mesh: data) { + const UnsignedInt indexSize = mesh.isIndexed() ? + meshIndexTypeSize(mesh.indexType()) : 4; + Containers::StridedArrayView2D indices{combinedIndices, + combinedIndices.data() + indexOffset, + {vertexCount, indexSize}, + {std::ptrdiff_t(indexStride), 1}}; + + for(UnsignedInt i = 0; i != mesh.attributeCount(); ++i) { + const UnsignedInt attributeSize = vertexFormatSize(mesh.attributeFormat(i)); + Containers::StridedArrayView2D dst{vertexData, + vertexData.data() + vertexOffset, + {vertexCount, attributeSize}, + {std::ptrdiff_t(vertexStride), 1}}; + duplicateInto(indices, mesh.attribute(i), dst); + vertexOffset += attributeSize; + attributeData[attributeOffset++] = Trade::MeshAttributeData{ + mesh.attributeName(i), mesh.attributeFormat(i), dst}; + } + + indexOffset += indexSize; + } + + /* Check we pre-calculated correctly */ + CORRADE_INTERNAL_ASSERT(attributeOffset == attributeCount && vertexOffset == vertexStride); + } + + return Trade::MeshData{primitive, + std::move(indexData), Trade::MeshIndexData{indexDataI}, + std::move(vertexData), std::move(attributeData)}; +} + +Trade::MeshData combineIndexedAttributes(std::initializer_list> data) { + return combineIndexedAttributes(Containers::arrayView(data)); +} + +}} diff --git a/src/Magnum/MeshTools/Combine.h b/src/Magnum/MeshTools/Combine.h new file mode 100644 index 000000000..a2c544171 --- /dev/null +++ b/src/Magnum/MeshTools/Combine.h @@ -0,0 +1,94 @@ +#ifndef Magnum_MeshTools_Combine_h +#define Magnum_MeshTools_Combine_h +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + 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. +*/ + +/** @file + * @brief Function @ref Magnum::MeshTools::combineIndexedAttributes() + * @m_since_latest + */ + +#include + +#include "Magnum/MeshTools/visibility.h" +#include "Magnum/Trade/Trade.h" + +namespace Magnum { namespace MeshTools { + +/** +@brief Combine differently indexed attributes into a single mesh +@m_since_latest + +Assuming each @p data contains only unique vertex data, creates an indexed mesh +that contains all attributes from @p data combined, with duplicate vertices +removed. For example, when you have a position and a normal array, each indexed +with separate indices like this: + +@code{.cpp} +{pA, pB, pC, pD, pE, pF} // positions +{nA, nB, nC, nD, nE, nF, nG} // normals + +{0, 2, 5, 0, 0, 1, 3, 2, 2} // position indices +{1, 3, 4, 1, 4, 6, 1, 3, 1} // normal indices +@endcode + +Then the first triangle in the mesh is defined as +@cb{.cpp} {pA, nB}, {pC, nD}, {pF, nE} @ce. When combined together using this +function, the resulting mesh stays the same but there's just one index array, +indexing both positions and normals: + +@code{.cpp} +{{pA, nB}, {pC, nD}, {pF, nE}, {pA, nE}, {pB, nG}, {pD, nB}, {pC, nB}} + // unique pairs of positions and normals + +{0, 1, 2, 0, 3, 4, 5, 1, 6} // unified indices +@endcode + +The function preserves all vertex data including repeated or custom attributes. +The resulting mesh is interleaved, with all attributes packed tightly together. +If you need to add specific padding for alignment preservation, pass the result +to @ref interleave() and specify the paddings between attributes manually. +Similarly, for simplicity the resulting mesh has always +@ref MeshIndexType::UnsignedInt --- use @ref compressIndices(const Trade::MeshData&, MeshIndexType) +if you want to have it compressed to a smaller type. + +Expects that @p data is non-empty and all data have the same primitive and +index count. All inputs have to be indexed, although the particular +@ref MeshIndexType doesn't matter. For non-indexed attributes combining can be +done much more efficiently using @ref duplicate(const Trade::MeshData&, Containers::ArrayView), +alternatively you can turn a non-indexed attribute to an indexed one first +using @ref removeDuplicatesInPlace() and then call this function. +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData combineIndexedAttributes(const Containers::ArrayView> data); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData combineIndexedAttributes(std::initializer_list> data); + +}} + +#endif diff --git a/src/Magnum/MeshTools/Test/CMakeLists.txt b/src/Magnum/MeshTools/Test/CMakeLists.txt index 31939f6eb..29b84aba4 100644 --- a/src/Magnum/MeshTools/Test/CMakeLists.txt +++ b/src/Magnum/MeshTools/Test/CMakeLists.txt @@ -23,6 +23,7 @@ # DEALINGS IN THE SOFTWARE. # +corrade_add_test(MeshToolsCombineTest CombineTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsCombineIndexedArraysTest CombineIndexedArraysTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsCompressIndicesTest CompressIndicesTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsDuplicateTest DuplicateTest.cpp LIBRARIES MagnumMeshToolsTestLib) @@ -45,6 +46,7 @@ set_property(TARGET APPEND PROPERTY COMPILE_DEFINITIONS "CORRADE_GRACEFUL_ASSERT") set_target_properties( + MeshToolsCombineTest MeshToolsCombineIndexedArraysTest MeshToolsCompressIndicesTest MeshToolsDuplicateTest diff --git a/src/Magnum/MeshTools/Test/CombineTest.cpp b/src/Magnum/MeshTools/Test/CombineTest.cpp new file mode 100644 index 000000000..db91dbd09 --- /dev/null +++ b/src/Magnum/MeshTools/Test/CombineTest.cpp @@ -0,0 +1,179 @@ +/* + This file is part of Magnum. + + Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019 + Vladimír Vondruš + + 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 +#include +#include +#include +#include + +#include "Magnum/MeshTools/Combine.h" +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace MeshTools { namespace Test { namespace { + +struct CombineTest: TestSuite::Tester { + explicit CombineTest(); + + void combineIndexedAttributes(); + void combineIndexedAttributesIndicesOnly(); + + void combineIndexedAttributesNoMeshes(); + void combineIndexedAttributesNotIndexed(); + void combineIndexedAttributesDifferentPrimitive(); + void combineIndexedAttributesDifferentIndexCount(); +}; + +CombineTest::CombineTest() { + addTests({&CombineTest::combineIndexedAttributes, + &CombineTest::combineIndexedAttributesIndicesOnly, + + &CombineTest::combineIndexedAttributesNoMeshes, + &CombineTest::combineIndexedAttributesNotIndexed, + &CombineTest::combineIndexedAttributesDifferentPrimitive, + &CombineTest::combineIndexedAttributesDifferentIndexCount}); +} + +void CombineTest::combineIndexedAttributes() { + const UnsignedInt indicesA[]{2, 1, 2, 0}; + const Int dataA[]{2, 1, 0}; + const UnsignedByte indicesB[]{3, 4, 3, 2}; + const Short dataB[]{4, 3, 2, 1, 0}; + const UnsignedShort indicesC[]{7, 6, 7, 5}; + const Float dataC[]{0.0f, 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f, 7.0f}; + Trade::MeshData a{MeshPrimitive::LineLoop, + {}, indicesA, Trade::MeshIndexData{indicesA}, + {}, dataA, {Trade::MeshAttributeData{ + Trade::meshAttributeCustom(2), Containers::arrayView(dataA)}}}; + Trade::MeshData b{MeshPrimitive::LineLoop, + {}, indicesB, Trade::MeshIndexData{indicesB}, + {}, dataB, {Trade::MeshAttributeData{ + Trade::meshAttributeCustom(17), Containers::arrayView(dataB)}}}; + Trade::MeshData c{MeshPrimitive::LineLoop, + {}, indicesC, Trade::MeshIndexData{indicesC}, + {}, dataC, {Trade::MeshAttributeData{ + Trade::meshAttributeCustom(22), Containers::arrayView(dataC)}}}; + + Trade::MeshData result = MeshTools::combineIndexedAttributes({a, b, c}); + CORRADE_COMPARE(result.primitive(), MeshPrimitive::LineLoop); + CORRADE_VERIFY(result.isIndexed()); + CORRADE_COMPARE(result.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE_AS(result.indices(), + Containers::arrayView({0, 1, 0, 2}), + TestSuite::Compare::Container); + + CORRADE_COMPARE(result.attributeCount(), 3); + CORRADE_COMPARE(result.attributeName(0), Trade::meshAttributeCustom(2)); + CORRADE_COMPARE(result.attributeFormat(0), VertexFormat::Int); + CORRADE_COMPARE_AS(result.attribute(0), + Containers::arrayView({0, 1, 2}), + TestSuite::Compare::Container); + CORRADE_COMPARE(result.attributeName(1), Trade::meshAttributeCustom(17)); + CORRADE_COMPARE(result.attributeFormat(1), VertexFormat::Short); + CORRADE_COMPARE_AS(result.attribute(1), + Containers::arrayView({1, 0, 2}), + TestSuite::Compare::Container); + CORRADE_COMPARE(result.attributeName(2), Trade::meshAttributeCustom(22)); + CORRADE_COMPARE(result.attributeFormat(2), VertexFormat::Float); + CORRADE_COMPARE_AS(result.attribute(2), + Containers::arrayView({7.0f, 6.0f, 5.0f}), + TestSuite::Compare::Container); +} + +void CombineTest::combineIndexedAttributesIndicesOnly() { + const UnsignedInt indicesA[]{2, 1, 2}; + const UnsignedShort indicesB[]{3, 4, 3}; + const UnsignedByte indicesC[]{7, 6, 7}; + Trade::MeshData a{MeshPrimitive::LineLoop, {}, indicesA, + Trade::MeshIndexData{indicesA}}; + Trade::MeshData b{MeshPrimitive::LineLoop, {}, indicesB, + Trade::MeshIndexData{indicesB}}; + Trade::MeshData c{MeshPrimitive::LineLoop, {}, indicesC, + Trade::MeshIndexData{indicesC}}; + + Trade::MeshData result = MeshTools::combineIndexedAttributes({a, b, c}); + CORRADE_COMPARE(result.primitive(), MeshPrimitive::LineLoop); + CORRADE_VERIFY(result.isIndexed()); + CORRADE_COMPARE(result.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE_AS(result.indices(), + Containers::arrayView({0, 1, 0}), + TestSuite::Compare::Container); + CORRADE_COMPARE(result.attributeCount(), 0); + CORRADE_COMPARE(result.vertexCount(), 0); +} + +void CombineTest::combineIndexedAttributesNoMeshes() { + std::ostringstream out; + Error redirectError{&out}; + MeshTools::combineIndexedAttributes({}); + CORRADE_COMPARE(out.str(), "MeshTools::combineIndexedAttributes(): no meshes passed\n"); +} + +void CombineTest::combineIndexedAttributesNotIndexed() { + const UnsignedShort indices[5]{}; + Trade::MeshData a{MeshPrimitive::Lines, + {}, indices, Trade::MeshIndexData{indices}}; + Trade::MeshData b{MeshPrimitive::Lines, + {}, indices, Trade::MeshIndexData{indices}}; + Trade::MeshData c{MeshPrimitive::Lines, 5}; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::combineIndexedAttributes({a, b, c}); + CORRADE_COMPARE(out.str(), "MeshTools::combineIndexedAttributes(): data 2 is not indexed\n"); +} + +void CombineTest::combineIndexedAttributesDifferentPrimitive() { + const UnsignedShort indices[5]{}; + Trade::MeshData a{MeshPrimitive::Lines, + {}, indices, Trade::MeshIndexData{indices}}; + Trade::MeshData b{MeshPrimitive::Points, + {}, indices, Trade::MeshIndexData{indices}}; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::combineIndexedAttributes({a, b}); + CORRADE_COMPARE(out.str(), "MeshTools::combineIndexedAttributes(): data 1 is MeshPrimitive::Points but expected MeshPrimitive::Lines\n"); +} + +void CombineTest::combineIndexedAttributesDifferentIndexCount() { + const UnsignedShort indices[5]{}; + Trade::MeshData a{MeshPrimitive::Lines, + {}, indices, Trade::MeshIndexData{indices}}; + Trade::MeshData b{MeshPrimitive::Lines, + {}, indices, Trade::MeshIndexData{indices}}; + Trade::MeshData c{MeshPrimitive::Lines, + {}, indices, + Trade::MeshIndexData{Containers::arrayView(indices).prefix(4)}}; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::combineIndexedAttributes({a, b, c}); + CORRADE_COMPARE(out.str(), "MeshTools::combineIndexedAttributes(): data 2 has 4 indices but expected 5\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::MeshTools::Test::CombineTest)