Feature guide » Mesh processing tools

Overview of algorithms and utilities in the MeshTools namespace.

The MeshTools namespace provides a broad set of tools for transforming, filtering, optimizing and merging mesh data, operating both on high-level Trade::MeshData instances as well as directly on concrete data views.

Creating MeshData instances from scratch

When it's desirable to create a Trade::MeshData instance from scratch, for example from runtime-generated data, MeshTools::interleave() is the most convenient way.

Containers::ArrayView<const UnsignedInt> indices = ;
Containers::ArrayView<const Vector3> positions = ;
Containers::ArrayView<const Vector3> normals = ;

Trade::MeshData mesh = MeshTools::interleave(
    MeshPrimitive::Triangles,
    Trade::MeshIndexData{indices}, {
        Trade::MeshAttributeData{Trade::MeshAttribute::Position, positions},
        Trade::MeshAttributeData{Trade::MeshAttribute::Normal, normals},
    });

Interleaving isn't the only possible layout option — you can also construct the MeshData instance directly. It's more involved, but gives you an ability to have any packing you need.

Note that, however, a Trade::MeshData instance isn't required in many cases — most MeshTools algorithms, including for example normal generation or duplicate removal, have alternatives that operate directly on plain data arrays, and wrapping those in a Trade::MeshData instance just to call a function may be a needless complication. On the other hand, having a Trade::MeshData instance may be beneficial when it's needed to abstract away optional vertex attributes or when the types or layout can be arbitrary. The only interface that unconditionally relies on Trade::MeshData is the Trade::AbstractSceneConverter, for example when you want to export a mesh to a file or when you want to call an external library to perform advanced tasks on the data.

Uploading a mesh to the GPU

The MeshTools::compile() utility creates a GL::Mesh instance out of an arbitrary Trade::MeshData, binding builtin attributes listed in the Trade::MeshData enum to known locations defined in Shaders::GenericGL.

Trade::MeshData data = ;

GL::Mesh mesh = MeshTools::compile(data);

For more control over index / vertex data storage and handling custom attributes, the MeshTools::compile(const Trade::MeshData&, GL::Buffer&, GL::Buffer&) overload can be used. Additionally, there's MeshTools::compileLines() with specialized handling for line meshes to be rendered with Shaders::LineGL, and the MeshTools::compiledPerVertexJointCount() utility gives back a count of primary and secondary per-vertex joints for setting up an appropriate skinning shader.

Data layout optimization

Mesh import in Trade::AbstractImporter subclasses is commonly done so that the input layout is preserved as much as possible, and without performing non-essential operations. Similarly, meshes coming from the Primitives library preference common, unsurprising formats over the most efficient representation. Thus, depending on quality of the input data, the target use case and whether given mesh is used for further processing or rendering, there are various optimization possibilities.

Interleaving vertex data

Assuming a mesh is processed vertex-by-vertex with all attributes used, which is the common case for both CPU- and GPU-side operation, the best memory layout is interleaving the data so that attributes for a particular vertex are next to each other in memory.

Trade::MeshData mesh = ;

mesh = MeshTools::interleave(std::move(mesh));

Interleaving is however the default behavior in most importers, and most MeshTools algorithms produce interleaved layouts by default as well. MeshTools::interleave() is thus implicitly a passthrough in case the data is already interleaved, so it's often desirable to pass a r-value there like shown above, which causes it to be just moved through if nothing needs to be done.

For interleaving of raw data arrays with types known at compile time, there's MeshTools::interleave(const T&, const U&... next), or its non-allocating MeshTools::interleaveInto() variant. If the vertex layout is a concrete struct, another way is for example using Utility::copy() to members slices made with Containers::StridedArrayView::slice(U T::*) const.

Packing index data

Especially when importing data from text-based formats such as OBJ, index buffers have full 32-bit values, which is not always necessary. With MeshTools::compressIndices() the index buffer gets reduced to a smaller type. Because vertex data are untouched by this operation, it's again desirable to pass a r-value in, which causes vertex data to be moved through instead of copying:

if(mesh.isIndexed())
    mesh = MeshTools::compressIndices(std::move(mesh));

You can also use the same function to unpack already-packed index data back to a larger type, by passing an appropriate MeshIndexType as the second argument.

Index packing can be also done directly on an index array using MeshTools::compressIndices(const Containers::StridedArrayView1D<const UnsignedInt>&, MeshIndexType, Long). There's no non-allocating variant in this case, because one would have to do another pass over the index array to figure out the target array size first. If you want to perform packing to a concrete type, use one of the Math::castInto() overloads.

