From b77eafb9deba8228214cd7759d12bbdff555eba0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vladim=C3=ADr=20Vondru=C5=A1?= Date: Sun, 23 Aug 2020 16:30:43 +0200 Subject: [PATCH] MeshTools: add generateQuadIndices(). --- doc/changelog.dox | 5 + doc/snippets/README.md | 9 + doc/snippets/triangulate.svg | 24 ++ doc/triangulate.svg | 212 ++++++++++++++++ src/Magnum/MeshTools/GenerateIndices.cpp | 101 ++++++++ src/Magnum/MeshTools/GenerateIndices.h | 63 +++++ .../MeshTools/Test/GenerateIndicesTest.cpp | 226 +++++++++++++++++- 7 files changed, 639 insertions(+), 1 deletion(-) create mode 100644 doc/snippets/triangulate.svg create mode 100644 doc/triangulate.svg diff --git a/doc/changelog.dox b/doc/changelog.dox index f86b8a614..d23240bf6 100644 --- a/doc/changelog.dox +++ b/doc/changelog.dox @@ -68,6 +68,11 @@ See also: - Added @ref Math::fmod() (see [mosra/magnum#454](https://github.com/mosra/magnum/pull/454)) - Added @ref Math::binomialCoefficient() (see [mosra/magnum#461](https://github.com/mosra/magnum/pull/461)) +@subsubsection changelog-latest-new-meshtools MeshTools library + +- Added @ref MeshTools::generateQuadIndices() for quad triangulation + including non-convex and non-planar quads + @subsubsection changelog-latest-new-scenegraph SceneGraph library - Added @ref SceneGraph::Object::move() diff --git a/doc/snippets/README.md b/doc/snippets/README.md index 0ac9850db..e8b3096a8 100644 --- a/doc/snippets/README.md +++ b/doc/snippets/README.md @@ -13,3 +13,12 @@ smaller file sizes: The output printed by the application can be used to update the example output in `doc/getting-started.dox`. + +### triangulate.svg + +Created by Inkscape from `doc/triangulate.svg` by saving as Optimized SVG and: + +- cleaning up the `` header +- converting to a `style=""`, *keeping* `viewBox` +- adding `class="m-image"` +- removing metadata and the background layer diff --git a/doc/snippets/triangulate.svg b/doc/snippets/triangulate.svg new file mode 100644 index 000000000..8b12f58d9 --- /dev/null +++ b/doc/snippets/triangulate.svg @@ -0,0 +1,24 @@ + + + + + + + + + B + A + C + + + + A + C + D + B + + + + D + + diff --git a/doc/triangulate.svg b/doc/triangulate.svg new file mode 100644 index 000000000..8ff80c6c1 --- /dev/null +++ b/doc/triangulate.svg @@ -0,0 +1,212 @@ + + + + + + + + image/svg+xml + + + + + + + + + + + B + A + C + + A + C + D + B + + + D + + diff --git a/src/Magnum/MeshTools/GenerateIndices.cpp b/src/Magnum/MeshTools/GenerateIndices.cpp index 343d490b3..aa377b35f 100644 --- a/src/Magnum/MeshTools/GenerateIndices.cpp +++ b/src/Magnum/MeshTools/GenerateIndices.cpp @@ -29,6 +29,7 @@ #include #include +#include "Magnum/Math/Vector3.h" #include "Magnum/Trade/MeshData.h" namespace Magnum { namespace MeshTools { @@ -168,6 +169,106 @@ Containers::Array generateTriangleFanIndices(const UnsignedInt vert return indices; } +namespace { + +template inline void generateQuadIndicesIntoImplementation(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads, const Containers::StridedArrayView1D& into) { + CORRADE_ASSERT(quads.size() % 4 == 0, + "MeshTools::generateQuadIndicesInto(): quad index count" << quads.size() << "not divisible by 4", ); + CORRADE_ASSERT(quads.size()*6/4 == into.size(), + "MeshTools::generateQuadIndicesInto(): bad output size, expected" << quads.size()*6/4 << "but got" << into.size(), ); + + for(std::size_t i = 0, max = quads.size()/4; i != max; ++i) { + auto get = [&](UnsignedInt j) -> const Vector3& { + UnsignedInt index = quads[4*i + j]; + CORRADE_ASSERT(index < positions.size(), + "MeshTools::generateQuadIndicesInto(): index" << index << "out of bounds for" << positions.size() << "elements", positions[0]); + return positions[index]; + }; + const Vector3& a = get(0); + const Vector3& b = get(1); + const Vector3& c = get(2); + const Vector3& d = get(3); + + constexpr UnsignedInt SplitAbcAcd[] { 0, 1, 2, 0, 2, 3 }; + constexpr UnsignedInt SplitDabDbc[] { 3, 0, 1, 3, 1, 2 }; + const UnsignedInt* split; + const bool abcAcdOppositeDirection = Math::dot(Math::cross(c - b, a - b), Math::cross(d - c, a - c)) < 0.0f; + const bool dabDbcOppositeDirection = Math::dot(Math::cross(d - b, a - b), Math::cross(c - b, d - b)) < 0.0f; + + /* If normals of ABC and ACD point in opposite direction and DAB DBC + point in the same direction, split as DAB DBC; and vice versa. */ + if(abcAcdOppositeDirection != dabDbcOppositeDirection) + split = abcAcdOppositeDirection ? SplitDabDbc : SplitAbcAcd; + + /* Otherwise the normals of both cases point in the same direction or + it's a pathological case where both cases point in the opposite. + Pick the shorter diagonal. If both are the same, pick the "obvious" + ABC ACD. */ + else split = (b - d).dot() < (c - a).dot() ? SplitDabDbc : SplitAbcAcd; + + /* Assign the two triangles */ + for(std::size_t j = 0; j != 6; ++j) + into[6*i + j] = quads[4*i + split[j]]; + } +} + +} + +Containers::Array generateQuadIndices(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads) { + /* We can skip zero-initialization here */ + Containers::Array out{Containers::NoInit, quads.size()*6/4}; + generateQuadIndicesIntoImplementation(positions, quads, Containers::stridedArrayView(out)); + return out; +} + +Containers::Array generateQuadIndices(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads) { + /* Explicitly ensure we have the unused bytes zeroed out */ + Containers::Array out{Containers::ValueInit, quads.size()*6/4}; + generateQuadIndicesIntoImplementation(positions, quads, + /* Could be just arrayCast(stridedArrayView(out) on LE, + but I want to be sure as much as possible that this compiles on BE + as well. Hmm, now I get why LE won. */ + Containers::StridedArrayView1D{ + Containers::arrayCast(out), + reinterpret_cast(out.data()) + #ifdef CORRADE_BIG_ENDIAN + + 1 + #endif + , out.size(), 4} + ); + return out; +} + +Containers::Array generateQuadIndices(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads) { + /* Explicitly ensure we have the unused bytes zeroed out */ + Containers::Array out{Containers::ValueInit, quads.size()*6/4}; + generateQuadIndicesIntoImplementation(positions, quads, + /* Could be just arrayCast(stridedArrayView(out) on LE, + but I want to be sure as much as possible that this compiles on BE + as well. Hmm, now I get why LE won. */ + Containers::StridedArrayView1D{ + Containers::arrayCast(out), + reinterpret_cast(out.data()) + #ifdef CORRADE_BIG_ENDIAN + + 3 + #endif + , out.size(), 4} + ); + return out; +} + +void generateQuadIndicesInto(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads, const Containers::StridedArrayView1D& into) { + return generateQuadIndicesIntoImplementation(positions, quads, into); +} + +void generateQuadIndicesInto(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads, const Containers::StridedArrayView1D& into) { + return generateQuadIndicesIntoImplementation(positions, quads, into); +} + +void generateQuadIndicesInto(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads, const Containers::StridedArrayView1D& into) { + return generateQuadIndicesIntoImplementation(positions, quads, into); +} + Trade::MeshData generateIndices(Trade::MeshData&& data) { CORRADE_ASSERT(!data.isIndexed(), "MeshTools::generateIndices(): mesh data already indexed", diff --git a/src/Magnum/MeshTools/GenerateIndices.h b/src/Magnum/MeshTools/GenerateIndices.h index f3ababa95..a1022da5a 100644 --- a/src/Magnum/MeshTools/GenerateIndices.h +++ b/src/Magnum/MeshTools/GenerateIndices.h @@ -144,6 +144,69 @@ least @cpp 3 @ce, the @p indices array is expected to have a size of */ MAGNUM_MESHTOOLS_EXPORT void generateTriangleFanIndicesInto(UnsignedInt vertexCount, const Containers::StridedArrayView1D& into); +/** +@brief Create a triangle index buffer for quad primitives +@m_since_latest + +@htmlinclude triangulate.svg + +For each quad `ABCD` gives a pair of triangles that is either `ABC ACD` or +`DAB DBC`, correctly handling cases of non-convex quads and avoiding thin +triangles where possible. Loosely based on [this SO question](https://stackoverflow.com/q/12239876): + +1. If normals of triangles `ABC` and `ACD` point in opposite direction and + `DAB DBC` not (which is equivalent to points `D` and `B` being on the same + side of a diagonal `AC` in a two-dimensional case), split as `DAB DBC` +2. Otherwise, if normals of triangles `DAB` and `DBC` point in opposite + direction and `ABC ACD` not (which is equivalent to points `A` and `C` + being on the same side of a diagonal `DB` in a two-dimensional case), split + as `ABC ACD` +3. Otherwise the normals either point in the same direction in both cases or + the quad is non-planar and ambiguous, pick the case where the diagonal is + shorter + +Size of @p quads is expected to be divisible by @cpp 4 @ce and all indices +being in bounds of the @p positions view. +@see @ref generateQuadIndicesInto(), \n + @ref Math::cross(const Vector3&, const Vector3&), \n + @ref Math::dot(const Vector&, const Vector&) +*/ +MAGNUM_MESHTOOLS_EXPORT Containers::Array generateQuadIndices(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Containers::Array generateQuadIndices(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT Containers::Array generateQuadIndices(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads); + +/** +@brief Create a triangle index buffer for quad primitives into an existing array +@m_since_latest + +A variant of @ref generateQuadIndices() that fills existing memory instead of +allocating a new array. Size of @p quads is expected to be divisible by @cpp 4 @ce +and @p into should have a size that's @cpp quads.size()*6/4 @ce. +*/ +MAGNUM_MESHTOOLS_EXPORT void generateQuadIndicesInto(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads, const Containers::StridedArrayView1D& into); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT void generateQuadIndicesInto(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads, const Containers::StridedArrayView1D& into); + +/** + * @overload + * @m_since_latest + */ +MAGNUM_MESHTOOLS_EXPORT void generateQuadIndicesInto(const Containers::StridedArrayView1D& positions, const Containers::StridedArrayView1D& quads, const Containers::StridedArrayView1D& into); + /** @brief Convert a mesh to plain indexed lines or triangles @m_since{2020,06} diff --git a/src/Magnum/MeshTools/Test/GenerateIndicesTest.cpp b/src/Magnum/MeshTools/Test/GenerateIndicesTest.cpp index 23b274f19..fb04b31ec 100644 --- a/src/Magnum/MeshTools/Test/GenerateIndicesTest.cpp +++ b/src/Magnum/MeshTools/Test/GenerateIndicesTest.cpp @@ -30,7 +30,7 @@ #include #include -#include "Magnum/Math/Vector2.h" +#include "Magnum/Math/Matrix4.h" #include "Magnum/MeshTools/GenerateIndices.h" #include "Magnum/Trade/MeshData.h" @@ -58,12 +58,77 @@ struct GenerateIndicesTest: TestSuite::Tester { void generateTriangleFanIndicesWrongVertexCount(); void generateTriangleFanIndicesIntoWrongSize(); + template void generateQuadIndices(); + template void generateQuadIndicesInto(); + void generateQuadIndicesWrongIndexCount(); + void generateQuadIndicesIndexOutOfBounds(); + void generateQuadIndicesIntoWrongSize(); + void generateIndicesMeshData(); void generateIndicesMeshDataMove(); void generateIndicesMeshDataIndexed(); void generateIndicesMeshDataInvalidPrimitive(); }; +using namespace Math::Literals; + +const struct { + const char* name; + Matrix4 transformation; + UnsignedInt remap[4]; + UnsignedInt expected[6*5]; +} QuadData[] { + {"", {}, {0, 1, 2, 3}, { + 0, 2, 3, 0, 3, 4, // ABC ACD + 9, 5, 6, 9, 6, 7, // DAB DBC + 10, 11, 14, 10, 14, 15, // ABC ACD + 19, 16, 17, 19, 17, 18, // DAB DBC + 20, 21, 22, 20, 22, 23 // ABC ACD + }}, + {"rotated indices 1", {}, {1, 2, 3, 0}, { + 2, 3, 4, 2, 4, 0, // BCD BDA (both splits are fine) + 6, 7, 9, 6, 9, 5, // BCD BDA + 10, 11, 14, 10, 14, 15, // ABC ACD + 17, 18, 19, 17, 19, 16, // BCD BDA + 20, 21, 22, 20, 22, 23 // ABC ACD + }}, + {"rotated indices 2", {}, {2, 3, 0, 1}, { + 3, 4, 0, 3, 0, 2, // CDA CAB + 6, 7, 9, 6, 9, 5, // BCD BDA + 14, 15, 10, 14, 10, 11, // CDA CAB + 17, 18, 19, 17, 19, 16, // BCD BDA + 22, 23, 20, 22, 20, 21 // CDA CAB + }}, + {"rotated indices 3", {}, {3, 0, 1, 2}, { + 4, 0, 2, 4, 2, 3, // DAB DBC (both splits are fine) + 9, 5, 6, 9, 6, 7, // DAB DBC + 14, 15, 10, 14, 10, 11, // CDA CAB + 19, 16, 17, 19, 17, 18, // DAB DBC + 22, 23, 20, 22, 20, 21 // CDA CAB + }}, + {"reversed indices", {}, {3, 2, 1, 0}, { + 4, 3, 2, 4, 2, 0, // DCB DBA (both splits are fine) + 9, 7, 6, 9, 6, 5, // DCB DBA + 10, 15, 14, 10, 14, 11, // ADC ACB + 19, 18, 17, 19, 17, 16, // DCB DBA + 20, 23, 22, 20, 22, 21 // ADC ACB + }}, + {"rotated positions", Matrix4::rotation(130.0_degf, Vector3{1.0f/Constants::sqrt3()}), {0, 1, 2, 3}, { + 0, 2, 3, 0, 3, 4, // ABC ACD + 9, 5, 6, 9, 6, 7, // DAB DBC + 10, 11, 14, 10, 14, 15, // ABC ACD + 19, 16, 17, 19, 17, 18, // DAB DBC + 20, 21, 22, 20, 22, 23 // ABC ACD + }}, + {"mirrored positions", Matrix4::scaling(Vector3::xScale(-1.0f)), {0, 1, 2, 3}, { + 0, 2, 3, 0, 3, 4, // ABC ACD + 9, 5, 6, 9, 6, 7, // DAB DBC + 10, 11, 14, 10, 14, 15, // ABC ACD + 19, 16, 17, 19, 17, 18, // DAB DBC + 20, 21, 22, 20, 22, 23 // ABC ACD + }} +}; + const struct { MeshPrimitive primitive; Containers::Array indices; @@ -113,6 +178,19 @@ GenerateIndicesTest::GenerateIndicesTest() { &GenerateIndicesTest::generateTriangleFanIndicesWrongVertexCount, &GenerateIndicesTest::generateTriangleFanIndicesIntoWrongSize}); + addInstancedTests({ + &GenerateIndicesTest::generateQuadIndices, + &GenerateIndicesTest::generateQuadIndices, + &GenerateIndicesTest::generateQuadIndices}, + Containers::arraySize(QuadData)); + + addTests({&GenerateIndicesTest::generateQuadIndicesInto, + &GenerateIndicesTest::generateQuadIndicesInto, + &GenerateIndicesTest::generateQuadIndicesInto, + &GenerateIndicesTest::generateQuadIndicesWrongIndexCount, + &GenerateIndicesTest::generateQuadIndicesIndexOutOfBounds, + &GenerateIndicesTest::generateQuadIndicesIntoWrongSize}); + addInstancedTests({&GenerateIndicesTest::generateIndicesMeshData}, Containers::arraySize(MeshDataData)); @@ -379,6 +457,152 @@ void GenerateIndicesTest::generateTriangleFanIndicesIntoWrongSize() { "MeshTools::generateTriangleFanIndicesInto(): bad output size, expected 9 but got 8\n"); } +constexpr Vector3 QuadPositions[] { + /* + D C + -> ABC ACD (trivial case) + A B + */ + {0.0f, 0.0f, 0.0f}, {}, // 0 + {1.0f, 0.0f, 0.0f}, // 2 + {1.0f, 1.0f, 0.0f}, // 3 + {0.0f, 1.0f, 0.0f}, // 4 + + /* + D + A C -> DAB DBC (shorter diagonal) + B + */ + { 0.0f, 0.0f, 1.0f}, // 5 + { 5.0f, 0.0f, 0.0f}, // 6 + {10.0f, 0.0f, 1.0f}, {}, // 7 + { 5.0f, 0.0f, 2.0f}, // 9 + + /* + D + A C -> ABC ACD (concave) + B + */ + {0.0f, 0.5f, 0.0f}, // 10 + {5.0f, 0.0f, 0.0f}, {}, {}, // 11 + {4.0f, 0.5f, 0.0f}, // 14 + {5.0f, 1.0f, 0.0f}, // 15 + + /* + C + D B -> DAB DBC (concave, non-planar) + A + */ + {5.0f, 0.0f, 0.5f}, // 16 + {4.0f, 0.5f, 1.0f}, // 17 + {5.0f, 1.0f, 0.5f}, // 18 + {0.0f, 0.5f, 1.0f}, // 19 + + /* + C + D B -> ABC ACD (concave, non-planar, ambiguous -> picking + A shorter diagonal) + */ + {5.0f, 0.0f, 0.5f}, // 20 + {4.0f, 0.5f, 2.0f}, // 21 + {5.0f, 1.0f, 0.5f}, // 22 + {0.0f, 0.5f, 1.0f}, // 23 +}; + +constexpr UnsignedInt QuadIndices[] { + 0, 2, 3, 4, + 5, 6, 7, 9, + 10, 11, 14, 15, + 16, 17, 18, 19, + 20, 21, 22, 23 +}; + +template void GenerateIndicesTest::generateQuadIndices() { + auto&& data = QuadData[testCaseInstanceId()]; + setTestCaseTemplateName(Math::TypeTraits::name()); + setTestCaseDescription(data.name); + + Vector3 transformedPositions[Containers::arraySize(QuadPositions)]; + for(std::size_t i = 0; i != Containers::arraySize(QuadPositions); ++i) + transformedPositions[i] = data.transformation.transformPoint(QuadPositions[i]); + + T remappedIndices[Containers::arraySize(QuadIndices)]; + for(std::size_t i = 0; i != Containers::arraySize(QuadIndices)/4; ++i) + for(std::size_t j = 0; j != 4; ++j) + remappedIndices[i*4 + j] = QuadIndices[i*4 + data.remap[j]]; + + Containers::Array triangleIndices = + MeshTools::generateQuadIndices(transformedPositions, remappedIndices); + + CORRADE_COMPARE_AS(Containers::arrayView(triangleIndices), Containers::arrayView(data.expected), TestSuite::Compare::Container); +} + +template void GenerateIndicesTest::generateQuadIndicesInto() { + setTestCaseTemplateName(Math::TypeTraits::name()); + + /* Simpler variant of the above w/o data transformations just to verify + everything is passed through as expected */ + + T indices[Containers::arraySize(QuadIndices)]; + for(std::size_t i = 0; i != Containers::arraySize(QuadIndices); ++i) + indices[i] = QuadIndices[i]; + + T triangleIndices[Containers::arraySize(QuadIndices)*6/4]; + MeshTools::generateQuadIndicesInto(QuadPositions, indices, triangleIndices); + + CORRADE_COMPARE_AS(Containers::arrayView(triangleIndices), Containers::arrayView({ + 0, 2, 3, 0, 3, 4, // ABC ACD + 9, 5, 6, 9, 6, 7, // DAB DBC + 10, 11, 14, 10, 14, 15, // ABC ACD + 19, 16, 17, 19, 17, 18, // DAB DBC + 20, 21, 22, 20, 22, 23 // ABC ACD + }), TestSuite::Compare::Container); +} + +void GenerateIndicesTest::generateQuadIndicesWrongIndexCount() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + UnsignedInt quads[13]; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::generateQuadIndices({}, quads); + CORRADE_COMPARE(out.str(), + "MeshTools::generateQuadIndicesInto(): quad index count 13 not divisible by 4\n"); +} + +void GenerateIndicesTest::generateQuadIndicesIndexOutOfBounds() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + UnsignedInt quads[]{5, 4, 6, 7}; + Vector3 positions[7]; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::generateQuadIndices(positions, quads); + CORRADE_COMPARE(out.str(), + "MeshTools::generateQuadIndicesInto(): index 7 out of bounds for 7 elements\n"); +} + +void GenerateIndicesTest::generateQuadIndicesIntoWrongSize() { + #ifdef CORRADE_NO_ASSERT + CORRADE_SKIP("CORRADE_NO_ASSERT defined, can't test assertions"); + #endif + + UnsignedInt quads[12]; + UnsignedInt output[19]; + + std::ostringstream out; + Error redirectError{&out}; + MeshTools::generateQuadIndicesInto({}, quads, output); + CORRADE_COMPARE(out.str(), + "MeshTools::generateQuadIndicesInto(): bad output size, expected 18 but got 19\n"); +} + void GenerateIndicesTest::generateIndicesMeshData() { auto&& data = MeshDataData[testCaseInstanceId()]; {