You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

412 lines
21 KiB

/*
This file is part of Magnum.
Copyright © 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019,
2020, 2021, 2022, 2023, 2024, 2025
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.
*/
namespace Magnum {
/** @page meshtools Mesh processing tools
@brief Overview of algorithms and utilities in the @ref MeshTools namespace
@m_keywords{MeshTools}
@tableofcontents
@m_footernavigation
The @ref MeshTools namespace provides a broad set of tools for transforming,
filtering, optimizing and merging mesh data, operating both on high-level
@ref Trade::MeshData instances as well as directly on concrete data views.
@todoc subdivide isn't mentioned because it's useless, neither is
fullScreenTriangle
@todoc normal generation and flipping once it's more than "sorry this is shit"
@section meshtools-create Creating MeshData instances from scratch
When it's desirable to create a @ref Trade::MeshData instance from scratch, for
example from runtime-generated data, @ref MeshTools::interleave(MeshPrimitive, const Trade::MeshIndexData&, Containers::ArrayView<const Trade::MeshAttributeData>) "MeshTools::interleave()"
is the most convenient way.
@snippet MeshTools.cpp interleave-meshdata
Interleaving isn't the only possible layout option --- you can also
@ref Trade-MeshData-populating "construct the MeshData instance directly". It's
more involved, but gives you an ability to have any packing you need.
Note that, however, a @ref Trade::MeshData instance isn't *required* in many
cases --- most @ref MeshTools algorithms, including for example normal
generation or duplicate removal, have alternatives that operate directly on
plain data arrays, and wrapping those in a @ref Trade::MeshData instance just
to call a function may be a needless complication. On the other hand, having a
@ref 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 @ref Trade::MeshData is the
@ref 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.
@section meshtools-gpu Uploading a mesh to the GPU
The @ref MeshTools::compile() utility creates a @ref GL::Mesh instance out of
an arbitrary @ref Trade::MeshData, binding builtin attributes listed in the
@ref Trade::MeshData enum to known locations defined in @ref Shaders::GenericGL.
@snippet Trade.cpp MeshData-gpu-opengl
For more control over index / vertex data storage and handling custom
attributes, the @ref MeshTools::compile(const Trade::MeshData&, GL::Buffer&, GL::Buffer&)
overload can be used. Additionally, there's @ref MeshTools::compileLines() with
specialized handling for line meshes to be rendered with @ref Shaders::LineGL,
and the @ref MeshTools::compiledPerVertexJointCount() utility gives back a
count of primary and secondary per-vertex joints for setting up an appropriate
@ref shaders-usage-skinning "skinning shader".
@section meshtools-optimization Data layout optimization
Mesh import in @ref 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 @ref 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.
@subsection meshtools-optimization-interleave 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.
@snippet MeshTools.cpp meshtools-interleave
Interleaving is however the default behavior in most importers, and most
@ref MeshTools algorithms produce interleaved layouts by default as well.
@ref MeshTools::interleave(const Trade::MeshData&, Containers::ArrayView<const Trade::MeshAttributeData>, InterleaveFlags) "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
@ref MeshTools::interleave(const T&, const U&... next), or its non-allocating
@ref MeshTools::interleaveInto() variant. If the vertex layout is a concrete
@cpp struct @ce, another way is for example using
@relativeref{Corrade,Utility::copy()} to members slices made with
@relativeref{Corrade,Containers::StridedArrayView::slice(U T::*) const}.
@subsection meshtools-optimization-pack-indices 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
@ref MeshTools::compressIndices(const Trade::MeshData&, MeshIndexType) "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:
@snippet MeshTools.cpp meshtools-compressindices
You can also use the same function to unpack already-packed index data back to
a larger type, by passing an appropriate @ref MeshIndexType as the second
argument.
Index packing can be also done directly on an index array using
@ref 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
@ref Math::castInto() overloads.
@subsection meshtools-optimization-cache Vertex transform cache optimization
The @ref 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
@relativeref{Trade,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.
@snippet MeshTools.cpp meshtools-meshoptimizer
See documentation of @ref Trade::AbstractSceneConverter for more information
about using the plugin interface.
@m_class{m-note m-success}
@par
If you're dealing with meshes loaded from files, you can call
the @ref magnum-sceneconverter "magnum-sceneconverter" utility with
`-C MeshOptimizerSceneConverter` to perform these optimizations offline.
@section meshtools-index 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 @ref 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
@ref MeshPrimitive::Triangles, @relativeref{MeshPrimitive,Lines} or
@relativeref{MeshPrimitive,Points}, so it can then be then passed straight to
an algorithm that expects indexed primitives.
@snippet MeshTools.cpp meshtools-generateindices
Ultimately, if a non-indexed list of primitives is expected, the mesh can be
subsequently passed through @ref MeshTools::duplicate(const Trade::MeshData&, Containers::ArrayView<const Trade::MeshAttributeData>) "MeshTools::duplicate()".
Besides the variant taking a @ref Trade::MeshData, there are low-level
@ref MeshTools::generateLineStripIndices(),
@relativeref{MeshTools,generateLineLoopIndices()},
@relativeref{MeshTools,generateTriangleStripIndices()} and
@relativeref{MeshTools,generateTriangleFanIndices()} utilities producing an
index buffer corresponding to a primitive strip, loop or fan. To complete the
offering, @relativeref{MeshTools,generateTrivialIndices()} outputs a
@cpp 0, 1, 2, 3, 4, 5, ... @ce 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 @relativeref{MeshTools,generateLineStripIndicesInto()} etc.
variants as well.
Finally, @ref MeshTools::generateQuadIndices() /
@relativeref{MeshTools,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.
@htmlinclude triangulate.svg
@section meshtools-vertex Vertex data transformation
The @ref MeshTools::transform2D(), @relativeref{MeshTools,transform3D()} and
@relativeref{MeshTools,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.
@snippet MeshTools.cpp meshtools-transform
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
@relativeref{MeshTools,transform2DInPlace()},
@relativeref{MeshTools,transform3DInPlace()} and
@relativeref{MeshTools,transformTextureCoordinates2DInPlace()} variants operate
in-place, not modifying the attribute layout in any way, but have with
additional restrictions on the attribute types.
@todoc mention the data overloads, once they're not laughable inline wrappers
over singular Math APIs
@section meshtools-concatenate 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
@ref 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:
@snippet MeshTools-gl.cpp meshtools-concatenate
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 @ref GL::Mesh::setIndexOffset() and @relativeref{GL::Mesh,setCount()},
or by making @ref GL::MeshView instances and rendering those instead:
@snippet MeshTools-gl.cpp meshtools-concatenate-offsets
Meshes joined this way can make use of various rendering optimizations, see
@ref shaders-usage-multidraw for the shader-side details. There's also a
@ref MeshTools::concatenateInto() variant that reuses a @ref Trade::MeshData
instance with previously allocated buffers to support use cases where meshes
are repeatedly batched on-the-fly.
@m_class{m-note m-success}
@par
Joining all meshes in a file with scene hierarchy baked in can be also done
using the `--concatenate-meshes` option of the
@ref magnum-sceneconverter "magnum-sceneconverter" utility.
@section meshtools-attributes-insert Inserting additional attributes into an existing mesh
The @ref MeshTools::interleave(const Trade::MeshData&, Containers::ArrayView<const Trade::MeshAttributeData>, InterleaveFlags) "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:
@snippet MeshTools.cpp meshtools-interleave-insert
It's also possible to add just an uninitialized attribute placeholder,
specifying just the desired type, and copy the data to it later using
@ref Trade-MeshData-access-mutable "mutable MeshData attribute access":
@m_class{m-console-wrap}
@snippet MeshTools.cpp meshtools-interleave-insert-placeholder
Similar functionality is available also in the above-mentioned
@ref MeshTools::duplicate(const Trade::MeshData&, Containers::ArrayView<const Trade::MeshAttributeData>) "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:
@snippet MeshTools.cpp meshtools-duplicate-insert
In case you need to insert attributes that differ not per vertex but per face,
@ref MeshTools::combineFaceAttributes(const Trade::MeshData&, Containers::ArrayView<const Trade::MeshAttributeData>) "MeshTools::combineFaceAttributes()"
can be used:
@snippet MeshTools.cpp combineFaceAttributes
Finally, for scenarios where a mesh has per-attribute index buffers, such as is
the case when directly importing data from OBJ files,
@ref MeshTools::combineIndexedAttributes() can be used to combine them into a
mesh with a single index buffer.
@section meshtools-attributes-filter Filtering mesh attributes
The inverse of attribute insertion is possible with
@ref MeshTools::filterAttributes(), @relativeref{MeshTools,filterOnlyAttributes()}
and @relativeref{MeshTools,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.
@snippet MeshTools.cpp meshtools-filter
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 @ref MeshTools::interleave(const Trade::MeshData&, Containers::ArrayView<const Trade::MeshAttributeData>, InterleaveFlags) "MeshTools::interleave()"
without @ref MeshTools::InterleaveFlag::PreserveInterleavedAttributes set to
create a copy that contains only the remaining attributes:
@snippet MeshTools.cpp meshtools-filter-unsparse
@section meshtools-duplicates Duplicate vertex removal, duplication based on an index buffer
The @ref MeshTools::removeDuplicates(const Trade::MeshData&) "MeshTools::removeDuplicates()"
function returns a @ref 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
@ref MeshTools::removeDuplicatesFuzzy(const Trade::MeshData&, Float, Double) "MeshTools::removeDuplicatesFuzzy()"
variant instead applies a fuzzy comparison to all floating-point attributes.
@snippet MeshTools.cpp meshtools-removeduplicates
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
@relativeref{Trade,MeshOptimizerSceneConverter} simplification feature instead.
Here for example attempting to reduce the mesh index count by a factor of 10:
@snippet MeshTools.cpp meshtools-meshoptimizer-simplify
<b></b>
@m_class{m-note m-success}
@par
The @ref magnum-sceneconverter "magnum-sceneconverter" utility can perform
duplicate vertex removal in all meshes in a file using
`--remove-duplicate-vertices` or `--remove-duplicate-vertices-fuzzy`. A
MeshOptimizer simplification equivalent to the above snippet is doable with
`-C MeshOptimizerSceneConverter -c simplify,simplifyTargetIndexCountThreshold=0.1`.
Internally, the duplicate vertex removal is implemented using
@ref MeshTools::removeDuplicatesInPlace(const Containers::StridedArrayView2D<char>&),
@ref 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 @ref MeshTools::removeDuplicatesIndexedInPlace()
/ @ref 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:
@snippet MeshTools.cpp meshtools-removeduplicates-position-only
@todoc Fix duplicate / duplicateInto to not need those massive casts, ffs
The `positionIndices` are made in two steps instead of using
@ref 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:
@snippet MeshTools.cpp meshtools-removeduplicates-position-only-copy
@section meshtools-bounding-volume Bounding volume calculation
The @ref MeshTools::boundingRange() and
@ref 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
@ref Trade::MeshData. The most straightforward way is to pass
@ref Trade::MeshData::positions3DAsArray() to them, see the
@ref Trade-MeshData-access "MeshData data access documentation" for more
details and alternative approaches that don't allocate a temporary array.
@section meshtools-helpers Memory ownership helpers
Much like all other heavier data structures in Magnum, a @ref 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,
@ref MeshTools::copy() can be used.
Another use case for it is creating a self-contained @ref Trade::MeshData
instance --- in some cases, such as for example with @ref Primitives::cubeSolid()
or with memory-mapped files, you may get back a @ref 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 @ref 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:
@snippet MeshTools.cpp meshtools-copy
An inverse to @ref MeshTools::copy() is @ref MeshTools::reference(). It makes a
non-owning reference to data contained in another @ref Trade::MeshData. It's
mainly useful for tool internals, for example to implement common handling for
@cpp const Trade::MeshData& @ce and @cpp Trade::MeshData& @ce.
*/
}