Vertex transform cache optimization

The MeshTools::tipsify() utility reorders the index buffer in a way that tries to maximize use of GPU vertex cache, resulting in possibly faster rendering. It's however recommended to use the MeshOptimizerSceneConverter plugin instead if possible. It contains a set of state-of-the-art algorithms and by default performs a non-destructive sequence of optimizations that make the mesh faster to render without affecting appearance in any way.

PluginManager::Manager<Trade::AbstractSceneConverter> manager;
Containers::Pointer<Trade::AbstractSceneConverter> meshOptimizer =
    manager.loadAndInstantiate("MeshOptimizerSceneConverter");

meshOptimizer->convertInPlace(mesh);

See documentation of Trade::AbstractSceneConverter for more information about using the plugin interface.

Index buffer generation

A mesh can be non-indexed, meaning that e.g. each three vertices form a triangle, it can have an index buffer of an arbitrary type, or it can be formed from strips or fans. While such flexibility allows to pick a representation that best fits given topology or use case, it can be a burden for algorithms that need to work with arbitrary input meshes. The MeshTools::generateIndices() helper takes an arbitrary mesh and produces an instance that always has a 32-bit index buffer and has one of the base primitive types such as MeshPrimitive::Triangles, Lines or Points, so it can then be then passed straight to an algorithm that expects indexed primitives.

Trade::MeshData indexed = MeshTools::generateIndices(mesh);

performSomeProcessing(indexed.indices<UnsignedInt>(),
                      indexed.positions3DAsArray());

Ultimately, if a non-indexed list of primitives is expected, the mesh can be subsequently passed through MeshTools::duplicate().

Besides the variant taking a Trade::MeshData, there are low-level MeshTools::generateLineStripIndices(), generateLineLoopIndices(), generateTriangleStripIndices() and generateTriangleFanIndices() utilities producing an index buffer corresponding to a primitive strip, loop or fan. To complete the offering, generateTrivialIndices() outputs a 0, 1, 2, 3, 4, 5, ... sequence if an index buffer needs to be added to a mesh that otherwise doesn't need it. For all of these there are non-allocating generateLineStripIndicesInto() etc. variants as well.

Finally, MeshTools::generateQuadIndices() / generateQuadIndicesInto() creates a triangle index buffer for a list of (indexed) quads. It additionally takes vertex positions to favor edges that don't create overlapping or too thin triangles.

B A C A C D B D

Vertex data transformation

The MeshTools::transform2D(), transform3D() and transformTextureCoordinates2D() functions can be used to bake a transformation into vertex positions, tangent space and texture coordinates. One use case is preparing a set of meshes to be joined together, baking their transform hierarchy directly into the data, or for example making a mesh ready to be used with a renderer that doesn't support passing scaling or texture coordinate transformation as a parameter.

mesh = MeshTools::transform3D(std::move(mesh), Matrix4::scaling({0.5f, 2.0f, 1.0f}));

If the mesh has the to-be-transformed attributes in a floating-point format, i.e. not packed in any way, the function can operate directly on the data itself without making a copy. For that reason, if the original unmodified instance isn't needed afterwards anymore, it's again useful to pass a r-value in, as shown in the snippet. Alternatively, the transform2DInPlace(), transform3DInPlace() and transformTextureCoordinates2DInPlace() variants operate in-place, not modifying the attribute layout in any way, but have with additional restrictions on the attribute types.

Joining multiple meshes together

While models usually contain multiple smaller meshes because it makes editing easier, for rendering it's often better to batch them together. The MeshTools::concatenate() function concatenates several input meshes into a single one. Then, if all the input meshes were using the same material and were already in their final transform relative to each other, you can render or further process them as a whole:

Trade::MeshData sphere = ;
Trade::MeshData cube = ;
Trade::MeshData cylinder = ;
Trade::MeshData primitives = MeshTools::concatenate({sphere, cube, cylinder});

GL::Mesh mesh = MeshTools::compile(primitives);

If not, their relative order is preserved in the output, so you can for example render each individual piece separately by passing an appropriate index range to GL::Mesh::setIndexOffset() and setCount(), or by making GL::MeshView instances and rendering those instead:

GL::MeshView meshSphereView{mesh},
    meshCubeView{mesh},
    meshCylinderView{mesh};
meshSphereView
    .setIndexOffset(0)
    .setCount(sphere.indexCount());
meshCubeView
    .setIndexOffset(meshSphereView.indexOffset() + meshSphereView.count())
    .setCount(cube.indexCount());
meshCylinderView
    .setIndexOffset(meshCubeView.indexOffset() + meshCubeView.count())
    .setCount(cylinder.indexCount());
