diff --git a/src/unittest/CMakeLists.txt b/src/unittest/CMakeLists.txt index 8466902df795..9f8061238afa 100644 --- a/src/unittest/CMakeLists.txt +++ b/src/unittest/CMakeLists.txt @@ -42,9 +42,11 @@ set (UNITTEST_SRCS PARENT_SCOPE) set (UNITTEST_CLIENT_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/mesh_compare.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_clientactiveobjectmgr.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_eventmanager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_gameui.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/test_mesh_compare.cpp ${CMAKE_CURRENT_SOURCE_DIR}/test_keycode.cpp PARENT_SCOPE) diff --git a/src/unittest/mesh_compare.cpp b/src/unittest/mesh_compare.cpp new file mode 100644 index 000000000000..6e21d377b3b3 --- /dev/null +++ b/src/unittest/mesh_compare.cpp @@ -0,0 +1,103 @@ +/* +Minetest +Copyright (C) 2023 Vitaliy Lobachevskiy + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "mesh_compare.h" +#include +#include +#include + +static std::vector expandMesh(const std::vector &vertices, const std::vector &indices) +{ + const int n_indices = indices.size(); + const int n_triangles = n_indices / 3; + if (n_indices % 3) + throw std::invalid_argument("got fractional number of triangles"); + + std::vector ret(n_triangles); + for (int i_triangle = 0; i_triangle < n_triangles; i_triangle++) { + ret.at(i_triangle) = { + vertices.at(indices.at(3 * i_triangle)), + vertices.at(indices.at(3 * i_triangle + 1)), + vertices.at(indices.at(3 * i_triangle + 2)), + }; + } + + return ret; +} + +/// Sorts triangle vertices, keeping winding order. +static Triangle sortTriangle(Triangle t) +{ + if (t[0] < t[1] && t[0] < t[2]) return {t[0], t[1], t[2]}; + if (t[1] < t[2] && t[1] < t[0]) return {t[1], t[2], t[0]}; + if (t[2] < t[0] && t[2] < t[1]) return {t[2], t[0], t[1]}; + throw std::invalid_argument("got bad triangle"); +} + +static std::vector canonicalizeMesh(const std::vector &vertices, const std::vector &indices) +{ + std::vector mesh = expandMesh(vertices, indices); + for (auto &triangle: mesh) + triangle = sortTriangle(triangle); + std::sort(std::begin(mesh), std::end(mesh)); + return mesh; +} + +bool checkMeshEqual(const std::vector &vertices, const std::vector &indices, const std::vector &expected) +{ + auto actual = canonicalizeMesh(vertices, indices); + return actual == expected; +} + +bool checkMeshEqual(const std::vector &vertices, const std::vector &indices, const std::vector &expected) +{ + using QuadRefCount = std::array; + struct QuadRef { + unsigned quad_id; + int quad_part; + }; + + std::vector refs(expected.size()); + std::map tris; + for (unsigned k = 0; k < expected.size(); k++) { + auto &&quad = expected[k]; + // There are 2 ways to split a quad into two triangles. So for each quad, + // the mesh must contain either triangles 0 and 1, or triangles 2 and 3, + // from the following list. No more, no less. + tris.insert({sortTriangle({quad[0], quad[1], quad[2]}), {k, 0}}); + tris.insert({sortTriangle({quad[0], quad[2], quad[3]}), {k, 1}}); + tris.insert({sortTriangle({quad[0], quad[1], quad[3]}), {k, 2}}); + tris.insert({sortTriangle({quad[1], quad[2], quad[3]}), {k, 3}}); + } + + auto actual = canonicalizeMesh(vertices, indices); + for (auto &&tri: actual) { + auto itri = tris.find(tri); + if (itri == tris.end()) + return false; + refs[itri->second.quad_id][itri->second.quad_part] += 1; + } + + for (unsigned k = 0; k < expected.size(); k++) { + if (refs[k] != QuadRefCount{1, 1, 0, 0} && refs[k] != QuadRefCount{0, 0, 1, 1}) + return false; + } + + return true; +} diff --git a/src/unittest/mesh_compare.h b/src/unittest/mesh_compare.h new file mode 100644 index 000000000000..3f0d53d675ac --- /dev/null +++ b/src/unittest/mesh_compare.h @@ -0,0 +1,47 @@ +/* +Minetest +Copyright (C) 2023 Vitaliy Lobachevskiy + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#pragma once +#include +#include +#include +#include + +/// Represents a triangle as three vertices. +/// “Smallest” (according to <) vertex is expected to be first, others should follow in the counter-clockwise order. +using Triangle = std::array; + +/// Represents a quad as four vertices. +/// Vertices should be in the counter-clockwise order. +using Quad = std::array; + +/// Compare two meshes for equality. +/// @param vertices Vertices of the first mesh. Order doesn’t matter. +/// @param indices Indices of the first mesh. Triangle order doesn’t matter. Vertex order in a triangle only matters for winding. +/// @param expected The second mesh, in an expanded form. Must be sorted. +/// @returns Whether the two meshes are equal. +[[nodiscard]] bool checkMeshEqual(const std::vector &vertices, const std::vector &indices, const std::vector &expected); + +/// Compare two meshes for equality. +/// @param vertices Vertices of the first mesh. Order doesn’t matter. +/// @param indices Indices of the first mesh. Triangle order doesn’t matter. Vertex order in a triangle only matters for winding. +/// @param expected The second mesh, in a quad form. +/// @returns Whether the two meshes are equal. +/// @note There are two ways to split a quad into 2 triangles; either is allowed. +[[nodiscard]] bool checkMeshEqual(const std::vector &vertices, const std::vector &indices, const std::vector &expected); diff --git a/src/unittest/test_mesh_compare.cpp b/src/unittest/test_mesh_compare.cpp new file mode 100644 index 000000000000..63ce04231f39 --- /dev/null +++ b/src/unittest/test_mesh_compare.cpp @@ -0,0 +1,152 @@ +/* +Minetest +Copyright (C) 2023 Vitaliy Lobachevskiy + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU Lesser General Public License as published by +the Free Software Foundation; either version 2.1 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Lesser General Public License for more details. + +You should have received a copy of the GNU Lesser General Public License along +with this program; if not, write to the Free Software Foundation, Inc., +51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. +*/ + +#include "test.h" + +#include "mesh_compare.h" + +// This is a self-test to ensure proper functionality of the vertex +// building functions (`Triangle`, `Quad`) and its validation function +// `checkMeshEqual` in preparation for the tests in test_content_mapblock.cpp +class TestMeshCompare : public TestBase { +public: + TestMeshCompare() { TestManager::registerTestModule(this); } + const char *getName() override { return "TestMeshCompare"; } + + void runTests(IGameDef *gamedef) override { + TEST(testTriangle); + TEST(testQuad); + } + + void testTriangle() { + UASSERT(checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + }, {0, 1, 2}, { + Triangle{{ + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + }}, + })); + UASSERT(checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + }, {2, 0, 1}, { + Triangle{{ + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + }}, + })); + UASSERT(!checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + }, {0, 2, 1}, { + Triangle{{ + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + }}, + })); + + UASSERT(checkMeshEqual({ + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + }, {0, 1, 2}, { + Triangle{{ + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + }}, + })); + UASSERT(!checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + }, {0, 1, 2}, { + Triangle{{ + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + }}, + })); + } + + void testQuad() { + UASSERT(checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }, {0, 1, 2, 0, 2, 3}, { + Quad{{ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }}, + })); + UASSERT(checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }, {2, 3, 0, 1, 2, 0}, { + Quad{{ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }}, + })); + UASSERT(checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }, {2, 3, 1, 0, 1, 3}, { + Quad{{ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }}, + })); + UASSERT(checkMeshEqual({ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }, {3, 0, 1, 1, 2, 3}, { + Quad{{ + {{1., 0., 0.}, {3., 0., 0.}, 1, {0., 0.}}, + {{0., 1., 0.}, {2., 0., 0.}, 2, {0., 0.}}, + {{0., 0., 1.}, {1., 0., 0.}, 3, {0., 0.}}, + {{1., -1., 1.}, {4., 0., 0.}, 4, {0., 0.}}, + }}, + })); + } +}; + +static TestMeshCompare mesh_compare_test;