diff --git a/doc/changelog.dox b/doc/changelog.dox index 0de8669d0..0df47ac2c 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -133,6 +133,8 @@ See also: differently indexed attributes into a single index buffer, and @ref MeshTools::combineFaceAttributes() for converting per-face attributes into per-vertex +- New @ref MeshTools::concatenate() and @ref MeshTools::concatenateInto() + tool for batching multiple generic meshes together @subsubsection changelog-latest-new-platform Platform libraries diff --git a/doc/snippets/MagnumMeshTools.cpp b/doc/snippets/MagnumMeshTools.cpp index 7c13141b4..32a991c88 100644 --- a/doc/snippets/MagnumMeshTools.cpp +++ b/doc/snippets/MagnumMeshTools.cpp @@ -26,11 +26,14 @@ #include "Magnum/Math/Color.h" #include "Magnum/Math/FunctionsBatch.h" #include "Magnum/MeshTools/CompressIndices.h" +#include "Magnum/MeshTools/Concatenate.h" #include "Magnum/MeshTools/Duplicate.h" +#include "Magnum/MeshTools/FlipNormals.h" #include "Magnum/MeshTools/GenerateNormals.h" #include "Magnum/MeshTools/Interleave.h" #include "Magnum/MeshTools/RemoveDuplicates.h" #include "Magnum/MeshTools/Transform.h" +#include "Magnum/Primitives/Cube.h" #include "Magnum/Trade/MeshData.h" #ifdef MAGNUM_BUILD_DEPRECATED @@ -74,6 +77,15 @@ std::pair, MeshIndexType> result = /* [compressIndices-offset] */ } +{ +/* [concatenate-make-mutable] */ +/* Flip triangles on a cube primitive so it's counterclockwise from the inside + in order to render a cube map */ +Trade::MeshData mesh = MeshTools::concatenate(Primitives::cubeSolid()); +MeshTools::flipFaceWindingInPlace(mesh.mutableIndices()); +/* [concatenate-make-mutable] */ +} + #ifdef MAGNUM_BUILD_DEPRECATED { CORRADE_IGNORE_DEPRECATED_PUSH diff --git a/src/Magnum/MeshTools/CMakeLists.txt b/src/Magnum/MeshTools/CMakeLists.txt index 8c3ca0e9c..b5df5d138 100644 --- a/src/Magnum/MeshTools/CMakeLists.txt +++ b/src/Magnum/MeshTools/CMakeLists.txt @@ -31,6 +31,7 @@ set(MagnumMeshTools_SRCS set(MagnumMeshTools_GracefulAssert_SRCS Combine.cpp CompressIndices.cpp + Concatenate.cpp Duplicate.cpp FlipNormals.cpp GenerateNormals.cpp @@ -40,6 +41,7 @@ set(MagnumMeshTools_GracefulAssert_SRCS set(MagnumMeshTools_HEADERS Combine.h CompressIndices.h + Concatenate.h Duplicate.h FlipNormals.h GenerateNormals.h diff --git a/src/Magnum/MeshTools/Concatenate.cpp b/src/Magnum/MeshTools/Concatenate.cpp new file mode 100644 index 000000000..ea08e5ed9 --- /dev/null +++ b/src/Magnum/MeshTools/Concatenate.cpp @@ -0,0 +1,247 @@ +/* + 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 "Concatenate.h" + +#include +#include +#include + +namespace Magnum { namespace MeshTools { + +namespace Implementation { + +std::pair concatenateIndexVertexCount(const Trade::MeshData& first, const Containers::ArrayView> next) { + UnsignedInt indexCount = first.isIndexed() ? first.indexCount() : 0; + UnsignedInt vertexCount = first.vertexCount(); + for(const Trade::MeshData& mesh: next) { + /* If the mesh is indexed, add to index count. If this is the first + indexed mesh, all previous meshes will have a trivial index buffer + generated for all their vertices */ + if(mesh.isIndexed()) { + if(!indexCount) indexCount = vertexCount; + indexCount += mesh.indexCount(); + + /* Otherwise, if some earlier mesh was indexed, this mesh will have a + trivial index buffer generated for all its vertices */ + } else if(indexCount) indexCount += mesh.vertexCount(); + + vertexCount += mesh.vertexCount(); + } + + return {indexCount, vertexCount}; +} + +/* std::hash for enumeration types is only since C++14, so we need to make our + own. It's amazing how extremely verbose this can get, ugh. */ +struct MeshAttributeHash: std::hash::type> { + std::size_t operator()(Trade::MeshAttribute value) const { + return std::hash::type>::operator()(static_cast::type>(value)); + } +}; + +Trade::MeshData concatenate(Containers::Array&& indexData, const UnsignedInt vertexCount, Containers::Array&& vertexData, Containers::Array&& attributeData, const Trade::MeshData& first, const Containers::ArrayView> next, const char* const assertPrefix, const std::size_t meshIndexOffset) { + #ifdef CORRADE_NO_ASSERT + static_cast(assertPrefix); + static_cast(meshIndexOffset); + #endif + + /* Convert the attributes from offset-only and zero vertex count to + absolute, referencing the vertex data array */ + for(Trade::MeshAttributeData& attribute: attributeData) { + attribute = Trade::MeshAttributeData{ + attribute.name(), attribute.format(), + Containers::StridedArrayView1D{vertexData, + vertexData + attribute.offset(vertexData), + vertexCount, attribute.stride()}}; + } + + /* Only list primitives are supported currently */ + /** @todo delegate to `indexTriangleStrip()` (`duplicate*()`?) etc when + those are done */ + CORRADE_ASSERT( + first.primitive() != MeshPrimitive::LineStrip && + first.primitive() != MeshPrimitive::LineLoop && + first.primitive() != MeshPrimitive::TriangleStrip && + first.primitive() != MeshPrimitive::TriangleFan, + assertPrefix << first.primitive() << "is not supported, turn it into a plain indexed mesh first", + (Trade::MeshData{MeshPrimitive{}, 0})); + + /* Populate the resulting instance with what we have. It'll be used below + for convenient access to vertex / index data */ + auto indices = Containers::arrayCast(indexData); + Trade::MeshData out{first.primitive(), + /* If the index array is empty, we're creating a non-indexed mesh (not + an indexed mesh with zero indices) */ + std::move(indexData), indices.empty() ? + Trade::MeshIndexData{} : Trade::MeshIndexData{indices}, + std::move(vertexData), std::move(attributeData), vertexCount}; + /* Create an attribute map. Yes, this is an inevitable fugly thing that + allocates like mad, while everything else is zero-alloc. + Containers::HashMap can't be here soon enough. */ + std::unordered_multimap, MeshAttributeHash> attributeMap; + attributeMap.reserve(out.attributeCount()); + for(UnsignedInt i = 0; i != out.attributeCount(); ++i) + attributeMap.emplace(out.attributeName(i), std::make_pair(i, false)); + + /* Go through all meshes and put all attributes and index arrays together. + The first mesh might get separately and thus can't be a part of the + view, so abuse the *defined* unsigned integer overflow to add it to the + loop. This probably breaks all coding guidelines on earth tho. */ + std::size_t indexOffset = 0; + std::size_t vertexOffset = 0; + for(std::size_t i = ~std::size_t{}; i != next.size(); ++i) { + const Trade::MeshData& mesh = i == ~std::size_t{} ? first : next[i].get(); + + /* This won't fire for i == ~std::size_t{}, as that's where + out.primitive() comes from */ + CORRADE_ASSERT(mesh.primitive() == out.primitive(), + assertPrefix << "expected" << out.primitive() << "but got" << mesh.primitive() << "in mesh" << i + meshIndexOffset, + (Trade::MeshData{MeshPrimitive{}, 0})); + + /* If the mesh is indexed, copy the indices over, expanded to 32bit */ + if(mesh.isIndexed()) { + Containers::ArrayView dst = indices.slice(indexOffset, indexOffset + mesh.indexCount()); + mesh.indicesInto(dst); + indexOffset += mesh.indexCount(); + + /* Adjust indices for current vertex offset */ + for(UnsignedInt& index: dst) index += vertexOffset; + + /* Otherwise, if we need an index buffer (meaning at least one of the + meshes is indexed), generate a trivial index buffer */ + } else if(!indices.empty()) { + std::iota(indices + indexOffset, indices + indexOffset + mesh.vertexCount(), UnsignedInt(vertexOffset)); + indexOffset += mesh.vertexCount(); + } + + /* Reset markers saying which attribute has already been copied */ + for(auto it = attributeMap.begin(); it != attributeMap.end(); ++it) + it->second.second = false; + + /* Copy attributes to their destination, skipping ones that don't have + any equivalent in the destination mesh */ + for(UnsignedInt src = 0; src != mesh.attributeCount(); ++src) { + /* Go through destination attributes of the same name and find the + earliest one that hasn't been copied yet */ + auto range = attributeMap.equal_range(mesh.attributeName(src)); + UnsignedInt dst = ~UnsignedInt{}; + auto found = attributeMap.end(); + for(auto it = range.first; it != range.second; ++it) { + if(it->second.second) continue; + + /* The range is unordered so we need to go through everything + and pick one with smallest ID */ + if(it->second.first < dst) { + dst = it->second.first; + found = it; + } + } + + /* No corresponding attribute found, continue */ + if(dst == ~UnsignedInt{}) continue; + + /* Check format compatibility. This won't fire for i == + ~std::size_t{}, as that's where out.primitive() comes from */ + CORRADE_ASSERT(out.attributeFormat(dst) == mesh.attributeFormat(src), + assertPrefix << "expected" << out.attributeFormat(dst) << "for attribute" << dst << "(" << Debug::nospace << out.attributeName(dst) << Debug::nospace << ") but got" << mesh.attributeFormat(src) << "in mesh" << i + meshIndexOffset << "attribute" << src, + (Trade::MeshData{MeshPrimitive{}, 0})); + + /* Copy the data to a slice of the output, mark the attribute as + copied */ + Utility::copy(mesh.attribute(src), out.mutableAttribute(dst) + .slice(vertexOffset, vertexOffset + mesh.vertexCount())); + found->second.second = true; + } + + /* Update vertex offset for the next mesh */ + vertexOffset += mesh.vertexCount(); + } + + return out; +} + +} + +Trade::MeshData concatenate(Trade::MeshData&& first, const Containers::ArrayView> next) { + /* If there's just a single non-empty mesh and its data is owned, pass it + through, as it passes the guarantee that the returned data is always + owned. If it's empty, it doesn't matter that we drag it through the rest + as there will be no heavy allocation / copy made (and that also makes + tests easier to write). */ + if(first.indexDataFlags() & Trade::DataFlag::Owned && + first.vertexDataFlags() & Trade::DataFlag::Owned && + first.attributeCount() && first.vertexCount() && next.empty()) + return std::move(first); + + /* Calculate final attribute stride and offsets. Make a non-owning copy of + the attribute data to avoid interleavedLayout() stealing the original + (we still need it to be able to reference the original data). If there's + no attributes in the original array, pass just vertex count --- + otherwise MeshData will assert on that to avoid it getting lost. */ + Containers::Array attributeData; + if(first.attributeCount()) + attributeData = Implementation::interleavedLayout(Trade::MeshData{first.primitive(), + {}, first.vertexData(), + Trade::meshAttributeDataNonOwningArray(first.attributeData())}, {}); + else attributeData = + Implementation::interleavedLayout(Trade::MeshData{first.primitive(), + first.vertexCount()}, {}); + + /* Calculate total index/vertex count and allocate the target memory. + Index data are allocated with NoInit as the whole array will be written, + however vertex data might have holes and thus it's zero-initialized. */ + const std::pair indexVertexCount = Implementation::concatenateIndexVertexCount(first, next); + Containers::Array indexData{Containers::NoInit, + indexVertexCount.first*sizeof(UnsignedInt)}; + Containers::Array vertexData{Containers::ValueInit, + attributeData.empty() ? 0 : (attributeData[0].stride()*indexVertexCount.second)}; + return Implementation::concatenate(std::move(indexData), indexVertexCount.second, std::move(vertexData), std::move(attributeData), first, next, "MeshTools::concatenate():", 0); +} + +Trade::MeshData concatenate(Trade::MeshData&& first, std::initializer_list> next) { + return concatenate(std::move(first), Containers::arrayView(next)); +} + +Trade::MeshData concatenate(const Trade::MeshData& first, const Containers::ArrayView> next) { + Containers::ArrayView indexData; + Trade::MeshIndexData indices; + if(first.isIndexed()) { + indexData = first.indexData(); + indices = Trade::MeshIndexData{first.indices()}; + } + + return concatenate(Trade::MeshData{first.primitive(), + {}, indexData, indices, + {}, first.vertexData(), Trade::meshAttributeDataNonOwningArray(first.attributeData()), + first.vertexCount(), + }, next); +} + +Trade::MeshData concatenate(const Trade::MeshData& first, std::initializer_list> next) { + return concatenate(first, Containers::arrayView(next)); +} + +}} diff --git a/src/Magnum/MeshTools/Concatenate.h b/src/Magnum/MeshTools/Concatenate.h new file mode 100644 index 000000000..9e78af4a8 --- /dev/null +++ b/src/Magnum/MeshTools/Concatenate.h @@ -0,0 +1,153 @@ +#ifndef Magnum_MeshTools_Concatenate_h +#define Magnum_MeshTools_Concatenate_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::concatenate(), @ref Magnum::MeshTools::concatenateInto() + * @m_since_latest + */ + +#include +#include + +#include "Magnum/MeshTools/Interleave.h" +#include "Magnum/Trade/MeshData.h" + +namespace Magnum { namespace MeshTools { + +namespace Implementation { + MAGNUM_MESHTOOLS_EXPORT std::pair concatenateIndexVertexCount(const Trade::MeshData& first, const Containers::ArrayView> next); + MAGNUM_MESHTOOLS_EXPORT Trade::MeshData concatenate(Containers::Array&& indexData, UnsignedInt vertexCount, Containers::Array&& vertexData, Containers::Array&& attributeData, const Trade::MeshData& first, const Containers::ArrayView> next, const char* assertPrefix, std::size_t meshIndexOffset); +} + +/** +@brief Concatenate meshes together +@m_since_latest + +The returned mesh contains vertices from all meshes concatenated together. If +any mesh is indexed, the resulting mesh is indexed as well, with indices +adjusted for vertex offsets of particular meshes. The behavior is undefined if +any mesh has indices out of bounds for its particular vertex count. + +All attributes from the @p first mesh are taken; for each mesh in @p next, +attributes present in @p first are copied, superfluous attributes ignored and +missing attributes zeroed out. Matching attributes are expected to have the +same type, all meshes are expected to have the same primitive. The vertex data +are concatenated in the same order as passed, with no duplicate removal. +Returned instance vertex and index data flags always have both +@ref Trade::DataFlag::Owned and @ref Trade::DataFlag::Mutable to guarante +mutable access to particular parts of the concatenated mesh --- for example for +applying transformations. + +If an index buffer is needed, @ref MeshIndexType::UnsignedInt is always used. +Call @ref compressIndices(const Trade::MeshData&, MeshIndexType) on the result +to compress it to a smaller type, if desired. +@see @ref concatenate(Trade::MeshData&&, const Containers::ArrayView>), + @ref concatenateInto() +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData concatenate(const Trade::MeshData& first, const Containers::ArrayView> next = {}); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData concatenate(const Trade::MeshData& first, std::initializer_list> next); + +/** +@brief Concatenate meshes together +@m_since_latest + +Compared to @ref concatenate(const Trade::MeshData&, const Containers::ArrayView>), +if @p first has both vertex and index data owned and @p next is empty, it's +passed through without any extra allocations or other work. This can be used +for example to ensure a mesh is mutable in order to do various modifications on +its data: + +@snippet MagnumMeshTools.cpp concatenate-make-mutable +*/ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData concatenate(Trade::MeshData&& first, const Containers::ArrayView> next = {}); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Trade::MeshData concatenate(Trade::MeshData&& first, std::initializer_list> next); + +/** +@brief Concatenate a list of meshes into a pre-existing destination, enlarging it if necessary +@tparam Allocator Allocator to use +@param[in,out] destination Destination mesh from which the output arrays as + well as desired attribute layout is taken +@param[in] meshes Meshes to concatenate +@m_since_latest + +Compared to @ref concatenate(const Trade::MeshData&, const Containers::ArrayView>) this +function resizes existing index and vertex buffers in @p destination using +@ref Containers::arrayResize() and given @p allocator, and reuses its +atttribute data array instead of always allocating new ones. Only the attribute +layout from @p destination is used, all vertex/index data are taken from +@p meshes. Expects that @p meshes contains at least one item. +*/ +template class Allocator = Containers::ArrayAllocator> void concatenateInto(Trade::MeshData& destination, const Containers::ArrayView> meshes) { + CORRADE_ASSERT(!meshes.empty(), + "MeshTools::concatenateInto(): no meshes passed", ); + + std::pair indexVertexCount = Implementation::concatenateIndexVertexCount(meshes[0], meshes.suffix(1)); + + Containers::Array indexData; + if(indexVertexCount.first) { + indexData = destination.releaseIndexData(); + /* Everything is overwritten here so we don't need to zero-out the + memory */ + Containers::arrayResize(indexData, Containers::NoInit, indexVertexCount.first*sizeof(UnsignedInt)); + } + + Containers::Array attributeData = Implementation::interleavedLayout(std::move(destination), {}); + Containers::Array vertexData; + if(!attributeData.empty() && indexVertexCount.second) { + const UnsignedInt attributeStride = attributeData[0].stride(); + vertexData = destination.releaseVertexData(); + /* Resize to 0 and then to the desired size to zero-out whatever was + there, otherwise attributes that are not present in `meshes` would + be garbage */ + Containers::arrayResize(vertexData, 0); + Containers::arrayResize(vertexData, Containers::ValueInit, attributeStride*indexVertexCount.second); + } + + destination = Implementation::concatenate(std::move(indexData), indexVertexCount.second, std::move(vertexData), std::move(attributeData), meshes[0], meshes.suffix(1), "MeshTools::concatenateInto():", 1); +} + +/** + * @overload + * @m_since_latest + */ +template class Allocator = Containers::ArrayAllocator> void concatenateInto(Trade::MeshData& destination, const std::initializer_list> meshes) { + concatenateInto(destination, Containers::arrayView(meshes)); +} + +}} + +#endif diff --git a/src/Magnum/MeshTools/Interleave.cpp b/src/Magnum/MeshTools/Interleave.cpp index 4f959bffc..913a16a67 100644 --- a/src/Magnum/MeshTools/Interleave.cpp +++ b/src/Magnum/MeshTools/Interleave.cpp @@ -75,11 +75,11 @@ Containers::StridedArrayView2D interleavedData(const Trade::MeshData return out; } -Trade::MeshData interleavedLayout(Trade::MeshData&& data, const UnsignedInt vertexCount, const Containers::ArrayView extra) { - /* If there are no attributes, bail -- return an empty mesh with desired - vertex count but nothing else */ - if(!data.attributeCount() && extra.empty()) - return Trade::MeshData{data.primitive(), vertexCount}; +namespace Implementation { + +Containers::Array interleavedLayout(Trade::MeshData&& data, const Containers::ArrayView extra) { + /* Nothing to do here, bye! */ + if(!data.attributeCount() && extra.empty()) return {}; const bool interleaved = isInterleaved(data); @@ -105,7 +105,7 @@ Trade::MeshData interleavedLayout(Trade::MeshData&& data, const UnsignedInt vert for(std::size_t i = 0; i != extra.size(); ++i) { if(extra[i].format() == VertexFormat{}) { CORRADE_ASSERT(extra[i].stride() > 0 || stride >= std::size_t(-extra[i].stride()), - "MeshTools::interleavedLayout(): negative padding" << extra[i].stride() << "in extra attribute" << i << "too large for stride" << stride, (Trade::MeshData{MeshPrimitive::Points, 0})); + "MeshTools::interleavedLayout(): negative padding" << extra[i].stride() << "in extra attribute" << i << "too large for stride" << stride, {}); stride += extra[i].stride(); } else { stride += vertexFormatSize(extra[i].format()); @@ -131,9 +131,6 @@ Trade::MeshData interleavedLayout(Trade::MeshData&& data, const UnsignedInt vert Utility::copy(originalAttributeData, attributeData.prefix(originalAttributeCount)); } - /* Allocate new data array */ - Containers::Array vertexData{Containers::NoInit, stride*vertexCount}; - /* Copy existing attribute layout. If the original is already interleaved, preserve relative attribute offsets, otherwise pack tightly. */ std::size_t offset = 0; @@ -142,8 +139,7 @@ Trade::MeshData interleavedLayout(Trade::MeshData&& data, const UnsignedInt vert attributeData[i] = Trade::MeshAttributeData{ attributeData[i].name(), attributeData[i].format(), - Containers::StridedArrayView1D{vertexData, vertexData + offset, - vertexCount, std::ptrdiff_t(stride)}}; + offset, 0, std::ptrdiff_t(stride)}; if(!interleaved) offset += vertexFormatSize(attributeData[i].format()); } @@ -164,12 +160,38 @@ Trade::MeshData interleavedLayout(Trade::MeshData&& data, const UnsignedInt vert } attributeData[attributeIndex++] = Trade::MeshAttributeData{ - extra[i].name(), extra[i].format(), Containers::StridedArrayView1D{vertexData, vertexData + offset, - vertexCount, std::ptrdiff_t(stride)}}; + extra[i].name(), extra[i].format(), + offset, 0, std::ptrdiff_t(stride)}; offset += vertexFormatSize(extra[i].format()); } + return attributeData; +} + +} + +Trade::MeshData interleavedLayout(Trade::MeshData&& data, const UnsignedInt vertexCount, const Containers::ArrayView extra) { + Containers::Array attributeData = Implementation::interleavedLayout(std::move(data), extra); + + /* If there are no attributes, bail -- return an empty mesh with desired + vertex count but nothing else */ + if(!attributeData) + return Trade::MeshData{data.primitive(), vertexCount}; + + /* Allocate new data array */ + Containers::Array vertexData{Containers::NoInit, attributeData[0].stride()*vertexCount}; + + /* Convert the attributes from offset-only and zero vertex count to + absolute, referencing the above-allocated data array */ + for(Trade::MeshAttributeData& attribute: attributeData) { + attribute = Trade::MeshAttributeData{ + attribute.name(), attribute.format(), + Containers::StridedArrayView1D{vertexData, + vertexData + attribute.offset(vertexData), + vertexCount, attribute.stride()}}; + } + return Trade::MeshData{data.primitive(), std::move(vertexData), std::move(attributeData)}; } diff --git a/src/Magnum/MeshTools/Interleave.h b/src/Magnum/MeshTools/Interleave.h index 1e2f38e67..9c73e64c8 100644 --- a/src/Magnum/MeshTools/Interleave.h +++ b/src/Magnum/MeshTools/Interleave.h @@ -114,6 +114,9 @@ template void writeInterleaved(std::size_t stride, char* st writeInterleaved(stride, startingOffset + writeOneInterleaved(stride, startingOffset, first), next...); } +/* Used internally by interleavedLayout() and concatenate() */ +MAGNUM_MESHTOOLS_EXPORT Containers::Array interleavedLayout(Trade::MeshData&& data, Containers::ArrayView extra); + } /** diff --git a/src/Magnum/MeshTools/Test/CMakeLists.txt b/src/Magnum/MeshTools/Test/CMakeLists.txt index cc9d558f8..8f1913b8b 100644 --- a/src/Magnum/MeshTools/Test/CMakeLists.txt +++ b/src/Magnum/MeshTools/Test/CMakeLists.txt @@ -25,6 +25,7 @@ corrade_add_test(MeshToolsCombineTest CombineTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsCompressIndicesTest CompressIndicesTest.cpp LIBRARIES MagnumMeshToolsTestLib) +corrade_add_test(MeshToolsConcatenateTest ConcatenateTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsDuplicateTest DuplicateTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsFlipNormalsTest FlipNormalsTest.cpp LIBRARIES MagnumMeshToolsTestLib) corrade_add_test(MeshToolsGenerateNormalsTest GenerateNormalsTest.cpp LIBRARIES MagnumMeshToolsTestLib MagnumPrimitives) @@ -37,6 +38,7 @@ corrade_add_test(MeshToolsSubdivideRemov___Benchmark SubdivideRemoveDuplicatesBe # Graceful assert for testing set_property(TARGET + MeshToolsConcatenateTest MeshToolsDuplicateTest MeshToolsInterleaveTest MeshToolsRemoveDuplicatesTest @@ -46,6 +48,7 @@ set_property(TARGET set_target_properties( MeshToolsCombineTest MeshToolsCompressIndicesTest + MeshToolsConcatenateTest MeshToolsDuplicateTest MeshToolsFlipNormalsTest MeshToolsGenerateNormalsTest diff --git a/src/Magnum/MeshTools/Test/ConcatenateTest.cpp b/src/Magnum/MeshTools/Test/ConcatenateTest.cpp new file mode 100644 index 000000000..393a4ce85 --- /dev/null +++ b/src/Magnum/MeshTools/Test/ConcatenateTest.cpp @@ -0,0 +1,595 @@ +/* + 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 "Magnum/Math/Color.h" +#include "Magnum/MeshTools/Concatenate.h" + +namespace Magnum { namespace MeshTools { namespace Test { namespace { + +struct ConcatenateTest: TestSuite::Tester { + explicit ConcatenateTest(); + + void concatenate(); + void concatenateNotIndexed(); + void concatenateNoAttributes(); + void concatenateNoAttributesNotIndexed(); + void concatenateOne(); + void concatenateOneRvalue(); + void concatenateInto(); + void concatenateIntoNoIndexArray(); + void concatenateIntoNonOwnedAttributeArray(); + + void concatenateUnsupportedPrimitive(); + void concatenateInconsistentPrimitive(); + void concatenateInconsistentAttributeType(); + void concatenateIntoNoMeshes(); +}; + +ConcatenateTest::ConcatenateTest() { + addTests({&ConcatenateTest::concatenate, + &ConcatenateTest::concatenateNotIndexed, + &ConcatenateTest::concatenateNoAttributes, + &ConcatenateTest::concatenateNoAttributesNotIndexed, + &ConcatenateTest::concatenateOne, + &ConcatenateTest::concatenateOneRvalue, + &ConcatenateTest::concatenateInto, + &ConcatenateTest::concatenateIntoNoIndexArray, + &ConcatenateTest::concatenateIntoNonOwnedAttributeArray, + + &ConcatenateTest::concatenateUnsupportedPrimitive, + &ConcatenateTest::concatenateInconsistentPrimitive, + &ConcatenateTest::concatenateInconsistentAttributeType, + &ConcatenateTest::concatenateIntoNoMeshes}); +} + +/* MSVC 2015 doesn't like unnamed bitfields in local structs, so thhis has to + be outside */ +struct VertexDataA { + Vector2 texcoords1; + Vector2 texcoords2; + Int:32; + Vector3 position; +}; + +void ConcatenateTest::concatenate() { + using namespace Math::Literals; + + /* First is non-indexed, this layout (including the gap) will be + preserved */ + const VertexDataA vertexDataA[]{ + {{0.1f, 0.2f}, {0.5f, 0.6f}, {1.0f, 2.0f, 3.0f}}, + {{0.3f, 0.4f}, {0.7f, 0.8f}, {4.0f, 5.0f, 6.0f}} + }; + Trade::MeshData a{MeshPrimitive::Points, {}, vertexDataA, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::stridedArrayView(vertexDataA, + &vertexDataA[0].texcoords1, 2, sizeof(VertexDataA))}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::stridedArrayView(vertexDataA, + &vertexDataA[0].texcoords2, 2, sizeof(VertexDataA))}, + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::stridedArrayView(vertexDataA, + &vertexDataA[0].position, 2, sizeof(VertexDataA))}, + }}; + + /* Second is indexed, has only one texture coordinate of the two, an extra + color (which gets ignored) and misses the position (which will be + zero-filled) */ + const struct VertexDataB { + Color4 color; + Vector2 texcoords1; + } vertexDataB[]{ + {0x112233_rgbf, {0.15f, 0.25f}}, + {0x445566_rgbf, {0.35f, 0.45f}}, + {0x778899_rgbf, {0.55f, 0.65f}}, + {0xaabbcc_rgbf, {0.75f, 0.85f}} + }; + const UnsignedShort indicesB[]{0, 2, 1, 0, 3, 2}; + Trade::MeshData b{MeshPrimitive::Points, + {}, indicesB, Trade::MeshIndexData{indicesB}, {}, vertexDataB, { + Trade::MeshAttributeData{Trade::MeshAttribute::Color, + Containers::stridedArrayView(vertexDataB, + &vertexDataB[0].color, 4, sizeof(VertexDataB))}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::stridedArrayView(vertexDataB, + &vertexDataB[0].texcoords1, 4, sizeof(VertexDataB))}, + }}; + + /* Third is again non-indexed, has one texcoord attribute more (which will + get ignored). Additionally, attribute memory order is inversed and mixed + together to verify the attributes are picked based on declaration order, + not memory order. */ + const struct VertexDataC { + Vector2 texcoords2; + Vector3 position; + Vector2 texcoords3; + Vector2 texcoords1; + } vertexDataC[]{ + {{0.425f, 0.475f}, {1.5f, 2.5f, 3.5f}, {0.725f, 0.775f}, {0.125f, 0.175f}}, + {{0.525f, 0.575f}, {4.5f, 5.5f, 6.5f}, {0.825f, 0.875f}, {0.225f, 0.275f}}, + {{0.625f, 0.675f}, {7.5f, 8.5f, 9.5f}, {0.925f, 0.975f}, {0.325f, 0.375f}}, + }; + Trade::MeshData c{MeshPrimitive::Points, {}, vertexDataC, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::stridedArrayView(vertexDataC, + &vertexDataC[0].texcoords1, 3, sizeof(VertexDataC))}, + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::stridedArrayView(vertexDataC, + &vertexDataC[0].position, 3, sizeof(VertexDataC))}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::stridedArrayView(vertexDataC, + &vertexDataC[0].texcoords2, 3, sizeof(VertexDataC))}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::stridedArrayView(vertexDataC, + &vertexDataC[0].texcoords3, 3, sizeof(VertexDataC))}, + }}; + + Trade::MeshData dst = MeshTools::concatenate(a, {b, c}); + CORRADE_COMPARE(dst.primitive(), MeshPrimitive::Points); + CORRADE_COMPARE(dst.attributeCount(), 3); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::Position), + Containers::arrayView({ + {1.0f, 2.0f, 3.0f}, + {4.0f, 5.0f, 6.0f}, + {}, {}, {}, {}, /* Missing in the second mesh */ + {1.5f, 2.5f, 3.5f}, + {4.5f, 5.5f, 6.5f}, + {7.5f, 8.5f, 9.5f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::TextureCoordinates), + Containers::arrayView({ + {0.1f, 0.2f}, + {0.3f, 0.4f}, + {0.15f, 0.25f}, + {0.35f, 0.45f}, + {0.55f, 0.65f}, + {0.75f, 0.85f}, + {0.125f, 0.175f}, + {0.225f, 0.275f}, + {0.325f, 0.375f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::TextureCoordinates, 1), + Containers::arrayView({ + {0.5f, 0.6f}, + {0.7f, 0.8f}, + {}, {}, {}, {}, /* Missing in the second mesh */ + {0.425f, 0.475f}, + {0.525f, 0.575f}, + {0.625f, 0.675f} + }), TestSuite::Compare::Container); + CORRADE_VERIFY(dst.isIndexed()); + CORRADE_COMPARE(dst.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE_AS(dst.indices(), + Containers::arrayView({ + 0, 1, /* implicit for the first nonindexed mesh */ + 2, 4, 3, 2, 5, 4, /* offset for the second indexed mesh */ + 6, 7, 8 /* implicit + offset for the third mesh */ + }), TestSuite::Compare::Container); + + /* The original interleaved layout should be preserved */ + CORRADE_VERIFY(isInterleaved(dst)); + CORRADE_COMPARE(dst.attributeStride(0), sizeof(VertexDataA)); + CORRADE_COMPARE(dst.attributeOffset(0), 0); + CORRADE_COMPARE(dst.attributeOffset(1), sizeof(Vector2)); + CORRADE_COMPARE(dst.attributeOffset(2), 2*sizeof(Vector2) + 4); +} + +void ConcatenateTest::concatenateNotIndexed() { + const Vector3 positionA[]{ + {1.0f, 2.0f, 3.0f}, + {4.0f, 5.0f, 6.0f} + }; + Trade::MeshData a{MeshPrimitive::Points, {}, positionA, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::arrayView(positionA)} + }}; + + const Vector3 positionB[]{ + {1.5f, 2.5f, 3.5f}, + {4.5f, 5.5f, 6.5f}, + {7.5f, 8.5f, 9.5f}, + }; + Trade::MeshData b{MeshPrimitive::Points, {}, positionB, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::arrayView(positionB)} + }}; + + Trade::MeshData dst = MeshTools::concatenate(a, {b, b}); + CORRADE_COMPARE(dst.primitive(), MeshPrimitive::Points); + CORRADE_COMPARE(dst.attributeCount(), 1); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::Position), + Containers::arrayView({ + {1.0f, 2.0f, 3.0f}, + {4.0f, 5.0f, 6.0f}, + {1.5f, 2.5f, 3.5f}, + {4.5f, 5.5f, 6.5f}, + {7.5f, 8.5f, 9.5f}, + {1.5f, 2.5f, 3.5f}, + {4.5f, 5.5f, 6.5f}, + {7.5f, 8.5f, 9.5f} + }), TestSuite::Compare::Container); + CORRADE_VERIFY(!dst.isIndexed()); +} + +void ConcatenateTest::concatenateNoAttributes() { + /* Compared to concatenate(), now the first and last is indexed */ + const UnsignedShort indicesA[]{1, 0}; + Trade::MeshData a{MeshPrimitive::Points, {}, indicesA, Trade::MeshIndexData{indicesA}, 2}; + + /* Second is not indexed, just a vertex count */ + Trade::MeshData b{MeshPrimitive::Points, 6}; + + const UnsignedByte indicesC[]{1, 0, 1, 0}; + Trade::MeshData c{MeshPrimitive::Points, {}, indicesC, Trade::MeshIndexData{indicesC}, 2}; + + Trade::MeshData dst = MeshTools::concatenate(a, {b, c}); + CORRADE_COMPARE(dst.primitive(), MeshPrimitive::Points); + CORRADE_COMPARE(dst.attributeCount(), 0); + CORRADE_COMPARE(dst.vertexCount(), 10); + CORRADE_VERIFY(!dst.vertexData()); + CORRADE_VERIFY(dst.isIndexed()); + CORRADE_COMPARE(dst.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE_AS(dst.indices(), + Containers::arrayView({ + 1, 0, + 2, 3, 4, 5, 6, 7, + 9, 8, 9, 8 + }), TestSuite::Compare::Container); +} + +void ConcatenateTest::concatenateNoAttributesNotIndexed() { + Trade::MeshData a{MeshPrimitive::Points, 3}; + Trade::MeshData b{MeshPrimitive::Points, 6}; + Trade::MeshData c{MeshPrimitive::Points, 2}; + + Trade::MeshData dst = MeshTools::concatenate(a, {b, c}); + CORRADE_COMPARE(dst.primitive(), MeshPrimitive::Points); + CORRADE_COMPARE(dst.attributeCount(), 0); + CORRADE_COMPARE(dst.vertexCount(), 11); + CORRADE_VERIFY(!dst.vertexData()); + CORRADE_VERIFY(!dst.isIndexed()); +} + +/* MSVC 2015 doesn't like unnamed bitfields in local structs, so thhis has to + be outside */ +struct VertexDataNonInterleaved { + Vector2 texcoords1[2]; + Vector2 texcoords2[2]; + Int:32; + Int:32; + Vector3 position[2]; +}; + +void ConcatenateTest::concatenateOne() { + const VertexDataNonInterleaved vertexData[]{{ + {{0.1f, 0.2f}, + {0.3f, 0.4f}}, + {{0.5f, 0.6f}, + {0.7f, 0.8f}}, + {{1.0f, 2.0f, 3.0f}, + {4.0f, 5.0f, 6.0f}} + }}; + const UnsignedByte indices[]{1, 0, 1}; + Trade::MeshData a{MeshPrimitive::Points, + {}, indices, Trade::MeshIndexData{indices}, {}, vertexData, { + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::arrayView(vertexData[0].texcoords1)}, + Trade::MeshAttributeData{Trade::MeshAttribute::TextureCoordinates, + Containers::arrayView(vertexData[0].texcoords2)}, + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::arrayView(vertexData[0].position)}, + }}; + + Trade::MeshData dst = MeshTools::concatenate(a); + CORRADE_COMPARE(dst.primitive(), MeshPrimitive::Points); + CORRADE_COMPARE(dst.attributeCount(), 3); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::Position), + Containers::arrayView({ + {1.0f, 2.0f, 3.0f}, + {4.0f, 5.0f, 6.0f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::TextureCoordinates), + Containers::arrayView({ + {0.1f, 0.2f}, + {0.3f, 0.4f} + }), TestSuite::Compare::Container); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::TextureCoordinates, 1), + Containers::arrayView({ + {0.5f, 0.6f}, + {0.7f, 0.8f} + }), TestSuite::Compare::Container); + CORRADE_VERIFY(dst.isIndexed()); + CORRADE_COMPARE(dst.indexType(), MeshIndexType::UnsignedInt); + CORRADE_COMPARE_AS(dst.indices(), + Containers::arrayView({ + 1, 0, 1 + }), TestSuite::Compare::Container); + + /* The mesh should get interleaved (w/o gaps) and owned */ + CORRADE_VERIFY(isInterleaved(dst)); + CORRADE_COMPARE(dst.attributeStride(0), 2*sizeof(Vector2) + sizeof(Vector3)); + CORRADE_COMPARE(dst.indexDataFlags(), Trade::DataFlag::Owned|Trade::DataFlag::Mutable); + CORRADE_COMPARE(dst.vertexDataFlags(), Trade::DataFlag::Owned|Trade::DataFlag::Mutable); +} + +void ConcatenateTest::concatenateOneRvalue() { + Containers::Array vertexData{sizeof(Vector2)*4}; + auto positions = Containers::arrayCast(vertexData); + Containers::Array indexData{sizeof(UnsignedInt)*6}; + auto indices = Containers::arrayCast(indexData); + Trade::MeshAttributeData attributeData[]{ + Trade::MeshAttributeData{Trade::MeshAttribute::Position, positions} + }; + + /* The result should be just a pass-through, as both index and vertex data + are already owned */ + Trade::MeshData dst = MeshTools::concatenate(Trade::MeshData{ + MeshPrimitive::Triangles, + std::move(indexData), Trade::MeshIndexData{indices}, + std::move(vertexData), Trade::meshAttributeDataNonOwningArray(attributeData)}, + /* Explicitly pass an empty init list to ensure this overload is + covered as well */ + std::initializer_list>{}); + CORRADE_COMPARE(dst.indexData().data(), static_cast(indices.data())); + CORRADE_COMPARE(dst.vertexData().data(), static_cast(positions.data())); +} + +void ConcatenateTest::concatenateInto() { + Containers::Array attributeData{2}; + Containers::Array vertexData; + Containers::Array indexData; + arrayResize(vertexData, Containers::DirectInit, (sizeof(Vector2) + sizeof(Vector3))*7, '\xff'); + arrayResize(vertexData, 0); + arrayResize(indexData, Containers::DirectInit, sizeof(UnsignedInt)*9, '\xff'); + arrayResize(indexData, 0); + const void* attributeDataPointer = attributeData; + const void* vertexDataPointer = vertexData; + const void* indexDataPointer = indexData; + + attributeData[0] = Trade::MeshAttributeData{Trade::MeshAttribute::Position, + VertexFormat::Vector2, nullptr}; + attributeData[1] = Trade::MeshAttributeData{Trade::MeshAttribute::Normal, + VertexFormat::Vector3, nullptr}; + Trade::MeshIndexData indices{MeshIndexType::UnsignedInt, indexData}; + Trade::MeshData dst{MeshPrimitive::Triangles, + std::move(indexData), indices, + std::move(vertexData), std::move(attributeData)}; + + const Vector2 positionsA[]{ + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + {-1.0f, 1.0f}, + { 1.0f, 1.0f} + }; + const UnsignedShort indicesA[]{ + 0, 1, 2, 2, 1, 3 + }; + Trade::MeshData a{MeshPrimitive::Triangles, + {}, indicesA, Trade::MeshIndexData{indicesA}, + {}, positionsA, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::arrayView(positionsA)} + }}; + + const Vector2 positionsB[]{ + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + { 0.0f, 1.0f} + }; + Trade::MeshData b{MeshPrimitive::Triangles, + {}, positionsB, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::arrayView(positionsB)} + }}; + + MeshTools::concatenateInto(dst, {a, b}); + CORRADE_COMPARE(dst.attributeCount(), 2); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::Position), + Containers::arrayView({ + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + {-1.0f, 1.0f}, + { 1.0f, 1.0f}, + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + { 0.0f, 1.0f} + }), TestSuite::Compare::Container); + /* The normal isn't present in any attribute and thus should be zeroed out + (*not* the whatever garbage present there from before) */ + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::Normal), + Containers::arrayView({ + {}, {}, {}, {}, {}, {}, {} + }), TestSuite::Compare::Container); + CORRADE_VERIFY(dst.isIndexed()); + CORRADE_COMPARE_AS(dst.indices(), + Containers::arrayView({ + 0, 1, 2, 2, 1, 3, + 4, 5, 6 + }), TestSuite::Compare::Container); + + /* Verify that no reallocation happened */ + CORRADE_COMPARE(dst.attributeData().size(), 2); + CORRADE_COMPARE(dst.attributeData().data(), attributeDataPointer); + CORRADE_COMPARE(dst.vertexData().size(), 7*(sizeof(Vector2) + sizeof(Vector3))); + CORRADE_COMPARE(dst.vertexData().data(), vertexDataPointer); + CORRADE_COMPARE(dst.indexData().size(), 9*sizeof(UnsignedInt)); + CORRADE_COMPARE(dst.indexData().data(), indexDataPointer); +} + +void ConcatenateTest::concatenateIntoNoIndexArray() { + Containers::Array attributeData{1}; + Containers::Array vertexData; + Containers::Array indexData; + arrayReserve(vertexData, sizeof(Vector2)*3); + arrayReserve(indexData, sizeof(UnsignedInt)); + const void* attributeDataPointer = attributeData; + const void* vertexDataPointer = vertexData; + + attributeData[0] = Trade::MeshAttributeData{Trade::MeshAttribute::Position, + VertexFormat::Vector2, nullptr}; + Trade::MeshIndexData indices{MeshIndexType::UnsignedInt, indexData}; + Trade::MeshData dst{MeshPrimitive::Triangles, + std::move(indexData), indices, + std::move(vertexData), std::move(attributeData)}; + CORRADE_VERIFY(dst.isIndexed()); + + const Vector2 positions[]{ + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + { 0.0f, 1.0f} + }; + Trade::MeshData a{MeshPrimitive::Triangles, + {}, positions, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::arrayView(positions)} + }}; + + MeshTools::concatenateInto(dst, {a}); + CORRADE_COMPARE(dst.attributeCount(), 1); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::Position), + Containers::arrayView({ + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + { 0.0f, 1.0f} + }), TestSuite::Compare::Container); + + /* The index array gets removed, but no reallocation happens for the other + two */ + CORRADE_VERIFY(!dst.isIndexed()); + CORRADE_COMPARE(dst.attributeData().size(), 1); + CORRADE_COMPARE(dst.attributeData().data(), attributeDataPointer); + CORRADE_COMPARE(dst.vertexData().size(), 3*sizeof(Vector2)); + CORRADE_COMPARE(dst.vertexData().data(), vertexDataPointer); +} + +void ConcatenateTest::concatenateIntoNonOwnedAttributeArray() { + Containers::Array vertexData; + arrayReserve(vertexData, sizeof(Vector2)*3); + const void* vertexDataPointer = vertexData; + + const Trade::MeshAttributeData attributeData[]{ + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + VertexFormat::Vector2, nullptr} + }; + Trade::MeshData dst{MeshPrimitive::Triangles, + std::move(vertexData), Trade::meshAttributeDataNonOwningArray(attributeData)}; + + const Vector2 positions[]{ + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + { 0.0f, 1.0f} + }; + Trade::MeshData a{MeshPrimitive::Triangles, + {}, positions, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + Containers::arrayView(positions)} + }}; + + MeshTools::concatenateInto(dst, {a}); + CORRADE_COMPARE(dst.attributeCount(), 1); + CORRADE_COMPARE_AS(dst.attribute(Trade::MeshAttribute::Position), + Containers::arrayView({ + {-1.0f, -1.0f}, + { 1.0f, -1.0f}, + { 0.0f, 1.0f} + }), TestSuite::Compare::Container); + + /* Reallocation happens only for the attribute data as it's not owned */ + CORRADE_VERIFY(!dst.isIndexed()); + CORRADE_COMPARE(dst.attributeData().size(), 1); + CORRADE_VERIFY(dst.attributeData().data() != attributeData); + CORRADE_COMPARE(dst.vertexData().size(), 3*sizeof(Vector2)); + CORRADE_COMPARE(dst.vertexData().data(), vertexDataPointer); +} + +void ConcatenateTest::concatenateUnsupportedPrimitive() { + Trade::MeshData a{MeshPrimitive::TriangleStrip, 0}; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::concatenate(a); + MeshTools::concatenateInto(a, {a}); + CORRADE_COMPARE(out.str(), + "MeshTools::concatenate(): MeshPrimitive::TriangleStrip is not supported, turn it into a plain indexed mesh first\n" + "MeshTools::concatenateInto(): MeshPrimitive::TriangleStrip is not supported, turn it into a plain indexed mesh first\n"); +} + +void ConcatenateTest::concatenateInconsistentPrimitive() { + /* Things are a bit duplicated to test correct numbering */ + Trade::MeshData a{MeshPrimitive::Triangles, 0}; + Trade::MeshData b{MeshPrimitive::Lines, 0}; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::concatenate(a, {a, b}); + MeshTools::concatenateInto(a, {a, b}); + CORRADE_COMPARE(out.str(), + "MeshTools::concatenate(): expected MeshPrimitive::Triangles but got MeshPrimitive::Lines in mesh 1\n" + "MeshTools::concatenateInto(): expected MeshPrimitive::Triangles but got MeshPrimitive::Lines in mesh 1\n"); +} + +void ConcatenateTest::concatenateInconsistentAttributeType() { + /* Things are a bit duplicated to test correct numbering */ + Trade::MeshData a{MeshPrimitive::Lines, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + VertexFormat::Vector3, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + VertexFormat::Vector3, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Color, + VertexFormat::Vector3ubNormalized, nullptr} + }}; + Trade::MeshData b{MeshPrimitive::Lines, nullptr, { + Trade::MeshAttributeData{Trade::MeshAttribute::Position, + VertexFormat::Vector3, nullptr}, + Trade::MeshAttributeData{Trade::MeshAttribute::Color, + VertexFormat::Vector3usNormalized, nullptr} + }}; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::concatenate(a, {a, a, a, b}); + MeshTools::concatenateInto(a, {a, a, a, b}); + CORRADE_COMPARE(out.str(), + "MeshTools::concatenate(): expected VertexFormat::Vector3ubNormalized for attribute 2 (Trade::MeshAttribute::Color) but got VertexFormat::Vector3usNormalized in mesh 3 attribute 1\n" + "MeshTools::concatenateInto(): expected VertexFormat::Vector3ubNormalized for attribute 2 (Trade::MeshAttribute::Color) but got VertexFormat::Vector3usNormalized in mesh 3 attribute 1\n"); +} + +void ConcatenateTest::concatenateIntoNoMeshes() { + Trade::MeshData destination{MeshPrimitive::Triangles, 0}; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::concatenateInto(destination, {}); + CORRADE_COMPARE(out.str(), "MeshTools::concatenateInto(): no meshes passed\n"); +} + +}}}} + +CORRADE_TEST_MAIN(Magnum::MeshTools::Test::ConcatenateTest)