shader
    .setColor(0x2f83cc_rgbf)
    .draw(meshSphereView)
    .setColor(0x3bd267_rgbf)
    .draw(meshCubeView)
    .setColor(0xc7cf2f_rgbf)
    .draw(meshCylinderView);

Meshes joined this way can make use of various rendering optimizations, see Multidraw and reducing driver overhead for the shader-side details. There's also a MeshTools::concatenateInto() variant that reuses a Trade::MeshData instance with previously allocated buffers to support use cases where meshes are repeatedly batched on-the-fly.

Inserting additional attributes into an existing mesh

The MeshTools::interleave() API shown above can be also used to insert additional attributes to an existing mesh. The following snippet takes a cube primitive and copies an external vertex color attribute alongside existing attributes:

Containers::ArrayView<const Color3> vertexColors = ;

Trade::MeshData coloredCube = MeshTools::interleave(Primitives::cubeSolid(), {
    Trade::MeshAttributeData{Trade::MeshAttribute::Color, vertexColors}
});

It's also possible to add just an uninitialized attribute placeholder, specifying just the desired type, and copy the data to it later using mutable MeshData attribute access:

Trade::MeshData coloredCube = MeshTools::interleave(Primitives::cubeSolid(), {
    Trade::MeshAttributeData{Trade::MeshAttribute::Color, VertexFormat::Vector3, nullptr}
});

for(Color3& i: coloredCube.mutableAttribute<Color3>(Trade::MeshAttribute::Color))
    i = ;

Similar functionality is available also in the above-mentioned MeshTools::duplicate() API, with the difference that the attribute gets inserted only after a duplication based on an index buffer is performed. This is useful for example when it's desirable to add a value that's different for each vertex:

Trade::MeshAttribute VertexIdAttribute = Trade::meshAttributeCustom();

Trade::MeshData vertexIdMesh = MeshTools::duplicate(mesh, {
    Trade::MeshAttributeData{VertexIdAttribute, VertexFormat::UnsignedInt, nullptr}
});

UnsignedInt id = 0;
for(UnsignedInt& i: vertexIdMesh.mutableAttribute<UnsignedInt>(VertexIdAttribute))
    i = id++;

In case you need to insert attributes that differ not per vertex but per face, MeshTools::combineFaceAttributes() can be used:

Containers::ArrayView<const Color3> faceColors = ;

Trade::MeshData meshWithFaceColors = MeshTools::combineFaceAttributes(mesh, {
    Trade::MeshAttributeData{Trade::MeshAttribute::Color, faceColors}
});

Finally, for scenarios where a mesh has per-attribute index buffers, such as is the case when directly importing data from OBJ files, MeshTools::combineIndexedAttributes() can be used to combine them into a mesh with a single index buffer.

Filtering mesh attributes

The inverse of attribute insertion is possible with MeshTools::filterAttributes(), filterOnlyAttributes() and filterExceptAttributes(). In this case however, the operation affects just the metadata and the result is a non-owning reference to data in the original mesh with just the attributes that passed the filter. In other words, the vertex data stay unchanged, there's just nothing referencing the data for attributes that were filtered away.

Trade::MeshData positionsNormals = MeshTools::filterOnlyAttributes(mesh, {
    Trade::MeshAttribute::Position,
    Trade::MeshAttribute::Normal
});

This avoids a needless copy in cases the result is passed to other algorithms that perform further operations on the data. If filtering is the final step, pass the result to MeshTools::interleave() without MeshTools::InterleaveFlag::PreserveInterleavedAttributes set to create a copy that contains only the remaining attributes:

positionsNormals = MeshTools::interleave(positionsNormals, {}, {});

Duplicate vertex removal, duplication based on an index buffer

The MeshTools::removeDuplicates() function returns a Trade::MeshData instance that has contains only unique vertices, and has an index buffer that maps them back to their original locations. Besides cleaning up messy models the function can be also used for converting non-indexed meshes (imported from STL files, for example) to indexed. Sometimes bit-exact comparison isn't enough however, and the MeshTools::removeDuplicatesFuzzy() variant instead applies a fuzzy comparison to all floating-point attributes.

Trade::MeshData deduplicated = MeshTools::removeDuplicatesFuzzy(mesh);

The fuzzy thresholds are adjustable and setting them to higher values can perform rudimentary mesh simplification, but for a robust behavior with higher simplification ratios it's recommended to use the MeshOptimizerSceneConverter simplification feature instead. Here for example attempting to reduce the mesh index count by a factor of 10:

