Skip to content

Commit

Permalink
MeshTools: implemented concatenate() and concatenateInto().
Browse files Browse the repository at this point in the history
  • Loading branch information
mosra committed Mar 9, 2020
1 parent ee0d43e commit eaa59cc
Show file tree
Hide file tree
Showing 9 changed files with 1,052 additions and 13 deletions.
2 changes: 2 additions & 0 deletions doc/changelog.dox
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
12 changes: 12 additions & 0 deletions doc/snippets/MagnumMeshTools.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -74,6 +77,15 @@ std::pair<Containers::Array<char>, 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
Expand Down
2 changes: 2 additions & 0 deletions src/Magnum/MeshTools/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ set(MagnumMeshTools_SRCS
set(MagnumMeshTools_GracefulAssert_SRCS
Combine.cpp
CompressIndices.cpp
Concatenate.cpp
Duplicate.cpp
FlipNormals.cpp
GenerateNormals.cpp
Expand All @@ -40,6 +41,7 @@ set(MagnumMeshTools_GracefulAssert_SRCS
set(MagnumMeshTools_HEADERS
Combine.h
CompressIndices.h
Concatenate.h
Duplicate.h
FlipNormals.h
GenerateNormals.h
Expand Down
247 changes: 247 additions & 0 deletions src/Magnum/MeshTools/Concatenate.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,247 @@
/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019
Vladimír Vondruš <mosra@centrum.cz>
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 <numeric>
#include <unordered_map>
#include <Corrade/Utility/Algorithms.h>

namespace Magnum { namespace MeshTools {

namespace Implementation {

std::pair<UnsignedInt, UnsignedInt> concatenateIndexVertexCount(const Trade::MeshData& first, const Containers::ArrayView<const Containers::Reference<const Trade::MeshData>> 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<typename std::underlying_type<Trade::MeshAttribute>::type> {
std::size_t operator()(Trade::MeshAttribute value) const {
return std::hash<typename std::underlying_type<Trade::MeshAttribute>::type>::operator()(static_cast<typename std::underlying_type<Trade::MeshAttribute>::type>(value));
}
};

Trade::MeshData concatenate(Containers::Array<char>&& indexData, const UnsignedInt vertexCount, Containers::Array<char>&& vertexData, Containers::Array<Trade::MeshAttributeData>&& attributeData, const Trade::MeshData& first, const Containers::ArrayView<const Containers::Reference<const Trade::MeshData>> next, const char* const assertPrefix, const std::size_t meshIndexOffset) {
#ifdef CORRADE_NO_ASSERT
static_cast<void>(assertPrefix);
static_cast<void>(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<void>{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<UnsignedInt>(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<Trade::MeshAttribute, std::pair<UnsignedInt, bool>, 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<UnsignedInt> 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<const Containers::Reference<const Trade::MeshData>> 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<Trade::MeshAttributeData> 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<UnsignedInt, UnsignedInt> indexVertexCount = Implementation::concatenateIndexVertexCount(first, next);
Containers::Array<char> indexData{Containers::NoInit,
indexVertexCount.first*sizeof(UnsignedInt)};
Containers::Array<char> 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<Containers::Reference<const Trade::MeshData>> next) {
return concatenate(std::move(first), Containers::arrayView(next));
}

Trade::MeshData concatenate(const Trade::MeshData& first, const Containers::ArrayView<const Containers::Reference<const Trade::MeshData>> next) {
Containers::ArrayView<const char> 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<Containers::Reference<const Trade::MeshData>> next) {
return concatenate(first, Containers::arrayView(next));
}

}}
Loading

0 comments on commit eaa59cc

Please sign in to comment.