PluginManager::Manager<Trade::AbstractSceneConverter> manager;
Containers::Pointer<Trade::AbstractSceneConverter> meshOptimizer =
    manager.loadAndInstantiate("MeshOptimizerSceneConverter");
meshOptimizer->configuration().setValue("simplify", true);
meshOptimizer->configuration().setValue("simplifyTargetIndexCountThreshold", 0.1f);

Containers::Optional<Trade::MeshData> simplified = meshOptimizer->convert(mesh);

Internally, the duplicate vertex removal is implemented using MeshTools::removeDuplicatesInPlace(const Containers::StridedArrayView2D<char>&), MeshTools::removeDuplicatesFuzzyInPlace(const Containers::StridedArrayView2D<Float>&, Float) and their non-in-place, and non-allocating *Into() variants. These functions return an index array that maps from the original data to the deduplicated locations. Commonly, the index array eventually becomes an index buffer of the resulting mesh, or one uses the MeshTools::removeDuplicatesIndexedInPlace() / MeshTools::removeDuplicatesFuzzyIndexedInPlace() variants if there's an existing buffer in the first place.

Another use case for the index buffer returned by these functions is to perform an operation of on the deduplicated data and then apply the updates back to the original. One such scenario is for example with soft body simulation, where a simulation engine requires a watertight indexed mesh containing only positions. Rendering however usually needs at least normals as well, and potentially texture coordinates, vertex colors and others. Assuming an indexed mesh with arbitrary attributes, a watertight mesh consisting of just indexed positions would be made like this:

Containers::Array<Vector3> positions = mesh.positions3DAsArray();

/* Deduplicate the positions and create a mapping array */
Containers::Pair<Containers::Array<UnsignedInt>, std::size_t> out =
    MeshTools::removeDuplicatesFuzzyInPlace(
        Containers::arrayCast<2, Float>(stridedArrayView(positions)));
Containers::Array<UnsignedInt> indexMapping = std::move(out.first());
arrayResize(positions, out.second());

/* Combine the original index buffer with the mapping array */
Containers::Array<UnsignedInt> positionIndices = MeshTools::duplicate(
    Containers::StridedArrayView1D<const UnsignedInt>{indexMapping},
    Containers::StridedArrayView1D<const UnsignedInt>{mesh.indicesAsArray()});

The positionIndices are made in two steps instead of using MeshTools::removeDuplicatesIndexedInPlace() because the indexMapping, without being mixed with the original index buffer, needs to be preserved for a later use. Once a simulation updates the positions, they get copied back to the original mesh:

performSimulation(positionIndices, positions);

/* Copy updated positions back to the original locations in the mesh */
MeshTools::duplicateInto(
    Containers::StridedArrayView1D<const UnsignedInt>{indexMapping},
    Containers::StridedArrayView1D<const Vector3>{positions},
    mesh.mutableAttribute<Vector3>(Trade::MeshAttribute::Position));

Bounding volume calculation

The MeshTools::boundingRange() and MeshTools::boundingSphereBouncingBubble() utilities can be used to calculate a bounding volume for a given list of vertex positions, for example to use for culling. Because their output is just a single value, they take a position view directly and don't have any convenience variant operating on a Trade::MeshData. The most straightforward way is to pass Trade::MeshData::positions3DAsArray() to them, see the MeshData data access documentation for more details and alternative approaches that don't allocate a temporary array.

Memory ownership helpers

Much like all other heavier data structures in Magnum, a Trade::MeshData is move-only, to prevent accidental copies when passing it around. If a copy is desirable and it cannot be made as a side effect of some other operation, MeshTools::copy() can be used.

Another use case for it is creating a self-contained Trade::MeshData instance — in some cases, such as for example with Primitives::cubeSolid() or with memory-mapped files, you may get back a Trade::MeshData that references external data, instead of owning them, to avoid unnecessary copies. Such instances usually cannot be modified in-place and one has to ensure that the memoory they reference stays in scope. Calling MeshTools::copy() on such an instance makes a self-contained copy that owns the data and can be modified. For example, turning the cube primitive into a skybox with normals flipped inwards:

Trade::MeshData skybox = MeshTools::copy(Primitives::cubeSolid());
MeshTools::flipNormalsInPlace(skybox.mutableIndices<UnsignedInt>(),
    skybox.mutableAttribute<Vector3>(Trade::MeshAttribute::Normal));

An inverse to MeshTools::copy() is MeshTools::reference(). It makes a non-owning reference to data contained in another Trade::MeshData. It's mainly useful for tool internals, for example to implement common handling for const Trade::MeshData& and Trade::MeshData&.